- 保留规则改为声明式(retention.ts 每个任务的 rules),定时任务、立即清理、脚本 dry-run 共用一份条件; 删得多(≥ 1000 行)的表删完 ANALYZE - 引擎 run 单独保留期 engineRun(缺省 14 天):只删终态、过期、没被保留中业务任务引用、父 run 已结束的; step_attempts 级联,运行元数据 / 事实 / 信号跟随 run(原来按时间直接删,长时间运行的 run 会丢元数据) - 业务任务被 agent_run / decision 等引用时不删(外键 set null,删了会断开引用) - 新任务 storage.retention.pipeline(每天 04:40):已投递 outbox 事件(只删无幂等键或一次性幂等键)、 识别尝试(每条资源留最近 3 次)、已识别且资源仍绑定作品的待识别条目(忽略 / 归档不删)、 站点快照(每站最近一次成功的列表快照保留) - settings.storage.retentionDays 追加 engineRun / outboxEvent / resolutionAttempt / identityResolved / siteSnapshot(旧库按缺省补),设置页「系统 → 数据保留」显示 - scripts/db-retention.ts:缺省 dry-run 输出每张表将删行数,--apply 执行,--only 选目标 - 测试:各表按规则删、被引用 / 未结束的不删、dry-run 与删除一致、分批;EXPLAIN 确认改写后的查询走索引 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
60 lines
3.1 KiB
TypeScript
60 lines
3.1 KiB
TypeScript
// 数据保留清理(docs/runbook/storage.md「保留规则」):按 settings.storage.retentionDays 统计 / 执行各保留任务。
|
|
// 规则与定时任务 storage.retention.* 是同一份代码(apps/server/src/modules/system/retention.ts)。
|
|
// 默认只读(dry-run),输出每张表将删的行数;--apply 才删除(分批、每批单独事务,删得多的表随后 ANALYZE)。
|
|
//
|
|
// 用法(DATABASE_URL 必须显式给出,脚本不带默认库):
|
|
// DATABASE_URL=postgres://umt:***@<host>:5432/umt deno run -A scripts/db-retention.ts [--dry-run] # 看每张表将删多少行
|
|
// DATABASE_URL=… deno run -A scripts/db-retention.ts --apply # 执行
|
|
// DATABASE_URL=… deno run -A scripts/db-retention.ts --only task_run,pipeline [--apply] # 只处理部分目标
|
|
// ENGINE_SCHEMA=openworkflow(缺省)
|
|
//
|
|
// 不包含:进程心跳(system.heartbeat_cleanup)、图片缓存与 kv_cache(cache 模块)、ANALYZE 任务(--apply 结束时对删过的表各自 ANALYZE)。
|
|
// 对真实库先 dry-run,逐表看过再 --apply(docs/plan/lessons.md 第 22 条)。
|
|
|
|
import { withStorageDefaults } from "@umt/contracts";
|
|
import { createDb, sql } from "@umt/db";
|
|
import { previewJob, RETENTION_JOBS } from "../apps/server/src/modules/system/retention.ts";
|
|
|
|
const argv = [...Deno.args];
|
|
const APPLY = argv.includes("--apply");
|
|
if (APPLY && argv.includes("--dry-run")) {
|
|
console.error("--apply 与 --dry-run 不能同时给出");
|
|
Deno.exit(2);
|
|
}
|
|
const onlyArg = argv.indexOf("--only");
|
|
const only = onlyArg >= 0 ? new Set((argv[onlyArg + 1] ?? "").split(",").filter(Boolean)) : null;
|
|
const url = Deno.env.get("DATABASE_URL");
|
|
if (!url) {
|
|
console.error("需要 DATABASE_URL(脚本不带默认库)");
|
|
Deno.exit(2);
|
|
}
|
|
const engineSchema = Deno.env.get("ENGINE_SCHEMA") ?? "openworkflow";
|
|
|
|
const db = createDb({ connectionString: url, max: 2 });
|
|
try {
|
|
const stored = await sql<{ value: unknown }>`select value from settings where key = 'storage'`.execute(db);
|
|
const settings = withStorageDefaults(stored.rows[0]?.value);
|
|
const now = new Date();
|
|
const jobs = RETENTION_JOBS.filter((j) => j.target !== "analyze" && (!only || only.has(j.target)));
|
|
console.log(`# db-retention (${APPLY ? "APPLY" : "DRY-RUN"}) ${now.toISOString()}`);
|
|
console.log(`# retentionDays ${JSON.stringify(settings.retentionDays)}\n`);
|
|
let total = 0;
|
|
for (const job of jobs) {
|
|
const counts = await previewJob(job, { db, engineSchema, settings, now });
|
|
console.log(`## ${job.target}(${job.jobId},保留 ${job.retention(settings) ?? "—"})`);
|
|
for (const c of counts) {
|
|
console.log(` ${c.table.padEnd(34)} ${String(c.rows).padStart(8)} ${c.label}`);
|
|
if (!c.cascade && job.rules) total += c.rows;
|
|
}
|
|
if (APPLY) {
|
|
const t0 = Date.now();
|
|
const summary = await job.run({ db, engineSchema, settings, now, batch: { budgetMs: 60 * 60_000 } });
|
|
console.log(` → ${summary}(${Math.round((Date.now() - t0) / 100) / 10} 秒)`);
|
|
}
|
|
console.log("");
|
|
}
|
|
console.log(`# 合计将删 ${total} 行${APPLY ? "(已执行)" : "(dry-run,未改库;确认后加 --apply)"}`);
|
|
} finally {
|
|
await db.destroy();
|
|
}
|