microsoft/garnet · error · Exception
Getting handle in disposed device
Error message
Getting handle in disposed device
What it means
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.
Source
Thrown at libs/client/ClientSession/AsyncPool.cs:49
public AsyncPool(int size, Func<T> creator)
{
this.size = size;
this.creator = creator;
this.handleAvailable = new SemaphoreSlim(0);
this.itemQueue = new ConcurrentQueue<T>();
}
/// <summary>
/// Get item synchronously
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public T Get(CancellationToken token = default)
{
for (; ; )
{
if (disposed)
throw new Exception("Getting handle in disposed device");
if (GetOrAdd(itemQueue, out T item))
return item;
handleAvailable.Wait(token);
}
}
/// <summary>
/// Get item asynchronously
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public async ValueTask<T> GetAsync(CancellationToken token = default)
{
for (; ; )
{
if (disposed)View on GitHub (pinned to 951b0fc683)
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).
Example fix
// before
var session = gcsPool.Get(token);
session.Execute(...);
// after — check a local stop flag before checkout
if (stopRequested) return;
GarnetClientSession session;
try { session = gcsPool.Get(token); }
catch (Exception) when (stopRequested) { return; }
// ... use session ... Defensive patterns
Strategy: try-catch
Validate before calling
// Check a stop flag before checkout instead of relying solely on the pool guard
if (stopRequested) return;
GarnetClientSession session;
try { session = gcsPool.Get(token); }
catch (Exception) when (stopRequested) { return; } Type guard
// AsyncPool exposes no public IsDisposed; guard at the call site with your own lifecycle flag if (poolStopped) return;
Try / catch
try { return gcsPool.Get(token); }
catch (Exception ex) when (stopRequested) { /* expected during shutdown */ return default; } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Throttle count is negative
- Exceeded maximum number of active LightEpoch instances {Acti
- LightEpoch
- 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/7ed0fddb405209f3.
Report an issue: GitHub.