| 1 | package hostclaim |
| 2 | |
| 3 | import ( |
| 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. |
| 13 | const 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. |
| 19 | type 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. |
| 51 | func 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. |
| 80 | func 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. |
| 92 | func LostTargetReleased(c *Claim, lossID string) bool { |
| 93 | return c != nil && lossID != "" && c.Op.Kind == KindRestoring && c.Op.Journal[journalLostTargetReleased] == lossID |
| 94 | } |
| 95 | |
| 96 | func 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 | |