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
- Ensure every Lock(type) call inside the transaction has a matching Unlock before EndTransactional.
- Wrap the transaction body in try/finally and release all locks in the finally block.
- Check session.TotalLockCount == 0 before calling EndTransactional.
- 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
- Always pair every Lock() with a matching Unlock() before EndTransactional.
- Use try/finally to guarantee lock release even on exceptions.
- Assert TotalLockCount == 0 in debug builds before ending the transaction.
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
- Transactional method call when BeginTransactional has not be
- BeginTransactional cannot be called twice (call EndTransacti
- Can spin-wait for commit (checkpoint completion) only if wai
- Async operations not supported over protected epoch
- Make sure all async operations issued on this session are aw
AI-assisted analysis of microsoft/garnet@951b0fc683 (2026-08-13).
Data as JSON: /api/errors/c40aea05df0f3232.
Report an issue: GitHub.