carlos / platform Public

host: pin the adopt dispatch to the version-aware bounce

Astra round 2 found no defect and two test gaps. This closes one: with
adoptBounceFn swapped back to the plain Bounce every other test passed.

The real-activator tests now ask "was it stopped?" of the spawn count
after a request. Owns stays true while an entry drains, so it could not
tell a spared child from one being stopped, and the first version of
this test passed against the plain Bounce.

Declined: a test for the one-line assignment of the leg in
fastConvergeLoop. That loop is thin and untested by the file's own
convention, like every other leg it assigns; convergeLegFor, which it
calls, is covered with a real activator.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 0d3f9f9cc84558c3450cb821623d1d8587ca16e1 parent dbf10da
1 file changed, +65 −4
  • internal/host/converge_adopted_test.go +65 −4

CI passed — run details

diff --git a/internal/host/converge_adopted_test.go b/internal/host/converge_adopted_test.go
index f20a51967..6934b1c88 100644
--- a/internal/host/converge_adopted_test.go
+++ b/internal/host/converge_adopted_test.go
@@ -318,11 +318,15 @@ func TestConvergeLegCyclesARootAdoptedInstanceOnARealActivator(t *testing.T) {
t.Fatal("convergeLegFor returned no leg for a box with an activator")
}
+ // "Was it stopped?" is asked of the spawn count, after a request: Owns
+ // stays true while an entry drains, so it cannot tell a spared child from
+ // one being stopped. A request against a stopped instance spawns again.
adopt("v1")
request() // wakes the instance onto v1
leg()
- if !svc.Owns(host) {
- t.Fatal("the leg stopped an instance whose build is the adopted one")
+ request()
+ if got, want := backend.started(), []string{"v1"}; !reflect.DeepEqual(got, want) {
+ t.Fatalf("spawned builds = %v, want %v: the leg stopped an instance whose build is the adopted one", got, want)
}
adopt("v2") // root: staged, repointed, stamped — and nothing cycled
@@ -343,8 +347,9 @@ func TestConvergeLegCyclesARootAdoptedInstanceOnARealActivator(t *testing.T) {
t.Fatalf("spawned builds = %v, want %v", got, want)
}
leg()
- if !svc.Owns(host) {
- t.Fatal("the leg cycled the instance again after it had converged")
+ request()
+ if got, want := backend.started(), []string{"v1", "v2"}; !reflect.DeepEqual(got, want) {
+ t.Fatalf("spawned builds = %v, want %v: the leg cycled the instance again after it had converged", got, want)
}
}
@@ -353,3 +358,59 @@ func TestConvergeLegForABoxWithNoActivator(t *testing.T) {
t.Fatal("convergeLegFor built a leg with no activator to ask")
}
}
+
+// The adopt passes' restart dispatch must be built on adoptBounceFn, not
+// bounceFn. With the converge leg in place an instance can already be on the
+// new build when a slower pass reaches its own restart; the plain Bounce would
+// stop that replacement for nothing. A stale child still gets its cycle.
+func TestAdoptRestartDispatchSparesAConvergedChild(t *testing.T) {
+ env := newFastFixture(t)
+ host := "warm.test"
+ rt := registry.Route{
+ Host: host, Kind: "instance", App: "fastapp", Channel: "edge",
+ Status: "ready", Hibernate: true,
+ }
+ if err := env.reg.Upsert(rt); err != nil {
+ t.Fatalf("upsert: %v", err)
+ }
+ env.stageLive(t, host, "v1")
+ backend := &versionedBackend{optDir: env.optDir}
+ svc := activator.New(activator.Config{Bucket: "test-bucket", Idle: time.Hour}, backend, env.reg)
+ t.Cleanup(func() {
+ sctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ svc.Shutdown(sctx)
+ })
+ wake := func() {
+ t.Helper()
+ rec := httptest.NewRecorder()
+ proxy := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })
+ if !svc.ServeHTTP(rec, httptest.NewRequest("GET", "http://"+host+"/", nil), rt, proxy) || rec.Code != http.StatusNoContent {
+ t.Fatalf("wake request = %d", rec.Code)
+ }
+ }
+ restart := makeRestartRoute(env.reg, func(string) {}, func(u string) error {
+ t.Fatalf("named-unit arm fired for %q on a route that names no unit", u)
+ return nil
+ }, adoptBounceFn(svc))
+
+ // Asked of the spawn count, after a request — see the note in
+ // TestConvergeLegCyclesARootAdoptedInstanceOnARealActivator.
+ wake() // exec'd from v1, and live/<host> names v1
+ if err := restart(host); err != nil {
+ t.Fatalf("restart: %v", err)
+ }
+ wake()
+ if got, want := backend.started(), []string{"v1"}; !reflect.DeepEqual(got, want) {
+ t.Fatalf("spawned builds = %v, want %v: the adopt dispatch stopped a child already running the build live/<host> names", got, want)
+ }
+
+ env.stageLive(t, host, "v2") // the symlink moves under the running child
+ if err := restart(host); err != nil {
+ t.Fatalf("restart: %v", err)
+ }
+ wake()
+ if got, want := backend.started(), []string{"v1", "v2"}; !reflect.DeepEqual(got, want) {
+ t.Fatalf("spawned builds = %v, want %v: the adopt dispatch left a stale child running", got, want)
+ }
+}