Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37b4031ca4 |
@@ -9,6 +9,7 @@ import dev.ltms.bridged.peer.PeerUnreachableException;
|
|||||||
import dev.ltms.bridged.peer.SpawnRequest;
|
import dev.ltms.bridged.peer.SpawnRequest;
|
||||||
import dev.ltms.bridged.placement.PlacementCandidate;
|
import dev.ltms.bridged.placement.PlacementCandidate;
|
||||||
import dev.ltms.bridged.placement.PlacementContext;
|
import dev.ltms.bridged.placement.PlacementContext;
|
||||||
|
import dev.ltms.bridged.placement.PlacementException;
|
||||||
import dev.ltms.bridged.placement.PlacementPolicies;
|
import dev.ltms.bridged.placement.PlacementPolicies;
|
||||||
import dev.ltms.bridged.placement.PlacementPolicy;
|
import dev.ltms.bridged.placement.PlacementPolicy;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -139,8 +140,12 @@ public final class CompositePeerLauncher implements PeerLauncher {
|
|||||||
public PeerHandle spawn(SpawnRequest req) {
|
public PeerHandle spawn(SpawnRequest req) {
|
||||||
String requestedProfile = req.profileName();
|
String requestedProfile = req.profileName();
|
||||||
if (requestedProfile != null && !requestedProfile.isBlank()) {
|
if (requestedProfile != null && !requestedProfile.isBlank()) {
|
||||||
// An explicit profile bypasses the policy entirely.
|
// An explicit profile bypasses the placement policy, but not the capacity cap: maxLoad
|
||||||
|
// is documented as an unconditional limit on this profile (BridgedConfig.Worker), and
|
||||||
|
// the charter makes explicit-profile spawns the normal path — so skipping the check
|
||||||
|
// here would leave the cap dead config in real operation.
|
||||||
HerdrPeerLauncher d = route(requestedProfile);
|
HerdrPeerLauncher d = route(requestedProfile);
|
||||||
|
enforceMaxLoad(requestedProfile);
|
||||||
PeerHandle handle = d.spawn(req);
|
PeerHandle handle = d.spawn(req);
|
||||||
spawnedBy.put(handle.id(), d);
|
spawnedBy.put(handle.id(), d);
|
||||||
return handle;
|
return handle;
|
||||||
@@ -190,6 +195,42 @@ public final class CompositePeerLauncher implements PeerLauncher {
|
|||||||
+ " candidate(s): " + String.join(", ", unreachable));
|
+ " candidate(s): " + String.join(", ", unreachable));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refuse an explicit-profile spawn when the profile is at its {@code maxLoad} cap.
|
||||||
|
*
|
||||||
|
* <p>maxLoad is a documented, unconditional capacity limit (see {@code BridgedConfig.Worker#maxLoad}),
|
||||||
|
* and the charter makes explicit-profile spawns the normal path — so enforcing it only in placement
|
||||||
|
* ({@link dev.ltms.bridged.placement.PlacementPolicyUtil}) would leave the cap dead config on every
|
||||||
|
* call that names a profile. Same rule as placement: {@code live >= cap} is at capacity.
|
||||||
|
*
|
||||||
|
* <p>Deliberately no fallback to another profile: the caller named {@code profile} for a cost/model
|
||||||
|
* reason, and silently re-routing a paid-tier (subscription) request elsewhere is worse than
|
||||||
|
* refusing it. A caller that wants placement should omit the profile and let the policy pick.
|
||||||
|
*
|
||||||
|
* <p>Known TOCTOU limitation — documented, not fixed. {@link #liveCount} is read outside any lock and
|
||||||
|
* {@code SessionManager} registers a session only after {@code launcher.spawn} returns, so two
|
||||||
|
* genuinely concurrent spawns can both pass this check. The race already exists on the placement
|
||||||
|
* path. Closing it needs slot reservation in the registry; serializing spawn here would block on
|
||||||
|
* the readiness gate and is a far worse trade.
|
||||||
|
*
|
||||||
|
* @param profile the profile the caller explicitly named
|
||||||
|
* @throws PlacementException when the profile is at capacity
|
||||||
|
*/
|
||||||
|
private void enforceMaxLoad(String profile) {
|
||||||
|
// Absent config, or a config whose maxLoad normalized to null (non-positive ⇒ unlimited at
|
||||||
|
// load), means no cap — never cap what wasn't configured.
|
||||||
|
BridgedConfig.Worker cfg = profileConfigs.get(profile);
|
||||||
|
Integer cap = (cfg == null) ? null : cfg.maxLoad();
|
||||||
|
if (cap == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int live = liveCount.apply(profile);
|
||||||
|
if (live >= cap) {
|
||||||
|
throw new PlacementException("worker profile '" + profile + "' is at maxLoad: " + live
|
||||||
|
+ " live >= " + cap + " cap; refusing spawn — no fallback to another profile");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Build the candidate list from the configured profiles, in definition order. */
|
/** Build the candidate list from the configured profiles, in definition order. */
|
||||||
private List<PlacementCandidate> candidates() {
|
private List<PlacementCandidate> candidates() {
|
||||||
List<PlacementCandidate> out = new ArrayList<>();
|
List<PlacementCandidate> out = new ArrayList<>();
|
||||||
|
|||||||
@@ -378,6 +378,54 @@ class CompositePeerLauncherTest {
|
|||||||
assertEquals(1, adapter.spawnCount("b"));
|
assertEquals(1, adapter.spawnCount("b"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void explicitSpawnAtMaxLoadThrowsPlacementExceptionNamingProfileLiveAndCap() {
|
||||||
|
FakeHerdr herdr = new FakeHerdr();
|
||||||
|
Map<String, BridgedConfig.Worker> profiles = ordered(
|
||||||
|
"a", stubWorker("a", 1.0f, 2),
|
||||||
|
"b", stubWorker("b"));
|
||||||
|
StubLauncher adapter = new StubLauncher("claude", herdr, profiles, "a", Set.of());
|
||||||
|
CompositePeerLauncher composite = new CompositePeerLauncher(
|
||||||
|
List.of(adapter), "a", profiles, PlacementPolicies.fixed(), name -> "a".equals(name) ? 2 : 0);
|
||||||
|
|
||||||
|
PlacementException e = assertThrows(PlacementException.class,
|
||||||
|
() -> composite.spawn(new SpawnRequest("a", null, null)));
|
||||||
|
assertTrue(e.getMessage().contains("'a'"), "message names the profile: " + e.getMessage());
|
||||||
|
assertTrue(e.getMessage().contains("2 live"), "message names the live count: " + e.getMessage());
|
||||||
|
assertTrue(e.getMessage().contains("2 cap"), "message names the cap: " + e.getMessage());
|
||||||
|
assertEquals(0, adapter.spawnCount("a"), "at cap, the spawn is refused before any delegation");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void explicitSpawnUnderMaxLoadStillSucceeds() {
|
||||||
|
FakeHerdr herdr = new FakeHerdr();
|
||||||
|
Map<String, BridgedConfig.Worker> profiles = ordered(
|
||||||
|
"a", stubWorker("a", 1.0f, 2),
|
||||||
|
"b", stubWorker("b"));
|
||||||
|
StubLauncher adapter = new StubLauncher("claude", herdr, profiles, "a", Set.of());
|
||||||
|
CompositePeerLauncher composite = new CompositePeerLauncher(
|
||||||
|
List.of(adapter), "a", profiles, PlacementPolicies.fixed(), name -> "a".equals(name) ? 1 : 0);
|
||||||
|
|
||||||
|
PeerHandle h = composite.spawn(new SpawnRequest("a", null, null));
|
||||||
|
assertEquals("a", h.profile(), "a profile under its cap accepts an explicit spawn");
|
||||||
|
assertEquals(1, adapter.spawnCount("a"), "the under-cap spawn is delegated");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void explicitSpawnWithNullMaxLoadIsNeverCapped() {
|
||||||
|
FakeHerdr herdr = new FakeHerdr();
|
||||||
|
Map<String, BridgedConfig.Worker> profiles = ordered(
|
||||||
|
"a", stubWorker("a", 1.0f, null),
|
||||||
|
"b", stubWorker("b"));
|
||||||
|
StubLauncher adapter = new StubLauncher("claude", herdr, profiles, "a", Set.of());
|
||||||
|
// A deliberately absurd live count: an unset maxLoad means unlimited, so it must never refuse.
|
||||||
|
CompositePeerLauncher composite = new CompositePeerLauncher(
|
||||||
|
List.of(adapter), "a", profiles, PlacementPolicies.fixed(), name -> 1000);
|
||||||
|
|
||||||
|
PeerHandle h = composite.spawn(new SpawnRequest("a", null, null));
|
||||||
|
assertEquals("a", h.profile(), "a profile with no maxLoad is never capped, however many live workers");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void emptyCandidateSetThrowsClearException() {
|
void emptyCandidateSetThrowsClearException() {
|
||||||
FakeHerdr herdr = new FakeHerdr();
|
FakeHerdr herdr = new FakeHerdr();
|
||||||
|
|||||||
Reference in New Issue
Block a user