Gunnar

nordisk/gunnar

.nornir/data-set-grow-drain.md

# Draining for a data-set growth — the four steps, today and with hot-plug

Written 2026-09-14 beside `private-gunnar-ops/DATA-SET-GROWTH-DESIGN.md`
(the contract) and the boot-stack grow in edda's
`korp-installer/src/grow.rs`. **Design only — no code implements the
hot-plug half yet**; §9 of the contract keeps no-restart growth out of scope
until the tunnr kernel has `CONFIG_HOTPLUG_PCI`. This note fixes the shape
of the drain, the journal and the `Draining{kind: growing}` announcement so
that the same four steps work unchanged when hot-plug exists.

## The four steps gunnar takes when it drains for a growth

1. **Stop admitting new git sessions.** The SSH front door refuses a new
   transfer with the growing sentence from the contract's §8 (through the
   standby's `err_packet`), and `Bootstrap.Status.serving_git` reads false
   with `readiness` naming the growth. The control plane stays up — it lives
   on the boot disk, not on `/data`.
2. **Wait for in-flight transfers to finish**: the admission counter (the
   one the shutdown path already waits on) reaching zero. Bounded by the
   existing drain timeout; a transfer that will not finish is cut the way a
   shutdown cuts it, never left holding `/data` open.
3. **`sync`, then unmount `/data`.** Everything the stores wrote is on the
   platter before the filesystem goes, and the unmount is the proof that no
   handle is open (an `EBUSY` is a finding, not something to force).
4. **Later, remount** — and only after gunnar has verified with its own
   eyes that the set is what it expects (below).

With today's kernel step 4's "later" is **the next boot**: `korp-start`
(`grow.rs`, G1–G5) grows and remounts before gunnar is exec'd, so nothing in
gunnar implements a remount. Steps 1–3 are the existing shutdown drain; the
growth only gives it a *kind*.

## The announcement

`Draining{kind: growing}` on the control plane (the `Draining` state the
shop-presence contract already carries, with a `kind` field — `growing` is
the first kind). It is what gunnar-ui turns into the customer sentence and
what monetize waits on before it stops the server. It is raised BEFORE step
1 and cleared by `Ready` after the remount verified.

## The remount trigger, when hot-plug exists

- **Primary: an explicit `resume` verb on the control plane** (`Admin.
  GrowResume`, or a `resume` arm on whatever verb raises the drain). The
  plane is on the boot disk and keeps answering while `/data` is unmounted.
  monetize calls it after `GET /1.3/storage` shows all four volumes
  `online` at the new size. An explicit verb, because a remount is a
  decision an operator can audit, not a side effect of a device appearing.
- **Fallback: a kernel uevent / periodic rescan.** A `change`/`add` uevent
  on a member's block device, or a rescan every N seconds while draining,
  runs the same verification; it never remounts on its own before the
  verification passes, and it logs what it saw either way. This is what
  makes a metal box, where nobody calls the verb, remount itself.
- **Either way gunnar verifies before it grows or remounts**: four members
  present, every one larger than the set it left, all equal within 1 %
  (the same `grow::detect` rule the boot path uses — one implementation,
  reached through the same crate). Anything else is a refusal by name on
  the control plane (`partial resize: k of 4`), the drain stays up, and
  nothing is written.

## The journal is already shaped for this

`/serialdat/grow-journal` (`grow::Record`: `epoch, target_sectors, member,
step, at`) does not care whether a step ran in `korp-start` or in gunnar: a
hot-plug grow appends the same G1…G6 records from the same engine, and the
disk — never the journal — says which steps are already true. The G4 door
(`Grow::xfs_allowed`) is what keeps a hot remount from growing the xfs
before every superblock agrees, exactly as at boot.

## What monetize sees

`Admin.Capacity.growth`: `GROWTH_AT_RESTART` on a box whose boot stack
carries the engine (today), later a `GROWTH_HOT` value when hot-plug lands —
added, never renumbered. `grow_state` carries the journal's last epoch and
verdict either way, so the operator console reads one story.