mirror of https://github.com/openclaw/openclaw.git
fix: avoid startup gateway reload loop (#58678) (thanks @yelog)
This commit is contained in:
parent
71f341c4b4
commit
7cf8ccf9b3
|
|
@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
|||
|
||||
### Fixes
|
||||
|
||||
- Gateway/reload: ignore startup config writes by persisted hash in the config reloader so generated auth tokens and seeded Control UI origins do not trigger a restart loop, while real `gateway.auth.*` edits still require restart. (#58678) Thanks @yelog
|
||||
- Discord/inbound media: pass Discord attachment and sticker downloads through the shared idle-timeout and worker-abort path so slow or stuck inbound media fetches stop hanging message processing. (#58593) Thanks @aquaright1
|
||||
- Telegram/local Bot API: preserve media MIME types for absolute-path downloads so local audio files still trigger transcription and other MIME-based handling. (#54603) Thanks @jzakirov
|
||||
- Tasks/gateway: re-check the current task record before maintenance marks runs lost or prunes them, so a task heartbeat or cleanup update that lands during a sweep no longer gets overwritten by stale snapshot state.
|
||||
|
|
|
|||
|
|
@ -199,6 +199,18 @@ describe("buildGatewayReloadPlan", () => {
|
|||
expect(plan.noopPaths).toContain("diagnostics.stuckSessionWarnMs");
|
||||
});
|
||||
|
||||
it("restarts for gateway.auth.token changes", () => {
|
||||
const plan = buildGatewayReloadPlan(["gateway.auth.token"]);
|
||||
expect(plan.restartGateway).toBe(true);
|
||||
expect(plan.restartReasons).toContain("gateway.auth.token");
|
||||
});
|
||||
|
||||
it("restarts for gateway.auth.mode changes", () => {
|
||||
const plan = buildGatewayReloadPlan(["gateway.auth.mode"]);
|
||||
expect(plan.restartGateway).toBe(true);
|
||||
expect(plan.restartReasons).toContain("gateway.auth.mode");
|
||||
});
|
||||
|
||||
it("defaults unknown paths to restart", () => {
|
||||
const plan = buildGatewayReloadPlan(["unknownField"]);
|
||||
expect(plan.restartGateway).toBe(true);
|
||||
|
|
@ -228,6 +240,11 @@ describe("buildGatewayReloadPlan", () => {
|
|||
expectRestartGateway: false,
|
||||
expectNoopPath: "gateway.remote.url",
|
||||
},
|
||||
{
|
||||
path: "gateway.auth.token",
|
||||
expectRestartGateway: true,
|
||||
expectRestartReason: "gateway.auth.token",
|
||||
},
|
||||
{
|
||||
path: "unknownField",
|
||||
expectRestartGateway: true,
|
||||
|
|
@ -304,7 +321,10 @@ function makeSnapshot(partial: Partial<ConfigFileSnapshot> = {}): ConfigFileSnap
|
|||
};
|
||||
}
|
||||
|
||||
function createReloaderHarness(readSnapshot: () => Promise<ConfigFileSnapshot>) {
|
||||
function createReloaderHarness(
|
||||
readSnapshot: () => Promise<ConfigFileSnapshot>,
|
||||
options: { initialInternalWriteHash?: string | null } = {},
|
||||
) {
|
||||
const watcher = createWatcherMock();
|
||||
vi.spyOn(chokidar, "watch").mockReturnValue(watcher as unknown as never);
|
||||
const onHotReload = vi.fn(async () => {});
|
||||
|
|
@ -325,6 +345,7 @@ function createReloaderHarness(readSnapshot: () => Promise<ConfigFileSnapshot>)
|
|||
};
|
||||
const reloader = startGatewayConfigReloader({
|
||||
initialConfig: { gateway: { reload: { debounceMs: 0 } } },
|
||||
initialInternalWriteHash: options.initialInternalWriteHash,
|
||||
readSnapshot,
|
||||
subscribeToWrites,
|
||||
onHotReload,
|
||||
|
|
@ -515,4 +536,43 @@ describe("startGatewayConfigReloader", () => {
|
|||
|
||||
await harness.reloader.stop();
|
||||
});
|
||||
|
||||
it("dedupes the first watcher reread for startup internal writes", async () => {
|
||||
const readSnapshot = vi
|
||||
.fn<() => Promise<ConfigFileSnapshot>>()
|
||||
.mockResolvedValueOnce(
|
||||
makeSnapshot({
|
||||
config: {
|
||||
gateway: { reload: { debounceMs: 0 }, auth: { mode: "token", token: "startup" } },
|
||||
},
|
||||
hash: "startup-internal-1",
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
makeSnapshot({
|
||||
config: {
|
||||
gateway: { reload: { debounceMs: 0 }, port: 19001 },
|
||||
},
|
||||
hash: "external-after-startup-1",
|
||||
}),
|
||||
);
|
||||
const harness = createReloaderHarness(readSnapshot, {
|
||||
initialInternalWriteHash: "startup-internal-1",
|
||||
});
|
||||
|
||||
harness.watcher.emit("change");
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
|
||||
expect(readSnapshot).toHaveBeenCalledTimes(1);
|
||||
expect(harness.onHotReload).not.toHaveBeenCalled();
|
||||
expect(harness.onRestart).not.toHaveBeenCalled();
|
||||
|
||||
harness.watcher.emit("change");
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
|
||||
expect(readSnapshot).toHaveBeenCalledTimes(2);
|
||||
expect(harness.onRestart).toHaveBeenCalledTimes(1);
|
||||
|
||||
await harness.reloader.stop();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export type GatewayConfigReloader = {
|
|||
|
||||
export function startGatewayConfigReloader(opts: {
|
||||
initialConfig: OpenClawConfig;
|
||||
initialInternalWriteHash?: string | null;
|
||||
readSnapshot: () => Promise<ConfigFileSnapshot>;
|
||||
onHotReload: (plan: GatewayReloadPlan, nextConfig: OpenClawConfig) => Promise<void>;
|
||||
onRestart: (plan: GatewayReloadPlan, nextConfig: OpenClawConfig) => void | Promise<void>;
|
||||
|
|
@ -96,7 +97,7 @@ export function startGatewayConfigReloader(opts: {
|
|||
let restartQueued = false;
|
||||
let missingConfigRetries = 0;
|
||||
let pendingInProcessConfig: OpenClawConfig | null = null;
|
||||
let lastAppliedWriteHash: string | null = null;
|
||||
let lastAppliedWriteHash = opts.initialInternalWriteHash ?? null;
|
||||
|
||||
const scheduleAfter = (wait: number) => {
|
||||
if (stopped) {
|
||||
|
|
|
|||
|
|
@ -505,6 +505,7 @@ export async function startGatewayServer(
|
|||
});
|
||||
|
||||
let cfgAtStart: OpenClawConfig;
|
||||
let startupInternalWriteHash: string | null = null;
|
||||
const startupRuntimeConfig = applyConfigOverrides(configSnapshot.config);
|
||||
const authBootstrap = await prepareGatewayStartupConfig({
|
||||
configSnapshot,
|
||||
|
|
@ -539,11 +540,16 @@ export async function startGatewayServer(
|
|||
);
|
||||
// Unconditional startup migration: seed gateway.controlUi.allowedOrigins for existing
|
||||
// non-loopback installs that upgraded to v2026.2.26+ without required origins.
|
||||
cfgAtStart = await maybeSeedControlUiAllowedOriginsAtStartup({
|
||||
const controlUiSeed = await maybeSeedControlUiAllowedOriginsAtStartup({
|
||||
config: cfgAtStart,
|
||||
writeConfig: writeConfigFile,
|
||||
log,
|
||||
});
|
||||
cfgAtStart = controlUiSeed.config;
|
||||
if (authBootstrap.persistedGeneratedToken || controlUiSeed.persistedAllowedOriginsSeed) {
|
||||
const startupSnapshot = await readConfigFileSnapshot();
|
||||
startupInternalWriteHash = startupSnapshot.hash ?? null;
|
||||
}
|
||||
await runStartupMatrixMigration({
|
||||
cfg: cfgAtStart,
|
||||
env: process.env,
|
||||
|
|
@ -1436,6 +1442,7 @@ export async function startGatewayServer(
|
|||
|
||||
return startGatewayConfigReloader({
|
||||
initialConfig: cfgAtStart,
|
||||
initialInternalWriteHash: startupInternalWriteHash,
|
||||
readSnapshot: readConfigFileSnapshot,
|
||||
subscribeToWrites: registerConfigWriteListener,
|
||||
onHotReload: async (plan, nextConfig) => {
|
||||
|
|
|
|||
|
|
@ -8,20 +8,21 @@ export async function maybeSeedControlUiAllowedOriginsAtStartup(params: {
|
|||
config: OpenClawConfig;
|
||||
writeConfig: (config: OpenClawConfig) => Promise<void>;
|
||||
log: { info: (msg: string) => void; warn: (msg: string) => void };
|
||||
}): Promise<OpenClawConfig> {
|
||||
}): Promise<{ config: OpenClawConfig; persistedAllowedOriginsSeed: boolean }> {
|
||||
const seeded = ensureControlUiAllowedOriginsForNonLoopbackBind(params.config);
|
||||
if (!seeded.seededOrigins || !seeded.bind) {
|
||||
return params.config;
|
||||
return { config: params.config, persistedAllowedOriginsSeed: false };
|
||||
}
|
||||
try {
|
||||
await params.writeConfig(seeded.config);
|
||||
params.log.info(buildSeededOriginsInfoLog(seeded.seededOrigins, seeded.bind));
|
||||
return { config: seeded.config, persistedAllowedOriginsSeed: true };
|
||||
} catch (err) {
|
||||
params.log.warn(
|
||||
`gateway: failed to persist gateway.controlUi.allowedOrigins seed: ${String(err)}. The gateway will start with the in-memory value but config was not saved.`,
|
||||
);
|
||||
}
|
||||
return seeded.config;
|
||||
return { config: seeded.config, persistedAllowedOriginsSeed: false };
|
||||
}
|
||||
|
||||
function buildSeededOriginsInfoLog(origins: string[], bind: GatewayNonLoopbackBindMode): string {
|
||||
|
|
|
|||
Loading…
Reference in New Issue