fleetd #811: move the repair interval to a fleet-wide key, clean up comments and logging
- repairIntervalSeconds moves off the per-lead Leader record onto the top-level Fleet record: the repair loop covers every lead in one pass, so a per-lead value only ever honored an arbitrary map entry. - drop ticket references from LeadLabelRepairLoop's javadoc and the FleetdAssembly wiring comment; trim the latter to the maintainer-facing constraint and drop the loop-count ordinal. - log.warn in the tick's catch now passes the exception object instead of only its message, so a stack trace survives an unexpected failure. - document that the repair's lead set and interval are fixed at boot.
This commit is contained in:
@@ -622,6 +622,11 @@ fleet:
|
||||
# comes from a closed enum, a generated label can never begin with a lead's tabPrefix.
|
||||
# tabLabel: "{role}: {profile} #{n}"
|
||||
|
||||
# How often a launched lead's tab label is re-asserted against whatever Claude Code last
|
||||
# retitled it to. Fleet-wide: one recurring pass covers every lead below, so this is not a
|
||||
# per-lead field.
|
||||
# repairIntervalSeconds: 30
|
||||
|
||||
# Panes that orchestrate rather than are orchestrated. A lead may now be CREATED as well as
|
||||
# recognised: give it a `profile:` and the daemon launches the shortfall when fewer than
|
||||
# `instances` are live. Omit `profile:` and it is recognise-only, as before.
|
||||
@@ -671,8 +676,6 @@ fleet:
|
||||
# tabPrefix: "lead:" # only used to guard against a worker tabLabel colliding with
|
||||
# # this convention at startup; plays no part in matching a lead
|
||||
# scanIntervalSeconds: 10 # rescan cadence, and the worst case before a new tab is seen
|
||||
# repairIntervalSeconds: 30 # how often a launched lead's tab label is re-asserted against
|
||||
# # whatever Claude Code last retitled it to
|
||||
# workspace: leads # where a launched lead's tab is created (default "fleet",
|
||||
# # the same shared space the members use). Sharing that space
|
||||
# # with members is the normal shipped shape: the scanner tells
|
||||
|
||||
@@ -565,15 +565,12 @@ final class FleetdAssembly {
|
||||
configWatcher = null;
|
||||
}
|
||||
|
||||
// fleetd #811: re-asserts a lead's tab label against the tab LeadLauncher launched it in.
|
||||
// ensureLeads() above calls the same repair once, at boot, but launchedTabByLead is only
|
||||
// ever filled by a launch that happens later in that same pass — so without this loop the
|
||||
// repair never actually runs. SEVENTH of the recurring background loops to start (optional
|
||||
// in the sense that an empty fleet.leaders gives it nothing to repair).
|
||||
// This loop is what makes the label repair reachable: ensureLeads() runs it once, at boot,
|
||||
// but only this recurring call drives it afterward.
|
||||
var leadLabelRepairScheduler = ports.newScheduler("bridge-lead-repair-");
|
||||
final LeadLabelRepairLoop leadLabelRepair;
|
||||
if (!leaders.isEmpty()) {
|
||||
long repairIntervalSeconds = leaders.values().iterator().next().repairIntervalSeconds();
|
||||
long repairIntervalSeconds = cfg.fleet().repairIntervalSeconds();
|
||||
leadLabelRepair = new LeadLabelRepairLoop(() -> leadLauncher.reassertLeadLabels(leaders),
|
||||
leadLabelRepairScheduler, repairIntervalSeconds);
|
||||
leadLabelRepair.start();
|
||||
|
||||
@@ -141,7 +141,7 @@ final class FleetdRuntime implements AutoCloseable {
|
||||
if (leadCoordScheduler != null) leadCoordScheduler.shutdownNow();
|
||||
if (healthMonitor != null) healthMonitor.stop();
|
||||
if (configWatcher != null) configWatcher.stop(); // CB-559: stop polling the config file
|
||||
if (leadLabelRepair != null) leadLabelRepair.stop(); // fleetd #811: stop the label repair loop
|
||||
if (leadLabelRepair != null) leadLabelRepair.stop(); // stop the label repair loop
|
||||
mcp.close();
|
||||
if (reaper != null) reaper.stop();
|
||||
// Idle-sleep guard: release unconditionally, even though sessions.close() above already
|
||||
|
||||
@@ -1160,13 +1160,11 @@ public record FleetConfig(
|
||||
* @param model the model or selector it runs, for operators reading the roster
|
||||
* @param workspace the space this lead's tab lives in — the uniqueness boundary
|
||||
* identity now depends on. Default {@link #DEFAULT_WORKSPACE}
|
||||
* @param repairIntervalSeconds how often the daemon re-asserts this lead's tab label against
|
||||
* the tab it launched. Default 30
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record Leader(String profile, String tab, Integer instances, String tabPrefix,
|
||||
Integer scanIntervalSeconds, String kind, String model,
|
||||
String workspace, String cwd, Integer repairIntervalSeconds) {
|
||||
String workspace, String cwd) {
|
||||
|
||||
/** The tab label every lead is found by, and an auto-launched instance is created with. */
|
||||
public static final String LEAD_TAB_LABEL = "lead";
|
||||
@@ -1187,14 +1185,12 @@ public record FleetConfig(
|
||||
workspace = (workspace == null || workspace.isBlank())
|
||||
? DEFAULT_WORKSPACE : workspace.strip();
|
||||
tab = (tab == null || tab.isBlank()) ? null : tab.strip();
|
||||
repairIntervalSeconds =
|
||||
(repairIntervalSeconds == null || repairIntervalSeconds <= 0) ? 30 : repairIntervalSeconds;
|
||||
}
|
||||
|
||||
/** Back-compat 7-arg form — no workspace, cwd or repairIntervalSeconds, so each takes its default. */
|
||||
/** Back-compat 7-arg form — no workspace or cwd, so each takes its default. */
|
||||
public Leader(String profile, String tab, Integer instances, String tabPrefix,
|
||||
Integer scanIntervalSeconds, String kind, String model) {
|
||||
this(profile, tab, instances, tabPrefix, scanIntervalSeconds, kind, model, null, null, null);
|
||||
this(profile, tab, instances, tabPrefix, scanIntervalSeconds, kind, model, null, null);
|
||||
}
|
||||
|
||||
/** True when this lead may be launched by the daemon rather than only recognised. */
|
||||
@@ -1290,6 +1286,8 @@ public record FleetConfig(
|
||||
* @param collaborators tabs fleetd recognises as collaborators (fleetd #669), keyed by name.
|
||||
* Recognise-only, exactly like a {@code profile}-less {@link Leader}:
|
||||
* nothing here is ever auto-launched.
|
||||
* @param repairIntervalSeconds how often a lead's tab label is re-asserted against the tab it
|
||||
* was launched in — fleet-wide, since one pass covers every lead. Default 30
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record Fleet(Map<String, Leader> leaders,
|
||||
@@ -1299,7 +1297,8 @@ public record FleetConfig(
|
||||
Map<String, Slot> reviewers,
|
||||
Map<String, String> charters,
|
||||
String tabLabel,
|
||||
Map<String, Collaborator> collaborators) {
|
||||
Map<String, Collaborator> collaborators,
|
||||
Integer repairIntervalSeconds) {
|
||||
|
||||
/**
|
||||
* Role first, so the tab bar identifies the member's fleet role.
|
||||
@@ -1315,6 +1314,8 @@ public record FleetConfig(
|
||||
charters = unmodifiableOrEmpty(charters);
|
||||
tabLabel = (tabLabel == null || tabLabel.isBlank()) ? DEFAULT_TAB_LABEL : tabLabel;
|
||||
collaborators = unmodifiableOrEmpty(collaborators);
|
||||
repairIntervalSeconds =
|
||||
(repairIntervalSeconds == null || repairIntervalSeconds <= 0) ? 30 : repairIntervalSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1332,12 +1333,12 @@ public record FleetConfig(
|
||||
public Fleet(Map<String, Leader> leaders, Map<String, Slot> architects,
|
||||
Map<String, Slot> developers, Map<String, Slot> reviewers,
|
||||
Map<String, String> charters, String tabLabel) {
|
||||
this(leaders, architects, developers, null, reviewers, charters, tabLabel, null);
|
||||
this(leaders, architects, developers, null, reviewers, charters, tabLabel, null, null);
|
||||
}
|
||||
|
||||
public Fleet(Map<String, Leader> leaders, Map<String, Slot> architects,
|
||||
Map<String, Slot> developers, Map<String, Slot> reviewers, String tabLabel) {
|
||||
this(leaders, architects, developers, null, reviewers, null, tabLabel, null);
|
||||
this(leaders, architects, developers, null, reviewers, null, tabLabel, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,9 +7,12 @@ import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Drives {@link LeadLauncher#reassertLeadLabels} on a recurring schedule (fleetd #811) — the
|
||||
* only caller that makes the repair reachable, since {@link LeadLauncher#ensureLeads()} runs
|
||||
* once, at boot, before any lead has a remembered tab id to repair.
|
||||
* Drives {@link LeadLauncher#reassertLeadLabels} on a recurring schedule — the only caller that
|
||||
* makes the repair reachable, since {@link LeadLauncher#ensureLeads()} runs once, at boot,
|
||||
* before any lead has a remembered tab id to repair.
|
||||
*
|
||||
* <p>The repair action passed to the constructor, and the interval it runs at, are fixed for
|
||||
* this loop's lifetime: neither changes if the underlying config is reloaded afterward.
|
||||
*/
|
||||
public final class LeadLabelRepairLoop {
|
||||
|
||||
@@ -46,7 +49,7 @@ public final class LeadLabelRepairLoop {
|
||||
try {
|
||||
repair.run();
|
||||
} catch (Throwable e) {
|
||||
log.warn("lead label repair tick failed, still scheduled: {}", e.getMessage());
|
||||
log.warn("lead label repair tick failed, still scheduled", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -95,7 +95,7 @@ class ConfigRefTopLevelReportingCoverageTest {
|
||||
v.put("primary", new FleetConfig.Primary("term-a", 1, 1000));
|
||||
v.put("fleet", new FleetConfig.Fleet(
|
||||
Map.of("opus", new FleetConfig.Leader("sonnet", "lead: opus-a", 1, null, 10,
|
||||
"claude", null, null, null, null)),
|
||||
"claude", null, null, null)),
|
||||
Map.of(), Map.of(), Map.of(), Map.of(), "{role}: {profile} #{n}"));
|
||||
v.put("leadHeartbeat", new FleetConfig.LeadHeartbeat(300, 60_000L, 3));
|
||||
v.put("health", new FleetConfig.Health(true, 30, 600, null, null));
|
||||
@@ -143,7 +143,7 @@ class ConfigRefTopLevelReportingCoverageTest {
|
||||
// test — that is by design, not a gap: this map exists to prove fleet.leaders is covered.
|
||||
v.put("fleet", new FleetConfig.Fleet(
|
||||
Map.of("opus", new FleetConfig.Leader("sonnet", "lead: opus-b", 1, null, 10,
|
||||
"claude", null, null, null, null)),
|
||||
"claude", null, null, null)),
|
||||
Map.of(), Map.of(), Map.of(), Map.of(), "{role}: {profile} #{n}"));
|
||||
v.put("leadHeartbeat", new FleetConfig.LeadHeartbeat(600, 120_000L, 5));
|
||||
v.put("health", new FleetConfig.Health(false, 90, 900, null, null));
|
||||
|
||||
@@ -485,8 +485,6 @@ class FleetConfigTest {
|
||||
assertEquals("lead:", lead.tabPrefix());
|
||||
assertEquals(10, lead.scanIntervalSeconds());
|
||||
assertEquals(1, lead.instances(), "one of a lead is the assumption worth defaulting to");
|
||||
assertEquals(30, lead.repairIntervalSeconds(),
|
||||
"no fleetd.yaml edit is required for the label repair loop to work");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -501,13 +499,43 @@ class FleetConfigTest {
|
||||
tab: "drive: opus"
|
||||
tabPrefix: "drive:"
|
||||
scanIntervalSeconds: 30
|
||||
repairIntervalSeconds: 45
|
||||
""");
|
||||
|
||||
FleetConfig.Leader lead = FleetConfig.load(f).fleet().leaders().get("opus");
|
||||
assertEquals("drive:", lead.tabPrefix());
|
||||
assertEquals(30, lead.scanIntervalSeconds());
|
||||
assertEquals(45, lead.repairIntervalSeconds());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fleetDefaultsItsLabelRepairIntervalWhenAbsent(@TempDir Path dir) throws Exception {
|
||||
Path f = dir.resolve("no-repair-interval.yaml");
|
||||
Files.writeString(f, """
|
||||
bind:
|
||||
port: 8080
|
||||
fleet:
|
||||
leaders:
|
||||
opus:
|
||||
tab: "lead: opus"
|
||||
""");
|
||||
|
||||
assertEquals(30, FleetConfig.load(f).fleet().repairIntervalSeconds(),
|
||||
"no fleetd.yaml edit is required for the label repair loop to work");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fleetReadsAnExplicitLabelRepairInterval(@TempDir Path dir) throws Exception {
|
||||
Path f = dir.resolve("repair-interval.yaml");
|
||||
Files.writeString(f, """
|
||||
bind:
|
||||
port: 8080
|
||||
fleet:
|
||||
repairIntervalSeconds: 45
|
||||
leaders:
|
||||
opus:
|
||||
tab: "lead: opus"
|
||||
""");
|
||||
|
||||
assertEquals(45, FleetConfig.load(f).fleet().repairIntervalSeconds());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ class FleetConfigWithDefaultsPreservesEveryComponentTest {
|
||||
v.put("primary", new FleetConfig.Primary("term-guard", 4, 4000));
|
||||
v.put("fleet", new FleetConfig.Fleet(
|
||||
Map.of("opus", new FleetConfig.Leader("sonnet", "lead: opus-guard", 1, null, 10,
|
||||
"claude", null, null, null, null)),
|
||||
"claude", null, null, null)),
|
||||
Map.of(), Map.of(), Map.of(), Map.of(), "{role}: {profile} #{n}"));
|
||||
v.put("leadHeartbeat", new FleetConfig.LeadHeartbeat(301, 61_000L, 4));
|
||||
v.put("health", new FleetConfig.Health(true, 31, 601, 61, null));
|
||||
|
||||
@@ -63,7 +63,7 @@ class LeadLauncherTest {
|
||||
|
||||
private static FleetConfig.Leader lead(String profile, String tab, int instances) {
|
||||
return new FleetConfig.Leader(profile, tab, instances, "lead:", 10, null, null,
|
||||
"fleet", "/repo", null);
|
||||
"fleet", "/repo");
|
||||
}
|
||||
|
||||
private static LeadLauncher launcher(FakeHerdr herdr, FleetConfig cfg) {
|
||||
|
||||
@@ -114,7 +114,7 @@ class LeadRolloverTest {
|
||||
null, null, null,
|
||||
Map.of(), null, null, true, null);
|
||||
FleetConfig.Leader leader = new FleetConfig.Leader("opus", "lead: opus", 1, "lead:", 10,
|
||||
null, null, "fleet", null, null);
|
||||
null, null, "fleet", null);
|
||||
Map<String, FleetConfig.Leader> leaders = new LinkedHashMap<>();
|
||||
leaders.put(LEAD_NAME, leader);
|
||||
FleetConfig.Fleet fleet = new FleetConfig.Fleet(leaders, Map.of(), Map.of(), Map.of(), Map.of(), null);
|
||||
|
||||
Reference in New Issue
Block a user