{"record":{"id":"7ed0fddb405209f3","repo":"microsoft/garnet","slug":"getting-handle-in-disposed-device","errorCode":null,"errorMessage":"Getting handle in disposed device","messagePattern":"Getting handle in disposed device","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"libs/client/ClientSession/AsyncPool.cs","lineNumber":49,"sourceCode":"        public AsyncPool(int size, Func<T> creator)\n        {\n            this.size = size;\n            this.creator = creator;\n            this.handleAvailable = new SemaphoreSlim(0);\n            this.itemQueue = new ConcurrentQueue<T>();\n        }\n\n        /// <summary>\n        /// Get item synchronously\n        /// </summary>\n        /// <param name=\"token\"></param>\n        /// <returns></returns>\n        public T Get(CancellationToken token = default)\n        {\n            for (; ; )\n            {\n                if (disposed)\n                    throw new Exception(\"Getting handle in disposed device\");\n\n                if (GetOrAdd(itemQueue, out T item))\n                    return item;\n\n                handleAvailable.Wait(token);\n            }\n        }\n\n        /// <summary>\n        /// Get item asynchronously\n        /// </summary>\n        /// <param name=\"token\"></param>\n        /// <returns></returns>\n        public async ValueTask<T> GetAsync(CancellationToken token = default)\n        {\n            for (; ; )\n            {\n                if (disposed)","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/microsoft/garnet/blob/951b0fc6838721f89d102c2bbe1b914e8d39d700/libs/client/ClientSession/AsyncPool.cs#L31-L67","documentation":"AsyncPool<T>.Get checks the pool's 'disposed' flag on every loop iteration and throws when you try to obtain a pooled item after Dispose() has been called. The word 'device' is a legacy artifact of the same pool pattern used by storage devices; in the client library the pool typically holds GarnetClientSession, GarnetClient, or IConnectionMultiplexer handles. The throw is a hard use-after-free guard: the pooled resources have already been disposed, so a returned handle would be unusable or dangerous.","triggerScenarios":"A caller invokes pool.Get(token) on an AsyncPool whose Dispose() method has already set disposed=true. This happens in benchmark/test shutdown paths (e.g. RespOnlineBench, TxnPerfBench) where one thread disposes the gcsPool while another worker thread is still calling Get to checkout a session.","commonSituations":"Coordinated shutdown where a cancellation token fires but the worker is already inside Get; a shared session pool disposed at application teardown while in-flight requests still pull from it; a thread that ignores a stop signal and keeps requesting handles.","solutions":["Stop enqueuing new Get calls before calling pool.Dispose() — drain workers first via a CancellationToken or a stop flag checked before Get.","If you must call Get during teardown, wrap it in try/catch and treat the exception as a 'pool stopped' signal rather than a fatal error.","Ensure only one owner disposes the pool and that no thread can reach Get after that point (guard the call site with the same disposed/stop flag)."],"exampleFix":"// before\nvar session = gcsPool.Get(token);\nsession.Execute(...);\n\n// after — check a local stop flag before checkout\nif (stopRequested) return;\nGarnetClientSession session;\ntry { session = gcsPool.Get(token); }\ncatch (Exception) when (stopRequested) { return; }\n// ... use session ...","handlingStrategy":"try-catch","validationCode":"// Check a stop flag before checkout instead of relying solely on the pool guard\nif (stopRequested) return;\nGarnetClientSession session;\ntry { session = gcsPool.Get(token); }\ncatch (Exception) when (stopRequested) { return; }","typeGuard":"// AsyncPool exposes no public IsDisposed; guard at the call site with your own lifecycle flag\nif (poolStopped) return;","tryCatchPattern":"try { return gcsPool.Get(token); }\ncatch (Exception ex) when (stopRequested) { /* expected during shutdown */ return default; }","preventionTips":["Drain all workers (await completion) before calling pool.Dispose().","Share a CancellationToken between workers and the shutdown path so no thread enters Get after dispose begins.","Treat the exception during teardown as a control-flow signal, not a fault."],"tags":["csharp","resource-pool","use-after-dispose","concurrency","async"],"backgroundTag":null,"analyzedSha":"951b0fc6838721f89d102c2bbe1b914e8d39d700","analyzedAt":"2026-08-13T19:01:32.939Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}