fix wave 1: TestARetainedCopyIsPublishedBeforeItIsRecorded waits for its wake goroutine's last generations write
A nil-in-production test seam (wakeEnded) marks the end of wake(); the test waits on it before t.TempDir's cleanup removes the file's directory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+23
−0
internal/activator/activator.go+8 −0internal/activator/wakecheck_test.go+15 −0
diff --git a/internal/activator/activator.go b/internal/activator/activator.go| index 7cae2b2a1..47f9419b9 100644 |
| --- a/internal/activator/activator.go |
| +++ b/internal/activator/activator.go |
| @@ -1219,6 +1219,11 @@ type Service struct { |
| // seam: the positive signal that a stop has reached a join. Set before |
| // the stop it observes; nil in production. |
| joinWaiting func(host, what string) |
| + // wakeEnded, when set, is called as a wake goroutine (wake) returns, |
| + // after its last write (the generations file's republish). It is a test |
| + // seam: a test that removes the generations file's directory waits on |
| + // it first (fix wave 1, item 6). Set before the wake; nil in production. |
| + wakeEnded func(host string) |
| // genHealth, when set, replaces a generation's health check. It is a |
| // test seam. Nil is waitHealthyCtx on the generation's own socket (spec |
| // §7.1: health uses the handle, never AppSocket). |
| @@ -1525,6 +1530,9 @@ func (a *Service) consumeDueTime(e *actEntry) { |
| // requests still need the wake to finish even if the requester who started it |
| // hangs up. |
| func (a *Service) wake(e *actEntry) { |
| + if a.wakeEnded != nil { |
| + defer a.wakeEnded(e.host) |
| + } |
| parent := context.Background() |
| if e.probe != nil && e.probe.parent != nil { |
| // A probe's wake ends with its caller (ProbeWake). |
diff --git a/internal/activator/wakecheck_test.go b/internal/activator/wakecheck_test.go| index 0fea754d5..c977f9e80 100644 |
| --- a/internal/activator/wakecheck_test.go |
| +++ b/internal/activator/wakecheck_test.go |
| @@ -240,10 +240,25 @@ func TestARetainedWakeIsNotCycledOnceRootRepoints(t *testing.T) { |
| // published. The wake publishes it as pending (ruling R-COLDPUBLISH) before |
| // it records, so root's sweep cannot remove the file between the capture |
| // and the exec. |
| +// |
| +// The wake goroutine republishes the generations file after the waiting |
| +// request is let go (wake's tail), so the test waits for that goroutine to |
| +// end before t.TempDir's cleanup removes the file's directory; a write still |
| +// in flight there failed the cleanup with "directory not empty" (fix wave 1, |
| +// item 6, about 15 in 200 runs). |
| func TestARetainedCopyIsPublishedBeforeItIsRecorded(t *testing.T) { |
| shrinkWakeWait(t, 2*time.Second) |
| a, b := identityActivator(t, "v3") |
| a.GenerationsFile = filepath.Join(t.TempDir(), "zdd", "generations.json") |
| + ended := make(chan struct{}, 1) |
| + a.wakeEnded = func(string) { ended <- struct{}{} } |
| + t.Cleanup(func() { // before TempDir's own cleanup, which was registered first |
| + select { |
| + case <-ended: |
| + case <-time.After(10 * time.Second): |
| + t.Error("the wake goroutine did not end") |
| + } |
| + }) |
| retained := stagedID(t, t.TempDir(), "v1") |
| a.HeldTarget = func(string) HeldWake { return HeldWake{Target: "v1", Held: true, Staged: &retained} } |
| names := func() bool { |