Skip to content

Wake the watchdog on release instead of polling - #4

Merged
tycooon merged 2 commits into
mainfrom
wake-watchdog-on-release
Aug 21, 2026
Merged

tycooon merged 2 commits into
mainfrom
wake-watchdog-on-release

Conversation

@tycooon

@tycooon tycooon commented Aug 21, 2026 •

Copy link
Copy Markdown
Member

Summary

synchronize spawns a watchdog thread to refresh the lock's TTL. The watchdog slept in WATCHDOG_SLEEP_INTERVAL steps 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 full WATCHDOG_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:

Critical section Cost of synchronize
~1 ms ~511 ms
~20 ms ~510 ms
~90 ms ~509 ms

The 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::Queue instead of sleeping, and release closes the queue, which wakes it immediately. The refresh cadence is unchanged: elapsed still accumulates one WATCHDOG_SLEEP_INTERVAL per wait and still refreshes at WATCHDOG_REFRESH_INTERVAL.

Queue#pop(timeout:) returns nil both on timeout and on a closed queue, so the loop distinguishes the two with closed?.

Same caller after the change: 77s → 7.8s.

Tests

New spec stubs WATCHDOG_SLEEP_INTERVAL to 5s and asserts a synchronize over 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, LockRefreshError raised 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_version is already >= 3.2, where Queue#pop(timeout:) is available.

Release

Version bumped to 0.5.1 — a fix with no API change. Merging to main will tag and publish it via the release workflow.

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.
@tycooon
tycooon merged commit f2fcede into main Aug 21, 2026
3 checks passed
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.

2 participants