microsoft/garnet · error · TsavoriteException

BeginTransactional cannot be called twice (call EndTransacti

Error message

BeginTransactional cannot be called twice (call EndTransactional first)

What it means

CheckIsNotAcquiredTransactional is called by BeginTransactional to prevent nested transactional scopes. If isAcquiredTransactional is already true (a previous BeginTransactional has not been ended), BeginTransactional throws. Tsavorite sessions support one active transaction at a time — you must call EndTransactional before starting a new one.

Source

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

            CheckIsAcquiredTransactional(sessionFunctions);
            if (TotalLockCount > 0)
                throw new TsavoriteException($"EndTransactional called with locks held: {sharedLockCount} shared locks, {exclusiveLockCount} exclusive locks");
            sessionFunctions.Ctx.isAcquiredTransactional = false;
            sessionFunctions.Ctx.txnVersion = 0;
        }

        internal void CheckIsAcquiredTransactional<TSessionFunctions>(TSessionFunctions sessionFunctions)
            where TSessionFunctions : ISessionFunctionsWrapper<TInput, TOutput, TContext, TStoreFunctions, TAllocator>
        {
            if (!sessionFunctions.Ctx.isAcquiredTransactional)
                throw new TsavoriteException("Transactional method call when BeginTransactional has not been called");
        }

        void CheckIsNotAcquiredTransactional<TSessionFunctions>(TSessionFunctions sessionFunctions)
            where TSessionFunctions : ISessionFunctionsWrapper<TInput, TOutput, TContext, TStoreFunctions, TAllocator>
        {
            if (sessionFunctions.Ctx.isAcquiredTransactional)
                throw new TsavoriteException("BeginTransactional cannot be called twice (call EndTransactional first)");
        }

        internal ClientSession(
            TsavoriteKV<TStoreFunctions, TAllocator> store,
            TsavoriteKV<TStoreFunctions, TAllocator>.TsavoriteExecutionContext<TInput, TOutput, TContext> ctx,
            TFunctions functions,
            bool enableConsistentRead = false,
            ILoggerFactory loggerFactory = null)
        {
            if (enableConsistentRead)
            {
                crContext = new(this);
                tcrContext = new(this);
                bContext = crContext.BasicContext;
                uContext = new(this);
                lContext = tcrContext.TransactionalContext;
                luContext = new(this);
            }

View on GitHub (pinned to 951b0fc683)

Solutions

  1. Always pair BeginTransactional with EndTransactional (ideally via try/finally or a using/dispose wrapper).
  2. Before calling BeginTransactional, ensure no prior transaction is active — track the state in your own code.
  3. Recover from a leaked transaction by calling EndTransactional (after releasing locks) before starting a new one.

Example fix

// before
session.BeginTransactional();
/* ... exception skips EndTransactional ... */
session.BeginTransactional(); // throws

// after
session.BeginTransactional();
try { /* ... work ... */ }
finally { session.EndTransactional(); }
session.BeginTransactional(); // now safe
Defensive patterns

Strategy: validation

Validate before calling

// Track transaction state; before BeginTransactional:
if (isInTransaction) throw new InvalidOperationException("EndTransactional must be called before starting a new transaction.");

Prevention

When it happens

Trigger: Calling session.BeginTransactional() twice in a row without an intervening EndTransactional.

Common situations: Forgetting EndTransactional from a previous transaction; exception in a transaction body that skips EndTransactional, then re-entering; nested method calls that both start transactions.

Related errors


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