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 "encoding/json"
5 "fmt"
6 "slices"
7 "time"
8)
9
10// ProbeStop is the target's journaled stop of its passed probe session
11// (region HA ruling 49): root's full-stop receipt for the writer-session
12// stop (hostguard.WriterStopObligation) of exactly that session, taken
13// while the op is still relocating/probing. The serving CAS requires it,
14// so the kind change never strands a running session.
15type ProbeStop struct {
16 Box string `json:"box"`
17 Session uint64 `json:"session"`
18 Receipt string `json:"receipt"`
19 ReceiptSHA256 string `json:"receipt_sha256"`
20}
21
22// RecordProbeStop journals the executor's stop of its passed probe session.
23// It is bound to op.exec, the journaled passing probe and the holder's
24// current session; a different stop already journaled is refused.
25func RecordProbeStop(opID string, s ProbeStop) func(*Claim) error {
26 return func(c *Claim) error {
27 if err := requireRelocation(c, opID, PhaseRelocatingProbing); err != nil {
28 return err
29 }
30 var p Probe
31 if !readJournal(c.Op, "probe", &p) || p.Box != c.Op.Exec || p.Session != c.Holder.Session {
32 return fmt.Errorf("%w: no passing probe of the current session to stop", ErrRelocation)
33 }
34 if s.Box != p.Box || s.Session != p.Session || s.Receipt == "" || !isHex64(s.ReceiptSHA256) {
35 return fmt.Errorf("%w: the stop is not of the probe's session", ErrRelocation)
36 }
37 var old ProbeStop
38 if readJournal(c.Op, "probe_stop", &old) {
39 if old == s {
40 return ErrAlreadyRecorded
41 }
42 return fmt.Errorf("%w: a different probe stop is journaled", ErrRelocation)
43 }
44 return journalJSON(c, "probe_stop", s)
45 }
46}
47
48// probeStopped reports whether the journaled probe stop names the passed
49// probe's session (CompleteRelocation's gate).
50func probeStopped(c *Claim, p Probe) bool {
51 var s ProbeStop
52 return readJournal(c.Op, "probe_stop", &s) && s.Box == p.Box && s.Session == p.Session && s.Receipt != ""
53}
54
55// CompletedRelocation is the serving op's record of the relocation it
56// ended (region HA ruling 56): the op, and the probe session's stop that
57// root receipted at relocating/probing. The next serving cold wake on Box
58// hands over from exactly that receipt (ColdHandover.Run takes the prior
59// receipt's target as it is), and the participation discharge (claim ask
60// C10) reads it. Journaled by CompleteRelocation under JournalCompleted.
61type CompletedRelocation struct {
62 Op string `json:"op"`
63 Stop ProbeStop `json:"stop"`
64 Stamp uint64 `json:"stamp"` // the stopped session's installation stamp
65 Phase string `json:"phase"` // the stop's phase: probing
66 Generation string `json:"generation"` // the stopped session's generation
67 // Discharged is set by MarkCompletedDischarged once the box the
68 // relocation completed on has discharged its probe's participation
69 // (claim ask C10). Until then no successor relocation starts from that
70 // box (RelocationStartGuard): its participation slot is still the old
71 // op's, and main's Prepare for a successor would answer ErrLost.
72 Discharged bool `json:"discharged,omitempty"`
73}
74
75// JournalCompleted is the serving op's journal key for CompletedRelocation.
76const JournalCompleted = "completed_relocation"
77
78// ReadCompletedRelocation reads the serving op's record. ok is false, with
79// no error, only when there is none: the op is not serving, or its journal
80// has no JournalCompleted entry. An entry that is present but does not
81// decode, or names no op, box or session, is an error, never "none"
82// (absence is never evidence): the barrier refuses over it and the sweep
83// keeps the op's journals.
84func ReadCompletedRelocation(c *Claim) (CompletedRelocation, bool, error) {
85 if c == nil || c.Op.Kind != KindServing {
86 return CompletedRelocation{}, false, nil
87 }
88 raw, present := c.Op.Journal[JournalCompleted]
89 if !present {
90 return CompletedRelocation{}, false, nil
91 }
92 var r CompletedRelocation
93 if err := json.Unmarshal([]byte(raw), &r); err != nil {
94 return CompletedRelocation{}, false, fmt.Errorf("%w: the completed relocation cannot be read: %v", ErrRelocation, err)
95 }
96 if r.Op == "" || r.Stop.Box == "" || r.Stop.Session == 0 {
97 return CompletedRelocation{}, false, fmt.Errorf("%w: the completed relocation names no op, box or session", ErrRelocation)
98 }
99 return r, true, nil
100}
101
102// completionBarrier is region HA ruling 77, the one barrier every start
103// from serving passes (StartGuard for moves, deletes, purges and
104// placements; RelocationStartGuard for relocations): while the serving
105// op's completed relocation is not marked discharged, the box it completed
106// on still holds that op's participation slot, and main's Prepare for any
107// successor operation would answer ErrLost, so nothing starts. A
108// completion box on lost, or the one the start itself cites as fenced
109// (fencedSource, a failover's source), holds nothing a successor needs: a
110// dead box's slot binds nothing on another box. A record that cannot be
111// read refuses every start, a failover's included: which box it binds is
112// unknown.
113func completionBarrier(c *Claim, fencedSource string) error {
114 done, ok, err := ReadCompletedRelocation(c)
115 if err != nil {
116 return err
117 }
118 if !ok || done.Discharged || slices.Contains(c.Lost, done.Stop.Box) || (fencedSource != "" && fencedSource == done.Stop.Box) {
119 return nil
120 }
121 return fmt.Errorf("%w: %w: relocation %s on %s", ErrPendingChore, ErrRelocationFinishing, done.Op, done.Stop.Box)
122}
123
124// MarkCompletedDischarged records, on the serving op, that the box the
125// relocation opID completed on has discharged its participation. It
126// changes nothing else; a repeat is ErrAlreadyRecorded. A serving op with
127// no completed relocation is ErrWrongState, and one that records another
128// op, or a record that cannot be read, is ErrRelocation.
129func MarkCompletedDischarged(opID string) func(*Claim) error {
130 return func(c *Claim) error {
131 r, ok, err := ReadCompletedRelocation(c)
132 if err != nil {
133 return err
134 }
135 if !ok {
136 return fmt.Errorf("%w: no completed relocation on the serving op", ErrWrongState)
137 }
138 if r.Op != opID {
139 return fmt.Errorf("%w: the serving op records %s, not %s", ErrRelocation, r.Op, opID)
140 }
141 if r.Discharged {
142 return ErrAlreadyRecorded
143 }
144 r.Discharged = true
145 return journalJSON(c, JournalCompleted, r)
146 }
147}
148
149// ToInstalledAt is ToInstalled that also journals installed_at, the start
150// of the installed bound (ruling 60).
151func ToInstalledAt(opID string, a PreparedAuthority, now time.Time) func(*Claim) error {
152 inner := ToInstalled(opID, a)
153 return func(c *Claim) error {
154 if now.IsZero() {
155 return fmt.Errorf("%w: installed needs its start time", ErrRelocation)
156 }
157 if err := inner(c); err != nil {
158 return err
159 }
160 c.Op.Journal["installed_at"] = now.UTC().Format(time.RFC3339Nano)
161 return nil
162 }
163}
164