feat: establish NekoNest Cloud control and relay
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user