# 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.