| index 52d19633f..2862871ec 100644 |
| --- a/internal/activator/opset_test.go |
| +++ b/internal/activator/opset_test.go |
| @@ -979,3 +979,121 @@ func TestSwapWitnessShutdownWaitsForAnUnprovenWake(t *testing.T) { |
| }) |
| } |
| } |
| + |
| +// sigtermIgnorer is a leader that does not stop when asked: Stop answers |
| +// ErrStillRunning, as unitProc does when SIGKILL could not prove the unit |
| +// gone, until the process exits on its own (exit). |
| +type sigtermIgnorer struct { |
| + exited atomic.Bool |
| + stops atomic.Int32 |
| +} |
| + |
| +func (p *sigtermIgnorer) Stop(context.Context) error { |
| + p.stops.Add(1) |
| + if p.exited.Load() { |
| + return nil |
| + } |
| + return ErrStillRunning |
| +} |
| +func (p *sigtermIgnorer) Exited() bool { return p.exited.Load() } |
| + |
| +// fencedStartBackend is a GenBackend whose StartGen has spawned p but |
| +// returns it only once gate opens: a fence lands between the spawn and the |
| +// wake's post-spawn check. No KillGen: the leader's exit is the proof. |
| +type fencedStartBackend struct { |
| + *fakeBackend |
| + p *sigtermIgnorer |
| + entered chan struct{} |
| + gate chan struct{} |
| +} |
| + |
| +func (s *fencedStartBackend) StartGen(_ context.Context, rt registry.Route, n uint64, _ bool) (Proc, Gen, error) { |
| + close(s.entered) |
| + <-s.gate |
| + return s.p, canonicalGen(rt, n, "v1"), nil |
| +} |
| + |
| +func (s *fencedStartBackend) NextDueVia(context.Context, registry.Route, string) (int64, error) { |
| + return 0, nil |
| +} |
| + |
| +var _ GenBackend = (*fencedStartBackend)(nil) |
| + |
| +// A wake fenced after its spawn (the post-spawn ctx/wakeAllowed check) stops |
| +// its process. If that process does not stop, the lease is kept until it is |
| +// proven gone, with generations off too: a fail-closed dormancy exception |
| +// (plan 3 task 2 fix), because releasing it would let another box take the |
| +// host while this process still writes. Before, with generations off, |
| +// cleanupFailedWake released the lease over the live process, as main does. |
| +// The wake is retained (unprovenWakes): the host's next wake is refused, |
| +// FenceAndStop reports the stop unproven, and the sweep gives the lease back |
| +// once the leader has exited. |
| +func TestSwapWitnessAFencedWakeKeepsItsLeaseWhileItsProcessRuns(t *testing.T) { |
| + for _, gens := range []bool{false, true} { |
| + t.Run(fmt.Sprintf("generations %v", gens), func(t *testing.T) { |
| + a, b, _, _ := testActivator(t, time.Hour) |
| + a.gens = gens |
| + p := &sigtermIgnorer{} |
| + s := &fencedStartBackend{fakeBackend: b, p: p, entered: make(chan struct{}), gate: make(chan struct{})} |
| + a.backend = s |
| + host := "fenced.example.com" |
| + woke := make(chan error, 1) |
| + go func() { |
| + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
| + defer cancel() |
| + _, err := a.beginRequest(ctx, testRoute(host)) |
| + woke <- err |
| + }() |
| + <-s.entered |
| + fenced := make(chan error, 1) |
| + go func() { |
| + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) |
| + defer cancel() |
| + _, err := a.FenceAndStop(ctx, host, 1) |
| + fenced <- err |
| + }() |
| + waitFor(t, "the fence", func() bool { return !a.wakeAllowed(host) }) |
| + close(s.gate) |
| + if err := <-woke; err == nil { |
| + t.Fatal("the fenced wake should have failed") |
| + } |
| + if p.stops.Load() == 0 { |
| + t.Fatal("the fenced wake did not stop its process") |
| + } |
| + if got := b.releaseCount(); got != 0 { |
| + t.Fatalf("lease releases = %d while the fenced wake's process runs, want 0", got) |
| + } |
| + retained := func() bool { |
| + a.mu.Lock() |
| + defer a.mu.Unlock() |
| + return a.unprovenWakes[host] != nil |
| + } |
| + settled := func() bool { |
| + a.mu.Lock() |
| + defer a.mu.Unlock() |
| + u := a.unprovenWakes[host] |
| + return u == nil || !u.busy |
| + } |
| + if !retained() { |
| + t.Fatal("the fenced wake was not retained") |
| + } |
| + if err := <-fenced; !errors.Is(err, ErrStopUnproven) { |
| + t.Fatalf("FenceAndStop = %v, want ErrStopUnproven while the process runs", err) |
| + } |
| + |
| + // A sweep while it still runs keeps the lease. |
| + a.sweepOnce() |
| + waitFor(t, "the retained wake's retry", settled) |
| + if got := b.releaseCount(); got != 0 || !retained() { |
| + t.Fatalf("lease releases = %d after a sweep with the process alive, want 0 and the wake retained", got) |
| + } |
| + |
| + // It exits; the next sweep proves it and gives the lease back. |
| + p.exited.Store(true) |
| + a.sweepOnce() |
| + waitFor(t, "the lease release", func() bool { return b.releaseCount() == 1 }) |
| + waitFor(t, "the retained wake dropped", func() bool { return !retained() }) |
| + a.LiftFence(host, 1) |
| + }) |
| + } |
| +} |