carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
4 "context"
5 "errors"
6 "github.com/carlosframework/platform/internal/hostclaim"
7 "github.com/carlosframework/platform/internal/hostwrite"
8 "github.com/carlosframework/platform/internal/moves"
9 "github.com/carlosframework/platform/internal/provision"
10 "github.com/carlosframework/platform/internal/registry"
11 "os/exec"
12 "testing"
13 "time"
14)
15
16func TestStartAdmittedPreparedInputsAndRestoreOutsideProbeWindow(t *testing.T) {
17 manifest := &moves.InstallManifest{Op: "op", Host: "x.example", SnapshotVersion: "pinned"}
18 inputs := &provision.MoveInputs{Release: "release", OverlayETag: "overlay"}
19 policy := startPolicy{MaxStarts: 5, Window: time.Millisecond, Deadline: time.Now().Add(time.Minute), Probe: true, Prepared: manifest, Inputs: inputs}
20 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"}}}
21 work, _ := (hostwrite.Config{}).Select(t.Context(), obs.Host, "spawn")
22 stop := errors.New("stop before kernel start")
23 restored, materialized, journaled := false, false, false
24 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(ctx context.Context, rt registry.Route, got hostclaim.Observed, p startPolicy) (ClaimStart, error) {
25 if p.Prepared != manifest || p.Inputs != inputs {
26 t.Fatal("C1 inputs lost before preparation")
27 }
28 return ClaimStart{RoleLauncher: &RoleLaunch{}, CurrentAttempt: func() hostclaim.WakeRetryAttempt { return hostclaim.WakeRetryAttempt{} }, Host: obs.Host, Work: work, Identity: hostwrite.Identity{Config: "c", Release: "r"},
29 OpenProbe: func(context.Context, time.Time) (time.Time, error) { journaled = true; return time.Now(), nil },
30 Restore: func(ctx context.Context) error {
31 restored = true
32 deadline, ok := ctx.Deadline()
33 if !ok || !deadline.Equal(policy.Deadline) {
34 t.Fatal("restore consumed probe window")
35 }
36 time.Sleep(5 * policy.Window)
37 return ctx.Err()
38 },
39 Materialize: func(context.Context) error { materialized = true; return stop }, Build: func() *exec.Cmd { return nil }, Ready: func(context.Context, *exec.Cmd) error { return nil },
40 Reserve: func(context.Context, int) error { return nil }, Failed: func(context.Context, int) error { 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) {},
41 }, nil
42 }}}}
43 err := a.startAdmitted(t.Context(), &actEntry{host: obs.Host}, obs, policy)
44 if !errors.Is(err, stop) || !restored || !materialized || journaled {
45 t.Fatalf("err=%v restore=%v materialize=%v journal=%v", err, restored, materialized, journaled)
46 }
47 policy.Deadline = time.Now().Add(-time.Second)
48 if err := a.startAdmitted(t.Context(), &actEntry{host: obs.Host}, obs, policy); !errors.Is(err, ErrAttemptOver) {
49 t.Fatalf("cut deadline: %v", err)
50 }
51}
52
53func TestClaimProbeWindowJournalsFirstStartAndSurvivesRetry(t *testing.T) {
54 now := time.Unix(1000, 0)
55 var saved time.Time
56 policy := startPolicy{Probe: true, Window: 30 * time.Second, Deadline: now.Add(time.Minute)}
57 journal := func(_ context.Context, proposed time.Time) (time.Time, error) {
58 if saved.IsZero() {
59 saved = proposed
60 }
61 return saved, nil
62 }
63 b := &probeWindow{policy: policy, now: func() time.Time { return now }}
64 // Restore took longer than the window, but has not started the app.
65 now = now.Add(40 * time.Second)
66 if err := b.start(t.Context(), journal); err != nil {
67 t.Fatal(err)
68 }
69 first := saved
70 now = now.Add(time.Second)
71 if err := b.start(t.Context(), journal); err != nil || saved != first {
72 t.Fatal("retry reset", err, saved)
73 }
74 b = &probeWindow{policy: policy, now: func() time.Time { return now }}
75 if err := b.start(t.Context(), journal); err != nil || b.opened != first {
76 t.Fatal("restart reset", err, b.opened)
77 }
78 now = policy.Deadline
79 if !errors.Is(b.result(nil), ErrAttemptOver) {
80 t.Fatal("late readiness accepted after cut")
81 }
82 b.policy.Deadline = time.Time{}
83 now = first.Add(policy.Window)
84 if err := b.result(nil); !errors.Is(err, ErrAttemptOver) {
85 t.Fatalf("late readiness ignored context: %v", err)
86 }
87 if err := b.start(t.Context(), journal); !errors.Is(err, ErrAttemptOver) {
88 t.Fatalf("expired retry: %v", err)
89 }
90}
91
92// Ported from the model's TestProbeWindow and wProbeWindowAt (R12-2,
93// INV-19, internal/sim/hostlife/witness_test.go): readiness k seconds after
94// the attempt's first start. Main's boundary is adopted (ruling H17): the
95// window and the cut deadline both fail at the instant, not after it
96// (claimwindow.go: !now.Before), so 29 is accepted, 30 and 31 are not.
97//
98// The loop around probeWindow is not reachable here. Its Ready hook runs
99// only after RoleLaunch.Start, which needs the isolated root, so the
100// readiness decision is tested where the loop reads it: probeWindow.start
101// before a start and probeWindow.result after readiness.
102func TestProbeWindowBoundaries(t *testing.T) {
103 t0 := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC)
104 far := t0.Add(time.Hour)
105 newWindow := func(deadline time.Time, now *time.Time) (*probeWindow, func(context.Context, time.Time) (time.Time, error), *int) {
106 var saved time.Time
107 calls := 0
108 journal := func(_ context.Context, proposed time.Time) (time.Time, error) {
109 calls++
110 if saved.IsZero() {
111 saved = proposed
112 }
113 return saved, nil
114 }
115 return &probeWindow{policy: startPolicy{MaxStarts: 5, Probe: true, Window: 30 * time.Second, Deadline: deadline}, now: func() time.Time { return *now }}, journal, &calls
116 }
117 for k, want := range map[int]bool{29: true, 30: false, 31: false} {
118 now := t0
119 b, journal, _ := newWindow(far, &now)
120 if err := b.start(t.Context(), journal); err != nil {
121 t.Fatalf("k=%d first start: %v", k, err)
122 }
123 now = t0.Add(time.Duration(k) * time.Second)
124 err := b.result(nil)
125 if (err == nil) != want || (err != nil && !errors.Is(err, ErrAttemptOver)) {
126 t.Fatalf("k=%d: readiness result %v, accepted want %v", k, err, want)
127 }
128 }
129 // The environment becomes ready only after the window: no further start
130 // is journaled, and the first start's time is not reset.
131 now := t0
132 b, journal, calls := newWindow(far, &now)
133 if err := b.start(t.Context(), journal); err != nil || *calls != 1 {
134 t.Fatalf("first start: %v (journal calls %d)", err, *calls)
135 }
136 now = t0.Add(30 * time.Second)
137 if err := b.start(t.Context(), journal); !errors.Is(err, ErrAttemptOver) || *calls != 1 {
138 t.Fatalf("start after the window: %v (journal calls %d)", err, *calls)
139 }
140 // The absolute cut deadline bounds the whole, inside the window.
141 now = t0
142 c, journal, _ := newWindow(t0.Add(4*time.Second), &now)
143 if err := c.start(t.Context(), journal); err != nil {
144 t.Fatal(err)
145 }
146 now = t0.Add(3 * time.Second)
147 if err := c.result(nil); err != nil {
148 t.Fatalf("readiness before the cut deadline: %v", err)
149 }
150 now = t0.Add(4 * time.Second)
151 if err := c.result(nil); !errors.Is(err, ErrAttemptOver) {
152 t.Fatalf("readiness at the cut deadline: %v", err)
153 }
154 now = t0.Add(5 * time.Second)
155 if err := c.start(t.Context(), journal); !errors.Is(err, ErrAttemptOver) {
156 t.Fatalf("start past the cut deadline: %v", err)
157 }
158 // A readiness already accepted is not revisited by a later clock.
159 now = t0
160 d, journal, _ := newWindow(far, &now)
161 if err := d.start(t.Context(), journal); err != nil {
162 t.Fatal(err)
163 }
164 d.ready = true
165 now = t0.Add(time.Hour)
166 if err := d.result(nil); err != nil {
167 t.Fatalf("accepted readiness revisited: %v", err)
168 }
169 // Five starts at most, whatever the clock says.
170 a := &Service{cfg: Config{HostWrites: hostwrite.Config{Enabled: true}, Claims: &ClaimRuntime{Box: "box", Read: func(context.Context, string) (hostclaim.Observed, error) { return hostclaim.Observed{}, nil }, Prepare: func(context.Context, registry.Route, hostclaim.Observed, startPolicy) (ClaimStart, error) {
171 t.Fatal("six starts were allowed to prepare")
172 return ClaimStart{}, nil
173 }}}}
174 obs := hostclaim.Observed{Host: "x.example"}
175 if err := a.startAdmitted(t.Context(), &actEntry{host: obs.Host}, obs, startPolicy{MaxStarts: 6, Window: 30 * time.Second, Probe: true}); !errors.Is(err, hostwrite.ErrUnavailable) {
176 t.Fatalf("a sixth start: %v", err)
177 }
178}
179