Cancel and drain the old runner in ProjectionCoordinatorBase.StartAsync (#592) - #593
Merged
Merged
Conversation
StartAsync did `_cancellation?.SafeDispose()` and then built a fresh source + runner. Disposing a CancellationTokenSource does not cancel it, and _runner was overwritten, so the previous executeAsync kept polling forever against a token that never trips, over a task no drainRunner would ever await. ResumeAsync is StartAsync, so any resume not preceded by PauseAsync orphaned a live leadership loop for the rest of the process. The orphan then re-attained the leadership lock right after StopAsync released it, and stranded the advisory lock handle it won after that lock had already been disposed (weasel#396) — a permanent 'idle in transaction' session that Marten's high-water gap detection reads as a live pre-gap reserver (marten#5090). StartAsync and PauseAsync now share one cancel-then-drain path, so a double StartAsync behaves like PauseAsync + StartAsync, and a spent source is never left behind to be cancelled or disposed twice. Regression test asserts nothing polls after StopAsync when a bare ResumeAsync preceded it (verified RED before the fix), plus guards that pause/resume still restarts the loop and that repeated start/stop cycles stay clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #592.
StartAsyncdisposed the previousCancellationTokenSourceand built a fresh source + runner:Disposing a CTS does not cancel it, and
_runnerwas overwritten, so the previousexecuteAsynckept polling forever over a token that never trips and a task nodrainRunner()would ever await.ResumeAsync()isStartAsync(default), so any resume not preceded byPauseAsyncorphaned a live leadership loop for the rest of the process.Two consequences, both observed:
StopAsynccalledReleaseAllLocks— a stopped coordinator quietly holding leadership again.DisposeAsynchas drained (AdvisoryLock strands a handle acquired after DisposeAsync — permanent 'idle in transaction' advisory-xact-lock session weasel#396), leaving a permanentidle in transactionsession onpg_try_advisory_xact_lockthat Marten's high-water gap detection reads as a live pre-gap reserver — Stopped host can leak the HotCold coordinator's advisory-xact-lock session ("idle in transaction"), which 9.16.1 gap detection treats as an eternal in-flight append — stalling every later daemon in the same process marten#5090.Change
StartAsyncandPauseAsyncnow share onestopRunnerAsync(): cancel, drain, dispose the spent source, clear the bookkeeping. A doubleStartAsyncbecomes equivalent toPauseAsync+StartAsync, which is what every caller already assumed, and no spent source is left behind to be cancelled or disposed a second time.StartAsyncbecomesasync Task(it awaits the drain). The signature is unchanged.Tests
a_second_start_does_not_leave_an_orphaned_leadership_loop— bareResumeAsync, thenStopAsync, then assert the distributor sees no further lock attempts. Verified RED before the fix (the only failure of the 19).pause_then_resume_still_restarts_the_leadership_loop— the drain must not break the legitimate pause/resume cycle.repeated_start_stop_cycles_never_throw_on_a_spent_cancellation_source.Local:
ProjectionCoordinatorBaseTests19/19,test-eventsandtest-event-storegreen on net10.0.🤖 Generated with Claude Code