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

  1. Stop enqueuing new Get calls before calling pool.Dispose() — drain workers first via a CancellationToken or a stop flag checked before Get.
  2. 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.
  3. 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

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


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