From c596c9be920b954bc07c82ec3e73d634016fb800 Mon Sep 17 00:00:00 2001 From: Dmitry Ratner <6830384+dmitrat@users.noreply.github.com> Date: Sat, 13 Jun 2026 19:09:01 +0300 Subject: [PATCH] fix(mvcc): wait for in-flight timer callback in GC Dispose (deterministic stop) DisposeStopsBackgroundCollectionTest flaked on main after #4 (Expected 5, was 6): MvccGarbageCollector.Dispose() called Timer.Dispose(), which does NOT wait for an already-running callback. A collection cycle that started just before Dispose could finish and bump RunCount afterwards, so 'no runs after Dispose' held only when the timing happened to cooperate (green on the PR runner, red on main). Use the Timer.Dispose(WaitHandle) overload and wait for the handle: it signals once all callbacks have drained, so no background collection runs after Dispose returns and RunCount is final. Pre-existing race in a subsystem untouched by #4 - just surfaced by runner timing. MVCC GC fixture 14/14, stable across repeated runs. Co-Authored-By: Claude Fable 5 --- .../Mvcc/MvccGarbageCollector.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Sources/Core/OutWit.Database.Core/Mvcc/MvccGarbageCollector.cs b/Sources/Core/OutWit.Database.Core/Mvcc/MvccGarbageCollector.cs index 1da27ae..8dfba8e 100644 --- a/Sources/Core/OutWit.Database.Core/Mvcc/MvccGarbageCollector.cs +++ b/Sources/Core/OutWit.Database.Core/Mvcc/MvccGarbageCollector.cs @@ -223,7 +223,18 @@ public void Dispose() m_disposed = true; m_cts.Cancel(); - m_timer.Dispose(); + + // Wait for an already-running timer callback to finish before returning. Timer.Dispose() + // alone does not wait for an in-flight callback, so a collection that started just before + // Dispose could still complete (and bump RunCount) afterwards. The WaitHandle overload + // signals once all callbacks have drained, guaranteeing no background collection runs + // after Dispose returns. + using (var timerCallbacksDrained = new ManualResetEvent(false)) + { + if (m_timer.Dispose(timerCallbacksDrained)) + timerCallbacksDrained.WaitOne(); + } + m_cts.Dispose(); }