dotnet/orleans · error · ArgumentException

Etag does not match

Error message

Etag does not match

What it means

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.

Source

Thrown at src/Azure/Orleans.Transactions.AzureStorage/TransactionalState/AzureTableTransactionalStateStorage.cs:135

            }
            catch (Exception ex)
            {
                LogErrorTransactionalStateLoadFailed(ex);
                throw;
            }
        }

        public async Task<string> Store(string? expectedETag, TransactionalStateMetaData metadata, List<PendingTransactionState<TState>>? statesToPrepare, long? commitUpTo, long? abortAfter)
        {
            if (_storeRequiresLoad)
            {
                throw new InvalidOperationException("Load must complete successfully before Store can be called again after a failed Store operation.");
            }

            var keyETag = key.ETag.ToString();
            if ((!string.IsNullOrWhiteSpace(keyETag) || !string.IsNullOrWhiteSpace(expectedETag)) && keyETag != expectedETag)
            {
                throw new ArgumentException(nameof(expectedETag), "Etag does not match");
            }

            try
            {
                return await StoreCore(metadata, statesToPrepare, commitUpTo, abortAfter).ConfigureAwait(false);
            }
            catch
            {
                _storeRequiresLoad = true;
                throw;
            }
        }

        private async Task<string> StoreCore(TransactionalStateMetaData metadata, List<PendingTransactionState<TState>>? statesToPrepare, long? commitUpTo, long? abortAfter)
        {
            if (string.IsNullOrEmpty(key.ETag.ToString()) && string.IsNullOrEmpty(key.Metadata))
            {
                // A split prepare can persist the fresh key before phase three publishes the incoming metadata.

View on GitHub (pinned to fca799fa70)

Solutions

  1. Re-run Load to obtain the current etag, then retry Store with the fresh etag.
  2. Reduce concurrency on the affected grain or serialize transactional access.
  3. Ensure only one activation owns the grain at a time (single-activation placement).

Example fix

// before
await storage.Store(staleETag, metadata, states, commit, abort); // etag mismatch

// after
var fresh = await storage.Load();
await storage.Store(fresh.ETag, metadata, states, commit, abort);
Defensive patterns

Strategy: retry

Try / catch

try { await storage.Store(expectedETag, ...); }
catch (ArgumentException ex) when (ex.Message == "Etag does not match")
{ var fresh = await storage.Load(); await storage.Store(fresh.ETag, ...); }

Prevention

When it happens

Trigger: Concurrent transactions on the same grain modifying the same partition; a stale etag passed because Load was not re-run after a prior failure.

Common situations: Multiple silos/activations racing on the same transactional grain; retries that reuse an old etag; _storeRequiresLoad not honored.

Related errors


AI-assisted analysis of dotnet/orleans@fca799fa70 (2026-08-13). Data as JSON: /api/errors/ac6e9cc30bb6f8b1. Report an issue: GitHub.