//go:build linux package activator import ( "context" "errors" "os/exec" "slices" "sync" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/registry" ) // Open question 5's premise, corrected (instance-key plan rev 2): on a // claim box a session's process retries do not go back to the launch // source, which is where the key is minted. Prepare runs once per // admitted session and captures the launch inputs, and every attempt // calls Materialize (the C8 provider's Establish) before it reserves a // start. So no attempt runs with a key minted after the files manifest // was established. This pins that shape of startAdmitted: one Prepare, // and Materialize before each attempt's start. func TestStartAdmittedRetriesReuseTheirInputsAndMaterializeFirst(t *testing.T) { t.Parallel() var ( mu sync.Mutex events []string prepares int ) note := func(e string) { mu.Lock(); events = append(events, e); mu.Unlock() } obs := hostclaim.Observed{Host: "x.example", ETag: "one", Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{Location: "instances/x.example/inc", Session: 1, Generation: "one"}, Op: hostclaim.Op{Kind: "landing", ID: "op", Phase: "probing", Exec: "box"}}} work, _ := (hostwrite.Config{}).Select(t.Context(), obs.Host, "spawn") a := &Service{cfg: Config{HostWrites: hostwrite.Config{Enabled: true}, Claims: &ClaimRuntime{Box: "box", Read: func(context.Context, string) (hostclaim.Observed, error) { return obs, nil }, Prepare: func(context.Context, registry.Route, hostclaim.Observed, startPolicy) (ClaimStart, error) { mu.Lock() prepares++ mu.Unlock() return ClaimStart{RoleLauncher: &RoleLaunch{}, CurrentAttempt: func() hostclaim.WakeRetryAttempt { return hostclaim.WakeRetryAttempt{} }, Host: obs.Host, Work: work, Identity: hostwrite.Identity{Config: "c", Release: "r"}, OpenProbe: func(context.Context, time.Time) (time.Time, error) { return time.Time{}, errors.New("unreachable while Start refuses") }, Restore: func(context.Context) error { note("restore"); return nil }, Materialize: func(context.Context) error { note("materialize") return nil }, Build: func() *exec.Cmd { return nil }, Ready: func(context.Context, *exec.Cmd) error { return nil }, Reserve: func(context.Context, int) error { note("reserve"); return nil }, Failed: func(context.Context, int) error { note("failed"); return nil }, Healthy: func(context.Context, int, *exec.Cmd) error { return nil }, Replicate: func(context.Context, string) (Rep, error) { return nil, nil }, Publish: func(context.Context, hostclaim.Observed) (hostclaim.Observed, error) { return obs, nil }, Retain: func(Proc, Rep, error) {}, }, nil }}}} // Two starts: the first refused (RoleLaunch.Start refuses until claim // Plan 9 Group D), a 1 s backoff, then the second. err := a.startAdmitted(t.Context(), &actEntry{host: obs.Host}, obs, startPolicy{MaxStarts: 2, Window: 30 * time.Second, Deadline: time.Now().Add(time.Minute), Probe: true}) if !errors.Is(err, ErrClaimIsolationUnavailable) { t.Fatalf("the loop should end on the last start's refusal: %v", err) } want := []string{"restore", "reserve", "materialize", "failed", "reserve", "materialize", "failed"} mu.Lock() defer mu.Unlock() if prepares != 1 || !slices.Equal(events, want) { t.Fatalf("prepares %d, events %v; want 1 and %v", prepares, events, want) } }