package hostclaim import ( "fmt" "strconv" "github.com/carlosframework/platform/internal/regionname" ) // journalLostTargetReleased marks the console's release of a declared-lost // target under the op (ReleaseLostTarget), with the declaration's id. A // replay that finds it is a no-op at every later phase. const journalLostTargetReleased = "lost_target_released" // LostTargetRelease is what the console's release of a declared-lost move // target writes (moves spec §7.10, J16). Every value derives from the // root-signed loss record the release follows; NewID must be deterministic // for that record, so a recomputation of the same write is byte-identical. type LostTargetRelease struct { OpID, RollbackID, LossID string Target, TargetRegion string SourceRegion string Location ParkScope // the target region's whole location Lane ParkScope // the move's own staging lane in the target region ReturnLane *ParkScope // a dirty rollback's return lane in the source region, when it may be partial NewID func() string } // ReleaseLostTarget is the claim half of the console's release of a // declared-lost target. It is legal only after the declaration's own claim // CAS: the loss id is in loss_refs with its loss. journal under this op, // the target is on lost, out of the participants and not executing. Then, // in one write: // // - a landing before its commit is rolled back (RollBack, which journals // the target installation's stamp); a restoring op must already be this // rollback; // - restoring/releasing steps to released, queues the target region's // whole location park and covers that region's predecessors, as // TargetReleased does for a target that released itself. Inventory of // the target installation also requires its exact local retirement; // - restoring/released (the target released dirty and died before its // return) keeps the location park its own release queued; // - the move's staging lane in the target region is queued, and a // possibly partial return lane in the source region (§7.10, model 27); // - the release is journalled with the loss id. // // A replay that finds the journal entry is a no-op, whatever has happened // since (the source's claimed CAS, chore acknowledgements). A refused // release leaves the claim as found. func ReleaseLostTarget(r LostTargetRelease) func(*Claim) error { return func(c *Claim) error { if err := RequireOp(r.OpID)(c); err != nil { return err } if !rollbackIDShape.MatchString(r.RollbackID) || !safeLossID(r.LossID) || r.Target == "" || r.TargetRegion == "" || r.SourceRegion == "" || r.SourceRegion == r.TargetRegion || r.NewID == nil { return fmt.Errorf("%w: a lost target's release names its rollback, declaration, target and both regions", ErrMalformed) } if c.Op.Kind == KindRestoring && c.Op.Journal[journalLostTargetReleased] == r.LossID && c.Op.Intent["rollback"] == r.RollbackID { return nil } if c.Holder == nil || !lossRefPresent(c, r.LossID) || !stringPresent(c.Lost, r.Target) { return fmt.Errorf("%w: the declaration is not on the claim", ErrWrongState) } if _, ok := c.Op.Journal["loss."+r.LossID]; !ok { return fmt.Errorf("%w: the declaration is not this op's", ErrWrongState) } if c.Op.Intent["target"] != r.Target || c.Op.Intent["source"] == r.Target || c.Op.Intent["to"] != r.TargetRegion || stringPresent(c.Op.Participants, r.Target) || c.Op.Exec == r.Target { return fmt.Errorf("%w: %q is not this move's declared-lost target", ErrWrongState, r.Target) } return inOneWrite(c, func(c *Claim) error { return releaseTargetParks(c, r) }, func(c *Claim) error { setJournal(c, journalLostTargetReleased, r.LossID); return nil }) } } // owesStagingPark reports a staging-lane park this op already owes in the // region, whatever its scope: the target's own lane queue, or an earlier // release of the same loss. func owesStagingPark(c *Claim, opID, region string) bool { for _, o := range c.Obligations { 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) { return true } } return false } // LostTargetReleased reports whether ReleaseLostTarget already ran for this // declaration under the claim's current op. A caller checks it before an // Update, so a replay issues no write at all. func LostTargetReleased(c *Claim, lossID string) bool { return c != nil && lossID != "" && c.Op.Kind == KindRestoring && c.Op.Journal[journalLostTargetReleased] == lossID } func releaseTargetParks(c *Claim, r LostTargetRelease) error { location := r.Location if err := location.Validate(c.Host); err != nil { return err } lane, err := parkQueueGuard(c, r.OpID, r.TargetRegion, r.Lane, r.NewID) if err != nil { return err } // The location park's scope is checked against holder.location by // QueueLocationPark; the lane is this move's own. if err := requireHolderLocation(c, r.Location); err != nil { return err } if lane.Location != "moves/"+c.Host+"/"+r.OpID { return fmt.Errorf("%w: a lost target's lane park is the move's own lane", ErrMalformed) } var returnLane *ParkScope if r.ReturnLane != nil { s, err := parkQueueGuard(c, r.OpID, r.SourceRegion, *r.ReturnLane, r.NewID) if err != nil { return err } returnLane = &s if s.Location != "moves/"+c.Host+"/"+r.RollbackID { return fmt.Errorf("%w: the return lane is this rollback's own lane", ErrMalformed) } } steps := []func(*Claim) error{} switch { case c.Op.Kind == KindLanding && c.Op.Phase != PhaseCommitted: steps = append(steps, RollBack(r.OpID, r.RollbackID)) case c.Op.Kind == KindRestoring && c.Op.Intent["rollback"] == r.RollbackID: default: 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) } steps = append(steps, func(c *Claim) error { switch c.Op.Phase { case PhaseReleasing: if c.Op.Exec != "" && c.Op.Exec != "none" { return fmt.Errorf("%w: exec is %q", ErrWrongState, c.Op.Exec) } c.Op.Phase = PhaseReleased parkBox := "" if stamp, err := strconv.ParseUint(c.Op.Journal[JournalTargetStamp], 10, 64); err == nil && InventoryHasStamp(c, r.Target, stamp) { parkBox = r.Target } if err := QueueLocationPark(r.OpID, r.TargetRegion, parkBox, r.Location, r.NewID)(c); err != nil { return err } return CoverRegionPredecessors(r.OpID, PhaseReleased, r.TargetRegion)(c) case PhaseReleased: // The target released itself before it was lost: its release // queued the location park and covered the region. if c.Op.Journal[JournalEvidenceTarget] == "" { return fmt.Errorf("%w: released without the target's evidence", ErrWrongState) } if returnLane == nil { return fmt.Errorf("%w: a dirty release that died before its return owes the return lane's park", ErrMalformed) } return nil } return fmt.Errorf("%w: op is restoring/%q, the lost target's release precedes the source's claim", ErrWrongState, c.Op.Phase) }, func(c *Claim) error { if owesStagingPark(c, r.OpID, r.TargetRegion) { return nil } _, err := queueReplicaPark(c, r.OpID, r.TargetRegion, lane, r.NewID) return err }, func(c *Claim) error { if returnLane == nil { return nil } if _, err := queueReplicaPark(c, r.OpID, r.SourceRegion, *returnLane, r.NewID); err != nil { return err } setJournal(c, journalReturnLane, returnLane.Location) return nil }) return inOneWrite(c, steps...) }