fleetd #425 rework: resolve acquireWithWorktree via real placement routing #433
Merged
ltms
merged 7 commits from 2026-09-10 09:35:02 +02:00
worker/425-rework-placement-resolve-c58ba1-9 into main
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4b10d02207 |
fleetd #425 rework round 4: mutation-pinning test for the dropped PlacementDecision
SessionManager.acquireWithWorktree's unqualified branch must carry the PlacementDecision it already resolved via launcher.place() into launcher.spawn(spawnReq, decision) rather than re-deriving it through a blank-profile launcher.spawn(spawnReq). Every existing test in this file uses PlacementPolicies.fixed(), which answers select() the same way on every call, so dropping the decision (handle = launcher.spawn(spawnReq);) was invisible: 186 tests stayed green under that mutation. acquireWithWorktreeSpawnsOnTheSameProfileItProvisionedTheWorktreeForUnderARotatingPolicy uses PlacementPolicies.roundRobin() instead — deterministic AND stateful, so two select() calls on the same policy instance disagree (index 0 then index 1 across a two-profile pool). It asserts AGREEMENT between the profile the worktree's parity overlay was provisioned for and the profile the member actually spawned on, never a hardcoded expected profile name. Verified as a real mutation, not a no-op: applying the exact mutation (handle = launcher.spawn(spawnReq);) turns it red — expected [b.mcp.json] but was [a.mcp.json] — and reverting turns it green again. Full build: 1572 tests, 0 failures, 0 errors, BUILD SUCCESS. |
||
|
|
c5fbfdbf4a | Merge main into #425 rework branch (brings #438 held-peer-mail read) | ||
|
|
9f3671b801 |
fleetd #425 rework round 3: rewrite prose after #435 made fixed honour maxLoad
fleetd #435 (merged to main) made FixedPlacementPolicy evaluate maxLoad during automatic selection, the same way weighted/round-robin already did. Six comments across CompositePeerLauncher.java, PeerLauncher.java, PlacementDecision.java, and SessionManager.java justified round 2's place()/spawn(req, decision) mechanism by saying fixed "deliberately never evaluates maxLoad" — that claim is now false, and needed restating, not just deleting. The honest case after #435: the two-path shape (routing branch falls through an excluded candidate; explicit-profile branch refuses on it) is still real and still deliberate — an operator who names a profile should get a refusal, not a silent substitution. What round 1 got wrong, and what round 2 still needs to prevent, is turning a fall-through into a refusal by accident: resolving a name via place() and then feeding it back to spawn(SpawnRequest) as an explicit profile. Before #435 that accident was reachable through maxLoad specifically, because fixed never evaluated it; #435 closed that specific gap, so a PlacementDecision can no longer be at-cap in the first place. What survives as the justification for spawn(req, decision): it never re-evaluates a condition place() already decided, and it closes the window between that decision and the spawn in which the underlying state could otherwise move — not a failure #435 already prevents. Re-measured the sibling paths this round exists to keep in agreement (one profile at maxLoad: 1, liveCount pinned at 1, PlacementPolicies.fixed(), unqualified spawn): both the with-worktree and without-worktree paths now throw the identical PlacementException — "worker profile 'a' is at maxLoad (1 live >= 1 cap), and no available candidate remains" — closed upstream by #435, at place()/select(), before either path ever reaches a spawn call. The observable asymmetry this PR was filed to fix is gone; what remains is the structural argument above. No behavior change: place()/PlacementDecision/spawn(req, decision) are untouched, and FixedPlacementPolicy/PlacementPolicyUtil are taken wholesale from main's merge. |
||
|
|
84034b34d1 | Merge remote-tracking branch 'origin/main' into worker/425-rework-placement-resolve-c58ba1-9 | ||
|
|
6b0a99b2b7 |
fleetd #425 rework round 2: stop routedProfileFor's caller re-entering the throwing branch
Round 1 closed quarantine/cool-off/model-off routing for acquireWithWorktree by resolving the profile through routedProfileFor(role) and handing that name back to launcher.spawn(SpawnRequest) as an EXPLICIT profile. That re-resolution has a cost the lead measured directly: naming a profile explicitly makes CompositePeerLauncher.spawn take its THROWING branch (enforceMaxLoad included), while the routing branch a blank spawn takes never calls enforceMaxLoad at all, and FixedPlacementPolicy (the default) deliberately never evaluates maxLoad during automatic selection. So an at-cap pool-first profile that placement itself would have picked for a plain unqualified spawn could die at enforceMaxLoad one call later, purely because the worktree path's route to the spawn passed through an explicit profile name — a new failure a worktree-less unqualified spawn never hits. This closes the two-path shape instead of moving it: PeerLauncher gains place(role), returning an opaque PlacementDecision, and spawn(req, decision), which honors that decision through the SAME routing branch a blank spawn uses — no enforce* check is newly applied. SessionManager. acquireWithWorktree now keeps the PlacementDecision from place() and hands it to spawn(req, decision) for an unqualified request, instead of re-resolving through an explicit profile name. An explicitly-named profile is unaffected: it still goes through spawn(req) and its throwing branch, exactly as before. Also corrects the acquireWithWorktree comment's false claim that round 1 "loses nothing else" — maxLoad was lost too, as a new hard failure, not a retry. The comment now names it explicitly. Kept the four round-1 tests (still pass — routedProfileFor now just delegates to place()). Added one class asserting the invariant itself: an unqualified spawn on a maxLoad-capped profile must land the same outcome with and without a worktree, asserting on the pair rather than a hardcoded direction, so it stays correct however fleetd #435 (not this ticket) resolves whether maxLoad should gate an unqualified spawn at all. |
||
|
|
b066eb1903 |
fleetd #425 rework: resolve acquireWithWorktree through real placement, not a blind pool-first read
|
||
|
|
051d320ea0 |
fleetd #425: fleet_profiles' default and worktree provisioning must read live placement
fleet_profiles' "default" was CompositePeerLauncher.defaultProfile, a value frozen at construction from cfg.effectiveDefaultProfile(). An unqualified fleet_spawn instead resolves the dev pool live via defaultProfileFor(DEV) on every call, so reordering fleet.developers and reloading changed where a spawn landed without ever changing what fleet_profiles reported. - CompositePeerLauncher.defaultProfile() now delegates to defaultProfileFor(MemberRole.DEV) -- the same live, reload-aware pool read placement already uses -- falling back to the frozen field only when no profiles are configured at all. - PeerLauncher gains a default defaultProfileFor(MemberRole) method so a generic PeerLauncher reference can ask for a role's live default; the default implementation delegates to defaultProfile() for launchers with no pool concept of their own. - SessionManager.acquireWithWorktree resolved a profile via launcher.defaultProfile() (DEV-only) to provision repoRoot/parityOverlay, then spawned with the original (possibly blank) profile, which re-resolves independently through placement -- for any non-DEV role, or across a config reload between the two reads, the two resolutions could disagree and provision a worktree for a profile the member never runs on. Fixed by resolving once, through defaultProfileFor(the caller's actual role), and reusing that same resolved name for repoRoot, parityOverlay, and the spawn itself. Trade-off: this path now spawns with an explicit profile rather than a blank one, so it loses CompositePeerLauncher's cross-candidate retry on PeerUnreachableException -- accepted because a worktree provisioned for the wrong backend is worse than a spawn that fails cleanly and can be retried. Tests: CompositePeerLauncherTest (live dev-pool reorder + empty-pool fallback), FleetProfilesLiveDefaultTest (drives FleetMcp.profilesView directly), SessionManagerTest (worktree overlay follows a reorder, and a non-DEV role's worktree spawn uses that role's pool, not DEV's). |