package activator import ( "context" "errors" "slices" "sync" "testing" "time" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // stagedHostKill is stagedFake plus KillHost, failing while bad: a staged // worker backend whose host-cgroup proof the test controls. type stagedHostKill struct { *stagedFake hmu sync.Mutex bad bool kills int } func (h *stagedHostKill) KillHost(context.Context, registry.Route) error { h.hmu.Lock() defer h.hmu.Unlock() h.kills++ if h.bad { return errors.New("the host cgroup still holds a process") } return nil } func (h *stagedHostKill) setBad(bad bool) { h.hmu.Lock() h.bad = bad h.hmu.Unlock() } func (h *stagedHostKill) killCount() int { h.hmu.Lock() defer h.hmu.Unlock() return h.kills } // generationsHost is the versions the generations file lists for host, // read after a publication of a's state as it is now (persistGenerations is // serialized, so nothing older can land after it). The caller makes sure no // other writer is under way. func generationsHost(t *testing.T, a *Service, path, host string) []string { t.Helper() if err := a.persistGenerations(); err != nil { t.Fatalf("persistGenerations: %v", err) } g, err := release.ReadGenerations(path) if err != nil { t.Fatalf("ReadGenerations: %v", err) } if g == nil { return nil } return g.Hosts[host] } // Final review F12: a failed worker wake that is retained, because its // termination is not proven, keeps its build in the generations file // (root's pin set) until the proof holds. The process may still run those // bytes, so root must not restage or sweep them. func TestARetainedFailedWorkerWakeKeepsItsBuildPinned(t *testing.T) { old := sidecarSettle sidecarSettle = 20 * time.Millisecond t.Cleanup(func() { sidecarSettle = old }) a, b, _, _, _ := stagedActivator(t) a.gens = true hk := &stagedHostKill{stagedFake: b, bad: true} a.backend = hk path := release.GenerationsPath(t.TempDir()) a.GenerationsFile = path host := "bot8.example.com" b.mu.Lock() b.workerDeadOnStart = true // the wake fails its settle b.mu.Unlock() if err := a.WakeWorker(testWorkerRoute(host)); err == nil { t.Fatal("a worker dead on start woke") } a.mu.Lock() _, retained := a.unprovenWakes[host] a.mu.Unlock() if !retained { t.Fatal("the failed wake was not retained over its unproven host cgroup") } if got := generationsHost(t, a, path, host); !slices.Equal(got, []string{"v1"}) { t.Fatalf("generations file lists %v for %s while its failed wake is retained; want [v1]", got, host) } hk.setBad(false) a.sweepOnce() waitFor(t, "the retained wake's proof and its unpin", func() bool { g, err := release.ReadGenerations(path) return err == nil && g != nil && len(g.Hosts[host]) == 0 }) } // Final review F12, T11: a respawn whose child is shed (here: its lease // is lost after the exec) and cannot be proven gone keeps that child's // build pinned while the member is retained, and drops it once the reaper // proves the child gone. The serving build stays pinned throughout. func TestARetainedShedRespawnKeepsItsBuildPinned(t *testing.T) { t.Parallel() a, b, _, _, ver := stagedActivator(t) a.gens = true hk := &stagedHostKill{stagedFake: b} a.backend = hk path := release.GenerationsPath(t.TempDir()) a.GenerationsFile = path rt := alwaysOnWorkerRoute("bot9.example.com") if err := a.WakeWorker(rt); err != nil { t.Fatal(err) } rep := b.lastRep() ver.Store("v2") // the respawn (from the live link) execs v2 b.mu.Lock() b.workerStartHook = func(registry.Route) { // After the exec: the lease fails, and the host cgroup will not // prove the shed child gone. rep.setErr(errors.New("renewal failed")) hk.setBad(true) } b.mu.Unlock() a.mu.Lock() e := a.entries[rt.Host] a.respawnWorkerLocked(e, nil, "test: shed after the exec", nil) done := e.respawnDone a.mu.Unlock() <-done // the respawn goroutine has finished, its shed and publication included if _, members := a.genStateForTest(rt.Host); len(members) != 1 { t.Fatalf("members = %v: want the shed child retained", members) } if got := generationsHost(t, a, path, rt.Host); !slices.Equal(got, []string{"v1", "v2"}) { t.Fatalf("generations file lists %v while the shed v2 child is retained; want [v1 v2]", got) } hk.setBad(false) a.sweepOnce() waitFor(t, "the shed child's proof and its unpin", func() bool { g, err := release.ReadGenerations(path) return err == nil && g != nil && !slices.Contains(g.Hosts[rt.Host], "v2") }) }