CB-585: maxLoad: 0 caps a profile at zero, negative refused at load
CI / contract (pull_request) Successful in 41s
CI / build (pull_request) Successful in 1m18s

This commit is contained in:
Dai Ha
2026-08-15 15:38:50 +02:00
parent 5fe02b7c98
commit 2f8c98dac9
5 changed files with 147 additions and 5 deletions
@@ -210,9 +210,20 @@ public record BridgedConfig(
* unreachable — an explicit {@code bridge_spawn{profile:"..."}} bypasses
* placement entirely and still resolves it. Weights among the remaining
* (non-excluded) candidates need not sum to 1.0; only their ratios matter.
* @param maxLoad max live workers allowed on this profile at one time; absent or
* non-positive ⇒ unlimited. Live means any session the registry still owns
* (acquired and not yet released), in any state.
* @param maxLoad max live workers allowed on this profile at one time. Absent
* (unset/{@code null}) ⇒ unlimited — most profiles rely on this. An
* explicit {@code 0} (CB-585) means "cap this profile at zero live
* members": it is excluded from every automatic policy's candidate pool
* the same way a {@code weight <= 0} profile is (see
* {@code PlacementPolicyUtil.available()}, which already treats "at cap"
* and "excluded" alike), and an explicit
* {@code bridge_spawn{profile:"..."}} against it is refused too (see
* {@code CompositePeerLauncher.enforceMaxLoad}) — a cap is a capacity
* statement that does not stop being true just because the profile was
* named directly. A negative value has no sane meaning (there is no
* "excluded" to degrade to below zero) and is refused at config load
* instead, naming the profile and the key. Live means any session the
* registry still owns (acquired and not yet released), in any state.
* @param kind which peer launcher spawns this profile: {@code "claude-code"} (default —
* the {@link dev.ltms.bridged.member.ClaudeCodeLauncher}) or {@code "opencode"}.
* The {@code CompositePeerLauncher} routes {@code spawn}/reap by this value, so
@@ -306,7 +317,12 @@ public record BridgedConfig(
// get coerced back up to 1.0 — that coercion was the bug (weight: 0 looked like "never
// pick me" and actually meant "pick me as often as anyone else").
weight = (weight == null) ? 1.0f : Math.max(weight, 0.0f);
maxLoad = (maxLoad == null || maxLoad <= 0) ? null : maxLoad;
// CB-585: absent still means unlimited (null), but an explicit maxLoad: 0 must survive
// as "capped at zero," not get coerced back up to null/unlimited — that coercion was
// the bug (maxLoad: 0 read as "never run anything here" and behaved as the opposite,
// the one throttle a subscription: true profile has against the operator's own paid
// plan). A negative value is refused earlier, at config load (rejectNegativeMaxLoad),
// so it never reaches this constructor and needs no clamping here.
subscription = (subscription != null && subscription) ? Boolean.TRUE : Boolean.FALSE;
// exhaustedPattern stays null when unset/blank (opt-in) — no defaulting, no vendor
// wording: an unconfigured profile keeps today's completion-fallback behaviour exactly.
@@ -956,6 +972,7 @@ public record BridgedConfig(
rejectLeaderTerminalKey(yaml);
warnUnknownTopLevelKeys(yaml, path);
rejectDuplicateMemberSlots(yaml);
rejectNegativeMaxLoad(yaml);
BridgedConfig cfg = YAML.readValue(yaml, BridgedConfig.class);
return cfg.withDefaults();
} catch (IOException e) {
@@ -1215,6 +1232,41 @@ public record BridgedConfig(
}
}
/**
* Reject a profile whose {@code maxLoad:} is negative, naming both the profile and the key.
*
* <p>Unlike {@code weight} (CB-554), where a negative value degrades to the same "excluded"
* meaning as an explicit {@code 0}, {@code maxLoad} has nowhere lower to degrade to — {@code 0}
* already means "capped at zero live members" (CB-585), the strictest cap there is. Silently
* normalising a negative value to something else is exactly the shape of bug this ticket fixes
* for {@code 0}, so it is refused instead, loud and specific, rather than guessed at.
*
* @param yaml the raw config text
* @throws IllegalStateException when any profile's {@code maxLoad} is negative
*/
static void rejectNegativeMaxLoad(String yaml) {
Map<?, ?> raw;
try {
raw = YAML.readValue(yaml, Map.class);
} catch (IOException | IllegalArgumentException e) {
return; // a malformed file is reported by the real parse, not here
}
if (raw == null || !(raw.get("profiles") instanceof Map<?, ?> profiles)) {
return;
}
List<String> bad = profiles.entrySet().stream()
.filter(e -> e.getValue() instanceof Map<?, ?> p
&& p.get("maxLoad") instanceof Number n && n.doubleValue() < 0)
.map(e -> String.valueOf(e.getKey()))
.sorted()
.toList();
if (!bad.isEmpty()) {
throw new IllegalStateException("refusing to start: profile(s) [" + String.join(", ", bad)
+ "] set a negative maxLoad — maxLoad caps live workers at a non-negative count;"
+ " use 0 to cap a profile at zero live members, or omit the key for unlimited.");
}
}
static List<String> unknownTopLevelKeys(String yaml) {
Map<?, ?> raw;
try {