{"record":{"id":"c40aea05df0f3232","repo":"microsoft/garnet","slug":"endtransactional-called-with-locks-held-sharedlo","errorCode":null,"errorMessage":"EndTransactional called with locks held: {sharedLockCount} shared locks, {exclusiveLockCount} exclusive locks","messagePattern":"EndTransactional called with locks held: (.+?) shared locks, (.+?) exclusive locks","errorType":"exception","errorClass":"TsavoriteException","httpStatus":null,"severity":"error","filePath":"libs/storage/Tsavorite/cs/src/core/ClientSession/ClientSession.cs","lineNumber":70,"sourceCode":"            where TSessionFunctions : ISessionFunctionsWrapper<TInput, TOutput, TContext, TStoreFunctions, TAllocator>\n        {\n            CheckIsNotAcquiredTransactional(sessionFunctions);\n            sessionFunctions.Ctx.isAcquiredTransactional = true;\n        }\n\n        internal void LocksAcquired<TSessionFunctions>(TSessionFunctions sessionFunctions, long txnVersion)\n            where TSessionFunctions : ISessionFunctionsWrapper<TInput, TOutput, TContext, TStoreFunctions, TAllocator>\n        {\n            CheckIsAcquiredTransactional(sessionFunctions);\n            sessionFunctions.Ctx.txnVersion = txnVersion;\n        }\n\n        internal void ReleaseTransactional<TSessionFunctions>(TSessionFunctions sessionFunctions)\n            where TSessionFunctions : ISessionFunctionsWrapper<TInput, TOutput, TContext, TStoreFunctions, TAllocator>\n        {\n            CheckIsAcquiredTransactional(sessionFunctions);\n            if (TotalLockCount > 0)\n                throw new TsavoriteException($\"EndTransactional called with locks held: {sharedLockCount} shared locks, {exclusiveLockCount} exclusive locks\");\n            sessionFunctions.Ctx.isAcquiredTransactional = false;\n            sessionFunctions.Ctx.txnVersion = 0;\n        }\n\n        internal void CheckIsAcquiredTransactional<TSessionFunctions>(TSessionFunctions sessionFunctions)\n            where TSessionFunctions : ISessionFunctionsWrapper<TInput, TOutput, TContext, TStoreFunctions, TAllocator>\n        {\n            if (!sessionFunctions.Ctx.isAcquiredTransactional)\n                throw new TsavoriteException(\"Transactional method call when BeginTransactional has not been called\");\n        }\n\n        void CheckIsNotAcquiredTransactional<TSessionFunctions>(TSessionFunctions sessionFunctions)\n            where TSessionFunctions : ISessionFunctionsWrapper<TInput, TOutput, TContext, TStoreFunctions, TAllocator>\n        {\n            if (sessionFunctions.Ctx.isAcquiredTransactional)\n                throw new TsavoriteException(\"BeginTransactional cannot be called twice (call EndTransactional first)\");\n        }\n","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/microsoft/garnet/blob/951b0fc6838721f89d102c2bbe1b914e8d39d700/libs/storage/Tsavorite/cs/src/core/ClientSession/ClientSession.cs#L52-L88","documentation":"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.","triggerScenarios":"Calling session.EndTransactional() (which maps to ReleaseTransactional) while sharedLockCount or exclusiveLockCount (exposed via TotalLockCount) is still greater than zero.","commonSituations":"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.","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."],"exampleFix":"// before\nsession.Lock(key, LockType.Shared);\n/* ... do work ... */\nsession.EndTransactional();\n\n// after\nsession.Lock(key, LockType.Shared);\ntry { /* ... do work ... */ }\nfinally { session.Unlock(key, LockType.Shared); }\nDebug.Assert(session.TotalLockCount == 0);\nsession.EndTransactional();","handlingStrategy":"validation","validationCode":"if (session.TotalLockCount > 0) throw new InvalidOperationException($\"Cannot end transaction: {session.sharedLockCount} shared, {session.exclusiveLockCount} exclusive locks still held.\");","typeGuard":null,"tryCatchPattern":"try { /* transaction body with locks */ }\nfinally {\n    // release all acquired locks here\n    session.EndTransactional();\n}","preventionTips":["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."],"tags":["tsavorite","transaction","locking","session","csharp"],"backgroundTag":null,"analyzedSha":"951b0fc6838721f89d102c2bbe1b914e8d39d700","analyzedAt":"2026-08-13T19:01:32.939Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}