Skip to content

fix: move once.Do(close(allDone)) from Reboot() to Release()#397

Merged
panjf2000 merged 4 commits into
devfrom
copilot/fix-data-race-issue
May 25, 2026
Merged

fix: move once.Do(close(allDone)) from Reboot() to Release()#397
panjf2000 merged 4 commits into
devfrom
copilot/fix-data-race-issue

Conversation

Copilot AI commented May 25, 2026

Copy link
Copy Markdown
Contributor

1. Are you opening this pull request for bug-fixs, optimizations or new feature?

Bug fix — data race between Reboot() and running workers on once.Do(func() { close(p.allDone) }).

2. Please describe how these code changes achieve your intention.

The once.Do(close(allDone)) in Reboot() existed solely to handle the edge case where a pool with zero workers would block on <-p.allDone indefinitely. But placing it in Reboot() creates a race with workers that are still shutting down and calling the same once.Do in their defer.

The fundamental fix: Release() is the state transition that marks the pool CLOSED, so it's the correct place to close allDone when no workers are running. If workers are still active, the last one to exit closes it in its defer as before. sync.Once guarantees exactly-one-execution regardless of ordering.

  • Move if p.Running() == 0 { p.once.Do(func() { close(p.allDone) }) } to end of Release()
  • Remove the same check from Reboot() — it now simply does <-p.allDone
  • Remove the redundant check from ReleaseContext() (already handled by Release())
  • Remove workerWg sync.WaitGroup — no longer needed since there's no race to coordinate in Reboot()

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

  • I have squashed all insignificant commits.
  • I have commented my code for explaining package types, values, functions, and non-obvious lines.
  • I have written unit tests and verified that all tests passes (if needed).
  • I have documented feature info on the README (only when this PR is adding a new feature).
  • (optional) I am willing to help maintain this change if there are issues with it later.

Copilot AI and others added 4 commits May 24, 2026 23:54
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>
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>
@codecov

codecov Bot commented May 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.11%. Comparing base (0191bb5) to head (a04a4dc).

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     
Flag Coverage Δ
unittests 95.11% <100.00%> (+0.84%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@panjf2000 panjf2000 marked this pull request as ready for review May 25, 2026 00:56
Copilot AI review requested due to automatic review settings May 25, 2026 00:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 from Reboot() into Release().
  • Remove the redundant allDone close fast-path from ReleaseContext().
  • Simplify Reboot() to always wait on <-p.allDone, relying on Release() or the last worker to close it.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ants.go
@panjf2000 panjf2000 merged commit deeaf50 into dev May 25, 2026
14 checks passed
@panjf2000 panjf2000 deleted the copilot/fix-data-race-issue branch May 25, 2026 01:08
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants