Repository navigation
Pool head: reconciler, controller, membership and a fleet simulator - #60
Closed
data-angel wants to merge 9 commits into
Closed
data-angel wants to merge 9 commits into
data-angel wants to merge 9 commits into
Conversation
- pool-reconciler: orders a plan into make-before-break steps (prefetch, start, route, drain+stop), swaps through interim members, residency downgrades before stops; dependency-aware executor skips dependents of a failed step so a lane that never starts never strands its aliases. - pool-controller: off/advise/auto loop with urgency (coverage restored), stable-plan confirmation, cooldowns, an hourly load budget, slower hysteresis for residency-only changes, and failure backoff. - pool-sim: discrete-event fleet model (requests, admission + LRU eviction, keep-warm restore, memory guard aborts, node hangs/failures, operator actions) driving the real planner/reconciler/controller; scenarios for pair taken, pair crash, a Mac joining, a low memory estimate and noisy demand, compared against today's behaviour (off). - planner: budgets capped at the runtime memory guard ceiling (0.9 by default) so plans never rely on loads the guard would abort; variants can exclude nodes after failed starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nodes can join a head with a one-time enrollment token and keep a lease with heartbeats from `serve`, so the pool planner knows who is in the pool. Membership is inert until a token is issued; static cluster.nodes config and `cluster add-node` are unchanged. - src/pool-membership.mjs: pure membership core (tokens, enroll, heartbeat, cordon/drain/leave, ready/suspect/lost/left leases, planner node shape). Tokens and node secrets are stored only as SHA-256 hashes. - src/pool-membership-store.mjs: pool-membership.json next to the config, loaded on first use, written atomically (0600) only on changes. - Head routes: POST /gateway/pool/enroll (token only, rate limited) and /gateway/pool/heartbeat (node secret only) authenticate in their handlers; GET /gateway/pool/members is admin-read; tokens and cordon/uncordon/drain are admin writes. - src/pool-node.mjs: `lloom pool join` and the serve heartbeat loop with backoff; the secret lives in a private file or an env var. - CLI: lloom pool token create | members | cordon | uncordon | drain | join. - Planner: members merge with configured nodes (config wins on endpoint and auth; membership adds liveness, cordon and inventory). Enrolled-only nodes are planned but flagged as needing `cluster add-node` to route. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fleet simulator showed admission could approve a load the runtime memory guard was certain to abort: admission kept only reserveMemoryGb (8 GB) free, while the guard trips at 90% host use (12.8 GB free on a 128 GB Spark). After an abort the runtime is latched until a manual start, so the lane stayed down. - runtime-policy: the host check keeps max(reserve, guard margin) free and cluster node budgets are capped at the guard ceiling. - runtime-memory-safety: memorySafetyCeiling() shared by the guard, admission and pool planning. - runtime-manager: a guard abort records usedGb and consumedGb (the load's lower-bound footprint) and status reports memorySafetyLatched. - pool-catalog: raises a variant's memory to the abort lower bound and excludes a latched lane until it is started by hand. - reconciler: per-step timeouts so a stuck step cannot wedge the controller; simulator found and now covers a drain that never settled. - simulator: models the latch and fixed admission (legacyAdmission keeps the old behaviour for comparison) and logs controller hold reasons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The simulator can run nodes through pool membership (enroll, heartbeat, lease loss) so node loss is detected the way a head detects it; new lease-loss scenario (crash plus a harmless 60 s partition). - The scenario found the controller blocked for 30 minutes on a manual restart of a lane whose node had just died. Stability now tracks the moves, not just the plan; an urgent plan for a different target cancels a change in progress (executeReconcile takes an AbortSignal); the same target never preempts itself. - Reconciler re-issues an idempotent start for lanes still loading so route flips wait for them. - Register membership tests in check:js and test:unit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- lloom pool simulate <scenario> [--mode off|advise|auto|compare]: runs a
scenario through the planner, reconciler and controller; exits non-zero
when expectations fail.
- docs/pool-planning.md: reconciling, the controller, simulation and the
scenario results; runtime-policy.md: the guard margin in admission.
- Membership tests sliced the token body with split('_'), but base64url
bodies can contain '_', so a short fragment sometimes matched the state
JSON; slice the fixed prefix instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security review findings: - Medium: any enrollment token could re-enroll any node id, replacing a live node's secret, endpoint and reported capacity (and flipping a configured peer unavailable in plans). Tokens can now be bound to a node id (lloom pool token create --node-id); claiming a current member or a configured cluster.nodes id needs a token bound to it (409 otherwise). lloom pool leave / POST /gateway/pool/nodes/:id/leave frees an id. - Heartbeats are refused before the body is read unless the bearer has the node-secret shape, and are rate limited per client. - Rate limiters evict least recently used clients instead of clearing the whole map when full. - Node-reported text with control characters is dropped (no escape sequences in lloom pool members output). - join reports the saved secret path and the pool.head to set when the config update fails after enrolling, and warns about plain HTTP heads off loopback and tailnets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugs (each with a test): - A model that moved between nodes left its old lane running: an unplanned lane is now retired once every planned lane serving its models is up. - Lanes already stopping were treated as freed memory; they now hold it and get an idempotent stop that starts on that node wait for. - The planner could plan past the guard when only memorySafety was set; the catalog now passes memorySafetyCeiling() itself. - A cluster node with unknown capacity got a negative admission budget, blocking no-request plans; the clamp is floored at zero, and the guard margin rounds up. - Memory-safety abort evidence never cleared; a healthy start clears it and a measured footprint supersedes it. - An alias moved to an interim member during a swap was never moved back; the flip back is queued and ordered after the interim flip. Hardening: ticks never overlap; throwing observers cannot wedge the executor or controller; concurrency is at least 1; a lane already loading is not charged to the load budget again; one move larger than the budget may go in an empty hour; only route changes preempt; vacating a cordoned or draining node is urgent. The simulator reports draining lanes as stopping and the controller's last plan, and memory-estimate-low now asserts the abort, the latch and video planned as uncovered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Superseded by #74, which integrates the reconciler/controller/membership/simulator work rebased onto current main (package.json and CHANGELOG resolved against main; pool sources/tests added to check:js and test:unit). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacked on #59 (
lloom pool plan). This PR adds the rest of the pool head and tests it all in a deterministic fleet simulator. Nothing here is wired into a live gateway's control loop.What's in it
src/pool-reconciler.mjs): turns a plan into ordered make-before-break steps: prefetch → start → route flip → drain and stop.src/pool-controller.mjs): runs inoff,adviseorautomode.src/pool-membership*.mjs,src/pool-node.mjs): enrollment tokens, node leases and heartbeats, and cordon, drain, uncordon and leave. It comes with HTTP routes and CLI commands, and feeds liveness intopool plan.src/pool-sim.mjs,lloom pool simulate): a discrete-event model of the fleet.test/fixtures/pool/scenarios/compare today's behaviour (off) with the controller (auto).Live-path changes (please review closely)
src/runtime-policy.mjs).reserveMemoryGb(8 GB) free, while the guard aborts at 90% use (12.8 GB free on a 128 GB Spark). Admission could approve a load the guard was certain to abort, and the guard then latched the runtime until a manual start.max(reserve, total × (1 − ceiling)), and cluster node budgets are capped at the ceiling (never below 0).src/runtime-manager.mjs). Status now includesmemorySafetyAbort(usedGb, plusconsumedGb, a lower bound on the load's footprint) andmemorySafetyLatched. A healthy start clears it.standaloneprofile: video cannot load next to a keep-warm 27B on spark-node-03.Scenario results (seeded simulation, not benchmarks)
off(today)autoReviews
The project rules require a full code and security review before commit, so both ran on this diff, and every verified finding is fixed with a test.
Security. The one medium finding was a node-id takeover: any token could re-enroll an existing node. Tokens can now be bound with
--node-id, and claiming a member or a configured peer requires a bound token. Low findings fixed:Correctness. Six bugs fixed:
Hardening items from the same review are in the last commit.
Tests
node --test test/pool-*.test.mjs: 109 pass.test:cluster,security,admin-browser-security,node-onboarding,cli-diagnostics,smoke,runtime-policy,runtime-memory-safety,check:js,prettier --check, andeslint(pre-existing warnings only).npm test.Not in this PR
serve. Advise mode first.🤖 Generated with Claude Code