fix: move once.Do(close(allDone)) from Reboot() to Release()#397
Merged
Conversation
The data race occurred between Reboot() writing p.once = &sync.Once{}
and worker goroutines reading p.once to call once.Do(close(allDone)).
By using atomic.Pointer[sync.Once], all reads (Load) and writes (Store)
to the once field are properly synchronized.
Agent-Logs-Url: https://github.com/panjf2000/ants/sessions/4a1d1b13-0a7f-4167-ae12-429e7719ea32
Co-authored-by: panjf2000 <7496278+panjf2000@users.noreply.github.com>
…fore Reboot Revert the superficial atomic.Pointer fix. The real issue is that Reboot() proceeded after allDone (running count hit 0) while worker goroutines were still executing deferred cleanup code. Add workerWg sync.WaitGroup to poolCommon. Each worker goroutine calls wg.Add(1) before spawning and defer wg.Done() as the outermost defer (runs last). Reboot() calls workerWg.Wait() after <-allDone to block until every goroutine has fully exited before resetting pool state. Agent-Logs-Url: https://github.com/panjf2000/ants/sessions/758f404a-fab6-4a4e-8f42-ea4fdfd2ab9a Co-authored-by: panjf2000 <7496278+panjf2000@users.noreply.github.com>
Agent-Logs-Url: https://github.com/panjf2000/ants/sessions/758f404a-fab6-4a4e-8f42-ea4fdfd2ab9a Co-authored-by: panjf2000 <7496278+panjf2000@users.noreply.github.com>
This is the fundamental fix: Release() is the proper place to close allDone when no workers are running, since it's the operation that transitions the pool to CLOSED state. If workers are still running, the last one to exit closes allDone in its defer as before. This eliminates the race between Reboot() and running workers on once.Do, and removes the unnecessary workerWg complexity. Agent-Logs-Url: https://github.com/panjf2000/ants/sessions/0d78b737-d4ef-4dbd-b021-3e532757c1f9 Co-authored-by: panjf2000 <7496278+panjf2000@users.noreply.github.com>
Copilot created this pull request from a session on behalf of
panjf2000
May 25, 2026 00:42
View session
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #397 +/- ##
==========================================
+ Coverage 94.26% 95.11% +0.84%
==========================================
Files 14 14
Lines 802 798 -4
==========================================
+ Hits 756 759 +3
+ Misses 31 26 -5
+ Partials 15 13 -2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Contributor
There was a problem hiding this comment.
Pull request overview
This PR addresses a concurrency issue around pool shutdown/reboot by changing when allDone is closed: instead of conditionally closing it in Reboot() / ReleaseContext(), it now closes in Release() when there are no running workers, with the intent to avoid a race with worker shutdown.
Changes:
- Move the
Running()==0+once.Do(close(allDone))fast-path fromReboot()intoRelease(). - Remove the redundant
allDoneclose fast-path fromReleaseContext(). - Simplify
Reboot()to always wait on<-p.allDone, relying onRelease()or the last worker to close it.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
panjf2000
added a commit
that referenced
this pull request
May 25, 2026
* chore: use my referral link of DO * docs: correct WithNonblocking comment about its returned error (#390) * fix: make Reboot() wait for pool to fully release before resetting state (#395) * fix: resolve data race on poolCommon.once field using atomic.Pointer The data race occurred between Reboot() writing p.once = &sync.Once{} and worker goroutines reading p.once.Do(...) concurrently. Fix: change the once field from *sync.Once to atomic.Pointer[sync.Once] so all reads (Load) and writes (Store) are atomic. Agent-Logs-Url: https://github.com/panjf2000/ants/sessions/b2f67c6c-ed78-4758-9185-643b3e09d143 Co-authored-by: panjf2000 <7496278+panjf2000@users.noreply.github.com> * fix: make Reboot() wait for all workers to exit before resetting pool The root cause of the data race is that Reboot() resets pool state (once, allDone) while workers are still shutting down and accessing those same fields. Fix: Reboot() now waits on the allDone channel (which is closed when the last worker exits) before resetting pool state. This ensures no worker goroutine is running when the fields are reassigned, eliminating the data race without needing atomic wrappers. Reverts the atomic.Pointer[sync.Once] approach in favor of this fundamental fix. Agent-Logs-Url: https://github.com/panjf2000/ants/sessions/7b1d6e25-546a-40ef-b31d-33e824186b31 Co-authored-by: panjf2000 <7496278+panjf2000@users.noreply.github.com> * fix: also wait for purge/ticktock goroutines to exit in Reboot() Ensures the old purge and ticktock goroutines have fully exited (set their done flags) before Reboot() resets those flags and starts new goroutines. This prevents a logic race where the old goroutine could set purgeDone/ticktockDone=1 after Reboot() resets them to 0. Agent-Logs-Url: https://github.com/panjf2000/ants/sessions/480234c8-a50f-4dc3-9352-0b337ebfc5d3 Co-authored-by: panjf2000 <7496278+panjf2000@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: panjf2000 <7496278+panjf2000@users.noreply.github.com> * chore: improve variable naming in binarySearch (#393) * fix: release created pools on NewMultiPool failure (#394) * chore: replace 1<<31-1 with math.MaxInt32 (#392) * fix: move once.Do(close(allDone)) from Reboot() to Release() (#397) * fix: use atomic.Pointer for once field to prevent data race in Reboot The data race occurred between Reboot() writing p.once = &sync.Once{} and worker goroutines reading p.once to call once.Do(close(allDone)). By using atomic.Pointer[sync.Once], all reads (Load) and writes (Store) to the once field are properly synchronized. Agent-Logs-Url: https://github.com/panjf2000/ants/sessions/4a1d1b13-0a7f-4167-ae12-429e7719ea32 Co-authored-by: panjf2000 <7496278+panjf2000@users.noreply.github.com> * fix: use sync.WaitGroup to ensure all worker goroutines fully exit before Reboot Revert the superficial atomic.Pointer fix. The real issue is that Reboot() proceeded after allDone (running count hit 0) while worker goroutines were still executing deferred cleanup code. Add workerWg sync.WaitGroup to poolCommon. Each worker goroutine calls wg.Add(1) before spawning and defer wg.Done() as the outermost defer (runs last). Reboot() calls workerWg.Wait() after <-allDone to block until every goroutine has fully exited before resetting pool state. Agent-Logs-Url: https://github.com/panjf2000/ants/sessions/758f404a-fab6-4a4e-8f42-ea4fdfd2ab9a Co-authored-by: panjf2000 <7496278+panjf2000@users.noreply.github.com> * fix: call workerWg.Add(1) before addRunning(1) for defensive ordering Agent-Logs-Url: https://github.com/panjf2000/ants/sessions/758f404a-fab6-4a4e-8f42-ea4fdfd2ab9a Co-authored-by: panjf2000 <7496278+panjf2000@users.noreply.github.com> * fix: move once.Do(close(allDone)) to Release() instead of Reboot() This is the fundamental fix: Release() is the proper place to close allDone when no workers are running, since it's the operation that transitions the pool to CLOSED state. If workers are still running, the last one to exit closes allDone in its defer as before. This eliminates the race between Reboot() and running workers on once.Do, and removes the unnecessary workerWg complexity. Agent-Logs-Url: https://github.com/panjf2000/ants/sessions/0d78b737-d4ef-4dbd-b021-3e532757c1f9 Co-authored-by: panjf2000 <7496278+panjf2000@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: panjf2000 <7496278+panjf2000@users.noreply.github.com> --------- Co-authored-by: hehaifeng526 <54569318+vibe-sudo@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: panjf2000 <7496278+panjf2000@users.noreply.github.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.
1. Are you opening this pull request for bug-fixs, optimizations or new feature?
Bug fix — data race between
Reboot()and running workers ononce.Do(func() { close(p.allDone) }).2. Please describe how these code changes achieve your intention.
The
once.Do(close(allDone))inReboot()existed solely to handle the edge case where a pool with zero workers would block on<-p.allDoneindefinitely. But placing it inReboot()creates a race with workers that are still shutting down and calling the sameonce.Doin their defer.The fundamental fix:
Release()is the state transition that marks the pool CLOSED, so it's the correct place to closeallDonewhen no workers are running. If workers are still active, the last one to exit closes it in its defer as before.sync.Onceguarantees exactly-one-execution regardless of ordering.if p.Running() == 0 { p.once.Do(func() { close(p.allDone) }) }to end ofRelease()Reboot()— it now simply does<-p.allDoneReleaseContext()(already handled byRelease())workerWg sync.WaitGroup— no longer needed since there's no race to coordinate inReboot()3. Please link to the relevant issues (if any).
4. Which documentation changes (if any) need to be made/updated because of this PR?
None.
4. Checklist