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 "context"
5 "encoding/json"
6 "github.com/carlosframework/platform/internal/hostops/fencerelease"
7 "github.com/carlosframework/platform/internal/regionname"
8 "github.com/carlosframework/platform/internal/release"
9 "strings"
10 "time"
11)
12
13const journalFencedTargetReleased = "fenced_target_released"
14
15// VerifyMoveTargetFence supplies a target-bound capability without writing the claim.
16func VerifyMoveTargetFence(ctx context.Context, s release.Store, o Observed, ref FenceRef, authority FenceAuthority) (ReturnFence, error) {
17 if o.Absent || o.Claim == nil || o.Claim.Holder == nil || o.ETag == "" {
18 return ReturnFence{}, ErrStaleOp
19 }
20 c := o.Claim
21 if (c.Op.Kind != KindLanding && c.Op.Kind != KindRestoring) || c.Op.Phase == PhaseCommitted || c.Op.ID == "" || c.Op.Intent["target"] == c.Op.Intent["source"] || c.Op.Intent["target_enrollment"] != ref.Enrollment {
22 return ReturnFence{}, ErrWrongState
23 }
24 f, err := VerifyFence(ctx, s, authority, ref)
25 if err != nil {
26 return ReturnFence{}, err
27 }
28 if f.Box() != c.Op.Intent["target"] || f.Region() != c.Op.Intent["to"] {
29 return ReturnFence{}, ErrFence
30 }
31 raw, err := json.Marshal(ref)
32 if err != nil {
33 return ReturnFence{}, err
34 }
35 at, err := time.Parse(time.RFC3339Nano, f.record.ObservedAt)
36 if err != nil || at.IsZero() {
37 return ReturnFence{}, ErrFence
38 }
39 return ReturnFence{op: c.Op.ID, box: f.Box(), ref: string(raw), observedAt: f.record.ObservedAt}, nil
40}
41
42// FencedTargetRelease has the same scopes as loss release, with verified fence authority.
43type FencedTargetRelease struct {
44 Token fencerelease.Token
45 OpID, RollbackID, Target, TargetRegion, SourceRegion string
46 Location, Lane ParkScope
47 ReturnLane *ParkScope
48 NewID func() string
49 Fence ReturnFence
50}
51
52// ReleaseFencedTarget declares and consumes the fence in one CAS, including all parks.
53func ReleaseFencedTarget(r FencedTargetRelease) func(*Claim) error {
54 return func(c *Claim) error {
55 if c == nil || c.Holder == nil || !r.Token.Consume(c.Host, c.Holder.Incarnation, c.Gen, r.OpID, r.RollbackID, r.Target) {
56 return ErrFence
57 }
58 return releaseFencedTarget(r)(c)
59 }
60}
61func releaseFencedTarget(r FencedTargetRelease) func(*Claim) error {
62 return func(c *Claim) error {
63 if err := RequireOp(r.OpID)(c); err != nil {
64 return err
65 }
66 if c.Holder == nil || !rollbackIDShape.MatchString(r.RollbackID) || r.Target == "" || r.NewID == nil || r.SourceRegion == "" || r.SourceRegion == r.TargetRegion {
67 return ErrMalformed
68 }
69 f := r.Fence
70 if f.op != r.OpID || f.box != r.Target || f.ref == "" || c.Op.Intent["target"] != r.Target || c.Op.Intent["source"] == r.Target || c.Op.Intent["to"] != r.TargetRegion {
71 return ErrFence
72 }
73 var ref FenceRef
74 if json.Unmarshal([]byte(f.ref), &ref) != nil || c.Op.Intent["target_enrollment"] != ref.Enrollment {
75 return ErrFence
76 }
77 if old := c.Op.Journal["fence."+r.Target]; old != "" && old != f.ref {
78 return ErrFence
79 }
80 if c.Op.Kind == KindRestoring && c.Op.Intent["rollback"] == r.RollbackID && c.Op.Journal[journalFencedTargetReleased] == f.ref {
81 return nil
82 }
83 if c.Op.Exec != r.Target && c.Op.Exec != "" && c.Op.Exec != "none" {
84 return ErrWrongState
85 }
86 at, err := time.Parse(time.RFC3339Nano, f.observedAt)
87 if err != nil || at.IsZero() || !r.Location.EvidenceAt.Equal(at) || !r.Lane.EvidenceAt.Equal(at) || (r.ReturnLane != nil && !r.ReturnLane.EvidenceAt.Equal(at)) {
88 return ErrFence
89 }
90 loss := AutomaticLoss{ID: automaticLossID(c.Op.ID, r.Target, ref), Box: r.Target, Enrollment: ref.Enrollment, Fence: ref, ObservedAt: f.observedAt, Op: c.Op.ID, Kind: c.Op.Kind, Phase: c.Op.Phase, Role: "target", Incarnation: c.Holder.Incarnation, Stamp: c.Holder.Stamp, Timers: TimersUnknown}
91 if c.Holder.WriterBox == r.Target {
92 loss.Session, loss.Generation = c.Holder.Session, c.Holder.Generation
93 }
94 for i := len(c.Holder.Admissions) - 1; i >= 0; i-- {
95 a := c.Holder.Admissions[i]
96 if a.Box == r.Target {
97 loss.Session, loss.Generation, loss.LastAdmission = a.Session, a.Generation, &a
98 break
99 }
100 }
101 owned := map[ObligationRef]bool{}
102 for _, ob := range c.Obligations {
103 if ob.Op == r.OpID && (ob.Kind == "park-generation" || ob.Kind == "park-replica") && ob.Owner == "box:"+r.Target {
104 owned[ObligationRef{ID: ob.ID, Op: ob.Op, Retirement: ob.Retirement}] = true
105 }
106 }
107 return inOneWrite(c, func(c *Claim) error {
108 setJournal(c, "fence."+r.Target, f.ref)
109 c.Op.Exec = "none"
110 c.Op.Participants = removeString(c.Op.Participants, r.Target)
111 return releaseTargetParks(c, LostTargetRelease{OpID: r.OpID, RollbackID: r.RollbackID, Target: r.Target, TargetRegion: r.TargetRegion, SourceRegion: r.SourceRegion, Location: r.Location, Lane: r.Lane, ReturnLane: r.ReturnLane, NewID: r.NewID})
112 }, func(c *Claim) error {
113 for i := range c.Obligations {
114 o := &c.Obligations[i]
115 if owned[ObligationRef{ID: o.ID, Op: o.Op, Retirement: o.Retirement}] {
116 o.Owner = "region:" + o.Region
117 loss.Reassigned = append(loss.Reassigned, ObligationRef{ID: o.ID, Op: o.Op, Retirement: o.Retirement})
118 }
119 }
120 if !stringPresent(c.Lost, r.Target) {
121 c.Lost = append(c.Lost, r.Target)
122 }
123 if !lossRefPresent(c, loss.ID) {
124 c.LossRefs = append(c.LossRefs, loss.ID)
125 c.EvidenceRefs = append(c.EvidenceRefs, loss)
126 }
127 setJournal(c, journalFencedTargetReleased, f.ref)
128 return nil
129 })
130 }
131}
132
133// FencedTargetReleased is the durable marker, checked without creating another CAS.
134func FencedTargetReleased(c *Claim, fence ReturnFence) bool {
135 ref, ok := ConsumedMoveTargetFence(c)
136 var expected FenceRef
137 return ok && c.Op.ID == fence.op && c.Op.Intent["target"] == fence.box && json.Unmarshal([]byte(fence.ref), &expected) == nil && ref == expected
138}
139
140// ConsumedMoveTargetFence reads the claim's completed revocation, not live roster authority.
141func ConsumedMoveTargetFence(c *Claim) (FenceRef, bool) {
142 if c == nil || c.Holder == nil || c.Op.Kind != KindRestoring {
143 return FenceRef{}, false
144 }
145 target := c.Op.Intent["target"]
146 raw := c.Op.Journal["fence."+target]
147 var ref FenceRef
148 if raw == "" || raw != c.Op.Journal[journalFencedTargetReleased] || json.Unmarshal([]byte(raw), &ref) != nil || !validFenceRef(ref) || ref.Enrollment != c.Op.Intent["target_enrollment"] || c.Op.Exec == target || stringPresent(c.Op.Participants, target) || !stringPresent(c.Lost, target) {
149 return FenceRef{}, false
150 }
151 for _, l := range c.EvidenceRefs {
152 if l.ID == automaticLossID(c.Op.ID, target, ref) && l.Op == c.Op.ID && l.Box == target && l.Fence == ref && l.Incarnation == c.Holder.Incarnation && lossRefPresent(c, l.ID) {
153 return ref, true
154 }
155 }
156 return FenceRef{}, false
157}
158
159// LostMoveTargetParked requires revocation and completion of every target chore.
160func LostMoveTargetParked(c *Claim, region string) bool {
161 if c == nil || c.Holder == nil || c.Op.Kind != KindRestoring || region == "" || !regionname.SameRegion(c.Op.Intent["home_region"], region, c.Op.Intent["to"]) {
162 return false
163 }
164 target := c.Op.Intent["target"]
165 if target == "" || !stringPresent(c.Lost, target) || c.Op.Exec == target || stringPresent(c.Op.Participants, target) {
166 return false
167 }
168 for _, o := range c.Obligations {
169 if o.Op == c.Op.ID && (o.Box == target || o.Owner == "box:"+target || regionname.SameRegion(c.Op.Intent["home_region"], o.Region, region) || (strings.HasPrefix(o.Owner, "region:") && regionname.SameRegion(c.Op.Intent["home_region"], strings.TrimPrefix(o.Owner, "region:"), region))) {
170 return false
171 }
172 }
173 return true
174}
175
176func (f ReturnFence) ObservedAt() time.Time {
177 at, _ := time.Parse(time.RFC3339Nano, f.observedAt)
178 return at
179}
180