| 1 | package hostclaim |
| 2 | |
| 3 | import ( |
| 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. |
| 15 | type 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. |
| 25 | func 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). |
| 50 | func 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. |
| 61 | type 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. |
| 76 | const 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. |
| 84 | func 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. |
| 113 | func 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. |
| 129 | func 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). |
| 151 | func 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 | |