microsoft/garnet · error · TsavoriteException

EndTransactional called with locks held: {sharedLockCount} s

Error message

EndTransactional called with locks held: {sharedLockCount} shared locks, {exclusiveLockCount} exclusive locks

What it means

ReleaseTransactional (the internal implementation of EndTransactional) checks that no locks are still held when the transaction is being released. Tsavorite transactions require all shared and exclusive locks to be released before the session exits the transactional scope. Holding locks past EndTransactional would leak locks and break the lock table, so it throws a TsavoriteException with the exact counts.

Source

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

            where TSessionFunctions : ISessionFunctionsWrapper<TInput, TOutput, TContext, TStoreFunctions, TAllocator>
        {
            CheckIsNotAcquiredTransactional(sessionFunctions);
            sessionFunctions.Ctx.isAcquiredTransactional = true;
        }

        internal void LocksAcquired<TSessionFunctions>(TSessionFunctions sessionFunctions, long txnVersion)
            where TSessionFunctions : ISessionFunctionsWrapper<TInput, TOutput, TContext, TStoreFunctions, TAllocator>
        {
            CheckIsAcquiredTransactional(sessionFunctions);
            sessionFunctions.Ctx.txnVersion = txnVersion;
        }

        internal void ReleaseTransactional<TSessionFunctions>(TSessionFunctions sessionFunctions)
            where TSessionFunctions : ISessionFunctionsWrapper<TInput, TOutput, TContext, TStoreFunctions, TAllocator>
        {
            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)");
        }

View on GitHub (pinned to 951b0fc683)

Solutions

  1. Ensure every Lock(type) call inside the transaction has a matching Unlock before EndTransactional.
  2. Wrap the transaction body in try/finally and release all locks in the finally block.
  3. Check session.TotalLockCount == 0 before calling EndTransactional.
  4. Use the transaction's automatic lock tracking if available rather than manual lock/unlock.

Example fix

// before
session.Lock(key, LockType.Shared);
/* ... do work ... */
session.EndTransactional();

// after
session.Lock(key, LockType.Shared);
try { /* ... do work ... */ }
finally { session.Unlock(key, LockType.Shared); }
Debug.Assert(session.TotalLockCount == 0);
session.EndTransactional();
Defensive patterns

Strategy: validation

Validate before calling

if (session.TotalLockCount > 0) throw new InvalidOperationException($"Cannot end transaction: {session.sharedLockCount} shared, {session.exclusiveLockCount} exclusive locks still held.");

Try / catch

try { /* transaction body with locks */ }
finally {
    // release all acquired locks here
    session.EndTransactional();
}

Prevention

When it happens

Trigger: Calling session.EndTransactional() (which maps to ReleaseTransactional) while sharedLockCount or exclusiveLockCount (exposed via TotalLockCount) is still greater than zero.

Common situations: Forgetting to release locks acquired during the transaction; an exception in the middle of the transaction body that skips lock-release; mixing manual lock acquire/release with automatic transaction cleanup.

Related errors


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