carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/hostclaim"
11 "github.com/carlosframework/platform/internal/hostops"
12 "github.com/carlosframework/platform/internal/provision"
13 "github.com/carlosframework/platform/internal/release"
14)
15
16// J17 rev2: the typed door binds the fresh observation to the move and record
17// its operator gate read. A store wrapper runs a replacement between the two
18// reads, on the real door.
19
20// j17RaceStore runs hook once, just before the door's first read of the
21// host claim (the fresh ObserveConsoleMove, after the gate's record read).
22type j17RaceStore struct {
23 release.Store
24 key string
25 hook func()
26}
27
28func (s *j17RaceStore) Get(ctx context.Context, key string) ([]byte, string, error) {
29 if key == s.key && s.hook != nil {
30 h := s.hook
31 s.hook = nil
32 h()
33 }
34 return s.Store.Get(ctx, key)
35}
36
37func j17Arm(fx *consoleFixture, hook func()) {
38 fx.srv.cfg.Releases = &j17RaceStore{Store: fx.srv.cfg.Releases, key: "control/hosts/acme.go.tito.io.json", hook: hook}
39}
40
41// j17Replace swaps the claim's op, the record's move and the registration to
42// a successor move id, as a finished move and a new request would.
43func j17Replace(t *testing.T, fx *consoleFixture, oldID string) {
44 t.Helper()
45 ctx := context.Background()
46 id := "mv_abcdefghijklmnopqrstuvwxyz"
47 if id == oldID {
48 id = "mv_zyxwvutsrqponmlkjihgfedcba"
49 }
50 obs, err := hostclaim.Read(ctx, fx.rel, "acme.go.tito.io")
51 if err != nil {
52 t.Fatal(err)
53 }
54 if _, err := hostclaim.Update(ctx, fx.rel, obs, func(c *hostclaim.Claim) error {
55 c.Op.ID, c.Op.Intent["admission_id"] = id, id
56 return nil
57 }); err != nil {
58 t.Fatal(err)
59 }
60 j17SetRecord(t, fx, func(r *provision.Record) { r.Move.ID = id })
61 a, err := hostops.ReadAdmission(ctx, fx.rel, "bab", "titogo", claimMoveAppID)
62 if err != nil {
63 t.Fatal(err)
64 }
65 a.Value.Registrations[id] = a.Value.Registrations[oldID]
66 b, _ := json.Marshal(a.Value)
67 if _, err := fx.rel.Put(ctx, a.Key, b, a.ETag); err != nil {
68 t.Fatal(err)
69 }
70}
71
72func j17RecordBytes(t *testing.T, fx *consoleFixture) string {
73 t.Helper()
74 b, _, err := fx.rel.Get(context.Background(), provision.RecordKey("bab", "titogo", "acme.go.tito.io"))
75 if err != nil {
76 t.Fatal(err)
77 }
78 return string(b)
79}
80
81func j17Serial(t *testing.T, fx *consoleFixture) int64 {
82 t.Helper()
83 d, _, err := release.ReadSerial(context.Background(), fx.rel)
84 if err != nil {
85 t.Fatal(err)
86 }
87 return d.Serial
88}
89
90// j17Raced fires the request with the hook between the gate and the
91// observation and requires the identity 409 with nothing written.
92func j17Raced(t *testing.T, fx *consoleFixture, method, url, token, body string, hook func()) {
93 t.Helper()
94 var claim, rec string
95 j17Arm(fx, func() {
96 hook()
97 claim, rec = claimBytes(t, fx, "acme.go.tito.io"), j17RecordBytes(t, fx)
98 })
99 serial := j17Serial(t, fx)
100 code, _, out := rawReq(t, method, url, token, body)
101 if claim == "" {
102 t.Fatal("the race hook never ran")
103 }
104 if code != http.StatusConflict || errorText(out) != moveChangedSentence {
105 t.Fatalf("raced door = %d %s", code, out)
106 }
107 if claimBytes(t, fx, "acme.go.tito.io") != claim {
108 t.Fatal("the raced door wrote the claim")
109 }
110 if j17RecordBytes(t, fx) != rec {
111 t.Fatal("the raced door mutated the successor record")
112 }
113 if j17Serial(t, fx) != serial {
114 t.Fatal("the raced door bumped the serial")
115 }
116}
117
118func j17RetryReady(t *testing.T, fx *consoleFixture) {
119 t.Helper()
120 ctx := context.Background()
121 obs, err := hostclaim.Read(ctx, fx.rel, "acme.go.tito.io")
122 if err != nil {
123 t.Fatal(err)
124 }
125 rb := "rb_abcdefghijklmnopqrstuvwxyz"
126 if _, err := hostclaim.Update(ctx, fx.rel, obs, func(c *hostclaim.Claim) error {
127 c.Op.Kind, c.Op.Phase = hostclaim.KindRestoring, hostclaim.PhaseProbing
128 c.Op.Intent["rollback"] = rb
129 return nil
130 }); err != nil {
131 t.Fatal(err)
132 }
133 j17SetRecord(t, fx, func(r *provision.Record) {
134 r.Move.State, r.Move.StuckReason = provision.MoveStuck, provision.StuckRecoveryProbeAttempts
135 r.Move.Rollback = &provision.MoveRollback{ID: rb, TargetPhase: provision.RbTargetReleased, SourcePhase: provision.RbSourceProbing, StuckAt: provision.RbSourceProbing}
136 })
137}
138
139func TestJ17DoorCutRefusesReplacedMove(t *testing.T) {
140 fx, token, moveURL, id := j17StartedMove(t, "code-j17-r2-cut")
141 j17SetRecord(t, fx, j17Ready)
142 j17Raced(t, fx, http.MethodPost, moveURL+"/cut", token, "", func() { j17Replace(t, fx, id) })
143}
144
145func TestJ17DoorAbortRefusesReplacedMove(t *testing.T) {
146 fx, token, moveURL, id := j17StartedMove(t, "code-j17-r2-abort")
147 j17Raced(t, fx, http.MethodDelete, moveURL, token, "", func() { j17Replace(t, fx, id) })
148}
149
150func TestJ17DoorRetryRefusesReplacedMove(t *testing.T) {
151 fx, token, moveURL, id := j17StartedMove(t, "code-j17-r2-retry")
152 j17RetryReady(t, fx)
153 j17Raced(t, fx, http.MethodPost, moveURL+"/resolve", token, `{"retry":true}`, func() { j17Replace(t, fx, id) })
154}
155
156// The same move and incarnation, the record advanced: the fresh observation
157// carries a newer record_seq than the one the gate read.
158func TestJ17DoorRefusesAdvancedRecordSeq(t *testing.T) {
159 advance := func(t *testing.T, fx *consoleFixture) func() {
160 return func() {
161 before := j17RecordBytes(t, fx)
162 j17SetRecord(t, fx, func(r *provision.Record) {})
163 if j17RecordBytes(t, fx) == before {
164 t.Fatal("the record did not advance")
165 }
166 }
167 }
168 t.Run("cut", func(t *testing.T) {
169 fx, token, moveURL, _ := j17StartedMove(t, "code-j17-r2-seq-cut")
170 j17SetRecord(t, fx, j17Ready)
171 j17Raced(t, fx, http.MethodPost, moveURL+"/cut", token, "", advance(t, fx))
172 })
173 t.Run("abort", func(t *testing.T) {
174 fx, token, moveURL, _ := j17StartedMove(t, "code-j17-r2-seq-abort")
175 j17Raced(t, fx, http.MethodDelete, moveURL, token, "", advance(t, fx))
176 })
177 t.Run("retry", func(t *testing.T) {
178 fx, token, moveURL, _ := j17StartedMove(t, "code-j17-r2-seq-retry")
179 j17RetryReady(t, fx)
180 j17Raced(t, fx, http.MethodPost, moveURL+"/resolve", token, `{"retry":true}`, advance(t, fx))
181 })
182}
183
184func TestJ17DoorRefusesReplacedIncarnation(t *testing.T) {
185 for _, action := range []string{"cut", "abort", "retry"} {
186 t.Run(action, func(t *testing.T) {
187 fx, token, url, id := j17StartedMove(t, "code-j17-r2-inc-"+action)
188 method, body := http.MethodDelete, ""
189 switch action {
190 case "cut":
191 j17SetRecord(t, fx, j17Ready)
192 method, url = http.MethodPost, url+"/cut"
193 case "retry":
194 j17RetryReady(t, fx)
195 method, url, body = http.MethodPost, url+"/resolve", `{"retry":true}`
196 }
197 j17Raced(t, fx, method, url, token, body, func() {
198 ctx := context.Background()
199 r, tag, err := provision.GetRecordETag(ctx, fx.rel, "bab", "titogo", "acme.go.tito.io")
200 if err != nil {
201 t.Fatal(err)
202 }
203 r.CreatedAt = r.CreatedAt.Add(time.Second)
204 inc, err := hostclaim.Incarnation(r.Account, r.App, r.Host, r.CreatedAt)
205 if err != nil {
206 t.Fatal(err)
207 }
208 r.Move.Incarnation = inc
209 b, _ := json.Marshal(r)
210 if _, err := fx.rel.Put(ctx, provision.RecordKey(r.Account, r.App, r.Host), b, tag); err != nil {
211 t.Fatal(err)
212 }
213 o, err := hostclaim.Read(ctx, fx.rel, r.Host)
214 if err != nil {
215 t.Fatal(err)
216 }
217 if _, err := hostclaim.Update(ctx, fx.rel, o, func(c *hostclaim.Claim) error {
218 c.Holder.Incarnation = inc
219 c.Holder.CreatedAt = r.CreatedAt
220 return nil
221 }); err != nil {
222 t.Fatal(err)
223 }
224 a, err := hostops.ReadAdmission(ctx, fx.rel, "bab", "titogo", claimMoveAppID)
225 if err != nil {
226 t.Fatal(err)
227 }
228 reg := a.Value.Registrations[id]
229 reg.Incarnation = inc
230 a.Value.Registrations[id] = reg
231 b, _ = json.Marshal(a.Value)
232 if _, err := fx.rel.Put(ctx, a.Key, b, a.ETag); err != nil {
233 t.Fatal(err)
234 }
235 })
236 })
237 }
238}
239