Wake the watchdog on release instead of polling - #4
Merged
Merged
Conversation
synchronize spawns a watchdog to refresh the lock's TTL, and the watchdog slept in WATCHDOG_SLEEP_INTERVAL steps while checking a flag. Releasing the lock set that flag and joined, so every synchronize blocked until the sleep in flight elapsed - up to half a second, however brief the critical section. A caller taking many short locks paid it on every one. The watchdog now waits on a queue that release closes, so it wakes at once. The refresh cadence is unchanged.
KirIgor
approved these changes
Aug 21, 2026
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.
Summary
synchronizespawns a watchdog thread to refresh the lock's TTL. The watchdog slept inWATCHDOG_SLEEP_INTERVALsteps and checked a flag between them; release set that flag and joined the thread. So releasing a lock blocked until the sleep already in flight elapsed — up to a fullWATCHDOG_SLEEP_INTERVAL(0.5s by default), no matter how short the critical section was.That makes the cost of a lock roughly constant and dominated by the poll interval. Measured with the current code, on a block doing real but brief work:
synchronizeThe work inside is irrelevant — it is the join that dominates. Callers taking many short locks in sequence pay it every time; a caller of ours that takes 150 brief locks in a row spent ~63 of its ~77 seconds parked in
Thread#join, which is what led here.Change
The watchdog now waits on a
Thread::Queueinstead of sleeping, and release closes the queue, which wakes it immediately. The refresh cadence is unchanged:elapsedstill accumulates oneWATCHDOG_SLEEP_INTERVALper wait and still refreshes atWATCHDOG_REFRESH_INTERVAL.Queue#pop(timeout:)returnsnilboth on timeout and on a closed queue, so the loop distinguishes the two withclosed?.Same caller after the change: 77s → 7.8s.
Tests
New spec stubs
WATCHDOG_SLEEP_INTERVALto 5s and asserts asynchronizeover a 50ms block returns in under a second — it fails on the old implementation and passes on the new one. The existing watchdog specs (TTL kept alive beyond expiry,LockRefreshErrorraised into the main thread, refresh exceptions propagated, release on raise) are unchanged and still pass. Suite is 79 examples, 0 failures; RuboCop clean.required_ruby_versionis already>= 3.2, whereQueue#pop(timeout:)is available.Release
Version bumped to 0.5.1 — a fix with no API change. Merging to
mainwill tag and publish it via the release workflow.