| 1 | //go:build linux |
| 2 | |
| 3 | package activator |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "os/exec" |
| 9 | "slices" |
| 10 | "sync" |
| 11 | "testing" |
| 12 | "time" |
| 13 | |
| 14 | "github.com/carlosframework/platform/internal/hostclaim" |
| 15 | "github.com/carlosframework/platform/internal/hostwrite" |
| 16 | "github.com/carlosframework/platform/internal/registry" |
| 17 | ) |
| 18 | |
| 19 | // Open question 5's premise, corrected (instance-key plan rev 2): on a |
| 20 | // claim box a session's process retries do not go back to the launch |
| 21 | // source, which is where the key is minted. Prepare runs once per |
| 22 | // admitted session and captures the launch inputs, and every attempt |
| 23 | // calls Materialize (the C8 provider's Establish) before it reserves a |
| 24 | // start. So no attempt runs with a key minted after the files manifest |
| 25 | // was established. This pins that shape of startAdmitted: one Prepare, |
| 26 | // and Materialize before each attempt's start. |
| 27 | func TestStartAdmittedRetriesReuseTheirInputsAndMaterializeFirst(t *testing.T) { |
| 28 | t.Parallel() |
| 29 | var ( |
| 30 | mu sync.Mutex |
| 31 | events []string |
| 32 | prepares int |
| 33 | ) |
| 34 | note := func(e string) { mu.Lock(); events = append(events, e); mu.Unlock() } |
| 35 | 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"}}} |
| 36 | work, _ := (hostwrite.Config{}).Select(t.Context(), obs.Host, "spawn") |
| 37 | 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) { |
| 38 | mu.Lock() |
| 39 | prepares++ |
| 40 | mu.Unlock() |
| 41 | return ClaimStart{RoleLauncher: &RoleLaunch{}, CurrentAttempt: func() hostclaim.WakeRetryAttempt { return hostclaim.WakeRetryAttempt{} }, Host: obs.Host, Work: work, Identity: hostwrite.Identity{Config: "c", Release: "r"}, |
| 42 | OpenProbe: func(context.Context, time.Time) (time.Time, error) { |
| 43 | return time.Time{}, errors.New("unreachable while Start refuses") |
| 44 | }, |
| 45 | Restore: func(context.Context) error { note("restore"); return nil }, |
| 46 | Materialize: func(context.Context) error { |
| 47 | note("materialize") |
| 48 | return nil |
| 49 | }, |
| 50 | Build: func() *exec.Cmd { return nil }, |
| 51 | Ready: func(context.Context, *exec.Cmd) error { return nil }, |
| 52 | Reserve: func(context.Context, int) error { note("reserve"); return nil }, |
| 53 | Failed: func(context.Context, int) error { note("failed"); return nil }, |
| 54 | Healthy: func(context.Context, int, *exec.Cmd) error { return nil }, |
| 55 | Replicate: func(context.Context, string) (Rep, error) { return nil, nil }, |
| 56 | Publish: func(context.Context, hostclaim.Observed) (hostclaim.Observed, error) { return obs, nil }, |
| 57 | Retain: func(Proc, Rep, error) {}, |
| 58 | }, nil |
| 59 | }}}} |
| 60 | // Two starts: the first refused (RoleLaunch.Start refuses until claim |
| 61 | // Plan 9 Group D), a 1 s backoff, then the second. |
| 62 | 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}) |
| 63 | if !errors.Is(err, ErrClaimIsolationUnavailable) { |
| 64 | t.Fatalf("the loop should end on the last start's refusal: %v", err) |
| 65 | } |
| 66 | want := []string{"restore", "reserve", "materialize", "failed", "reserve", "materialize", "failed"} |
| 67 | mu.Lock() |
| 68 | defer mu.Unlock() |
| 69 | if prepares != 1 || !slices.Equal(events, want) { |
| 70 | t.Fatalf("prepares %d, events %v; want 1 and %v", prepares, events, want) |
| 71 | } |
| 72 | } |
| 73 | |