carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package provision
2
3import (
4 "encoding/json"
5 "fmt"
6 "time"
7)
8
9// MoveRelocating is the record state of a move whose source was terminated
10// before the flip (claim Plan 5, C7). Every forward and abort transition
11// lists its legal states, so none of them accepts it: the decision is
12// terminal for the move.
13const MoveRelocating = "relocating"
14
15// ValidMoveID reports a Phase 1 move id (moveIDShape). A claim-mode move's
16// admission registration is minted under this id.
17func ValidMoveID(id string) bool { return moveIDShape.MatchString(id) }
18
19// MoveRelocationDecision is a durable record decision, not an installation.
20// Every other MoveState field is preserved so a crash can reconstruct the
21// move.
22type MoveRelocationDecision struct {
23 MovingIdentity string `json:"moving_identity"`
24 HolderIdentity string `json:"holder_identity"`
25 ClaimGen uint64 `json:"claim_gen"`
26 ClaimETag string `json:"claim_etag"`
27 SourceBox string `json:"source_box"`
28 Enrollment string `json:"enrollment"`
29 Region string `json:"region"`
30 Fence string `json:"fence"`
31 PreviousState string `json:"previous_state"`
32 PreviousCutPhase string `json:"previous_cut_phase,omitempty"`
33 DecidedAt time.Time `json:"decided_at"`
34}
35
36// MoveRelocatingDecision competes with the flip under the same record CAS:
37// only one of them can win. region must be the record's own region and the
38// move's origin; the caller has also bound it to the fence's roster region.
39// A replay with the same identities is a no-op that keeps the original
40// decision time.
41func MoveRelocatingDecision(g MoveGuard, box, enrollment, region, fence string, claimGen uint64, claimETag, movingIdentity, holderIdentity string, now time.Time) func(*Record) error {
42 return func(rec *Record) error {
43 if err := g.check(rec); err != nil {
44 return err
45 }
46 var ref struct{ Enrollment, Key, SHA256 string }
47 if len(movingIdentity) != 64 || len(holderIdentity) != 64 || claimGen == 0 || claimETag == "" || box == "" || enrollment == "" || !ValidRegion(region) || now.IsZero() ||
48 json.Unmarshal([]byte(fence), &ref) != nil || ref.Enrollment != enrollment || ref.Key != "control/box-fences/"+enrollment+".json" || len(ref.SHA256) != 64 ||
49 rec.Region != region || rec.Move.From != region {
50 return fmt.Errorf("%w: the relocation decision does not match this move", ErrMoveConflict)
51 }
52 if rec.Move.State == MoveRelocating {
53 d := rec.Move.Relocation
54 if d == nil || d.SourceBox != box || d.Enrollment != enrollment || d.Region != region || d.Fence != fence || d.MovingIdentity != movingIdentity || d.HolderIdentity != holderIdentity {
55 return fmt.Errorf("%w: another relocation decision is recorded", ErrMoveConflict)
56 }
57 return nil
58 }
59 switch rec.Move.State {
60 case MovePreparing, MoveReady, MoveCutting:
61 default:
62 return fmt.Errorf("%w: a move in state %q cannot relocate", ErrMoveConflict, rec.Move.State)
63 }
64 if rec.Move.CutPhase == CutFlipped || rec.Move.FlippedAt != nil || rec.Move.CommittedAt != nil || rec.Move.Rollback != nil || rec.Move.Relocation != nil {
65 return fmt.Errorf("%w: the move is past its flip", ErrMoveConflict)
66 }
67 rec.Move.Relocation = &MoveRelocationDecision{MovingIdentity: movingIdentity, HolderIdentity: holderIdentity, ClaimGen: claimGen, ClaimETag: claimETag, SourceBox: box, Enrollment: enrollment, Region: region, Fence: fence, PreviousState: rec.Move.State, PreviousCutPhase: rec.Move.CutPhase, DecidedAt: now.UTC()}
68 rec.Move.State = MoveRelocating
69 return nil
70 }
71}
72
73// MoveLostTargetParked is separate from the ordinary forward phase table:
74// a permanently lost target cannot run returning/cleaning itself. The caller
75// must freshly prove the claim's lost binding and drained target obligations.
76func MoveLostTargetParked(g MoveGuard, rollbackID string, now time.Time) func(*Record) error {
77 return func(rec *Record) error {
78 if err := g.check(rec); err != nil {
79 return err
80 }
81 rb := rec.Move.Rollback
82 if now.IsZero() || rec.Move.State != MoveRollingBack || rb == nil || rollbackID == "" || rb.ID != rollbackID {
83 return fmt.Errorf("%w: no matching rollback", ErrMoveConflict)
84 }
85 if rb.TargetPhase == RbTargetCleaned && rb.TargetParkedAt != nil {
86 return nil
87 }
88 if rb.TargetPhase != RbTargetReleased || rb.Dirty || rb.TargetParkedAt != nil {
89 return fmt.Errorf("%w: the lost target is not cleanly released", ErrMoveConflict)
90 }
91 at := now.UTC()
92 rb.TargetPhase, rb.TargetParkedAt = RbTargetCleaned, &at
93 return nil
94 }
95}
96