package hostclaim import ( "errors" "strings" "testing" "time" ) // Abort (moves spec ยง7.11, Plan 5 Task 1): moving before the seal goes to // moving/aborting, and the lane's park-replica is queued in the same write, // owned by the target region and scoped to moves//. Execution // stays with the source. A replay allocates no ids; a replay with another // scope is refused, not overwritten. func TestAbortQueuesTheTargetsLanePark(t *testing.T) { c, scope := parkFixture(t) lane := scope("moves/" + c.Host + "/" + moveA) ids := 0 newID := func() string { ids++; return "id" + strings.Repeat("x", ids) } if err := Abort(moveA, "uk", lane, newID)(c); err != nil { t.Fatal(err) } if c.Op.Kind != KindMoving || c.Op.Phase != PhaseAborting || c.Op.Exec != "ie-box" || c.Op.ID != moveA { t.Fatalf("op after the abort: %+v", c.Op) } if len(c.Obligations) != 1 { t.Fatalf("obligations: %+v", c.Obligations) } o := c.Obligations[0] 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 == "" { t.Fatalf("the lane park: %+v", o) } c.Gen++ if err := c.Validate(); err != nil { t.Fatalf("the claim after the abort must validate: %v", err) } // A replay with the same scope is a no-op and allocates nothing. before, n := string(mustJSON(t, c)), ids if err := Abort(moveA, "uk", lane, newID)(c); err != nil || ids != n || string(mustJSON(t, c)) != before { t.Fatalf("replay: %v, ids %d to %d", err, n, ids) } // Another scope for the same lane is refused, not overwritten. later := lane later.EvidenceAt = lane.EvidenceAt.Add(time.Second) if err := Abort(moveA, "uk", later, newID)(c); !errors.Is(err, ErrWrongState) || string(mustJSON(t, c)) != before { t.Fatalf("replay with another scope: %v", err) } // Once the target acknowledged the park, a late replay queues nothing. if err := CompleteObligation(moveA, o.ID)(c); err != nil { t.Fatal(err) } n = ids if err := Abort(moveA, "uk", lane, newID)(c); err != nil || len(c.Obligations) != 0 || ids != n { t.Fatalf("replay after the acknowledgement: %v, obligations %+v", err, c.Obligations) } } // Abort is legal only on this move's moving op before the seal, with the // source still executing, for the move's own target region and its own // lane; every refusal leaves the claim as it was and allocates nothing. func TestAbortRefusals(t *testing.T) { for name, tc := range map[string]struct { edit func(c *Claim) op string region string loc func(c *Claim) string inc string zero bool nilID bool want error }{ "another op": {op: moveB, want: ErrStaleOp}, "sealed": {edit: func(c *Claim) { c.Op.Phase = PhaseSealed }, want: ErrWrongState}, "execution given up": {edit: func(c *Claim) { c.Op.Exec = "" }, want: ErrWrongState}, "a landing": {edit: func(c *Claim) { c.Op.Kind = KindLanding }, want: ErrWrongState}, "a revert": {edit: func(c *Claim) { c.Op.Kind = KindReverting }, want: ErrWrongState}, "another region": {region: "se", want: ErrWrongState}, "no region": {region: "-", want: ErrWrongState}, "the live location": {loc: func(c *Claim) string { return "instances/" + c.Host + "/" + c.Holder.Incarnation }, want: ErrMalformed}, "the staging prefix": {loc: func(c *Claim) string { return "moves/" + c.Host }, want: ErrMalformed}, "another move's lane": {loc: func(c *Claim) string { return "moves/" + c.Host + "/" + moveB }, want: ErrMalformed}, "another incarnation": {inc: strings.Repeat("a", 64), want: ErrMalformed}, "no bound": {zero: true, want: ErrMalformed}, "no way to mint ids": {nilID: true, want: ErrMalformed}, "a dirty claim (no move)": {edit: func(c *Claim) { c.Op = Op{Kind: KindServing, Exec: "ie-box"} }, want: ErrStaleOp}, } { t.Run(name, func(t *testing.T) { c, scope := parkFixture(t) if tc.edit != nil { tc.edit(c) } location := "moves/" + c.Host + "/" + moveA if tc.loc != nil { location = tc.loc(c) } s := scope(location) if tc.inc != "" { s.Incarnation = tc.inc } if tc.zero { s.EvidenceAt = time.Time{} } op, region := moveA, "uk" if tc.op != "" { op = tc.op } if tc.region == "-" { region = "" } else if tc.region != "" { region = tc.region } ids := 0 newID := func() string { ids++; return "x" } if tc.nilID { newID = nil } before := string(mustJSON(t, c)) err := Abort(op, region, s, newID)(c) if !errors.Is(err, tc.want) { t.Fatalf("err %v, want %v", err, tc.want) } if string(mustJSON(t, c)) != before || ids != 0 { t.Fatalf("a refused abort changed the claim or minted %d ids", ids) } }) } } // ReleaseAbort (ruling K7) is the abort's only release: from moving/aborting, // with the source still executing, once no chore of the op remains. The // serving op keeps the source as its executor and records the move it // released. ReleaseMove's moving/"" stays the door's, and it no longer // releases an aborting op. func TestReleaseAbortFollowsTheLanePark(t *testing.T) { c, scope := parkFixture(t) lane := scope("moves/" + c.Host + "/" + moveA) // Before the abort's CAS there is nothing to release from. if err := ReleaseAbort(moveA, "ie-box")(c); !errors.Is(err, ErrWrongState) { t.Fatalf("release before the abort: %v", err) } if err := Abort(moveA, "uk", lane, counter())(c); err != nil { t.Fatal(err) } before := string(mustJSON(t, c)) if err := ReleaseAbort(moveA, "ie-box")(c); !errors.Is(err, ErrPendingChore) || string(mustJSON(t, c)) != before { t.Fatalf("release with the lane park owed: %v", err) } if err := ReleaseMove(moveA, "ie-box")(c); !errors.Is(err, ErrWrongState) || string(mustJSON(t, c)) != before { t.Fatalf("ReleaseMove released an aborting op: %v", err) } if err := CompleteObligation(moveA, c.Obligations[0].ID)(c); err != nil { t.Fatal(err) } for name, rel := range map[string]func(*Claim) error{ "another box": ReleaseAbort(moveA, "uk-box"), "no box": ReleaseAbort(moveA, ""), "another op": ReleaseAbort(moveB, "ie-box"), "ReleaseMove's": ReleaseMove(moveA, "ie-box"), } { mid := string(mustJSON(t, c)) if err := rel(c); err == nil || string(mustJSON(t, c)) != mid { t.Fatalf("%s: released (%v)", name, err) } } if err := ReleaseAbort(moveA, "ie-box")(c); err != nil { t.Fatal(err) } 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 { t.Fatalf("op after the release: %+v", c.Op) } if err := RequireReleasedMove(moveA)(c); err != nil { t.Fatal(err) } c.Gen++ if err := c.Validate(); err != nil { t.Fatalf("the claim after the release must validate: %v", err) } // Execution moved away (it never does in an abort) refuses. d, scope := parkFixture(t) if err := Abort(moveA, "uk", scope("moves/"+d.Host+"/"+moveA), counter())(d); err != nil { t.Fatal(err) } d.Obligations = nil d.Op.Exec = "uk-box" if err := ReleaseAbort(moveA, "ie-box")(d); !errors.Is(err, ErrWrongState) { t.Fatalf("exec is not the source: %v", err) } }