microsoft/garnet · error · ObjectDisposedException
LightEpoch
Error message
LightEpoch
What it means
ReserveEntryWait is the slow path when the epoch table is full: it increments waiterCount and, if the high bit (kDisposedFlag) is set, throws ObjectDisposedException('LightEpoch'). Dispose sets waiterCount's MSB (via CompareExchange to kDisposedFlag from 0) to signal teardown, so any thread entering the slow path during disposal is rejected. This is the use-during-dispose guard for the epoch's entry-reservation path.
Source
Thrown at libs/client/LightEpoch.cs:632
// Table is full, fall back to slow path with waiting
ReserveEntryWait(ref entry);
}
/// <summary>
/// Slow path for reserving an entry when the table is full.
/// Waits on semaphore until an entry becomes available.
/// </summary>
/// <returns>Reserved entry</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
void ReserveEntryWait(ref int entry)
{
int newCount = Interlocked.Increment(ref waiterCount);
try
{
// If the MSB (disposed flag) is set, the epoch is being disposed.
if ((newCount & kDisposedFlag) != 0)
throw new ObjectDisposedException(nameof(LightEpoch));
while (true)
{
// Re-check for free slot after incrementing waiterCount. This avoids
// us waiting on the semaphore forever in case we increment waiterCount
// immediately after the epoch releaser sees a zero waiterCount (and
// therefore does not release the semaphore).
if (TryAcquireEntry(ref entry))
return;
// No slot available, wait for a signal from Release()
waiterSemaphore.Wait(cts.Token);
}
}
catch (OperationCanceledException)
{
throw new ObjectDisposedException(nameof(LightEpoch));
}View on GitHub (pinned to 951b0fc683)
Solutions
- Drain all threads that use the epoch (ensure no in-flight ReserveEntry) before calling Dispose.
- Treat ObjectDisposedException from this path as expected during teardown and stop work gracefully.
- Order teardown: signal workers to stop, wait for them, then dispose the epoch.
Example fix
// before workers.Stop(); // signals stop but does not wait epoch.Dispose(); // races with workers still reserving entries // after — wait for full drain before dispose await workers.DrainAsync(); epoch.Dispose();
Defensive patterns
Strategy: try-catch
Validate before calling
// Drain threads that use the epoch before disposing it await workers.DrainAsync(); epoch.Dispose();
Try / catch
try { epoch.ReserveEntryForThread(); }
catch (ObjectDisposedException) { /* epoch is shutting down; stop work */ return; } Prevention
- Drain all epoch-using threads before Dispose.
- Treat ObjectDisposedException from ReserveEntry as an expected shutdown signal.
- Order teardown: stop workers, wait, then dispose the epoch.
When it happens
Trigger: A thread tries to reserve an epoch table entry (ReserveEntry → ReserveEntryWait) concurrently with LightEpoch.Dispose(), which sets the kDisposedFlag and thereby makes the incremented waiterCount negative-flagged.
Common situations: Concurrent shutdown where worker threads still protect/reclaim via the epoch while Dispose races; a missing drain before disposing the epoch; stress tests tearing down the store while background threads are still active.
Related errors
- Exceeded maximum number of active LightEpoch instances {Acti
- Getting handle in disposed device
- GarnetClientSession
- Throttle count is negative
- Async operations not supported over protected epoch
AI-assisted analysis of microsoft/garnet@951b0fc683 (2026-08-13).
Data as JSON: /api/errors/94fc96d3f4e276b9.
Report an issue: GitHub.