//go:build linux package activator import ( "context" "errors" "path/filepath" "slices" "sync" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/registry" ) // The held wake check on the claimed path (CARLOS_HOST_CLAIM; settings plan // 4 Task 7). Its launcher execs the claim inputs' file, so a retained copy // cannot be injected: the wake asks root, waits for the install, and the // launch's before hook refuses a hashed build that is not the target. type heldClaimed struct { a *Service b *planGenBackend f *rootLaunchFixture log *execLog rt registry.Route mu sync.Mutex reserved int failed int } func (c *heldClaimed) counts() (reserved, failed, launches int) { c.mu.Lock() reserved, failed = c.reserved, c.failed c.mu.Unlock() c.f.root.mu.Lock() defer c.f.root.mu.Unlock() return reserved, failed, len(c.f.root.launches) } // newHeldClaimed is a claims-on Service whose wakes go through doClaimWake // and launch through the root fixture. hashed is the build the launch // snapshot hashes and names; live is what live/ names at the start. func newHeldClaimed(t *testing.T, live, hashed string) *heldClaimed { t.Helper() f := newRootLaunchFixture(t) f.root.grant = launchedChild(t) host := f.launch.Target.Host obs := claimedObs(host) obs.Claim.Holder.WriterBox = "box" admitted := probeAdmitted(obs) work, _ := (hostwrite.Config{}).Select(t.Context(), host, "spawn") c := &heldClaimed{f: f, b: newPlanGenBackend()} c.b.fakeBackend.setLive(live) reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } var mu sync.Mutex reads := 0 c.a = New(Config{Idle: time.Hour, HostWrites: hostwrite.Config{Enabled: true}, Claims: &ClaimRuntime{Box: "box", Bucket: "data", Read: func(context.Context, string) (hostclaim.Observed, error) { mu.Lock() defer mu.Unlock() if reads++; reads > 1 { return admitted, nil } return obs, nil }, StopForeign: func(context.Context, string) error { return nil }, Prerequisites: func(context.Context, registry.Route, hostclaim.Observed, startPolicy) error { return nil }, Admit: func(context.Context, registry.Route, hostclaim.Observed, startPolicy) (hostclaim.Observed, error) { return admitted, nil }, FailedAdmission: func(context.Context, hostclaim.Observed, error) error { return nil }, Prepare: func(context.Context, registry.Route, hostclaim.Observed, startPolicy) (ClaimStart, error) { s := rootLaunched(t, f, claimedStartFor(admitted, work, "v1")) s.Staged, s.Version = claimedBuildOf(t, f, hashed), hashed s.Replicate = func(context.Context, string) (Rep, error) { return &fakeRep{}, nil } s.Reserve = func(context.Context, int) error { c.mu.Lock(); c.reserved++; c.mu.Unlock(); return nil } s.Failed = func(context.Context, int) error { c.mu.Lock(); c.failed++; c.mu.Unlock(); return nil } return s, nil }}}, c.b, reg) c.log = &execLog{spawns: func() int { f.root.mu.Lock(); defer f.root.mu.Unlock(); return len(f.root.launches) }} c.a.BeforeExec = c.log.hook c.rt = registry.Route{Host: host, Kind: "instance", Hibernate: true, Socket: filepath.Join(f.runDir, "app.sock"), DBPath: filepath.Join(f.launchDir, "app.db")} return c } // A claimed held wake on a stale live link asks root for the target and // waits for the install; the launch then snapshots the target and runs it, // recorded first. func TestAClaimedHeldWakeWaitsForRootsInstall(t *testing.T) { t.Parallel() c := newHeldClaimed(t, "v3", "v1") c.a.HeldTarget = heldOn("v1") r := &asks{then: func(string, string) { c.b.fakeBackend.setLive("v1") }} c.a.RequestStage = r.ask done, err := c.a.beginRequest(t.Context(), c.rt) if err != nil { t.Fatalf("the claimed held wake: %v", err) } done() if got := r.seen(); !slices.Equal(got, []string{c.rt.Host + "@v1"}) { t.Fatalf("asked root %v, want v1", got) } calls, at, _ := c.log.seen() if _, _, launches := c.counts(); !slices.Equal(calls, []string{c.rt.Host + "@v1"}) || at[0] != 0 || launches != 1 { t.Fatalf("records %v at exec counts %v, %d launches; want v1 recorded, then launched once", calls, at, launches) } } // The race half: root installed something other than the target between // the wait and the launch snapshot. The before hook refuses it before the // record: nothing records or execs, no failed attempt is written, and the // start loop does not retry it (ruling R-EXEC-CLAIMRETRY's shape). func TestAClaimedHeldWakeRefusesAnIdentityThatIsNotItsTarget(t *testing.T) { c := newHeldClaimed(t, "v1", "v2") c.a.HeldTarget = heldOn("v1") c.a.RequestStage = func(string, string) { t.Error("asked root, with the target already installed") } began := time.Now() _, err := c.a.beginRequest(t.Context(), c.rt) if !errors.Is(err, errStaleLink) { t.Fatalf("err = %v, want errStaleLink", err) } waitFor(t, "the claimed wake to end", func() bool { return c.a.entryGone(c.rt.Host) }) if calls, _, _ := c.log.seen(); len(calls) != 0 { t.Fatalf("records %v: a build that is not the target was recorded", calls) } if reserved, failed, launches := c.counts(); launches != 0 || failed != 0 || reserved != 1 { t.Fatalf("reserved=%d failed=%d launches=%d, want one attempt, nothing run, no failed attempt", reserved, failed, launches) } if took := time.Since(began); took >= 5*time.Second { t.Fatalf("took %v: the refusal waited out a retry backoff", took) } if got := memberSafeWakeReason(err); got != memberSafeWakeReason(ErrFenced) { t.Fatalf("member reads %q, want the fence's wording", got) } }