feat(server): unified scheduled-job registry and scheduler; module timers moved onto it
- modules/schedules: registerScheduledJob({key, defaults, run}) registry; defaults are
synced into scheduled_job without overriding user-changed enabled/schedule
- scheduler runs only on the io worker, one instance via pg_try_advisory_lock held on a
dedicated connection; due = enabled and next_run_at reached, or run requested;
jobs run in parallel, never re-entrant; interval next = finish + period, cron next match;
run-now does not move next_run_at and merges duplicate requests; skip advances one period;
history trimmed to the last 50 runs; interrupted runs marked failed on takeover
- agent_check jobs: trigger condition checked first, goal template rendered, runAgent
enqueued via startRun(trigger=schedule) with allowlist and budget, one run per time slot;
allowRecover uses the autoInvestigate grant check; results harvested back into the run
history and optionally reported as an info decision; 3 built-in checks, off by default
- sources-identity (feeds.poll, sites.snapshot, identity.review), acquisition-organize
(downloads.sync, cache.threshold, downloads.orchestrate, slots.reevaluate,
subscriptions.search, subscriptions.metadata, library.reconcile) and file-manager
(trash.purge) register jobs instead of running their own setInterval; default
intervals unchanged; backup registered as an unconfigured placeholder
- endpoints under /schedules (admin, console session only)
TODO: pi refreshPiTokens and auth-settings session cleanup still own their timers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
f8f70d8e09
commit
a68fcea13b
16 files changed
+1732
-127
No files matched your search
@@ -3,9 +3,9 @@ import { defineTask } from "@umt/tasks";
|
||||
import { z } from "zod";
|
||||
import type { AoOverrides } from "../acquisition-organize/index.ts";
|
||||
import { createPorts } from "../acquisition-organize/ports.ts";
|
||||
import { timeSlot } from "../acquisition-organize/services/common.ts";
|
||||
import { registerScheduledJob } from "../schedules/registry.ts";
|
||||
import type { ServerContext, ServerModule } from "../types.ts";
|
||||
import { defaultOptions, errMessage, type FmDeps, type FmOptions } from "./common.ts";
|
||||
import { defaultOptions, type FmDeps, type FmOptions } from "./common.ts";
|
||||
import { registerFileRoutes } from "./routes.ts";
|
||||
import { purgeTrash } from "./trash.ts";
|
||||
|
||||
@@ -13,7 +13,7 @@ import { purgeTrash } from "./trash.ts";
|
||||
* W5-A 文件管理(/files,docs/plan/06-file-manager-spec.md):只浏览已登记根(库根 + 下载器路径映射),
|
||||
* 行内业务状态、改名同步资产与路径绑定修订、移动/复制两段式 planId、整理走 planImport、回收站 + 引用检查、识别/探测/刮削。
|
||||
* 复用 acquisition-organize 的服务导出(planImport / 路径守卫 / Move 授权 / H&R),不改其文件。
|
||||
* 持久任务 purgeTrash:每日清理过期回收站项(FILES_TRASH_RETENTION_DAYS,默认 7 天)。
|
||||
* 持久任务 purgeTrash:每日清理过期回收站项(FILES_TRASH_RETENTION_DAYS,默认 7 天);周期登记为定时任务 trash.purge。
|
||||
*/
|
||||
export interface FmOverrides {
|
||||
ao?: AoOverrides;
|
||||
@@ -22,7 +22,6 @@ export interface FmOverrides {
|
||||
|
||||
export function createFileManagerModule(overrides: FmOverrides = {}): ServerModule & { deps(): FmDeps } {
|
||||
let deps: FmDeps | null = null;
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
const lazy = (ctx: ServerContext): FmDeps => {
|
||||
if (!deps) {
|
||||
const { options: aoOptions, ...aoRest } = overrides.ao ?? {};
|
||||
@@ -34,20 +33,6 @@ export function createFileManagerModule(overrides: FmOverrides = {}): ServerModu
|
||||
}
|
||||
return deps;
|
||||
};
|
||||
const enqueuePurge = async (d: FmDeps) => {
|
||||
try {
|
||||
await d.ctx.engine.enqueue(
|
||||
"purgeTrash",
|
||||
{},
|
||||
{
|
||||
idempotencyKey: `purgeTrash:${timeSlot(d.ao.ports.now(), d.options.purgeIntervalMs)}`,
|
||||
traceId: randomUUID(),
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
d.ctx.logger.warn({ err: { message: errMessage(err) } }, "回收站清理入队失败");
|
||||
}
|
||||
};
|
||||
return {
|
||||
name: "file-manager",
|
||||
deps() {
|
||||
@@ -75,12 +60,23 @@ export function createFileManagerModule(overrides: FmOverrides = {}): ServerModu
|
||||
start(ctx) {
|
||||
const d = lazy(ctx);
|
||||
if (!d.options.backgroundLoops || d.options.purgeIntervalMs <= 0) return;
|
||||
void enqueuePurge(d);
|
||||
timer = setInterval(() => void enqueuePurge(d), Math.min(d.options.purgeIntervalMs, 3600_000));
|
||||
},
|
||||
stop() {
|
||||
if (timer) clearInterval(timer);
|
||||
timer = null;
|
||||
registerScheduledJob(ctx, {
|
||||
key: "trash.purge",
|
||||
defaults: {
|
||||
name: "回收站清理",
|
||||
description: "清除过期的回收站项",
|
||||
group: "maintain",
|
||||
schedule: { type: "interval", everyMs: d.options.purgeIntervalMs },
|
||||
},
|
||||
run: async (c) => {
|
||||
const r = await d.ctx.engine.enqueue(
|
||||
"purgeTrash",
|
||||
{},
|
||||
{ idempotencyKey: `purgeTrash:${c.slot}`, traceId: randomUUID() },
|
||||
);
|
||||
return { summary: r.deduplicated ? "本周期已入队" : "已入队清理" };
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user