package moves import ( "errors" "fmt" "strings" "time" "github.com/carlosframework/platform/internal/hostclaim" ) // The park scopes a move's chores carry (claim rev 6.23 ยง2, INV-22; Plan 5 // ruling K8). These helpers are the one place the box and the console build // them. Each takes its genuine bound as an argument and never reads the // clock: the scope is bound once, in the CAS that queues the chore, and a // retry or a restart that computes a later bound is refused // (hostclaim.QueueStagingPark and friends, park.Engine.Bind), so the // horizon is the evidence's, never the cleanup's start. The bound is stored // in UTC. // ParkRequestLimit is every moves park's request limit: the longest a // staging request runs (lease's stagingRequestTimeout). const ParkRequestLimit = 120 * time.Second // SourceLocationScope is the source region's location park a commit queues: // holder.location, in the holder's form without a trailing slash, and the // holder's incarnation. Its horizon starts at the flip (ruling H7): the flip // comes after ExecToNone, after which the source never writes its location // again. func SourceLocationScope(holder hostclaim.Holder, flippedAt time.Time) hostclaim.ParkScope { return locationScope(holder, flippedAt) } // TargetLocationScope is a rolled-back target region's location park, queued // when the target releases (hostclaim.TargetReleased): holder.location and // the holder's incarnation. Its horizon is the target's stop report's // CheckedAt, after which the target writes its location no more. func TargetLocationScope(holder hostclaim.Holder, stopCheckedAt time.Time) hostclaim.ParkScope { return locationScope(holder, stopCheckedAt) } // ResidueScope is the location a landing's earlier-stay residue parks // (hostclaim.QueueResidueParks) are scoped to: holder.location and the // holder's incarnation. Its horizon starts at the flip, like the source's. func ResidueScope(holder hostclaim.Holder, flippedAt time.Time) hostclaim.ParkScope { return locationScope(holder, flippedAt) } func locationScope(holder hostclaim.Holder, bound time.Time) hostclaim.ParkScope { return hostclaim.ParkScope{Location: strings.TrimSuffix(holder.Location, "/"), Incarnation: holder.Incarnation, EvidenceAt: bound.UTC(), RequestLimit: ParkRequestLimit} } // StagingScope is the park of a move's own staging lane, moves//, // after a seal: its horizon is the seal's bound (a staged seal's for a // rollback's target, the journalled revert bound for a revert). func StagingScope(host, op, inc string, sealBound time.Time) hostclaim.ParkScope { return laneScope(host, op, inc, sealBound) } // AbortScope is the park of an aborted move's staging lane. No seal exists // before an abort, so its horizon is the journalled abort bound (Plan 5 // ruling K5): the later of the decision's time plus a signature's life and // the skew, and any seal's bound. func AbortScope(host, op, inc string, abortBound time.Time) hostclaim.ParkScope { return laneScope(host, op, inc, abortBound) } // ReturnLaneScope is the park of a dirty rollback's return lane, // moves//: its horizon is the return seal's bound. func ReturnLaneScope(host, rbID, inc string, returnSealBound time.Time) hostclaim.ParkScope { return laneScope(host, rbID, inc, returnSealBound) } func laneScope(host, lane, inc string, bound time.Time) hostclaim.ParkScope { return hostclaim.ParkScope{Location: ScopeLocation(host, lane), Incarnation: inc, EvidenceAt: bound.UTC(), RequestLimit: ParkRequestLimit} } // CommitScopes are the two parks a commit queues (hostclaim.Commit), built // from the claim and the record's flip time alone (Plan 5 Task 3, ruling // K9): the source region's location from the flip (SourceLocationScope, // ruling H7) and the move's own lane to the seal the claim journals // (StagingScope with the seal's bound). The target box and the console's // pass (Task 7a) both call it, so whichever commits first writes the // obligations the other's replay finds; a scope built any other way would // be refused as "another scope" by the queue helpers. It refuses without // the flip's time, the holder or the journalled seal. func CommitScopes(c *hostclaim.Claim, flippedAt *time.Time) (source, staging hostclaim.ParkScope, err error) { if c == nil || c.Holder == nil { return source, staging, errors.New("a commit's parks need the claim's holder") } if flippedAt == nil || flippedAt.IsZero() { return source, staging, errors.New("the source's location park needs the flip's time") } raw, ok := c.Op.Journal[hostclaim.JournalSeal] if !ok { return source, staging, fmt.Errorf("%w: the claim carries no seal", ErrBadSeal) } seal, err := DecodeSeal(raw) if err != nil { return source, staging, err } return SourceLocationScope(*c.Holder, *flippedAt), StagingScope(c.Host, c.Op.ID, c.Holder.Incarnation, seal.Bound()), nil }