microsoft/garnet · error · NotSupportedException

Async operations not supported over protected epoch

Error message

Async operations not supported over protected epoch

What it means

CompletePendingAsync is the async path for completing outstanding operations on a session. Tsavorite uses an epoch-based memory-reclamation model where threads protect the epoch before touching shared in-memory structures. The async completion path must release the epoch (leave unprotected) so continuations can run on any thread; calling it while ThisInstanceProtected() is true would be unsafe, so it throws NotSupportedException.

Solutions

  1. Ensure the epoch is not protected on the calling thread before awaiting async session operations.
  2. Use the synchronous CompletePending (with wait=true) inside epoch-protected scopes instead of the async variant.
  3. Refactor to release epoch protection before the first await and re-protect only when needed afterward.

Example fix

// before
store.epoch.Suspend(); // or some scope that left epoch protected
await session.CompletePendingAsync(funcs, token);

// after
// Ensure epoch is not protected before async call:
if (store.epoch.ThisInstanceProtected()) store.epoch.Resume();
await session.CompletePendingAsync(funcs, token);
Defensive patterns

Strategy: validation

Validate before calling

if (store.epoch.ThisInstanceProtected()) throw new InvalidOperationException("Release epoch protection before calling async session methods.");

Prevention

When it happens

Trigger: Calling CompletePendingAsync (or ReadAsync/WriteAsync that internally completes pending) while the calling thread has the epoch protected, e.g. inside a synchronized callback or within an epoch-protected scope.

Common situations: Mixing sync and async session APIs; calling async methods from within epoch-protected callbacks (e.g. compaction, scan callbacks); older code that protected the epoch before awaiting.

Related errors


AI-assisted analysis of microsoft/garnet@951b0fc683 (2026-08-13). Data as JSON: /api/errors/26d52a37713b182f. Report an issue: GitHub.

Appendix: source

Thrown at libs/storage/Tsavorite/cs/src/core/ClientSession/ClientSession.cs:332

            => CompletePendingAsync(sessionFunctions, getOutputs: false, waitForCommit, token);

        /// <inheritdoc/>
        internal async ValueTask<CompletedOutputIterator<TInput, TOutput, TContext>> CompletePendingWithOutputsAsync<TSessionFunctionsWrapper>(TSessionFunctionsWrapper sessionFunctions,
                bool waitForCommit = false, CancellationToken token = default)
            where TSessionFunctionsWrapper : ISessionFunctionsWrapper<TInput, TOutput, TContext, TStoreFunctions, TAllocator>
        {
            InitializeCompletedOutputs();
            await CompletePendingAsync(sessionFunctions, getOutputs: true, waitForCommit, token).ConfigureAwait(false);
            return completedOutputs;
        }

        private async ValueTask CompletePendingAsync<TSessionFunctionsWrapper>(TSessionFunctionsWrapper sessionFunctions, bool getOutputs, bool waitForCommit = false, CancellationToken token = default)
            where TSessionFunctionsWrapper : ISessionFunctionsWrapper<TInput, TOutput, TContext, TStoreFunctions, TAllocator>
        {
            token.ThrowIfCancellationRequested();

            if (store.epoch.ThisInstanceProtected())
                throw new NotSupportedException("Async operations not supported over protected epoch");

            // Complete all pending operations on session
            await store.CompletePendingAsync(sessionFunctions, token, getOutputs ? completedOutputs : null).ConfigureAwait(false);

            // Wait for commit if necessary
            if (waitForCommit)
                await WaitForCommitAsync(sessionFunctions, token).ConfigureAwait(false);
        }

        /// <summary>
        /// Check if at least one synchronous request is ready for CompletePending to be called on
        /// Returns completed immediately if there are no outstanding synchronous requests
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        public async ValueTask ReadyToCompletePendingAsync(CancellationToken token = default)
        {
            token.ThrowIfCancellationRequested();

View on GitHub (pinned to 951b0fc683)