carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package activator
4
5import (
6 "context"
7 "errors"
8 "os/exec"
9 "sync"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/hostclaim"
14 "github.com/carlosframework/platform/internal/hostwrite"
15 "github.com/carlosframework/platform/internal/registry"
16)
17
18// The start loop of startAdmitted, driven for real (ruling H17, fix round 1).
19//
20// What can and cannot be driven. RoleLaunch.Start (rolelaunch_linux.go)
21// returns ErrClaimIsolationUnavailable unconditionally, until claim Plan 9
22// Group D replaces the by-name refusal with the isolated-root launcher. It
23// is a value method on a concrete type, with no seam, so nothing a test can
24// supply runs inside it. Two of the loop's hooks run only from inside Start
25// or after it: beforeStart (which journals the attempt's first start through
26// OpenProbe and so opens the 30 s window) and Ready. Hence the window can
27// not be opened, and readiness can not be offered, through this entry point
28// today. The loop itself, ahead of and around Start, can be driven: the
29// refusal is an ordinary failed start, and these tests use it as one.
30// OpenProbe and Ready are tripwires: the day Start stops refusing they fail,
31// and the skipped test below is to be written then.
32
33type loopRun struct {
34 mu sync.Mutex
35 reserved []time.Time
36 failed int
37}
38
39func (r *loopRun) starts() []time.Time {
40 r.mu.Lock()
41 defer r.mu.Unlock()
42 return append([]time.Time(nil), r.reserved...)
43}
44
45func driveLoop(t *testing.T, policy startPolicy) (*loopRun, error) {
46 t.Helper()
47 run := &loopRun{}
48 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"}}}
49 work, _ := (hostwrite.Config{}).Select(t.Context(), obs.Host, "spawn")
50 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) {
51 return ClaimStart{RoleLauncher: &RoleLaunch{}, CurrentAttempt: func() hostclaim.WakeRetryAttempt { return hostclaim.WakeRetryAttempt{} }, Host: obs.Host, Work: work, Identity: hostwrite.Identity{Config: "c", Release: "r"},
52 OpenProbe: func(context.Context, time.Time) (time.Time, error) {
53 t.Fatal("OpenProbe ran: Start no longer refuses, replace the skipped window test")
54 return time.Time{}, nil
55 },
56 Restore: func(context.Context) error { return nil },
57 Materialize: func(context.Context) error { return nil },
58 Build: func() *exec.Cmd { return nil },
59 Ready: func(context.Context, *exec.Cmd) error {
60 t.Fatal("Ready ran: Start no longer refuses, replace the skipped window test")
61 return nil
62 },
63 Reserve: func(context.Context, int) error {
64 run.mu.Lock()
65 run.reserved = append(run.reserved, time.Now())
66 run.mu.Unlock()
67 return nil
68 },
69 Failed: func(context.Context, int) error { run.mu.Lock(); run.failed++; run.mu.Unlock(); return nil },
70 Healthy: func(context.Context, int, *exec.Cmd) error { return nil },
71 Replicate: func(context.Context, string) (Rep, error) { return nil, nil },
72 Publish: func(context.Context, hostclaim.Observed) (hostclaim.Observed, error) { return obs, nil },
73 Retain: func(Proc, Rep, error) {},
74 }, nil
75 }}}}
76 return run, a.startAdmitted(t.Context(), &actEntry{host: obs.Host}, obs, policy)
77}
78
79// Five starts, then the loop gives up, with the delay between starts
80// doubling: 1, 2, 4 and 8 seconds. The loop's backoff is time.NewTimer
81// inline (claimwake.go), with no clock or sleeper to substitute, so this
82// reads real time. It runs on a quarter of the production base
83// (claimStartBackoff), so 0.25, 0.5, 1 and 2 s, each delay checked to
84// within 200 ms, which keeps every step apart from its neighbours: the
85// doubling is what is proven, on 3.75 s of waiting rather than 15.
86func TestStartAdmittedLoopStopsAfterFiveStartsWithDoublingBackoff(t *testing.T) {
87 if claimStartBackoff != time.Second {
88 t.Fatalf("claimStartBackoff = %v: the production schedule is 1, 2, 4, 8 s", claimStartBackoff)
89 }
90 base := 250 * time.Millisecond
91 shrinkClaimStartBackoff(t, base)
92 run, err := driveLoop(t, startPolicy{MaxStarts: 5, Window: 30 * time.Second, Deadline: time.Now().Add(time.Hour), Probe: true})
93 if !errors.Is(err, ErrClaimIsolationUnavailable) || errors.Is(err, ErrAttemptOver) {
94 t.Fatalf("the last start's refusal should end the loop: %v", err)
95 }
96 at := run.starts()
97 if len(at) != 5 || run.failed != 5 {
98 t.Fatalf("starts=%d failed=%d, want 5 and 5", len(at), run.failed)
99 }
100 for i, want := range []time.Duration{base, 2 * base, 4 * base, 8 * base} {
101 if gap := at[i+1].Sub(at[i]); gap < want || gap > want+200*time.Millisecond {
102 t.Errorf("delay before start %d = %v, want %v", i+2, gap, want)
103 }
104 }
105}
106
107// The cut deadline ends the attempt while it waits between starts: one
108// start, then ErrAttemptOver at the deadline, not after the 1 s backoff.
109func TestStartAdmittedLoopCutDeadlineEndsTheBackoffWait(t *testing.T) {
110 deadline := time.Now().Add(300 * time.Millisecond)
111 run, err := driveLoop(t, startPolicy{MaxStarts: 5, Window: 30 * time.Second, Deadline: deadline, Probe: true})
112 if !errors.Is(err, ErrAttemptOver) {
113 t.Fatalf("err=%v", err)
114 }
115 if n := len(run.starts()); n != 1 {
116 t.Fatalf("starts=%d, want 1", n)
117 }
118 if late := time.Since(deadline); late > 500*time.Millisecond {
119 t.Fatalf("the attempt ended %v after the deadline", late)
120 }
121}
122
123// A deadline already passed ends the attempt before any start: Reserve is
124// never reached.
125func TestStartAdmittedLoopNoStartPastTheCutDeadline(t *testing.T) {
126 run, err := driveLoop(t, startPolicy{MaxStarts: 5, Window: 30 * time.Second, Deadline: time.Now().Add(-time.Second), Probe: true})
127 if !errors.Is(err, ErrAttemptOver) || len(run.starts()) != 0 {
128 t.Fatalf("err=%v starts=%d", err, len(run.starts()))
129 }
130}
131
132func TestStartAdmittedLoopWindowAndReadinessAtTheBoundary(t *testing.T) {
133 t.Skip("blocked on claim Plan 9 Group D: RoleLaunch.Start (rolelaunch_linux.go) returns ErrClaimIsolationUnavailable " +
134 "unconditionally, so beforeStart/OpenProbe (which opens the 30 s window) and Ready never run through startAdmitted. " +
135 "When Group D replaces that refusal, drive: window expiry before a start (OpenProbe returns a time older than the window), " +
136 "readiness at opened+Window and at the cut deadline rejected with ErrAttemptOver, readiness at opened+Window-1s accepted.")
137}
138