mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(memory-core): keep startup cron retries quiet (#89075)
Summary: - The branch adds a memory-core `startup_retry` reconciliation mode and regression tests for quiet startup retries, retry-window exhaustion, and live-config retry semantics. - PR surface: Source +9, Tests +114. Total +123 across 2 files. - Reproducibility: yes. from source: current main routes the first startup retry through runtime reconciliatio ... st expects the warn-level `cron service unavailable` log. I did not execute tests in this read-only review. Automerge notes: - Ran the ClawSweeper repair loop before final review. - Included post-review commit in the final squash: fix(memory-core): keep startup cron retries quiet Validation: - ClawSweeper review passed for head7220f940d0. - Required merge gates passed before the squash merge. Prepared head SHA:7220f940d0Review: https://github.com/openclaw/openclaw/pull/89075#issuecomment-4592446250 Co-authored-by: bennewell35 <newelljben@gmail.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -1682,7 +1682,8 @@ describe("gateway startup reconciliation", () => {
|
||||
|
||||
await vi.advanceTimersByTimeAsync(constants.STARTUP_CRON_RETRY_DELAY_MS);
|
||||
expect(harness.addCalls).toHaveLength(0);
|
||||
expectLogContains(logger.warn, "cron service unavailable");
|
||||
expectLogNotContains(logger.warn, "cron service unavailable");
|
||||
expectLogContains(logger.debug, "cron service not yet available at gateway_start");
|
||||
|
||||
cronAvailable = true;
|
||||
await vi.advanceTimersByTimeAsync(constants.STARTUP_CRON_RETRY_DELAY_MS);
|
||||
@@ -1701,6 +1702,58 @@ describe("gateway startup reconciliation", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps startup cron retry warnings quiet until the retry window is exhausted", async () => {
|
||||
vi.useFakeTimers();
|
||||
clearInternalHooks();
|
||||
const logger = createLogger();
|
||||
const onMock = vi.fn();
|
||||
const api: DreamingPluginApiTestDouble = {
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-core": {
|
||||
config: {
|
||||
dreaming: {
|
||||
enabled: true,
|
||||
frequency: "15 4 * * *",
|
||||
timezone: "UTC",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginConfig: {},
|
||||
logger,
|
||||
runtime: {},
|
||||
on: onMock,
|
||||
};
|
||||
|
||||
try {
|
||||
registerShortTermPromotionDreamingForTest(api);
|
||||
await triggerGatewayStart(onMock, {
|
||||
config: api.config,
|
||||
getCron: () => undefined,
|
||||
});
|
||||
|
||||
expectLogContains(logger.debug, "cron service not yet available at gateway_start");
|
||||
|
||||
await vi.advanceTimersByTimeAsync(
|
||||
constants.STARTUP_CRON_RETRY_DELAY_MS * (constants.STARTUP_CRON_RETRY_MAX_ATTEMPTS - 1),
|
||||
);
|
||||
expectLogNotContains(logger.warn, "cron service unavailable");
|
||||
|
||||
await vi.advanceTimersByTimeAsync(constants.STARTUP_CRON_RETRY_DELAY_MS);
|
||||
|
||||
expectLogContains(logger.warn, "cron service unavailable");
|
||||
expect(logger.warn).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
await triggerGatewayStop(onMock);
|
||||
clearInternalHooks();
|
||||
}
|
||||
});
|
||||
|
||||
it("retries disabled startup cleanup until cron is available", async () => {
|
||||
vi.useFakeTimers();
|
||||
clearInternalHooks();
|
||||
@@ -1829,6 +1882,67 @@ describe("gateway startup reconciliation", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not recreate startup cron from stale enabled config after live memory-core config is removed", async () => {
|
||||
vi.useFakeTimers();
|
||||
clearInternalHooks();
|
||||
const logger = createLogger();
|
||||
const harness = createCronHarness();
|
||||
const onMock = vi.fn();
|
||||
const runtimeCurrentConfig = vi.fn(
|
||||
() =>
|
||||
({
|
||||
plugins: {
|
||||
entries: {},
|
||||
},
|
||||
}) as OpenClawConfig,
|
||||
);
|
||||
const api: DreamingPluginApiTestDouble = {
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-core": {
|
||||
config: {
|
||||
dreaming: {
|
||||
enabled: true,
|
||||
frequency: "15 4 * * *",
|
||||
timezone: "UTC",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
pluginConfig: {},
|
||||
logger,
|
||||
runtime: {
|
||||
config: {
|
||||
current: runtimeCurrentConfig,
|
||||
},
|
||||
},
|
||||
on: onMock,
|
||||
};
|
||||
|
||||
try {
|
||||
registerShortTermPromotionDreamingForTest(api);
|
||||
let cronAvailable = false;
|
||||
await triggerGatewayStart(onMock, {
|
||||
config: api.config,
|
||||
getCron: () => (cronAvailable ? harness.cron : undefined),
|
||||
});
|
||||
|
||||
cronAvailable = true;
|
||||
await vi.advanceTimersByTimeAsync(constants.STARTUP_CRON_RETRY_DELAY_MS);
|
||||
|
||||
expect(runtimeCurrentConfig).toHaveBeenCalled();
|
||||
expect(harness.addCalls).toHaveLength(0);
|
||||
expectLogNotContains(logger.warn, "cron service unavailable");
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
await triggerGatewayStop(onMock).catch(() => undefined);
|
||||
clearInternalHooks();
|
||||
}
|
||||
});
|
||||
|
||||
it("clears pending startup cron retry on gateway stop", async () => {
|
||||
vi.useFakeTimers();
|
||||
clearInternalHooks();
|
||||
|
||||
@@ -760,18 +760,18 @@ export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void
|
||||
].join("|");
|
||||
|
||||
const reconcileManagedDreamingCron = async (params: {
|
||||
reason: "startup" | "runtime";
|
||||
reason: "startup" | "startup_retry" | "runtime";
|
||||
startupConfig?: OpenClawConfig;
|
||||
startupCron?: (() => CronServiceLike | null) | null;
|
||||
}): Promise<ShortTermPromotionDreamingConfig> => {
|
||||
const startupCfg =
|
||||
params.reason === "startup" ? (params.startupConfig ?? api.config) : resolveCurrentConfig();
|
||||
const pluginConfig =
|
||||
params.reason === "runtime"
|
||||
? resolveMemoryCorePluginConfig(startupCfg)
|
||||
: (resolveMemoryCorePluginConfig(startupCfg) ??
|
||||
params.reason === "startup"
|
||||
? (resolveMemoryCorePluginConfig(startupCfg) ??
|
||||
resolveMemoryCorePluginConfig(api.config) ??
|
||||
api.pluginConfig);
|
||||
api.pluginConfig)
|
||||
: resolveMemoryCorePluginConfig(startupCfg);
|
||||
const config = resolveShortTermPromotionDreamingConfig({
|
||||
pluginConfig,
|
||||
cfg: startupCfg,
|
||||
@@ -784,7 +784,7 @@ export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void
|
||||
// This handles the case where the cron service was not yet available during
|
||||
// gateway_start (250ms deferred init race in startGatewaySidecars) but is
|
||||
// available now. Fixes #67362.
|
||||
if (!cron && params.reason === "runtime" && gatewayContext) {
|
||||
if (!cron && params.reason !== "startup" && gatewayContext) {
|
||||
try {
|
||||
cron = resolveCronServiceFromGatewayContext(gatewayContext);
|
||||
if (cron) {
|
||||
@@ -800,7 +800,7 @@ export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void
|
||||
// Avoid a noisy startup-path warning when the gateway has not exposed cron yet.
|
||||
// The runtime reconciliation path (heartbeat-driven) will still warn if the
|
||||
// cron service remains unavailable after boot.
|
||||
if (params.reason === "startup") {
|
||||
if (params.reason === "startup" || params.reason === "startup_retry") {
|
||||
api.logger.debug?.(
|
||||
"memory-core: cron service not yet available at gateway_start; deferring to runtime reconciliation.",
|
||||
);
|
||||
@@ -815,6 +815,11 @@ export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void
|
||||
unavailableCronWarningEmitted = false;
|
||||
clearStartupCronRetry();
|
||||
}
|
||||
// Startup retries only probe cron availability; the exhausted retry path
|
||||
// re-enters runtime reconciliation so persistent failures still warn once.
|
||||
if (!cron && params.reason === "startup_retry") {
|
||||
return config;
|
||||
}
|
||||
if (params.reason === "runtime") {
|
||||
const now = Date.now();
|
||||
const withinThrottleWindow =
|
||||
@@ -852,12 +857,16 @@ export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void
|
||||
return;
|
||||
}
|
||||
startupCronRetryAttempts += 1;
|
||||
void reconcileManagedDreamingCron({ reason: "runtime" })
|
||||
.then(() => {
|
||||
void reconcileManagedDreamingCron({ reason: "startup_retry" })
|
||||
.then(async () => {
|
||||
if (disposed || hasStartupCron()) {
|
||||
clearStartupCronRetry();
|
||||
return;
|
||||
}
|
||||
if (startupCronRetryAttempts >= STARTUP_CRON_RETRY_MAX_ATTEMPTS) {
|
||||
await reconcileManagedDreamingCron({ reason: "runtime" });
|
||||
return;
|
||||
}
|
||||
scheduleStartupCronRetry();
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
|
||||
Reference in New Issue
Block a user