carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package registry
2
3import (
4 "errors"
5 "path/filepath"
6 "testing"
7 "time"
8)
9
10const (
11 holdOpA = "mv_abcdefghijklmnopqrstuvwxyz"
12 holdOpB = "mv_other0000000000000000000000"
13)
14
15func holdFixture(t *testing.T, host string) *Registry {
16 t.Helper()
17 r := openTestRegistry(t)
18 if err := r.Upsert(Route{Host: host, Kind: "instance", Socket: "/s", App: "a", Hibernate: true}); err != nil {
19 t.Fatal(err)
20 }
21 return r
22}
23
24func TestHoldIsConditional(t *testing.T) {
25 r := holdFixture(t, "h")
26 if err := r.SetHold("h", "moving", holdOpA); err != nil {
27 t.Fatal(err)
28 }
29 rt, _ := r.Get("h")
30 if rt.Status != "moving" || rt.HoldOp != holdOpA || !IsHold(rt.Status) {
31 t.Fatalf("held row: %+v", rt)
32 }
33 if err := r.SetHold("h", "ready", "x"); err == nil {
34 t.Fatal("ready is not a hold")
35 }
36 // §5.2: a late write from a finished operation matches nothing.
37 if changed, err := r.ReleaseHold("h", holdOpB); err != nil || changed {
38 t.Fatalf("release under another op: %v %v", changed, err)
39 }
40 if changed, err := r.ReleaseHold("h", holdOpA); err != nil || !changed {
41 t.Fatalf("release: %v %v", changed, err)
42 }
43 rt, _ = r.Get("h")
44 if rt.Status != "ready" || rt.HoldOp != "" {
45 t.Fatalf("after release: %+v", rt)
46 }
47 // Upsert preserves hold_op through routeCols.
48 if err := r.SetHold("h", "landing", holdOpA); err != nil {
49 t.Fatal(err)
50 }
51 rt, _ = r.Get("h")
52 if err := r.Upsert(*rt); err != nil {
53 t.Fatal(err)
54 }
55 rt, _ = r.Get("h")
56 if rt.HoldOp == "" {
57 t.Fatal("Upsert dropped hold_op")
58 }
59}
60
61func TestSetHoldRefusals(t *testing.T) {
62 r := holdFixture(t, "h")
63 for _, status := range []string{"moving", "landing", "restoring", "releasing"} {
64 if err := r.SetHold("h", status, holdOpA); err != nil {
65 t.Fatalf("%s: %v", status, err)
66 }
67 if rt, _ := r.Get("h"); rt.Status != status || rt.HoldOp != holdOpA {
68 t.Fatalf("%s: %+v", status, rt)
69 }
70 }
71 for _, status := range []string{"", "ready", "pending", "failed", "Moving"} {
72 if err := r.SetHold("h", status, holdOpA); err == nil {
73 t.Fatalf("status %q accepted as a hold", status)
74 }
75 }
76 if err := r.SetHold("h", "moving", ""); err == nil {
77 t.Fatal("a hold with no op was accepted")
78 }
79 if err := r.SetHold("nope", "moving", holdOpA); err == nil {
80 t.Fatal("SetHold on an unknown host did not error")
81 }
82 if IsHold("ready") || !IsHold("releasing") {
83 t.Fatal("IsHold disagrees with HoldStatuses")
84 }
85}
86
87// An Upsert from a writer that knows nothing of holds (carlos add, the
88// reconciler's mint paths) must not release one: the incoming row has an empty
89// hold_op and a ready status, and the held row keeps both (F29).
90func TestUpsertDoesNotClearHold(t *testing.T) {
91 r := holdFixture(t, "h")
92 if err := r.SetHold("h", "moving", holdOpA); err != nil {
93 t.Fatal(err)
94 }
95 if err := r.Upsert(Route{Host: "h", Kind: "instance", Socket: "/s2", App: "a", Hibernate: true}); err != nil {
96 t.Fatal(err)
97 }
98 rt, _ := r.Get("h")
99 if rt.Status != "moving" || rt.HoldOp != holdOpA {
100 t.Fatalf("Upsert released the hold: %+v", rt)
101 }
102 if rt.Socket != "/s2" {
103 t.Fatalf("the rest of the row did not update: %+v", rt)
104 }
105 // Only ReleaseHold ends it, after which Upsert is an ordinary write again.
106 if ok, err := r.ReleaseHold("h", holdOpA); err != nil || !ok {
107 t.Fatalf("release: %v %v", ok, err)
108 }
109 if err := r.Upsert(Route{Host: "h", Kind: "instance", Socket: "/s3", Status: "starting"}); err != nil {
110 t.Fatal(err)
111 }
112 if rt, _ = r.Get("h"); rt.Status != "starting" || rt.HoldOp != "" {
113 t.Fatalf("Upsert on an unheld row: %+v", rt)
114 }
115}
116
117func TestHoldRows(t *testing.T) {
118 r := openTestRegistry(t)
119 if rows, err := r.HoldRows(); err != nil || len(rows) != 0 {
120 t.Fatalf("empty registry: %v %v", rows, err)
121 }
122 for _, h := range []string{"a", "b", "c"} {
123 if err := r.Upsert(Route{Host: h, Kind: "instance", Socket: "/s"}); err != nil {
124 t.Fatal(err)
125 }
126 }
127 if err := r.SetHold("c", "restoring", holdOpA); err != nil {
128 t.Fatal(err)
129 }
130 if err := r.SetHold("a", "landing", holdOpB); err != nil {
131 t.Fatal(err)
132 }
133 rows, err := r.HoldRows()
134 if err != nil {
135 t.Fatal(err)
136 }
137 if len(rows) != 2 || rows[0].Host != "a" || rows[0].HoldOp != holdOpB || rows[0].Status != "landing" ||
138 rows[1].Host != "c" || rows[1].HoldOp != holdOpA || rows[1].Status != "restoring" {
139 t.Fatalf("HoldRows = %+v", rows)
140 }
141 // A bare "moving" status written by SetStatus has no op, so it is not a
142 // held row in this sense.
143 if err := r.SetStatus("b", "moving"); err != nil {
144 t.Fatal(err)
145 }
146 if rows, _ = r.HoldRows(); len(rows) != 2 {
147 t.Fatalf("a status without an op counted as a hold: %+v", rows)
148 }
149}
150
151func TestDeleteIfHold(t *testing.T) {
152 r := holdFixture(t, "h")
153 // Not held: nothing is deleted.
154 if ok, err := r.DeleteIfHold("h", holdOpA); err != nil || ok {
155 t.Fatalf("unheld row: %v %v", ok, err)
156 }
157 if err := r.SetHold("h", "releasing", holdOpA); err != nil {
158 t.Fatal(err)
159 }
160 // Held by another op: nothing is deleted.
161 if ok, err := r.DeleteIfHold("h", holdOpB); err != nil || ok {
162 t.Fatalf("row held by another op: %v %v", ok, err)
163 }
164 // An empty op must not match unheld rows' empty hold_op.
165 if err := r.Upsert(Route{Host: "plain", Kind: "instance", Socket: "/s"}); err != nil {
166 t.Fatal(err)
167 }
168 if ok, err := r.DeleteIfHold("plain", ""); err != nil || ok {
169 t.Fatalf("empty op deleted an unheld row: %v %v", ok, err)
170 }
171 if rt, _ := r.Get("plain"); rt == nil {
172 t.Fatal("unheld row was deleted")
173 }
174 if rt, _ := r.Get("h"); rt == nil || rt.HoldOp != holdOpA {
175 t.Fatalf("held row changed by the refusals: %+v", rt)
176 }
177 if ok, err := r.DeleteIfHold("h", holdOpA); err != nil || !ok {
178 t.Fatalf("delete under the holding op: %v %v", ok, err)
179 }
180 if rt, _ := r.Get("h"); rt != nil {
181 t.Fatalf("row survived: %+v", rt)
182 }
183 if ok, err := r.DeleteIfHold("h", holdOpA); err != nil || ok {
184 t.Fatalf("second delete: %v %v", ok, err)
185 }
186}
187
188// The source's finalization (moves spec §7.9 step 3, ruling H5(d)): the row
189// and the host's timer rows go in one transaction, and only while the op
190// holds the row. Any refusal leaves both, and the neighbour's timers stay.
191func TestDeleteIfHoldWithTimers(t *testing.T) {
192 r := holdFixture(t, "h")
193 if err := r.Upsert(Route{Host: "n", Kind: "instance", Socket: "/n", App: "a", Hibernate: true}); err != nil {
194 t.Fatal(err)
195 }
196 for _, tm := range []Timer{{Host: "h", Name: "nightly", Kind: "cron", Spec: "0 3 * * *", NextAt: 1000},
197 {Host: "h", Name: "hourly", Kind: "cron", Spec: "0 * * * *", NextAt: 2000},
198 {Host: "n", Name: "nightly", Kind: "cron", Spec: "0 3 * * *", NextAt: 1000}} {
199 if err := r.UpsertTimer(tm); err != nil {
200 t.Fatal(err)
201 }
202 }
203 timers := func(host string) int {
204 t.Helper()
205 got, err := r.TimersFor(host)
206 if err != nil {
207 t.Fatal(err)
208 }
209 return len(got)
210 }
211 // Not held, held by another op, an empty op: nothing goes.
212 for _, c := range []struct{ name, hold, op string }{{"unheld", "", holdOpA}, {"another op", holdOpB, holdOpA}, {"empty op", holdOpA, ""}} {
213 if c.hold != "" {
214 if err := r.SetHold("h", "moving", c.hold); err != nil {
215 t.Fatal(err)
216 }
217 }
218 if ok, err := r.DeleteIfHoldWithTimers("h", c.op); err != nil || ok {
219 t.Fatalf("%s: %v %v", c.name, ok, err)
220 }
221 if rt, _ := r.Get("h"); rt == nil || timers("h") != 2 {
222 t.Fatalf("%s: the row or its timers changed: %+v, %d timers", c.name, rt, timers("h"))
223 }
224 if c.hold != "" {
225 if ok, err := r.ReleaseHold("h", c.hold); err != nil || !ok {
226 t.Fatal(ok, err)
227 }
228 }
229 }
230 if err := r.SetHold("h", "moving", holdOpA); err != nil {
231 t.Fatal(err)
232 }
233 if ok, err := r.DeleteIfHoldWithTimers("h", holdOpA); err != nil || !ok {
234 t.Fatalf("delete under the holding op: %v %v", ok, err)
235 }
236 if rt, _ := r.Get("h"); rt != nil || timers("h") != 0 {
237 t.Fatalf("after the delete: %+v, %d timers", rt, timers("h"))
238 }
239 if rt, _ := r.Get("n"); rt == nil || timers("n") != 1 {
240 t.Fatalf("the neighbour changed: %+v, %d timers", rt, timers("n"))
241 }
242 // A replay finds no row held by the op and deletes nothing, timers
243 // written since included.
244 if err := r.UpsertTimer(Timer{Host: "h", Name: "late", Kind: "cron", Spec: "0 3 * * *", NextAt: 1000}); err != nil {
245 t.Fatal(err)
246 }
247 if ok, err := r.DeleteIfHoldWithTimers("h", holdOpA); err != nil || ok || timers("h") != 1 {
248 t.Fatalf("replay: %v %v, %d timers", ok, err, timers("h"))
249 }
250}
251
252func TestReleaseHoldEmptyOpMatchesNothing(t *testing.T) {
253 r := holdFixture(t, "h")
254 if ok, err := r.ReleaseHold("h", ""); err != nil || ok {
255 t.Fatalf("release with empty op on an unheld row: %v %v", ok, err)
256 }
257 if err := r.SetStatus("h", "starting"); err != nil {
258 t.Fatal(err)
259 }
260 if ok, _ := r.ReleaseHold("h", ""); ok {
261 t.Fatal("an empty op released an unheld row and reset its status")
262 }
263 if rt, _ := r.Get("h"); rt.Status != "starting" {
264 t.Fatalf("status moved: %+v", rt)
265 }
266}
267
268// The migration is additive and idempotent: opening a registry that predates
269// the column adds it, and opening again is a no-op.
270func TestHoldOpMigrationIdempotentOnLegacyDB(t *testing.T) {
271 path := filepath.Join(t.TempDir(), "registry.db")
272 r, err := Open(path)
273 if err != nil {
274 t.Fatal(err)
275 }
276 if err := r.Upsert(Route{Host: "h", Kind: "instance", Socket: "/s"}); err != nil {
277 t.Fatal(err)
278 }
279 // Strip the column to look like a pre-hold registry.
280 if _, err := r.sql.Exec(`ALTER TABLE routes DROP COLUMN hold_op`); err != nil {
281 t.Fatal(err)
282 }
283 r.Close()
284 for i := 0; i < 2; i++ {
285 r, err = Open(path)
286 if err != nil {
287 t.Fatalf("open %d: %v", i, err)
288 }
289 rt, err := r.Get("h")
290 if err != nil || rt == nil || rt.HoldOp != "" || rt.Status != "ready" {
291 t.Fatalf("open %d: %+v %v", i, rt, err)
292 }
293 r.Close()
294 }
295}
296
297// Fix round 1: on conflict Upsert never changes hold_op, and a held row keeps
298// its status whatever the incoming row carries. Only SetHold, ReleaseHold and
299// DeleteIfHold change a hold.
300func TestUpsertStaleOpCannotTakeAHoldBack(t *testing.T) {
301 r := holdFixture(t, "h")
302 if err := r.SetHold("h", "moving", holdOpA); err != nil {
303 t.Fatal(err)
304 }
305 stale, _ := r.Get("h") // a caller reads op A's row ...
306 if ok, err := r.ReleaseHold("h", holdOpA); err != nil || !ok {
307 t.Fatal(ok, err)
308 }
309 if err := r.SetHold("h", "landing", holdOpB); err != nil { // ... B takes the row ...
310 t.Fatal(err)
311 }
312 if err := r.Upsert(*stale); err != nil { // ... and the stale write arrives.
313 t.Fatal(err)
314 }
315 rt, _ := r.Get("h")
316 if rt.HoldOp != holdOpB || rt.Status != "landing" {
317 t.Fatalf("stale Upsert changed B's hold: %+v", rt)
318 }
319 if ok, err := r.ReleaseHold("h", holdOpA); err != nil || ok {
320 t.Fatalf("ReleaseHold under the stale op: %v %v", ok, err)
321 }
322 if rt, _ = r.Get("h"); rt.HoldOp != holdOpB {
323 t.Fatalf("hold lost: %+v", rt)
324 }
325}
326
327func TestUpsertSameOpWithReadyStatusKeepsHeldStatus(t *testing.T) {
328 r := holdFixture(t, "h")
329 if err := r.SetHold("h", "restoring", holdOpA); err != nil {
330 t.Fatal(err)
331 }
332 for _, status := range []string{"ready", "failed", "", "landing"} {
333 if err := r.Upsert(Route{Host: "h", Kind: "instance", Socket: "/s", Status: status, HoldOp: holdOpA}); err != nil {
334 t.Fatal(err)
335 }
336 rt, _ := r.Get("h")
337 if rt.Status != "restoring" || rt.HoldOp != holdOpA {
338 t.Fatalf("incoming status %q: %+v", status, rt)
339 }
340 }
341}
342
343func TestUpsertCannotHoldAnUnheldRowOnConflict(t *testing.T) {
344 r := holdFixture(t, "h")
345 if err := r.Upsert(Route{Host: "h", Kind: "instance", Socket: "/s", Status: "moving", HoldOp: holdOpA}); err != nil {
346 t.Fatal(err)
347 }
348 rt, _ := r.Get("h")
349 if rt.HoldOp != "" {
350 t.Fatalf("Upsert took a hold on conflict: %+v", rt)
351 }
352 if rows, _ := r.HoldRows(); len(rows) != 0 {
353 t.Fatalf("HoldRows = %+v", rows)
354 }
355 if err := r.SetHold("h", "moving", holdOpA); err != nil {
356 t.Fatal(err)
357 }
358}
359
360// A held row can be materialised: a fresh INSERT writes the incoming hold.
361func TestUpsertInsertMaterialisesAHeldRow(t *testing.T) {
362 r := openTestRegistry(t)
363 if err := r.Upsert(Route{Host: "new", Kind: "instance", Socket: "/s", Status: "landing", HoldOp: holdOpA}); err != nil {
364 t.Fatal(err)
365 }
366 rt, _ := r.Get("new")
367 if rt.Status != "landing" || rt.HoldOp != holdOpA {
368 t.Fatalf("%+v", rt)
369 }
370}
371
372// The edge reloads only when Fingerprint moves: a hold handed from one op to
373// another with the status unchanged must move it, and an unheld registry must
374// hash exactly as it did before the column existed.
375func TestFingerprintSeesHoldOpChangeAndIgnoresUnheld(t *testing.T) {
376 r := holdFixture(t, "h")
377 fp := func() (string, string) {
378 a, err := r.Fingerprint()
379 if err != nil {
380 t.Fatal(err)
381 }
382 b, err := r.SnapshotFingerprint()
383 if err != nil {
384 t.Fatal(err)
385 }
386 return a, b
387 }
388 if err := r.SetHold("h", "moving", holdOpA); err != nil {
389 t.Fatal(err)
390 }
391 f1, s1 := fp()
392 if ok, _ := r.ReleaseHold("h", holdOpA); !ok {
393 t.Fatal("release")
394 }
395 if err := r.SetHold("h", "moving", holdOpB); err != nil { // same status, other op
396 t.Fatal(err)
397 }
398 f2, s2 := fp()
399 if f1 == f2 {
400 t.Fatal("Fingerprint did not move when only hold_op changed")
401 }
402 if s1 == s2 {
403 t.Fatal("SnapshotFingerprint did not move when only hold_op changed")
404 }
405 // Direct column change with the status untouched.
406 if _, err := r.sql.Exec(`UPDATE routes SET hold_op = ? WHERE host = 'h'`, holdOpA); err != nil {
407 t.Fatal(err)
408 }
409 if f3, s3 := fp(); f3 == f2 || s3 == s2 {
410 t.Fatal("a bare hold_op change did not move both fingerprints")
411 }
412
413}
414
415// Switch-off contract: an unheld registry hashes byte-for-byte as it did
416// before hold_op existed. The goldens were produced by the same two rows on
417// the parent commit's registry.go (8437004c^).
418func TestFingerprintsUnchangedForUnheldRows(t *testing.T) {
419 u := openTestRegistry(t)
420 if err := u.Upsert(Route{Host: "u", Kind: "instance", Socket: "/s", App: "a", CreatedAt: 1700000000}); err != nil {
421 t.Fatal(err)
422 }
423 if err := u.Upsert(Route{Host: "v", Kind: "static", Status: "starting", Hibernate: true, CreatedAt: 1700000001}); err != nil {
424 t.Fatal(err)
425 }
426 const wantF = "2:f1013971d496e9401cd6a9928a1f4e18669deda40089d2c2a8b84263b32f34c6"
427 const wantS = "2:149e8f668771c7493bdb7d4245fdabe03e957b0909c4daa1d037484b11064b80"
428 if got, _ := u.Fingerprint(); got != wantF {
429 t.Fatalf("Fingerprint = %s, want %s", got, wantF)
430 }
431 if got, _ := u.SnapshotFingerprint(); got != wantS {
432 t.Fatalf("SnapshotFingerprint = %s, want %s", got, wantS)
433 }
434}
435
436// WithHoldsFrozen is root's activation guard (moves Task 13): while fn runs,
437// no other handle can write the registry, so a SetHold from the agent waits
438// and the held rows fn saw are still the held rows when it returns.
439func TestWithHoldsFrozenHoldsOffASetHold(t *testing.T) {
440 r := holdFixture(t, "h")
441 other, err := Open(r.Path())
442 if err != nil {
443 t.Fatal(err)
444 }
445 defer other.Close()
446 done := make(chan error, 1)
447 err = r.WithHoldsFrozen(func(held []Route) error {
448 if len(held) != 0 {
449 t.Errorf("held = %+v", held)
450 }
451 go func() { done <- other.SetHold("h", "moving", holdOpA) }()
452 select {
453 case err := <-done:
454 t.Errorf("SetHold finished while the holds were frozen: %v", err)
455 done <- err // for the read below
456 case <-time.After(300 * time.Millisecond):
457 }
458 return nil
459 })
460 if err != nil {
461 t.Fatal(err)
462 }
463 if err := <-done; err != nil {
464 t.Fatalf("SetHold after the freeze: %v", err)
465 }
466 var seen []Route
467 if err := r.WithHoldsFrozen(func(held []Route) error { seen = held; return nil }); err != nil {
468 t.Fatal(err)
469 }
470 if len(seen) != 1 || seen[0].Host != "h" || seen[0].HoldOp != holdOpA {
471 t.Fatalf("held after SetHold = %+v", seen)
472 }
473 // fn's error is the call's, and nothing fn did not do is written.
474 sentinel := errors.New("refused")
475 if err := r.WithHoldsFrozen(func([]Route) error { return sentinel }); !errors.Is(err, sentinel) {
476 t.Fatalf("err = %v", err)
477 }
478}
479
480// Moves Task 6 fix round 1: SetHoldIf writes a hold only while the row's
481// hold_op is what the caller expects, in one statement, so a hold decided
482// from an earlier read cannot land on a row another op has taken since.
483// expectOp is "" (the row was unheld) or op (the op changes its own hold's
484// status); taking over another op's hold is never one statement.
485func TestSetHoldIfIsConditionalOnTheCurrentOp(t *testing.T) {
486 r := holdFixture(t, "h")
487 if ok, err := r.SetHoldIf("h", "moving", holdOpA, ""); err != nil || !ok {
488 t.Fatalf("unheld row, expecting unheld: %v %v", ok, err)
489 }
490 if rt, _ := r.Get("h"); rt.Status != "moving" || rt.HoldOp != holdOpA {
491 t.Fatalf("after the first hold: %+v", rt)
492 }
493 // Held by A now; a writer that still thinks the row is unheld is refused
494 // and changes nothing.
495 if ok, err := r.SetHoldIf("h", "landing", holdOpB, ""); err != nil || ok {
496 t.Fatalf("stale write over A's hold: %v %v", ok, err)
497 }
498 if rt, _ := r.Get("h"); rt.Status != "moving" || rt.HoldOp != holdOpA {
499 t.Fatalf("the refused write changed the row: %+v", rt)
500 }
501 // A may change its own hold's status.
502 if ok, err := r.SetHoldIf("h", "landing", holdOpA, holdOpA); err != nil || !ok {
503 t.Fatalf("A changing its own hold: %v %v", ok, err)
504 }
505 if rt, _ := r.Get("h"); rt.Status != "landing" || rt.HoldOp != holdOpA {
506 t.Fatalf("after A's status change: %+v", rt)
507 }
508 // B expecting A's hold: not a takeover this statement allows.
509 if _, err := r.SetHoldIf("h", "moving", holdOpB, holdOpA); err == nil {
510 t.Fatal("SetHoldIf accepted expectOp of another op")
511 }
512 if rt, _ := r.Get("h"); rt.HoldOp != holdOpA {
513 t.Fatalf("the refused takeover changed the row: %+v", rt)
514 }
515 // SetHold's own refusals hold.
516 if _, err := r.SetHoldIf("h", "ready", holdOpA, holdOpA); err == nil {
517 t.Fatal("ready accepted as a hold")
518 }
519 if _, err := r.SetHoldIf("h", "moving", "", ""); err == nil {
520 t.Fatal("a hold with no op was accepted")
521 }
522 // No such row: nothing matched, not an error (the caller re-judges
523 // next pass either way).
524 if ok, err := r.SetHoldIf("nope", "moving", holdOpA, ""); err != nil || ok {
525 t.Fatalf("unknown host: %v %v", ok, err)
526 }
527 // After a release the row is unheld again and "" matches once more.
528 if ok, _ := r.ReleaseHold("h", holdOpA); !ok {
529 t.Fatal("release")
530 }
531 if ok, err := r.SetHoldIf("h", "restoring", holdOpB, ""); err != nil || !ok {
532 t.Fatalf("hold after release: %v %v", ok, err)
533 }
534}
535
536// Ruling J3: a rollback takes over the move's hold in one statement, so the
537// row is never unheld between the two ops.
538func TestHandHold(t *testing.T) {
539 const rb = "rb_abcdefghijklmnopqrstuvwxyz"
540 r := holdFixture(t, "h")
541 if err := r.SetHold("h", "landing", holdOpA); err != nil {
542 t.Fatal(err)
543 }
544 // Held by another op: nothing moves.
545 if ok, err := r.HandHold("h", "releasing", rb, holdOpB); err != nil || ok {
546 t.Fatalf("hand-over from an op that does not hold the row: %v %v", ok, err)
547 }
548 if rt, _ := r.Get("h"); rt.Status != "landing" || rt.HoldOp != holdOpA {
549 t.Fatalf("row changed by the refusal: %+v", rt)
550 }
551 // The hand-over: one write, the new op and status.
552 if ok, err := r.HandHold("h", "releasing", rb, holdOpA); err != nil || !ok {
553 t.Fatalf("hand-over: %v %v", ok, err)
554 }
555 if rt, _ := r.Get("h"); rt.Status != "releasing" || rt.HoldOp != rb {
556 t.Fatalf("after the hand-over: %+v", rt)
557 }
558 // A replay finds the row already handed over and matches nothing.
559 if ok, err := r.HandHold("h", "releasing", rb, holdOpA); err != nil || ok {
560 t.Fatalf("replay: %v %v", ok, err)
561 }
562 // An unheld row is not taken: that is SetHoldIf's, with expectOp "".
563 if err := r.Upsert(Route{Host: "plain", Kind: "instance", Socket: "/s"}); err != nil {
564 t.Fatal(err)
565 }
566 if ok, err := r.HandHold("plain", "restoring", rb, holdOpA); err != nil || ok {
567 t.Fatalf("unheld row: %v %v", ok, err)
568 }
569 if rt, _ := r.Get("plain"); rt.HoldOp != "" || IsHold(rt.Status) {
570 t.Fatalf("unheld row taken: %+v", rt)
571 }
572 // An absent row matches nothing.
573 if ok, err := r.HandHold("absent", "restoring", rb, holdOpA); err != nil || ok {
574 t.Fatalf("absent row: %v %v", ok, err)
575 }
576 // Malformed: not a hold status, an empty op on either side, the same op.
577 for name, c := range map[string][3]string{
578 "ready": {"ready", rb, rb},
579 "no to": {"restoring", "", holdOpA},
580 "no from": {"restoring", rb, ""},
581 "same op": {"restoring", rb, rb},
582 } {
583 if ok, err := r.HandHold("h", c[0], c[1], c[2]); err == nil || ok {
584 t.Fatalf("%s: %v %v", name, ok, err)
585 }
586 }
587 if rt, _ := r.Get("h"); rt.Status != "releasing" || rt.HoldOp != rb {
588 t.Fatalf("row changed by a malformed hand-over: %+v", rt)
589 }
590}
591