| 1 | package hostclaim |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | "time" |
| 8 | ) |
| 9 | |
| 10 | // Abort (moves spec §7.11, Plan 5 Task 1): moving before the seal goes to |
| 11 | // moving/aborting, and the lane's park-replica is queued in the same write, |
| 12 | // owned by the target region and scoped to moves/<host>/<op>. Execution |
| 13 | // stays with the source. A replay allocates no ids; a replay with another |
| 14 | // scope is refused, not overwritten. |
| 15 | func TestAbortQueuesTheTargetsLanePark(t *testing.T) { |
| 16 | c, scope := parkFixture(t) |
| 17 | lane := scope("moves/" + c.Host + "/" + moveA) |
| 18 | ids := 0 |
| 19 | newID := func() string { ids++; return "id" + strings.Repeat("x", ids) } |
| 20 | if err := Abort(moveA, "uk", lane, newID)(c); err != nil { |
| 21 | t.Fatal(err) |
| 22 | } |
| 23 | if c.Op.Kind != KindMoving || c.Op.Phase != PhaseAborting || c.Op.Exec != "ie-box" || c.Op.ID != moveA { |
| 24 | t.Fatalf("op after the abort: %+v", c.Op) |
| 25 | } |
| 26 | if len(c.Obligations) != 1 { |
| 27 | t.Fatalf("obligations: %+v", c.Obligations) |
| 28 | } |
| 29 | o := c.Obligations[0] |
| 30 | if o.Op != moveA || o.Kind != "park-replica" || o.Region != "uk" || o.Owner != "region:uk" || o.Box != "" || o.Park == nil || !sameParkScope(*o.Park, lane) || o.Retirement == "" { |
| 31 | t.Fatalf("the lane park: %+v", o) |
| 32 | } |
| 33 | c.Gen++ |
| 34 | if err := c.Validate(); err != nil { |
| 35 | t.Fatalf("the claim after the abort must validate: %v", err) |
| 36 | } |
| 37 | // A replay with the same scope is a no-op and allocates nothing. |
| 38 | before, n := string(mustJSON(t, c)), ids |
| 39 | if err := Abort(moveA, "uk", lane, newID)(c); err != nil || ids != n || string(mustJSON(t, c)) != before { |
| 40 | t.Fatalf("replay: %v, ids %d to %d", err, n, ids) |
| 41 | } |
| 42 | // Another scope for the same lane is refused, not overwritten. |
| 43 | later := lane |
| 44 | later.EvidenceAt = lane.EvidenceAt.Add(time.Second) |
| 45 | if err := Abort(moveA, "uk", later, newID)(c); !errors.Is(err, ErrWrongState) || string(mustJSON(t, c)) != before { |
| 46 | t.Fatalf("replay with another scope: %v", err) |
| 47 | } |
| 48 | // Once the target acknowledged the park, a late replay queues nothing. |
| 49 | if err := CompleteObligation(moveA, o.ID)(c); err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | n = ids |
| 53 | if err := Abort(moveA, "uk", lane, newID)(c); err != nil || len(c.Obligations) != 0 || ids != n { |
| 54 | t.Fatalf("replay after the acknowledgement: %v, obligations %+v", err, c.Obligations) |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | // Abort is legal only on this move's moving op before the seal, with the |
| 59 | // source still executing, for the move's own target region and its own |
| 60 | // lane; every refusal leaves the claim as it was and allocates nothing. |
| 61 | func TestAbortRefusals(t *testing.T) { |
| 62 | for name, tc := range map[string]struct { |
| 63 | edit func(c *Claim) |
| 64 | op string |
| 65 | region string |
| 66 | loc func(c *Claim) string |
| 67 | inc string |
| 68 | zero bool |
| 69 | nilID bool |
| 70 | want error |
| 71 | }{ |
| 72 | "another op": {op: moveB, want: ErrStaleOp}, |
| 73 | "sealed": {edit: func(c *Claim) { c.Op.Phase = PhaseSealed }, want: ErrWrongState}, |
| 74 | "execution given up": {edit: func(c *Claim) { c.Op.Exec = "" }, want: ErrWrongState}, |
| 75 | "a landing": {edit: func(c *Claim) { c.Op.Kind = KindLanding }, want: ErrWrongState}, |
| 76 | "a revert": {edit: func(c *Claim) { c.Op.Kind = KindReverting }, want: ErrWrongState}, |
| 77 | "another region": {region: "se", want: ErrWrongState}, |
| 78 | "no region": {region: "-", want: ErrWrongState}, |
| 79 | "the live location": {loc: func(c *Claim) string { return "instances/" + c.Host + "/" + c.Holder.Incarnation }, want: ErrMalformed}, |
| 80 | "the staging prefix": {loc: func(c *Claim) string { return "moves/" + c.Host }, want: ErrMalformed}, |
| 81 | "another move's lane": {loc: func(c *Claim) string { return "moves/" + c.Host + "/" + moveB }, want: ErrMalformed}, |
| 82 | "another incarnation": {inc: strings.Repeat("a", 64), want: ErrMalformed}, |
| 83 | "no bound": {zero: true, want: ErrMalformed}, |
| 84 | "no way to mint ids": {nilID: true, want: ErrMalformed}, |
| 85 | "a dirty claim (no move)": {edit: func(c *Claim) { c.Op = Op{Kind: KindServing, Exec: "ie-box"} }, want: ErrStaleOp}, |
| 86 | } { |
| 87 | t.Run(name, func(t *testing.T) { |
| 88 | c, scope := parkFixture(t) |
| 89 | if tc.edit != nil { |
| 90 | tc.edit(c) |
| 91 | } |
| 92 | location := "moves/" + c.Host + "/" + moveA |
| 93 | if tc.loc != nil { |
| 94 | location = tc.loc(c) |
| 95 | } |
| 96 | s := scope(location) |
| 97 | if tc.inc != "" { |
| 98 | s.Incarnation = tc.inc |
| 99 | } |
| 100 | if tc.zero { |
| 101 | s.EvidenceAt = time.Time{} |
| 102 | } |
| 103 | op, region := moveA, "uk" |
| 104 | if tc.op != "" { |
| 105 | op = tc.op |
| 106 | } |
| 107 | if tc.region == "-" { |
| 108 | region = "" |
| 109 | } else if tc.region != "" { |
| 110 | region = tc.region |
| 111 | } |
| 112 | ids := 0 |
| 113 | newID := func() string { ids++; return "x" } |
| 114 | if tc.nilID { |
| 115 | newID = nil |
| 116 | } |
| 117 | before := string(mustJSON(t, c)) |
| 118 | err := Abort(op, region, s, newID)(c) |
| 119 | if !errors.Is(err, tc.want) { |
| 120 | t.Fatalf("err %v, want %v", err, tc.want) |
| 121 | } |
| 122 | if string(mustJSON(t, c)) != before || ids != 0 { |
| 123 | t.Fatalf("a refused abort changed the claim or minted %d ids", ids) |
| 124 | } |
| 125 | }) |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | // ReleaseAbort (ruling K7) is the abort's only release: from moving/aborting, |
| 130 | // with the source still executing, once no chore of the op remains. The |
| 131 | // serving op keeps the source as its executor and records the move it |
| 132 | // released. ReleaseMove's moving/"" stays the door's, and it no longer |
| 133 | // releases an aborting op. |
| 134 | func TestReleaseAbortFollowsTheLanePark(t *testing.T) { |
| 135 | c, scope := parkFixture(t) |
| 136 | lane := scope("moves/" + c.Host + "/" + moveA) |
| 137 | // Before the abort's CAS there is nothing to release from. |
| 138 | if err := ReleaseAbort(moveA, "ie-box")(c); !errors.Is(err, ErrWrongState) { |
| 139 | t.Fatalf("release before the abort: %v", err) |
| 140 | } |
| 141 | if err := Abort(moveA, "uk", lane, counter())(c); err != nil { |
| 142 | t.Fatal(err) |
| 143 | } |
| 144 | before := string(mustJSON(t, c)) |
| 145 | if err := ReleaseAbort(moveA, "ie-box")(c); !errors.Is(err, ErrPendingChore) || string(mustJSON(t, c)) != before { |
| 146 | t.Fatalf("release with the lane park owed: %v", err) |
| 147 | } |
| 148 | if err := ReleaseMove(moveA, "ie-box")(c); !errors.Is(err, ErrWrongState) || string(mustJSON(t, c)) != before { |
| 149 | t.Fatalf("ReleaseMove released an aborting op: %v", err) |
| 150 | } |
| 151 | if err := CompleteObligation(moveA, c.Obligations[0].ID)(c); err != nil { |
| 152 | t.Fatal(err) |
| 153 | } |
| 154 | for name, rel := range map[string]func(*Claim) error{ |
| 155 | "another box": ReleaseAbort(moveA, "uk-box"), |
| 156 | "no box": ReleaseAbort(moveA, ""), |
| 157 | "another op": ReleaseAbort(moveB, "ie-box"), |
| 158 | "ReleaseMove's": ReleaseMove(moveA, "ie-box"), |
| 159 | } { |
| 160 | mid := string(mustJSON(t, c)) |
| 161 | if err := rel(c); err == nil || string(mustJSON(t, c)) != mid { |
| 162 | t.Fatalf("%s: released (%v)", name, err) |
| 163 | } |
| 164 | } |
| 165 | if err := ReleaseAbort(moveA, "ie-box")(c); err != nil { |
| 166 | t.Fatal(err) |
| 167 | } |
| 168 | if c.Op.Kind != KindServing || c.Op.Exec != "ie-box" || c.Op.ID != "" || c.Op.Phase != "" || len(c.Op.Participants) != 0 || len(c.Op.Journal) != 1 { |
| 169 | t.Fatalf("op after the release: %+v", c.Op) |
| 170 | } |
| 171 | if err := RequireReleasedMove(moveA)(c); err != nil { |
| 172 | t.Fatal(err) |
| 173 | } |
| 174 | c.Gen++ |
| 175 | if err := c.Validate(); err != nil { |
| 176 | t.Fatalf("the claim after the release must validate: %v", err) |
| 177 | } |
| 178 | // Execution moved away (it never does in an abort) refuses. |
| 179 | d, scope := parkFixture(t) |
| 180 | if err := Abort(moveA, "uk", scope("moves/"+d.Host+"/"+moveA), counter())(d); err != nil { |
| 181 | t.Fatal(err) |
| 182 | } |
| 183 | d.Obligations = nil |
| 184 | d.Op.Exec = "uk-box" |
| 185 | if err := ReleaseAbort(moveA, "ie-box")(d); !errors.Is(err, ErrWrongState) { |
| 186 | t.Fatalf("exec is not the source: %v", err) |
| 187 | } |
| 188 | } |
| 189 | |