104 lines
2.9 KiB
TypeScript
104 lines
2.9 KiB
TypeScript
import { ensureDatabase } from "./bootstrap";
|
|
import { DomainError, runRetentionMaintenance, type RetentionMaintenanceResult } from "./repository";
|
|
import {
|
|
ACQUIRE_RETENTION_JOB_SQL,
|
|
COMPLETE_RETENTION_JOB_SQL,
|
|
FAIL_RETENTION_JOB_SQL,
|
|
RETENTION_JOB_KEY,
|
|
RETENTION_RUNNING_STALE_MS,
|
|
} from "./retention";
|
|
|
|
export type ScheduledRetentionOutcome =
|
|
| { status: "skipped"; reason: "already_running" }
|
|
| { status: "succeeded"; result: RetentionMaintenanceResult };
|
|
|
|
function scheduledTimestamp(value: number): string {
|
|
if (!Number.isFinite(value)) {
|
|
throw new TypeError("scheduled retention requires a finite timestamp");
|
|
}
|
|
const timestamp = new Date(value);
|
|
if (!Number.isFinite(timestamp.getTime())) {
|
|
throw new TypeError("scheduled retention requires a valid timestamp");
|
|
}
|
|
return timestamp.toISOString();
|
|
}
|
|
|
|
function safeErrorCode(error: unknown): string {
|
|
if (error instanceof DomainError && /^[a-z0-9_]{3,64}$/.test(error.code)) {
|
|
return error.code;
|
|
}
|
|
return "retention_maintenance_failed";
|
|
}
|
|
|
|
export async function runScheduledRetentionMaintenance(
|
|
db: D1Database,
|
|
scheduledTime: number,
|
|
): Promise<ScheduledRetentionOutcome> {
|
|
await ensureDatabase();
|
|
|
|
const scheduledAt = scheduledTimestamp(scheduledTime);
|
|
const startedAt = new Date().toISOString();
|
|
const staleBefore = new Date(
|
|
new Date(startedAt).getTime() - RETENTION_RUNNING_STALE_MS,
|
|
).toISOString();
|
|
const runId = `maintenance_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
const acquired = await db
|
|
.prepare(ACQUIRE_RETENTION_JOB_SQL)
|
|
.bind(
|
|
RETENTION_JOB_KEY,
|
|
runId,
|
|
"scheduled",
|
|
scheduledAt,
|
|
startedAt,
|
|
startedAt,
|
|
startedAt,
|
|
staleBefore,
|
|
)
|
|
.run();
|
|
if (Number(acquired.meta.changes ?? 0) !== 1) {
|
|
return { status: "skipped", reason: "already_running" };
|
|
}
|
|
|
|
try {
|
|
const result = await runRetentionMaintenance({
|
|
actorId: "system:scheduled-retention",
|
|
confirmed: true,
|
|
reason: "每日自动清理已经到期的技术记录",
|
|
now: startedAt,
|
|
});
|
|
const completedAt = new Date().toISOString();
|
|
const completion = await db
|
|
.prepare(COMPLETE_RETENTION_JOB_SQL)
|
|
.bind(
|
|
completedAt,
|
|
completedAt,
|
|
JSON.stringify(result),
|
|
completedAt,
|
|
RETENTION_JOB_KEY,
|
|
runId,
|
|
)
|
|
.run();
|
|
if (Number(completion.meta.changes ?? 0) !== 1) {
|
|
throw new DomainError(
|
|
"retention_run_fenced",
|
|
"到期数据清理完成,但运行状态已经被更新的任务接管",
|
|
409,
|
|
);
|
|
}
|
|
return { status: "succeeded", result };
|
|
} catch (error) {
|
|
const failedAt = new Date().toISOString();
|
|
await db
|
|
.prepare(FAIL_RETENTION_JOB_SQL)
|
|
.bind(
|
|
failedAt,
|
|
safeErrorCode(error),
|
|
failedAt,
|
|
RETENTION_JOB_KEY,
|
|
runId,
|
|
)
|
|
.run();
|
|
throw error;
|
|
}
|
|
}
|