package hostbox import ( "context" "fmt" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/registry" ) // AdmissionFenceClosed is Reconciler.FenceClosed over this box's host // guard (region HA ยง5.3, R5-3): whether the host's admission is closed // for the installation the guard record has active, on the claim // observed, read exactly as With reads it (hostguard.Guard.AdmissionFenced). // Root writes that fence durably when it begins the source's stop // (hostguard.Guard.BeginFence, before any privileged stop work), so it // survives an agent restart and a completed stop still reads closed. It // is not the activator's in-process fence (FenceAndStopAs), which a // restart empties. A record that cannot be read, or a claim for another // host or incarnation, is an error, never "open": absence is never // evidence, and the Reconciler then keeps every row and mints none. It // takes no lock: Install asks it outside the host lock and holdOnly // under it, and the record is replaced atomically. func AdmissionFenceClosed(g hostguard.Guard) func(context.Context, hostclaim.Observed) (bool, error) { return func(ctx context.Context, o hostclaim.Observed) (bool, error) { if err := ctx.Err(); err != nil { return false, err } captured, err := g.Capture(o.Host) if err != nil { return false, err } return g.AdmissionFenced(o.Host, o.Claim, captured) } } // RegistryHoldRow is Reconciler.HoldRow over this box's registry (R5-3, // and the completed relocation's catch-up hold, L18 finding 3): // it keeps the host's existing row held for op in "relocating", a status // the edge never serves (registry.HoldStatuses), with registry.SetHoldIf // expecting the row unheld, the conditional write moves' holdExisting // makes. A row already held for op is a replay and is left exactly as it // is: nothing is written. A row another op holds is never taken, and // neither is one whose hold changed between the read and the write // (SetHoldIf matches only an unheld row): both are ErrChanged with // nothing written, and the next pass judges afresh. No row is ErrChanged // and nothing is minted (ruling 17); a row that cannot be read is its // error. Neither is ever "held". The write sets status and hold_op only, // so the row's claim ownership stays as it was and the park-local's // retirement, which deletes by ownership whatever the hold, still finds // it. func RegistryHoldRow(reg *registry.Registry) func(context.Context, string, string) error { return func(ctx context.Context, host, op string) error { if reg == nil || host == "" || op == "" { return ErrUnsupported } if err := ctx.Err(); err != nil { return err } row, err := reg.Get(host) if err != nil { return err } switch { case row == nil: return fmt.Errorf("%w: %s has no row to hold for %s", ErrChanged, host, op) case row.HoldOp == op && registry.IsHold(row.Status): return nil case row.HoldOp != "": return fmt.Errorf("%w: %s's row is held by %q, not %s", ErrChanged, host, row.HoldOp, op) } held, err := reg.SetHoldIf(host, "relocating", op, "") if err != nil { return err } if !held { return fmt.Errorf("%w: %s's row changed before it was held for %s", ErrChanged, host, op) } return nil } }