package hostclaim import ( "encoding/json" "fmt" "slices" "time" ) // ProbeStop is the target's journaled stop of its passed probe session // (region HA ruling 49): root's full-stop receipt for the writer-session // stop (hostguard.WriterStopObligation) of exactly that session, taken // while the op is still relocating/probing. The serving CAS requires it, // so the kind change never strands a running session. type ProbeStop struct { Box string `json:"box"` Session uint64 `json:"session"` Receipt string `json:"receipt"` ReceiptSHA256 string `json:"receipt_sha256"` } // RecordProbeStop journals the executor's stop of its passed probe session. // It is bound to op.exec, the journaled passing probe and the holder's // current session; a different stop already journaled is refused. func RecordProbeStop(opID string, s ProbeStop) func(*Claim) error { return func(c *Claim) error { if err := requireRelocation(c, opID, PhaseRelocatingProbing); err != nil { return err } var p Probe if !readJournal(c.Op, "probe", &p) || p.Box != c.Op.Exec || p.Session != c.Holder.Session { return fmt.Errorf("%w: no passing probe of the current session to stop", ErrRelocation) } if s.Box != p.Box || s.Session != p.Session || s.Receipt == "" || !isHex64(s.ReceiptSHA256) { return fmt.Errorf("%w: the stop is not of the probe's session", ErrRelocation) } var old ProbeStop if readJournal(c.Op, "probe_stop", &old) { if old == s { return ErrAlreadyRecorded } return fmt.Errorf("%w: a different probe stop is journaled", ErrRelocation) } return journalJSON(c, "probe_stop", s) } } // probeStopped reports whether the journaled probe stop names the passed // probe's session (CompleteRelocation's gate). func probeStopped(c *Claim, p Probe) bool { var s ProbeStop return readJournal(c.Op, "probe_stop", &s) && s.Box == p.Box && s.Session == p.Session && s.Receipt != "" } // CompletedRelocation is the serving op's record of the relocation it // ended (region HA ruling 56): the op, and the probe session's stop that // root receipted at relocating/probing. The next serving cold wake on Box // hands over from exactly that receipt (ColdHandover.Run takes the prior // receipt's target as it is), and the participation discharge (claim ask // C10) reads it. Journaled by CompleteRelocation under JournalCompleted. type CompletedRelocation struct { Op string `json:"op"` Stop ProbeStop `json:"stop"` Stamp uint64 `json:"stamp"` // the stopped session's installation stamp Phase string `json:"phase"` // the stop's phase: probing Generation string `json:"generation"` // the stopped session's generation // Discharged is set by MarkCompletedDischarged once the box the // relocation completed on has discharged its probe's participation // (claim ask C10). Until then no successor relocation starts from that // box (RelocationStartGuard): its participation slot is still the old // op's, and main's Prepare for a successor would answer ErrLost. Discharged bool `json:"discharged,omitempty"` } // JournalCompleted is the serving op's journal key for CompletedRelocation. const JournalCompleted = "completed_relocation" // ReadCompletedRelocation reads the serving op's record. ok is false, with // no error, only when there is none: the op is not serving, or its journal // has no JournalCompleted entry. An entry that is present but does not // decode, or names no op, box or session, is an error, never "none" // (absence is never evidence): the barrier refuses over it and the sweep // keeps the op's journals. func ReadCompletedRelocation(c *Claim) (CompletedRelocation, bool, error) { if c == nil || c.Op.Kind != KindServing { return CompletedRelocation{}, false, nil } raw, present := c.Op.Journal[JournalCompleted] if !present { return CompletedRelocation{}, false, nil } var r CompletedRelocation if err := json.Unmarshal([]byte(raw), &r); err != nil { return CompletedRelocation{}, false, fmt.Errorf("%w: the completed relocation cannot be read: %v", ErrRelocation, err) } if r.Op == "" || r.Stop.Box == "" || r.Stop.Session == 0 { return CompletedRelocation{}, false, fmt.Errorf("%w: the completed relocation names no op, box or session", ErrRelocation) } return r, true, nil } // completionBarrier is region HA ruling 77, the one barrier every start // from serving passes (StartGuard for moves, deletes, purges and // placements; RelocationStartGuard for relocations): while the serving // op's completed relocation is not marked discharged, the box it completed // on still holds that op's participation slot, and main's Prepare for any // successor operation would answer ErrLost, so nothing starts. A // completion box on lost, or the one the start itself cites as fenced // (fencedSource, a failover's source), holds nothing a successor needs: a // dead box's slot binds nothing on another box. A record that cannot be // read refuses every start, a failover's included: which box it binds is // unknown. func completionBarrier(c *Claim, fencedSource string) error { done, ok, err := ReadCompletedRelocation(c) if err != nil { return err } if !ok || done.Discharged || slices.Contains(c.Lost, done.Stop.Box) || (fencedSource != "" && fencedSource == done.Stop.Box) { return nil } return fmt.Errorf("%w: %w: relocation %s on %s", ErrPendingChore, ErrRelocationFinishing, done.Op, done.Stop.Box) } // MarkCompletedDischarged records, on the serving op, that the box the // relocation opID completed on has discharged its participation. It // changes nothing else; a repeat is ErrAlreadyRecorded. A serving op with // no completed relocation is ErrWrongState, and one that records another // op, or a record that cannot be read, is ErrRelocation. func MarkCompletedDischarged(opID string) func(*Claim) error { return func(c *Claim) error { r, ok, err := ReadCompletedRelocation(c) if err != nil { return err } if !ok { return fmt.Errorf("%w: no completed relocation on the serving op", ErrWrongState) } if r.Op != opID { return fmt.Errorf("%w: the serving op records %s, not %s", ErrRelocation, r.Op, opID) } if r.Discharged { return ErrAlreadyRecorded } r.Discharged = true return journalJSON(c, JournalCompleted, r) } } // ToInstalledAt is ToInstalled that also journals installed_at, the start // of the installed bound (ruling 60). func ToInstalledAt(opID string, a PreparedAuthority, now time.Time) func(*Claim) error { inner := ToInstalled(opID, a) return func(c *Claim) error { if now.IsZero() { return fmt.Errorf("%w: installed needs its start time", ErrRelocation) } if err := inner(c); err != nil { return err } c.Op.Journal["installed_at"] = now.UTC().Format(time.RFC3339Nano) return nil } }