mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 21:28:26 +00:00
Cron: add scheduler, wakeups, and run history
This commit is contained in:
101
src/cron/run-log.ts
Normal file
101
src/cron/run-log.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
export type CronRunLogEntry = {
|
||||
ts: number;
|
||||
jobId: string;
|
||||
action: "finished";
|
||||
status?: "ok" | "error" | "skipped";
|
||||
error?: string;
|
||||
runAtMs?: number;
|
||||
durationMs?: number;
|
||||
nextRunAtMs?: number;
|
||||
};
|
||||
|
||||
export function resolveCronRunLogPath(params: {
|
||||
storePath: string;
|
||||
jobId: string;
|
||||
}) {
|
||||
const storePath = path.resolve(params.storePath);
|
||||
const dir = path.dirname(storePath);
|
||||
const base = path.basename(storePath);
|
||||
if (base === "jobs.json") {
|
||||
return path.join(dir, "runs", `${params.jobId}.jsonl`);
|
||||
}
|
||||
|
||||
const ext = path.extname(base);
|
||||
const baseNoExt = ext ? base.slice(0, -ext.length) : base;
|
||||
return path.join(dir, `${baseNoExt}.runs.jsonl`);
|
||||
}
|
||||
|
||||
const writesByPath = new Map<string, Promise<void>>();
|
||||
|
||||
async function pruneIfNeeded(
|
||||
filePath: string,
|
||||
opts: { maxBytes: number; keepLines: number },
|
||||
) {
|
||||
const stat = await fs.stat(filePath).catch(() => null);
|
||||
if (!stat || stat.size <= opts.maxBytes) return;
|
||||
|
||||
const raw = await fs.readFile(filePath, "utf-8").catch(() => "");
|
||||
const lines = raw
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
const kept = lines.slice(Math.max(0, lines.length - opts.keepLines));
|
||||
const tmp = `${filePath}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
||||
await fs.writeFile(tmp, `${kept.join("\n")}\n`, "utf-8");
|
||||
await fs.rename(tmp, filePath);
|
||||
}
|
||||
|
||||
export async function appendCronRunLog(
|
||||
filePath: string,
|
||||
entry: CronRunLogEntry,
|
||||
opts?: { maxBytes?: number; keepLines?: number },
|
||||
) {
|
||||
const resolved = path.resolve(filePath);
|
||||
const prev = writesByPath.get(resolved) ?? Promise.resolve();
|
||||
const next = prev
|
||||
.catch(() => undefined)
|
||||
.then(async () => {
|
||||
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
||||
await fs.appendFile(resolved, `${JSON.stringify(entry)}\n`, "utf-8");
|
||||
await pruneIfNeeded(resolved, {
|
||||
maxBytes: opts?.maxBytes ?? 2_000_000,
|
||||
keepLines: opts?.keepLines ?? 2_000,
|
||||
});
|
||||
});
|
||||
writesByPath.set(resolved, next);
|
||||
await next;
|
||||
}
|
||||
|
||||
export async function readCronRunLogEntries(
|
||||
filePath: string,
|
||||
opts?: { limit?: number; jobId?: string },
|
||||
): Promise<CronRunLogEntry[]> {
|
||||
const limit = Math.max(1, Math.min(5000, Math.floor(opts?.limit ?? 200)));
|
||||
const jobId = opts?.jobId?.trim() || undefined;
|
||||
const raw = await fs
|
||||
.readFile(path.resolve(filePath), "utf-8")
|
||||
.catch(() => "");
|
||||
if (!raw.trim()) return [];
|
||||
const parsed: CronRunLogEntry[] = [];
|
||||
const lines = raw.split("\n");
|
||||
for (let i = lines.length - 1; i >= 0 && parsed.length < limit; i--) {
|
||||
const line = lines[i]?.trim();
|
||||
if (!line) continue;
|
||||
try {
|
||||
const obj = JSON.parse(line) as Partial<CronRunLogEntry> | null;
|
||||
if (!obj || typeof obj !== "object") continue;
|
||||
if (obj.action !== "finished") continue;
|
||||
if (typeof obj.jobId !== "string" || obj.jobId.trim().length === 0)
|
||||
continue;
|
||||
if (typeof obj.ts !== "number" || !Number.isFinite(obj.ts)) continue;
|
||||
if (jobId && obj.jobId !== jobId) continue;
|
||||
parsed.push(obj as CronRunLogEntry);
|
||||
} catch {
|
||||
// ignore invalid lines
|
||||
}
|
||||
}
|
||||
return parsed.reverse();
|
||||
}
|
||||
Reference in New Issue
Block a user