Skip to content

pool: Manager carries most of the node's state machine #419

Open

Description

@CMGS

sandboxd/pool holds most of a node's state machine in one type. This issue is for discussion: it proposes no change yet.

The shape today

Counted on main at d383d32:

  • pool has 9251 production lines in 36 files, 37% of sandboxd's 24742. Its tests are 21883 lines in 58 files.
  • Manager has 351 methods and 95 fields. Volumes, archives, replication, templates, healing, the journals and the tick loops all hang off it.
  • m.mu is taken at 123 sites in 28 files. It guards pools, claimed, archiving, pendingCks, tenantLive, draining and poolVersion together.

Production lines by area:

area files lines
Archive and failover archive, failover, replica, lease, reconcile 1944
Claims and lifecycle claim, hibernate, operator, liveness, fork, drain, remove 1818
Warm pools pool, refill, watermark, setpools, poolstore, reload 1813
Capture and templates checkpoint, template, capture, reader, reclock, digest 1292
Volumes volume, volume_mutation, volume_catalog, disk 1103
Persistence and telemetry claims, journal, telemetry, stats, view 851
Tenants and egress env, egress, tenants 430

Why it may be worth doing

  • Following one archive flow means holding archive.go, failover.go, replica.go, lease.go, reconcile.go and claim.go at once.
  • Manager's fields state their locking in scattered comments, and related state sits apart: the archive*, heal* and *Sweep fields are spread through the struct.
  • The outbound extraction (outbound: a sandbox's doors, proxy and NIC lock move out of pool into their own package #314) moved a sandbox's doors, proxy and NIC lock out of pool behind outbound.Host, now 1113 production lines, so a move of this kind has a precedent here.

Why it may not be

  • Most areas change a types.Sandbox under m.mu, and the invariants span areas: a claim moves between running, hibernated, archived and replicated. A split along file lines can leave one lock shared across packages, which is more coupling, not less.
  • A large move costs review time and risks losing a rollback ordering that only a test records today.
  • An interface call or an extra lock on the claim path has to pay for itself: performance is a product requirement.

Candidates, cheapest first

  1. Group Manager's fields into named structs with a stated owner and lock, with no package move. It costs little and documents the locking.
  2. Pure data with no m.mu: the volume catalog's validation, the watermark maths, the journal and telemetry types. Little risk, little gain.
  3. Tick loops: Run hand-writes five tickers and their fan-out. A registered list means a new sweep does not touch Run, at the cost of an indirection that may not earn its lines.
  4. Replication and failover behind a small interface, as outbound was. The largest gain and the largest risk: it needs an answer on m.mu first.

To decide

  • Is the goal fewer lines to hold in the head, or independently testable packages? They lead to different cuts.
  • Must m.mu stay one lock? If so, candidate 4 is off.
  • Hot-path rule for any split: claim and release keep their cost, shown by an interleaved A/B.

Any change lands as behaviour-preserving steps, one area per PR.

Activity

  1. CMGS commented on Nov 3, 2026

    @CMGS
    ContributorAuthor

    Direction after measuring the tree with every open audit PR applied (#420, #422, #424, #425, #426, #427, #428, #429, #430, #431 plus a follow-up cleanup). Regex counts, so treat them as approximate. No change lands from this comment.

    The shape now

    • pool: 9440 production lines, Manager with 362 methods, m.mu.Lock at 130 sites (claims and lifecycle 38, archive and failover 35, volumes 20).
    • 255 of the 362 methods never write m.mu in their own body. They hang off Manager for its fields, not because they hold its lock.

    Distinct method pairs called across areas, caller on the left:

    claim warm failover capture volume
    claim 116 25 19 3 28
    warm 14 69 5 1 1
    failover 27 17 138 6 9
    capture 8 8 2 68 4
    volume 1 1 0 0 48
    • Claims and failover call each other (27 and 19 pairs), write nearly all types.Sandbox fields between them (35 and 39 sites) and share m.claimed (21 and 19 uses). The recent archive rollback, intent settle and unfence bugs all crossed that line.
    • Volumes call out least, and catalog, volumeRuns and volumeErrors are almost only theirs. Their admission still needs m.mu.
    • Heal, the template cache and the record locks already have their own locks and data. Only their fields sit on Manager.

    Answers to the open questions

    • Goal: fewer things to hold in the head, not independently testable packages. The hard bugs live on the state machine across areas, which a package split would not test.
    • m.mu: stays one lock. Claim admission, quota and volume holds rely on its atomicity, and a second lock needs a cross-lock order.
    • Hot path: the steps below add no interface call and no lock. Each PR still shows the interleaved A/B this issue asks for.

    Proposal, one PR per step, behaviour-preserving, stop and assess after each

    1. Group Manager's fields into named structs with a stated owner and lock: volumes, heal, templates, recLocks, failover (fence, lease, replicas, registry), sweeps (the *Sweep atomics), limits (the semaphores).
    2. Move methods onto those small types, in this package: heal, record locks, the template cache, the volume catalog and any sweep that owns its data, so calls read m.heal.pull(...).
    3. Split the large files by flow: failover.go into row, adopt and intent; claim.go into claim, release and reap; refill.go into golden and refill.

    Not proposed

    • Failover behind an interface (candidate 4): the coupling above is too dense, and an interface would hide rollback orderings that are visible today.
    • A registered list of tick loops (candidate 3): five tickers do not repay the indirection.
    • Volumes as a subpackage: admission needs m.mu. The catalog moves in step 2 as a type in this package, which gets most of the gain.

    Production lines stay about flat, possibly slightly up for the type declarations. This is about reading the code, not about reducing it.

    Timing

    After the open audit PRs merge, to avoid rebasing every one of them over a mechanical move.

  2. CMGS commented on Nov 3, 2026

    @CMGS
    ContributorAuthor

    Every PR named above is merged, and #432 is the follow-up cleanup.

    #433 lists the windows the audit accepted in claims, warm pools and failover. The steps here keep those behaviours, so a reader does not have to re-derive them during a move.

  3. CMGS commented on Nov 4, 2026

    @CMGS
    ContributorAuthor

    Status after #438, which ran steps 1 to 3 in part, together, not one PR per step as proposed. Behaviour-preserving moves, proved by sorted-line comparison per commit.

    Step 1, fields: sweeps groups the single-flight guards; recs (recordLocks), heals (healVetoes) and ckMarks (ckMarkers) own their state.

    Step 2, methods: the record locks, the heal vetoes, the archive delete markers and the volume catalog's holder bookkeeping are now methods of their own types, in this package.

    Step 3, files: refill.go splits into refill.go, golden.go and provision.go; reap.go and archive_delete.go are new; the pool type joins its methods in watermark.go; types.go splits into env.go and volume.go.

    Not done:

    • failover.go (723 lines) into row, adopt and intent.
    • claim.go (664 lines) into claim and release.
    • The field groups volumes, templates, failover and limits.
    • Heal and template cache methods onto their own types.

    Hot path: warm claim, 8 interleaved rounds with the order swapped, pinned to one NUMA node: T-B p50 +0.0015 ms [-0.0068, +0.0097], p90 -0.0016 ms [-0.0158, +0.0127]. Clone tier, 6 rounds: T-B p50 -0.311 ms [-3.175, +2.553]. No measurable cost.

    The shape now, regex counts: 357 Manager methods, 140 m.mu.Lock sites, 9757 production lines. Manager's field count depends on how the groups are counted, so it is not compared with the 95 above.

    This stays open for the steps not done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions