openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
120 lines (119 loc) • 5.11 kB
JavaScript
//#region src/cron/service/active-run-cancellation.ts
const activeCronTaskRunsByRunId = /* @__PURE__ */ new Map();
const settlingCronTaskRuns = /* @__PURE__ */ new Map();
const activeCronTaskRunDrainWaiters = /* @__PURE__ */ new Set();
const suspensionVisibleCronTaskRuns = /* @__PURE__ */ new Set();
const CRON_TASK_RUN_SETTLEMENT_TRACKING_MAX_MS = 6e4;
function notifyActiveCronTaskRunDrainWaitersIfEmpty() {
if (activeCronTaskRunsByRunId.size > 0 || settlingCronTaskRuns.size > 0) return;
for (const resolve of activeCronTaskRunDrainWaiters) resolve();
activeCronTaskRunDrainWaiters.clear();
}
function startActiveCronTaskRunSettlementGrace(promise) {
const entry = settlingCronTaskRuns.get(promise);
if (!entry || entry.retirementTimer) return;
entry.retirementTimer = setTimeout(() => {
settlingCronTaskRuns.delete(promise);
notifyActiveCronTaskRunDrainWaitersIfEmpty();
}, CRON_TASK_RUN_SETTLEMENT_TRACKING_MAX_MS);
entry.retirementTimer.unref?.();
}
function registerActiveCronTaskRun(params) {
const runId = params.runId?.trim();
if (!runId) return;
const handle = {
controller: params.controller,
onCancel: params.onCancel
};
activeCronTaskRunsByRunId.set(runId, handle);
const cancelJobRun = (reason) => {
cancelActiveCronTaskRun({
runId,
reason
});
};
if (params.activeJobMarker?.cancellation?.kind === "requested") cancelJobRun(params.activeJobMarker.cancellation.reason);
else if (params.activeJobMarker) params.activeJobMarker.cancellation = {
kind: "bound",
cancel: cancelJobRun
};
return () => {
if (params.activeJobMarker?.cancellation?.kind === "bound" && params.activeJobMarker.cancellation.cancel === cancelJobRun) delete params.activeJobMarker.cancellation;
if (activeCronTaskRunsByRunId.get(runId)?.controller === params.controller) {
activeCronTaskRunsByRunId.delete(runId);
notifyActiveCronTaskRunDrainWaitersIfEmpty();
}
};
}
function abortActiveCronTaskRuns(reason = "Gateway restarting.") {
let aborted = 0;
for (const handle of activeCronTaskRunsByRunId.values()) {
if (handle.controller.signal.aborted) continue;
handle.controller.abort(reason);
handle.onCancel?.(reason);
aborted += 1;
}
for (const promise of settlingCronTaskRuns.keys()) startActiveCronTaskRunSettlementGrace(promise);
return aborted;
}
function trackActiveCronTaskRunSettlement(promise, abortSignal) {
settlingCronTaskRuns.set(promise, {});
suspensionVisibleCronTaskRuns.add(promise);
const startSettlementGrace = () => startActiveCronTaskRunSettlementGrace(promise);
abortSignal?.addEventListener("abort", startSettlementGrace, { once: true });
if (abortSignal?.aborted) startSettlementGrace();
promise.catch(() => void 0).finally(() => {
abortSignal?.removeEventListener("abort", startSettlementGrace);
const entry = settlingCronTaskRuns.get(promise);
if (entry?.retirementTimer) clearTimeout(entry.retirementTimer);
settlingCronTaskRuns.delete(promise);
suspensionVisibleCronTaskRuns.delete(promise);
notifyActiveCronTaskRunDrainWaitersIfEmpty();
});
}
/** Cron cores that can still mutate state even after timeout/cancel returned. */
function getSuspensionVisibleCronTaskRunCount() {
return suspensionVisibleCronTaskRuns.size;
}
/** Retires restart-drain bookkeeping without hiding still-running cores from suspension. */
function retireActiveCronTaskRunTracking() {
activeCronTaskRunsByRunId.clear();
for (const entry of settlingCronTaskRuns.values()) if (entry.retirementTimer) clearTimeout(entry.retirementTimer);
settlingCronTaskRuns.clear();
notifyActiveCronTaskRunDrainWaitersIfEmpty();
}
async function waitForActiveCronTaskRuns(timeoutMs) {
const waitMs = Math.max(0, Math.floor(timeoutMs));
if (waitMs > 0 && (activeCronTaskRunsByRunId.size > 0 || settlingCronTaskRuns.size > 0)) await new Promise((resolve) => {
const waiter = () => {
clearTimeout(timeout);
resolve();
};
const timeout = setTimeout(() => {
activeCronTaskRunDrainWaiters.delete(waiter);
resolve();
}, waitMs);
activeCronTaskRunDrainWaiters.add(waiter);
});
return {
drained: activeCronTaskRunsByRunId.size === 0 && settlingCronTaskRuns.size === 0,
active: activeCronTaskRunsByRunId.size + settlingCronTaskRuns.size
};
}
function cancelActiveCronTaskRun(params) {
const runId = params.runId?.trim();
if (!runId) return false;
const handle = activeCronTaskRunsByRunId.get(runId);
if (!handle || handle.controller.signal.aborted) return false;
const reason = params.reason?.trim() || "Cancelled by operator.";
handle.controller.abort(reason);
handle.onCancel?.(reason);
return true;
}
function resetActiveCronTaskRunsForTests() {
retireActiveCronTaskRunTracking();
suspensionVisibleCronTaskRuns.clear();
}
if (process.env.VITEST || false) globalThis[Symbol.for("openclaw.activeCronTaskRunTestApi")] = { resetActiveCronTaskRunsForTests };
//#endregion
export { retireActiveCronTaskRunTracking as a, registerActiveCronTaskRun as i, cancelActiveCronTaskRun as n, trackActiveCronTaskRunSettlement as o, getSuspensionVisibleCronTaskRunCount as r, waitForActiveCronTaskRuns as s, abortActiveCronTaskRuns as t };