carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "log"
8
9 "github.com/carlosframework/platform/internal/hostclaim"
10 "github.com/carlosframework/platform/internal/provision"
11 "github.com/carlosframework/platform/internal/release"
12)
13
14// The target's staging park after the commit and the close (moves spec §7.9
15// steps 4 and 5; Plan 5 Task 3 with rulings K4, K6, K9 and K15), behind
16// CARLOS_MOVES_PHASE1. Both run in Plan 3's committed arm on the target
17// (committedTarget, moveslandcommit.go), routed as it is; there is no second
18// committed dispatcher.
19//
20// Step 4, the staging park. The commit queued the lane's park-replica for
21// the target region in its own write, scoped to moves/<host>/<op> with the
22// seal's bound as its evidence (moves.CommitScopes, the one builder the
23// console's pass uses too, K9). After the claim half and the open, every
24// pass while the record is committed runs it through laneParkTarget with the
25// claim at landing/committed (K6, the abort's and the revert's dispatcher):
26// the park engine over this box's own bucket archives the lane, answers
27// ErrPending until seal bound + SignatureAge + RequestLimit, and then
28// acknowledges on a clean listing. A late request found by the verification
29// listing is a further retirement event under the same scope.
30//
31// Step 5, the close (closeMove). Any participant that sees opened_at and
32// source_parked_at on the record and no obligation of the op in the claim
33// closes: the terminal event `done` (moves.LogEvent when the hook is wired,
34// Plan 6; a log line until then), then provision.MoveClose (the record
35// first: LastMove{Outcome: "done"}, index D11), then hostclaim.ReleaseMove
36// to serving under the target (§7.5, §19 K8 as Paul's ruling M4 amends
37// them: record first, then the claim's release). A crash between the two
38// leaves the record at rest and the claim still landing/committed with
39// nothing owed; no box pass visits a record without a move again, and the
40// console's recovery pass releases from LastMove (Recovery.Moves, Plan 5
41// Task 7). A claim found already released by this move (a binary that
42// closed claim first) gets only the record's close. The target calls it
43// every committed pass; the source calls it in the pass that stamps
44// source_parked_at, which is the last pass that routes the record to it.
45//
46// Production (K4). The staging park and the close are production-live only
47// after Plan 5 Task 9 (park arming) and claim Plans 10 and 11. The committed
48// arm runs only where the landing is armed (landingArmed), and production
49// sets the claim's park leases, view and authorizer (armMovesParks, Plan 5
50// Task 9a) only once the wake-source gate opens (movesWakeSourceRegistered,
51// Plan 5 Task 9b); the commit and the open themselves need the probe wake of
52// claim Plan 11's launcher; and the source's location park is acknowledged
53// by C4's regional runner, whose moves evidence is claim Plan 10's. Until
54// then the tests arm everything with fakes.
55
56// closeMove is the close (§7.9 step 5; see the file comment). fault is the
57// caller's test seam, passed "after-close-record" between the record's close
58// and the claim's release. It does nothing until the record carries both
59// stamps and the claim owes nothing under the op.
60func (w *movesWorker) closeMove(ctx context.Context, s release.Store, rec provision.Record, fault func(string) error) error {
61 host, id, inc := rec.Host, rec.Move.ID, rec.Move.Incarnation
62 fresh, err := provision.GetRecord(ctx, s, rec.Account, rec.App, host)
63 if err != nil {
64 return fmt.Errorf("re-read the record: %w", err)
65 }
66 if fresh == nil {
67 return nil
68 }
69 m := fresh.Move
70 if m == nil || m.Legacy() || m.ID != id || m.Incarnation != inc || m.State != provision.MoveCommitted {
71 return nil
72 }
73 if m.OpenedAt == nil || m.SourceParkedAt == nil {
74 w.notice(host+"#close", "moves: %s: move %s closes once the target has opened and the source has parked (opened %t, parked %t)", host, id, m.OpenedAt != nil, m.SourceParkedAt != nil)
75 return nil
76 }
77 obs, err := hostclaim.Read(ctx, s, host)
78 if err != nil {
79 return fmt.Errorf("read the host claim: %w", err)
80 }
81 if obs.Absent {
82 return fmt.Errorf("move %s has no host claim", id)
83 }
84 c := obs.Claim
85 // A claim this move already released was released by a binary that
86 // closed claim first: only the record's half is left.
87 released := hostclaim.RequireReleasedMove(id)(c) == nil
88 target := ""
89 if !released {
90 if c.Op.ID != id || c.Op.Kind != hostclaim.KindLanding || c.Op.Phase != hostclaim.PhaseCommitted || len(c.Op.Participants) != 2 {
91 w.notice(host+"#close", "moves: %s: move %s waits to close: the claim is %s/%q (%s), not this move's commit", host, id, c.Op.Kind, c.Op.Phase, c.Op.ID)
92 return nil
93 }
94 if c.Holder == nil || c.Holder.Incarnation != inc {
95 return fmt.Errorf("move %s: the host claim names another incarnation", id)
96 }
97 // Nothing owed under the op, checked here before the record's
98 // close (M4), as the console's grant checks it: a record closed
99 // over an owed chore would leave a release ReleaseMove refuses.
100 if owed := owedUnder(c, id); owed > 0 {
101 w.notice(host+"#close", "moves: %s: move %s waits for %d park(s) of the move to be acknowledged before it closes", host, id, owed)
102 return nil
103 }
104 target = c.Op.Participants[1]
105 }
106 g := provision.MoveGuard{ID: id, Incarnation: inc}
107 var pre provision.Record
108 closed, err := provision.SetRecordMove(ctx, s, fresh.Account, fresh.App, host, observedMove(&pre, provision.MoveClose(g, w.now())))
109 if err != nil {
110 if errors.Is(err, provision.ErrMoveConflict) {
111 log.Printf("moves: %s: the close of move %s refused (%v); the record moved on", host, id, err)
112 return nil
113 }
114 return fmt.Errorf("close the move: %w", err)
115 }
116 w.logTransition(ctx, s, pre, *closed)
117 log.Printf("moves: %s: move %s is done; the instance is in %s", host, id, m.To)
118 nudgeSerial(ctx, s)
119 if released {
120 return nil
121 }
122 if fault != nil {
123 if err := fault("after-close-record"); err != nil {
124 return err
125 }
126 }
127 if err := releaseAfterClose(ctx, s, host, id, hostclaim.ReleaseMove(id, target)); err != nil {
128 return err
129 }
130 log.Printf("moves: %s: the claim is serving under %s after move %s", host, target, id)
131 return nil
132}
133
134// owedUnder counts the obligations of the claim under op id.
135func owedUnder(c *hostclaim.Claim, id string) int {
136 owed := 0
137 for _, o := range c.Obligations {
138 if o.Op == id {
139 owed++
140 }
141 }
142 return owed
143}
144
145// releaseAfterClose is the second half of every box close (ruling M4): the
146// record already carries the outcome in LastMove, so the claim is released
147// from a fresh read with the op's own release transition. A claim this move
148// already released (the console's pass finished it first) is success. Any
149// other failure is returned and left to the console's recovery pass, which
150// releases from LastMove: the box pass never visits a record without a move
151// again.
152func releaseAfterClose(ctx context.Context, s release.Store, host, id string, release func(*hostclaim.Claim) error) error {
153 obs, err := hostclaim.Read(ctx, s, host)
154 if err != nil {
155 return fmt.Errorf("read the host claim to release move %s: %w", id, err)
156 }
157 if obs.Absent {
158 return fmt.Errorf("move %s has no host claim to release", id)
159 }
160 if hostclaim.RequireReleasedMove(id)(obs.Claim) == nil {
161 return nil
162 }
163 if _, err := hostclaim.Update(ctx, s, obs, release); err != nil {
164 return fmt.Errorf("release the claim after the close of move %s (the console's recovery pass finishes it): %w", id, err)
165 }
166 return nil
167}
168