| 1 | package hostbox |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | |
| 7 | "github.com/carlosframework/platform/internal/hostclaim" |
| 8 | "github.com/carlosframework/platform/internal/hostguard" |
| 9 | "github.com/carlosframework/platform/internal/registry" |
| 10 | ) |
| 11 | |
| 12 | // AdmissionFenceClosed is Reconciler.FenceClosed over this box's host |
| 13 | // guard (region HA §5.3, R5-3): whether the host's admission is closed |
| 14 | // for the installation the guard record has active, on the claim |
| 15 | // observed, read exactly as With reads it (hostguard.Guard.AdmissionFenced). |
| 16 | // Root writes that fence durably when it begins the source's stop |
| 17 | // (hostguard.Guard.BeginFence, before any privileged stop work), so it |
| 18 | // survives an agent restart and a completed stop still reads closed. It |
| 19 | // is not the activator's in-process fence (FenceAndStopAs), which a |
| 20 | // restart empties. A record that cannot be read, or a claim for another |
| 21 | // host or incarnation, is an error, never "open": absence is never |
| 22 | // evidence, and the Reconciler then keeps every row and mints none. It |
| 23 | // takes no lock: Install asks it outside the host lock and holdOnly |
| 24 | // under it, and the record is replaced atomically. |
| 25 | func AdmissionFenceClosed(g hostguard.Guard) func(context.Context, hostclaim.Observed) (bool, error) { |
| 26 | return func(ctx context.Context, o hostclaim.Observed) (bool, error) { |
| 27 | if err := ctx.Err(); err != nil { |
| 28 | return false, err |
| 29 | } |
| 30 | captured, err := g.Capture(o.Host) |
| 31 | if err != nil { |
| 32 | return false, err |
| 33 | } |
| 34 | return g.AdmissionFenced(o.Host, o.Claim, captured) |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | // RegistryHoldRow is Reconciler.HoldRow over this box's registry (R5-3, |
| 39 | // and the completed relocation's catch-up hold, L18 finding 3): |
| 40 | // it keeps the host's existing row held for op in "relocating", a status |
| 41 | // the edge never serves (registry.HoldStatuses), with registry.SetHoldIf |
| 42 | // expecting the row unheld, the conditional write moves' holdExisting |
| 43 | // makes. A row already held for op is a replay and is left exactly as it |
| 44 | // is: nothing is written. A row another op holds is never taken, and |
| 45 | // neither is one whose hold changed between the read and the write |
| 46 | // (SetHoldIf matches only an unheld row): both are ErrChanged with |
| 47 | // nothing written, and the next pass judges afresh. No row is ErrChanged |
| 48 | // and nothing is minted (ruling 17); a row that cannot be read is its |
| 49 | // error. Neither is ever "held". The write sets status and hold_op only, |
| 50 | // so the row's claim ownership stays as it was and the park-local's |
| 51 | // retirement, which deletes by ownership whatever the hold, still finds |
| 52 | // it. |
| 53 | func RegistryHoldRow(reg *registry.Registry) func(context.Context, string, string) error { |
| 54 | return func(ctx context.Context, host, op string) error { |
| 55 | if reg == nil || host == "" || op == "" { |
| 56 | return ErrUnsupported |
| 57 | } |
| 58 | if err := ctx.Err(); err != nil { |
| 59 | return err |
| 60 | } |
| 61 | row, err := reg.Get(host) |
| 62 | if err != nil { |
| 63 | return err |
| 64 | } |
| 65 | switch { |
| 66 | case row == nil: |
| 67 | return fmt.Errorf("%w: %s has no row to hold for %s", ErrChanged, host, op) |
| 68 | case row.HoldOp == op && registry.IsHold(row.Status): |
| 69 | return nil |
| 70 | case row.HoldOp != "": |
| 71 | return fmt.Errorf("%w: %s's row is held by %q, not %s", ErrChanged, host, row.HoldOp, op) |
| 72 | } |
| 73 | held, err := reg.SetHoldIf(host, "relocating", op, "") |
| 74 | if err != nil { |
| 75 | return err |
| 76 | } |
| 77 | if !held { |
| 78 | return fmt.Errorf("%w: %s's row changed before it was held for %s", ErrChanged, host, op) |
| 79 | } |
| 80 | return nil |
| 81 | } |
| 82 | } |
| 83 | |