carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3import (
4 "context"
5 "crypto/rand"
6 "encoding/json"
7 "github.com/carlosframework/platform/internal/hostclaim"
8 "github.com/carlosframework/platform/internal/hostops/internal/fencerelease"
9 "github.com/carlosframework/platform/internal/moves"
10 "github.com/carlosframework/platform/internal/park"
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15// FencedTargetOutcome is what one ReleaseFencedTarget step found or wrote,
16// as values, never authority. VerifiedDirty is set only with ErrPending, and
17// only after the fence verified: the target released dirty, which the grant
18// leaves for J4. Every other ErrPending (a withdrawn registration, a spent
19// grant) leaves it unset. Transition is the step's record write, named as the
20// move log names it (moves.TransitionOf over the record before and after the
21// write), and ClaimRolledBack that the step's claim write rolled a landing
22// claim back: the caller logs the write from this evidence, never from a
23// later read (Transition's rule).
24type FencedTargetOutcome struct {
25 VerifiedDirty bool
26 Transition *moves.Transition
27 ClaimRolledBack bool
28}
29
30// ReleaseFencedTarget advances one record-first recovery step per grant:
31// record decision, claim release with parks, then record release. Each pass
32// verifies unconsumed authority; completion uses the durable claim evidence.
33func (g *MoveRecoveryGrant) ReleaseFencedTarget(ctx context.Context, ref hostclaim.FenceRef) (FencedTargetOutcome, error) {
34 var out FencedTargetOutcome
35 err := g.releaseFencedTarget(ctx, ref, &out)
36 return out, err
37}
38
39func (g *MoveRecoveryGrant) releaseFencedTarget(ctx context.Context, ref hostclaim.FenceRef, out *FencedTargetOutcome) error {
40 if g == nil || g.state == nil {
41 return ErrPending
42 }
43 g.state.mu.Lock()
44 defer g.state.mu.Unlock()
45 o, record, rec, err := g.current(ctx)
46 if err != nil {
47 return err
48 }
49
50 m := rec.Move
51 if m == nil || m.ID != g.o.OpID || m.Incarnation != g.o.Incarnation {
52 return ErrIdentity
53 }
54 guard := provision.MoveGuard{ID: m.ID, Incarnation: m.Incarnation}
55 writeRecord := func() error {
56 body, err := g.prepareRecord(ctx, rec)
57 if err != nil {
58 return err
59 }
60 if _, _, err = provision.WriteRecord(provision.WithRecordSequence(ctx), g.w.raw, record.Holder.Key, record.Body, body, record.ETag); err != nil {
61 return err
62 }
63 if before, err := provision.DecodeRecord(record.Body); err == nil {
64 if t, ok := moves.TransitionOf(before, rec); ok {
65 out.Transition = &t
66 }
67 }
68 return nil
69 }
70 if consumed, ok := hostclaim.ConsumedMoveTargetFence(o.Claim); ok {
71 if consumed != ref || m.Rollback == nil || m.State != provision.MoveRollingBack || rec.Region != m.From {
72 return ErrIdentity
73 }
74 rb := m.Rollback
75 if (rb.TargetPhase == provision.RbTargetReleased || rb.TargetPhase == provision.RbTargetCleaned && rb.TargetParkedAt != nil) && !rb.Dirty {
76 if rb.LossRef == ref.Key && rb.Evidence == ref.Key {
77 return nil
78 }
79 return g.verifyReleasedTarget(ctx, o, rec)
80 }
81 if rb.LossRef != ref.Key {
82 return ErrIdentity
83 }
84 if err := provision.MoveRollbackLossCommitted(guard)(&rec); err != nil {
85 return err
86 }
87 return writeRecord()
88 }
89 fence, err := hostclaim.VerifyMoveTargetFence(ctx, g.w.raw, o, ref, MoveFenceRegions{Authority: g.authority, HomeRegion: o.Claim.Op.Intent["home_region"]})
90 if err != nil {
91 return err
92 }
93 if m.State == provision.MoveLanding {
94 if rec.Region != m.To {
95 return ErrIdentity
96 }
97 id, err := moves.NewRollbackID(rand.Reader)
98 if err != nil {
99 return err
100 }
101 if err := provision.MoveRollBack(guard, id, provision.RollbackTargetFenced, g.w.now())(&rec); err != nil {
102 return err
103 }
104 } else if m.State != provision.MoveRollingBack || m.Rollback == nil || rec.Region != m.From {
105 return ErrIdentity
106 }
107 rb := m.Rollback
108 if rb.Dirty {
109 out.VerifiedDirty = true
110 return ErrPending
111 } // J4: a dead dirty target cannot build a return lane.
112 released := rb.TargetPhase == provision.RbTargetReleased && !rb.Dirty
113 if released {
114 if err := g.verifyReleasedTarget(ctx, o, rec); err != nil {
115 return err
116 }
117 if o.Claim.Op.Kind != hostclaim.KindRestoring || o.Claim.Op.Phase != hostclaim.PhaseReleasing || o.Claim.Op.Exec != o.Claim.Op.Intent["target"] {
118 return ErrPending
119 }
120 } else if rb.TargetPhase != provision.RbTargetLost || rb.LossRef != ref.Key {
121 if err := provision.MoveRollbackTargetLost(guard, ref.Key, g.w.now())(&rec); err != nil {
122 return err
123 }
124 return writeRecord()
125 }
126 h := o.Claim.Holder
127 // The record's codes become engine regions (park.PlacementRegion): a
128 // move out of home names its source park.HomeRegion, never "", which
129 // the claim refuses (home-region parks, part 1).
130 r := hostclaim.FencedTargetRelease{OpID: m.ID, RollbackID: rb.ID, Target: o.Claim.Op.Intent["target"], TargetRegion: park.PlacementRegion(m.To), SourceRegion: park.PlacementRegion(m.From),
131 Location: moves.TargetLocationScope(*h, fence.ObservedAt()), Lane: moves.StagingScope(o.Host, m.ID, h.Incarnation, fence.ObservedAt()), NewID: moveLossNewID(ref.SHA256), Fence: fence}
132 if err := CheckCreateFreeze(ctx, g.w.raw, g.w.pub); err != nil {
133 return err
134 }
135 // Fence and roster reads may have raced a new record. Bind both ETag and sequence again.
136 if _, _, _, err := g.current(ctx); err != nil {
137 return err
138 }
139 g.state.active = false
140 landing := false
141 _, err = hostclaim.Update(ctx, g.w.raw, o, func(c *hostclaim.Claim) error {
142 record, err := ReadRecord(ctx, g.w.raw, h.Key)
143 if err != nil {
144 return err
145 }
146 _, seq, err := recordSequence(record.Body)
147 if err != nil {
148 return err
149 }
150 if record.ETag != g.o.RecordETag || seq != g.o.RecordSeq {
151 return release.ErrCASConflict
152 }
153 if err := moveAdmission(ctx, g.w.raw, o.Host, c); err != nil {
154 return err
155 }
156 r.Token = fencerelease.New(c.Host, c.Holder.Incarnation, c.Gen, r.OpID, r.RollbackID, r.Target)
157 defer r.Token.Revoke()
158 landing = c.Op.Kind == hostclaim.KindLanding
159 return hostclaim.ReleaseFencedTarget(r)(c)
160 })
161 out.ClaimRolledBack = err == nil && landing
162 return err
163}
164
165// The record-first release can outlive its target. Require the canonical complete report.
166func (g *MoveRecoveryGrant) verifyReleasedTarget(ctx context.Context, o hostclaim.Observed, rec provision.Record) error {
167 m, h := rec.Move, o.Claim.Holder
168 if m == nil || m.Rollback == nil || m.Rollback.Dirty || (m.Rollback.TargetPhase != provision.RbTargetReleased && (m.Rollback.TargetPhase != provision.RbTargetCleaned || m.Rollback.TargetParkedAt == nil)) {
169 return ErrIdentity
170 }
171 key := moves.StoppedKey(moves.ReportPrefix(h.Account, h.App, o.Host, m.ID), "target")
172 if m.Rollback.Evidence != key {
173 return ErrIdentity
174 }
175 raw, _, err := g.w.raw.Get(ctx, key)
176 if err != nil {
177 return err
178 }
179 var report moves.StoppedReport
180 if json.Unmarshal(raw, &report) != nil || !report.Complete() || report.Role != "target" || report.Box != o.Claim.Op.Intent["target"] || report.Host != o.Host || report.OpID != m.ID || report.Incarnation != h.Incarnation {
181 return ErrIdentity
182 }
183 return nil
184}
185
186// StampLostMoveTargetParked uses the same expiring, record-sequenced grant as closure.
187func (g *MoveRecoveryGrant) StampLostMoveTargetParked(ctx context.Context) error {
188 if g == nil || g.state == nil {
189 return ErrPending
190 }
191 g.state.mu.Lock()
192 defer g.state.mu.Unlock()
193 o, r, rec, err := g.current(ctx)
194 if err != nil {
195 return err
196 }
197 m := rec.Move
198 if m == nil || m.ID != g.o.OpID || m.Incarnation != g.o.Incarnation || m.Rollback == nil || !hostclaim.LostMoveTargetParked(o.Claim, park.PlacementRegion(m.To)) {
199 return ErrPending
200 }
201 if m.Rollback.TargetParkedAt != nil {
202 return nil
203 }
204 if err := provision.MoveLostTargetParked(provision.MoveGuard{ID: m.ID, Incarnation: m.Incarnation}, m.Rollback.ID, g.w.now())(&rec); err != nil {
205 return err
206 }
207 body, err := g.prepareRecord(ctx, rec)
208 if err != nil {
209 return err
210 }
211 _, _, err = provision.WriteRecord(provision.WithRecordSequence(ctx), g.w.raw, r.Holder.Key, r.Body, body, r.ETag)
212 return err
213}
214