carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostclaim
2
3import (
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.
15func 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.
61func 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.
134func 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