Repository navigation
fix(common/countdown): stop draining the timer channel on Reset - #3034
Conversation
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 10 minutes. View limit details
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
9e9eda2 to
fa615de
Compare
fa615de to
54f3f3a
Compare
startTimer drained timer.C whenever Stop reported false, which was the documented way to re-arm a timer before Go 1.23: the channel was buffered, so a value the select had not taken yet was still there to take. Go 1.23 made the channel unbuffered, and the same receive now waits for a send that never comes once the value has been taken, wedging the countdown goroutine with every later Reset and StopTimer queued behind it. Reset stops the timer and discards a pending value on its own, so the drain is dead weight with a live failure mode: the automatic re-arm after a timeout is the only thing that keeps it out of reach today, and skipping that re-arm turns it into a permanent block. countdown_reset_test.go covers the path: a Reset issued from a callback after the timer fired has to return and leave the timer armed for the next callback. With the re-arm removed, that test fails inside its one second bound on the old code and passes on this one.
54f3f3a to
ffab945
Compare
Proposed changes
startTimerdrainstimer.CwhenStopreports false, then re-arms the timer. That was the documented idiom before Go 1.23, when timer channels were buffered: a value theselecthad not taken yet was still in the channel, so the drain returned and theResetthat followed re-armed the timer. Go 1.23 made the channel unbuffered, and the same receive now waits for a send that never arrives once the value has been taken, leavingstartTimerwedged with every laterResetandStopTimerqueued behind it.Resetstops the timer and discards a pending value by itself, so the drain is dead weight with a live failure mode. The automatic re-arm after a timeout is the only thing keeping it out of reach today.Summary
startTimer select case <-timer.C go OnTimeoutFn(...) timer.Reset(duration) # automatic re-arm case <-t.resetc duration = GetTimeoutDuration(...) - if !timer.Stop() { - <-timer.C # blocks once the select took the value - } timer.Reset(duration) # stops and re-arms in one stepEvidence
Before:
go test ./common/countdown/ -run TestResetAfterCallbackReturns -timeout 30son this branch's base (4ac2cc42cd) with the automatic re-arm removed:After: the same mutation on this branch:
The re-arm is what keeps the drain out of reach, so removing it is the smallest edit that reaches the branch. A
Resetissued from a callback after the timer fired is the production shape, andTestCountdownShouldAvoidDeadlockalready drives it.Timer semantics on Go 1.25, standalone:
time.NewTimer(50ms)→ receive →Stop()reportsfalse→ a second receive blocks, because the channel is unbuffered and the value is gone.New test:
-count=10010.07s,-race -count=304.05s,-race -cpu=1 -count=203.03s, all green.Full suite on
9e9eda27e5:make all,make generate(No stale files detected),make tidy(No untidy module files detected),make lint(0 issues),go vet ./...,make test— 130 packages ok,common/countdownok 38.1s.Merge Danger
Door: two-way
One drain removed; reverting restores it.
Blast Radius: consensus
common/countdownis the XDPoS v2 timeout timer, and every engineResetgoes through this branch. The behaviour is unchanged on Go 1.23+:Resetalready stopped the timer and discarded a pending value, so the drain only ever ran on a stale value that the nextResetoverwrote anyway.Types of changes
What types of changes does your code introduce to XDC network?
Put an
✅in the boxes that applyImpacted Components
Which parts of the codebase does this PR touch?
Put an
✅in the boxes that applyChecklist
Put an
✅in the boxes once you have confirmed below actions (or provide reasons on not doing so) thatThe four unchecked item are node-level: the reset path is covered by
TestResetAfterCallbackReturnsand the existing package tests, and on Go 1.23+ the change removes a drain that never ran on a live timer, so there is no devnet behaviour to observe, no cross-version difference to check, and no documentation to update.Summary by CodeRabbit