{"record":{"id":"4486f58340a0089b","repo":"dotnet/orleans","slug":"etag-does-not-match","errorCode":null,"errorMessage":"Etag does not match","messagePattern":"Etag does not match","errorType":"exception","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/AWS/Orleans.Transactions.DynamoDB/TransactionalState/DynamoDBTransactionalStateStorage.cs","lineNumber":159,"sourceCode":"\n    /// <inheritdoc />\n    public async Task<string> Store(string? expectedETag, TransactionalStateMetaData metadata, List<PendingTransactionState<TState>>? statesToPrepare, long? commitUpTo, long? abortAfter)\n    {\n        if (this.requiresReload)\n        {\n            throw new InvalidOperationException(\"Load must be called after a failed Store before this storage instance can be reused.\");\n        }\n\n        var batchOperation = new BatchOperation(this.storage, this.tableName, this.key, this.logger);\n        var keyWasNew = !this.key.ETag.HasValue;\n\n        try\n        {\n            var keyETag = key.ETag?.ToString();\n            if ((!string.IsNullOrWhiteSpace(keyETag) || !string.IsNullOrWhiteSpace(expectedETag)) &&\n                keyETag != expectedETag)\n            {\n                throw new ArgumentException(\"Etag does not match\", nameof(expectedETag));\n            }\n\n            var serializedMetadata = this.ConvertToStorageFormat(metadata);\n            var timestamp = DateTimeOffset.UtcNow;\n            var committedSequenceId = commitUpTo.HasValue && commitUpTo.Value > key.CommittedSequenceId\n                ? commitUpTo.Value\n                : key.CommittedSequenceId;\n            this.ValidateKeyItemSize(serializedMetadata, timestamp, committedSequenceId);\n\n            // Store mutates the cached entities while constructing the transaction. If any operation\n            // fails, Load must restore the cache before this instance can be used again.\n            this.requiresReload = true;\n\n            // assemble all storage operations into a single batch\n            // these operations must commit in sequence, but not necessarily atomically\n            // so we can split this up if needed\n\n            // first, clean up aborted records","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/dotnet/orleans/blob/fca799fa70ecb6ad975224271703ca43221f58de/src/AWS/Orleans.Transactions.DynamoDB/TransactionalState/DynamoDBTransactionalStateStorage.cs#L141-L177","documentation":"Thrown by DynamoDBTransactionalStateStorage.Store when the cached key ETag does not match the expectedETag supplied by the caller. This is an optimistic-concurrency check: between the caller's Load and Store, another write changed the row's ETag, so the caller's view is stale and the batch must not be applied. The exception is an ArgumentException naming expectedETag.","triggerScenarios":"Two concurrent transactions on the same grain/state both read the same ETag, then both attempt to Store; the second sees key.ETag already advanced by the first and is rejected. Also if a caller passes a stale cached ETag after the row was written elsewhere.","commonSituations":"Concurrent grain activations or transaction participants contending on one transactional state; retrying Store with an ETag obtained before a successful competing write; cross-silo races on the same grain key.","solutions":["Treat this as a transient optimistic-concurrency conflict: call Load to get the fresh ETag, recompute the change, and retry Store.","Reduce concurrency on the same transactional grain key where possible (single-writer pattern).","Ensure the caller always stores using the ETag returned by its most recent Load, not a cached older value.","Cap retries with backoff to avoid live-lock under heavy contention."],"exampleFix":"// before\nvar resp = await store.Load();\n// ... another writer commits here ...\nawait store.Store(resp.ETag, meta, prep, commit, abort); // throws: Etag mismatch\n\n// after\ntry { await store.Store(resp.ETag, meta, prep, commit, abort); }\ncatch (ArgumentException ex) when (ex.Message.Contains(\"Etag\"))\n{\n    resp = await store.Load(); // refresh ETag\n    await store.Store(resp.ETag, meta, prep, commit, abort);\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"for (int attempt = 0; ; attempt++)\n{\n    try { return await store.Store(etag, meta, prep, commit, abort); }\n    catch (ArgumentException ex) when (ex.ParamName == \"expectedETag\" && ex.Message.Contains(\"Etag\"))\n    {\n        if (attempt >= MaxRetries) throw;\n        var fresh = await store.Load(); // refresh ETag\n        etag = fresh.ETag;\n        // optionally recompute prep/meta against fresh.CommittedSequenceId\n        await Task.Delay(Backoff(attempt));\n    }\n}","preventionTips":["Always Store with the ETag from your most recent Load.","On ETag mismatch, reload and recompute before retrying.","Use a single-writer pattern per grain key to reduce contention.","Cap retries with backoff to avoid live-lock."],"tags":["aws","dynamodb","transactions","optimistic-concurrency","orleans"],"backgroundTag":null,"analyzedSha":"fca799fa70ecb6ad975224271703ca43221f58de","analyzedAt":"2026-08-13T19:55:57.938Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}