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 "fmt"
5 "strconv"
6
7 "github.com/carlosframework/platform/internal/regionname"
8)
9
10// journalLostTargetReleased marks the console's release of a declared-lost
11// target under the op (ReleaseLostTarget), with the declaration's id. A
12// replay that finds it is a no-op at every later phase.
13const journalLostTargetReleased = "lost_target_released"
14
15// LostTargetRelease is what the console's release of a declared-lost move
16// target writes (moves spec §7.10, J16). Every value derives from the
17// root-signed loss record the release follows; NewID must be deterministic
18// for that record, so a recomputation of the same write is byte-identical.
19type LostTargetRelease struct {
20 OpID, RollbackID, LossID string
21 Target, TargetRegion string
22 SourceRegion string
23 Location ParkScope // the target region's whole location
24 Lane ParkScope // the move's own staging lane in the target region
25 ReturnLane *ParkScope // a dirty rollback's return lane in the source region, when it may be partial
26 NewID func() string
27}
28
29// ReleaseLostTarget is the claim half of the console's release of a
30// declared-lost target. It is legal only after the declaration's own claim
31// CAS: the loss id is in loss_refs with its loss.<id> journal under this op,
32// the target is on lost, out of the participants and not executing. Then,
33// in one write:
34//
35// - a landing before its commit is rolled back (RollBack, which journals
36// the target installation's stamp); a restoring op must already be this
37// rollback;
38// - restoring/releasing steps to released, queues the target region's
39// whole location park and covers that region's predecessors, as
40// TargetReleased does for a target that released itself. Inventory of
41// the target installation also requires its exact local retirement;
42// - restoring/released (the target released dirty and died before its
43// return) keeps the location park its own release queued;
44// - the move's staging lane in the target region is queued, and a
45// possibly partial return lane in the source region (§7.10, model 27);
46// - the release is journalled with the loss id.
47//
48// A replay that finds the journal entry is a no-op, whatever has happened
49// since (the source's claimed CAS, chore acknowledgements). A refused
50// release leaves the claim as found.
51func ReleaseLostTarget(r LostTargetRelease) func(*Claim) error {
52 return func(c *Claim) error {
53 if err := RequireOp(r.OpID)(c); err != nil {
54 return err
55 }
56 if !rollbackIDShape.MatchString(r.RollbackID) || !safeLossID(r.LossID) || r.Target == "" || r.TargetRegion == "" || r.SourceRegion == "" ||
57 r.SourceRegion == r.TargetRegion || r.NewID == nil {
58 return fmt.Errorf("%w: a lost target's release names its rollback, declaration, target and both regions", ErrMalformed)
59 }
60 if c.Op.Kind == KindRestoring && c.Op.Journal[journalLostTargetReleased] == r.LossID && c.Op.Intent["rollback"] == r.RollbackID {
61 return nil
62 }
63 if c.Holder == nil || !lossRefPresent(c, r.LossID) || !stringPresent(c.Lost, r.Target) {
64 return fmt.Errorf("%w: the declaration is not on the claim", ErrWrongState)
65 }
66 if _, ok := c.Op.Journal["loss."+r.LossID]; !ok {
67 return fmt.Errorf("%w: the declaration is not this op's", ErrWrongState)
68 }
69 if c.Op.Intent["target"] != r.Target || c.Op.Intent["source"] == r.Target || c.Op.Intent["to"] != r.TargetRegion ||
70 stringPresent(c.Op.Participants, r.Target) || c.Op.Exec == r.Target {
71 return fmt.Errorf("%w: %q is not this move's declared-lost target", ErrWrongState, r.Target)
72 }
73 return inOneWrite(c, func(c *Claim) error { return releaseTargetParks(c, r) }, func(c *Claim) error { setJournal(c, journalLostTargetReleased, r.LossID); return nil })
74 }
75}
76
77// owesStagingPark reports a staging-lane park this op already owes in the
78// region, whatever its scope: the target's own lane queue, or an earlier
79// release of the same loss.
80func owesStagingPark(c *Claim, opID, region string) bool {
81 for _, o := range c.Obligations {
82 if o.Op == opID && o.Kind == "park-replica" && regionname.SameRegion(c.Op.Intent["home_region"], o.Region, region) && o.Park != nil && isStagingLocation(o.Park.Location) {
83 return true
84 }
85 }
86 return false
87}
88
89// LostTargetReleased reports whether ReleaseLostTarget already ran for this
90// declaration under the claim's current op. A caller checks it before an
91// Update, so a replay issues no write at all.
92func LostTargetReleased(c *Claim, lossID string) bool {
93 return c != nil && lossID != "" && c.Op.Kind == KindRestoring && c.Op.Journal[journalLostTargetReleased] == lossID
94}
95
96func releaseTargetParks(c *Claim, r LostTargetRelease) error {
97 location := r.Location
98 if err := location.Validate(c.Host); err != nil {
99 return err
100 }
101 lane, err := parkQueueGuard(c, r.OpID, r.TargetRegion, r.Lane, r.NewID)
102 if err != nil {
103 return err
104 }
105 // The location park's scope is checked against holder.location by
106 // QueueLocationPark; the lane is this move's own.
107 if err := requireHolderLocation(c, r.Location); err != nil {
108 return err
109 }
110 if lane.Location != "moves/"+c.Host+"/"+r.OpID {
111 return fmt.Errorf("%w: a lost target's lane park is the move's own lane", ErrMalformed)
112 }
113 var returnLane *ParkScope
114 if r.ReturnLane != nil {
115 s, err := parkQueueGuard(c, r.OpID, r.SourceRegion, *r.ReturnLane, r.NewID)
116 if err != nil {
117 return err
118 }
119 returnLane = &s
120 if s.Location != "moves/"+c.Host+"/"+r.RollbackID {
121 return fmt.Errorf("%w: the return lane is this rollback's own lane", ErrMalformed)
122 }
123 }
124 steps := []func(*Claim) error{}
125 switch {
126 case c.Op.Kind == KindLanding && c.Op.Phase != PhaseCommitted:
127 steps = append(steps, RollBack(r.OpID, r.RollbackID))
128 case c.Op.Kind == KindRestoring && c.Op.Intent["rollback"] == r.RollbackID:
129 default:
130 return fmt.Errorf("%w: op is %s/%q, a lost target is released from a landing before its commit or from its own rollback", ErrWrongState, c.Op.Kind, c.Op.Phase)
131 }
132 steps = append(steps, func(c *Claim) error {
133 switch c.Op.Phase {
134 case PhaseReleasing:
135 if c.Op.Exec != "" && c.Op.Exec != "none" {
136 return fmt.Errorf("%w: exec is %q", ErrWrongState, c.Op.Exec)
137 }
138 c.Op.Phase = PhaseReleased
139 parkBox := ""
140 if stamp, err := strconv.ParseUint(c.Op.Journal[JournalTargetStamp], 10, 64); err == nil && InventoryHasStamp(c, r.Target, stamp) {
141 parkBox = r.Target
142 }
143 if err := QueueLocationPark(r.OpID, r.TargetRegion, parkBox, r.Location, r.NewID)(c); err != nil {
144 return err
145 }
146 return CoverRegionPredecessors(r.OpID, PhaseReleased, r.TargetRegion)(c)
147 case PhaseReleased:
148 // The target released itself before it was lost: its release
149 // queued the location park and covered the region.
150 if c.Op.Journal[JournalEvidenceTarget] == "" {
151 return fmt.Errorf("%w: released without the target's evidence", ErrWrongState)
152 }
153 if returnLane == nil {
154 return fmt.Errorf("%w: a dirty release that died before its return owes the return lane's park", ErrMalformed)
155 }
156 return nil
157 }
158 return fmt.Errorf("%w: op is restoring/%q, the lost target's release precedes the source's claim", ErrWrongState, c.Op.Phase)
159 }, func(c *Claim) error {
160 if owesStagingPark(c, r.OpID, r.TargetRegion) {
161 return nil
162 }
163 _, err := queueReplicaPark(c, r.OpID, r.TargetRegion, lane, r.NewID)
164 return err
165 }, func(c *Claim) error {
166 if returnLane == nil {
167 return nil
168 }
169 if _, err := queueReplicaPark(c, r.OpID, r.SourceRegion, *returnLane, r.NewID); err != nil {
170 return err
171 }
172 setJournal(c, journalReturnLane, returnLane.Location)
173 return nil
174 })
175 return inOneWrite(c, steps...)
176}
177