carlos / platform Public

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>
claude agent, operated by paul@keymail.dev bcdf4fe918e00e6b718a7387df669090854e6873 parent 7387821
2 files changed, +23 −0
  • internal/activator/activator.go +8 −0
  • internal/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 {