mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-21 05:32:53 +00:00
fix(gateway): skip restart when config.patch has no actual changes (#58502)
config.patch unconditionally writes the config file and sends SIGUSR1 even when diffConfigPaths detects zero changed paths. This causes a full gateway restart (~10s downtime, all SSE/WebSocket connections dropped) on every control-plane config.patch call, even when the config is identical — e.g. a model hot-apply that doesn't change any gateway.* paths. Fix: when changedPaths is empty, return early with `noop: true` without writing the file or scheduling SIGUSR1. The validated config is still returned so the caller knows the current state. This lets control-plane clients safely call config.patch for idempotent updates without triggering unnecessary restarts.
This commit is contained in:
@@ -476,6 +476,28 @@ export const configHandlers: GatewayRequestHandlers = {
|
||||
}
|
||||
const changedPaths = diffConfigPaths(snapshot.config, validated.config);
|
||||
const actor = resolveControlPlaneActor(client);
|
||||
|
||||
// No-op: if the validated config is identical to the current config,
|
||||
// skip the file write and SIGUSR1 restart entirely. This avoids a full
|
||||
// gateway restart (and the resulting connection drop) when a control-plane
|
||||
// client re-sends the same config (e.g. hot-apply with no actual changes).
|
||||
if (changedPaths.length === 0) {
|
||||
context?.logGateway?.info(
|
||||
`config.patch noop ${formatControlPlaneActor(actor)} (no changed paths)`,
|
||||
);
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
ok: true,
|
||||
noop: true,
|
||||
path: createConfigIO().configPath,
|
||||
config: redactConfigObject(validated.config, schemaPatch.uiHints),
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
context?.logGateway?.info(
|
||||
`config.patch write ${formatControlPlaneActor(actor)} changedPaths=${summarizeChangedPaths(changedPaths)} restartReason=config.patch`,
|
||||
);
|
||||
|
||||
@@ -252,6 +252,30 @@ describe("gateway config methods", () => {
|
||||
expect(res.error?.message).toBe("config schema path not found");
|
||||
});
|
||||
|
||||
it("returns noop for config.patch when config is unchanged", async () => {
|
||||
const current = await rpcReq<{
|
||||
config?: Record<string, unknown>;
|
||||
hash?: string;
|
||||
}>(requireWs(), "config.get", {});
|
||||
expect(current.ok).toBe(true);
|
||||
|
||||
// Patch with the same config — no actual changes
|
||||
const res = await rpcReq<{
|
||||
ok?: boolean;
|
||||
noop?: boolean;
|
||||
config?: Record<string, unknown>;
|
||||
}>(requireWs(), "config.patch", {
|
||||
raw: JSON.stringify(current.payload?.config ?? {}),
|
||||
baseHash: current.payload?.hash,
|
||||
});
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.payload?.noop).toBe(true);
|
||||
// Config hash should not change (no file write)
|
||||
const after = await rpcReq<{ hash?: string }>(requireWs(), "config.get", {});
|
||||
expect(after.payload?.hash).toBe(current.payload?.hash);
|
||||
});
|
||||
|
||||
it("rejects config.patch when raw is null", async () => {
|
||||
const res = await rpcReq<{ ok?: boolean }>(requireWs(), "config.patch", {
|
||||
raw: "null",
|
||||
|
||||
Reference in New Issue
Block a user