Skip to content

fix: correct disposal ordering to prevent ObjectDisposedException during shutdown#519

Merged
niemyjski merged 3 commits into
mainfrom
fix/dispose-cts-ordering
Jun 9, 2026
Merged

fix: correct disposal ordering to prevent ObjectDisposedException during shutdown#519
niemyjski merged 3 commits into
mainfrom
fix/dispose-cts-ordering

Conversation

@niemyjski

Copy link
Copy Markdown
Member

Summary

  • Introduces SignalDispose() in MaintenanceBase to separate cancellation signaling from resource disposal
  • Reorders disposal in QueueBase, InMemoryQueue to: signal cancellation, wait for background tasks, then dispose resources (CTS last)
  • Adds Dispose_WithMaintenanceRunning_DoesNotThrowObjectDisposedException test to QueueTestBase

Root Cause

MaintenanceBase.Dispose() cancelled AND disposed the CancellationTokenSource before derived classes had a chance to wait on their background tasks. The still-running maintenance loop accessed DisposedCancellationToken (which calls .Token on the disposed CTS), throwing ObjectDisposedException.

Test plan

  • New test Dispose_WithMaintenanceRunning_DoesNotThrowObjectDisposedException validates the fix
  • All 1894 existing tests pass with no regressions

Fixes FoundatioFx/Foundatio.Redis#165

…ing shutdown

The CancellationTokenSource in MaintenanceBase was being disposed before
background tasks (maintenance loops, workers) had a chance to observe the
cancellation and terminate. This caused ObjectDisposedException when the
still-running tasks accessed DisposedCancellationToken after the CTS was
already disposed.

Changes:
- Use Interlocked.CompareExchange in SignalDispose() for thread-safe disposal
  (BCL best practice matching MessageBusBase pattern)
- Reorder InMemoryQueue.Dispose() to clear queues before waiting for workers
  (prevents workers from picking up additional work during shutdown)
- Remove redundant IsDisposed guard from QueueBase.Dispose() since derived
  classes own the idempotency check via SignalDispose()
- Fix pre-existing NRT warning in CacheClientTestsBase

Fixes FoundatioFx/Foundatio.Redis#165
@niemyjski niemyjski force-pushed the fix/dispose-cts-ordering branch from 8e6d306 to b6187bd Compare June 9, 2026 15:45
- MessageBusBase.IsDisposed: use Volatile.Read for thread-safe reads (matches MaintenanceBase pattern)
- MessageBusBase.DisposeAsync: add diagnostic trace log on re-entry (matches Dispose behavior)
- InMemoryQueue.Dispose: add diagnostic trace log on re-entry (matches RedisQueue pattern)
- CacheClientTestsBase: use Assert.NotNull before .Value to properly validate expiration exists (fixes NRT warning without weakening assertion semantics)
Assert.True((await cache.GetExpirationAsync("test")).Value < TimeSpan.FromSeconds(1));
var expiration = await cache.GetExpirationAsync("test");
Assert.NotNull(expiration);
Assert.True(expiration.Value < TimeSpan.FromSeconds(1));
SignalDispose() now returns true on first call, false if already signaled.
This eliminates the TOCTOU gap between checking IsDisposed and calling
SignalDispose() in leaf classes, collapsing two operations into one atomic check.

Also extracts SignalDispose() in MessageBusBase for the same pattern consistency.
@niemyjski niemyjski merged commit ec666c2 into main Jun 9, 2026
4 checks passed
@niemyjski niemyjski deleted the fix/dispose-cts-ordering branch June 9, 2026 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Redis Queue Maintenance Job uses disposed CancellationTokenSource and Logs Errors

1 participant