{"record":{"id":"ac6e9cc30bb6f8b1","repo":"dotnet/orleans","slug":"etag-does-not-match-ac6e9c","errorCode":null,"errorMessage":"Etag does not match","messagePattern":"Etag does not match","errorType":"exception","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/Azure/Orleans.Transactions.AzureStorage/TransactionalState/AzureTableTransactionalStateStorage.cs","lineNumber":135,"sourceCode":"            }\n            catch (Exception ex)\n            {\n                LogErrorTransactionalStateLoadFailed(ex);\n                throw;\n            }\n        }\n\n        public async Task<string> Store(string? expectedETag, TransactionalStateMetaData metadata, List<PendingTransactionState<TState>>? statesToPrepare, long? commitUpTo, long? abortAfter)\n        {\n            if (_storeRequiresLoad)\n            {\n                throw new InvalidOperationException(\"Load must complete successfully before Store can be called again after a failed Store operation.\");\n            }\n\n            var keyETag = key.ETag.ToString();\n            if ((!string.IsNullOrWhiteSpace(keyETag) || !string.IsNullOrWhiteSpace(expectedETag)) && keyETag != expectedETag)\n            {\n                throw new ArgumentException(nameof(expectedETag), \"Etag does not match\");\n            }\n\n            try\n            {\n                return await StoreCore(metadata, statesToPrepare, commitUpTo, abortAfter).ConfigureAwait(false);\n            }\n            catch\n            {\n                _storeRequiresLoad = true;\n                throw;\n            }\n        }\n\n        private async Task<string> StoreCore(TransactionalStateMetaData metadata, List<PendingTransactionState<TState>>? statesToPrepare, long? commitUpTo, long? abortAfter)\n        {\n            if (string.IsNullOrEmpty(key.ETag.ToString()) && string.IsNullOrEmpty(key.Metadata))\n            {\n                // A split prepare can persist the fresh key before phase three publishes the incoming metadata.","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/dotnet/orleans/blob/fca799fa70ecb6ad975224271703ca43221f58de/src/Azure/Orleans.Transactions.AzureStorage/TransactionalState/AzureTableTransactionalStateStorage.cs#L117-L153","documentation":"Thrown by AzureTableTransactionalStateStorage.Store when the expectedETag argument does not match the etag currently held in the loaded key entity. This is optimistic-concurrency control: the row was changed by another writer between Load and Store.","triggerScenarios":"Concurrent transactions on the same grain modifying the same partition; a stale etag passed because Load was not re-run after a prior failure.","commonSituations":"Multiple silos/activations racing on the same transactional grain; retries that reuse an old etag; _storeRequiresLoad not honored.","solutions":["Re-run Load to obtain the current etag, then retry Store with the fresh etag.","Reduce concurrency on the affected grain or serialize transactional access.","Ensure only one activation owns the grain at a time (single-activation placement)."],"exampleFix":"// before\nawait storage.Store(staleETag, metadata, states, commit, abort); // etag mismatch\n\n// after\nvar fresh = await storage.Load();\nawait storage.Store(fresh.ETag, metadata, states, commit, abort);","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { await storage.Store(expectedETag, ...); }\ncatch (ArgumentException ex) when (ex.Message == \"Etag does not match\")\n{ var fresh = await storage.Load(); await storage.Store(fresh.ETag, ...); }","preventionTips":["Always refresh the etag via Load before retrying a contested Store.","Reduce concurrent writers on the same transactional grain."],"tags":["transactions","azure-table","optimistic-concurrency","etag","azure"],"backgroundTag":null,"analyzedSha":"fca799fa70ecb6ad975224271703ca43221f58de","analyzedAt":"2026-08-13T19:55:57.938Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}