跳转到内容

durable-routine

@asm/durable-routine 提供日历计算和可恢复的任务执行。宿主保存定义、安排投递,并在中断后重新投递;包不会启动轮询服务。

import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import {
nextRoutineOccurrence,
withTenantContext,
} from "@asm/durable-routine";
const definition = {
tenantId: "demo-tenant",
scopeId: "demo-store",
routineId: "inventory.refresh",
profile: "inventory",
schedule: {
hour: 7,
minute: 15,
daysOfWeek: [1, 2, 3, 4, 5],
timezone: "Asia/Tokyo",
},
staggerWindowMs: 0,
};
const context = withTenantContext(definition, BACKGROUND_CONTEXT);
const occurrence = nextRoutineOccurrence(definition, Date.now(), context);
console.log(occurrence.scheduledAt, occurrence.fireAt, occurrence.runKey);

scheduledAt 是日历上的名义时间,fireAt 是加入错峰偏移后的实际触发时间。宿主在 fireAt 投递,重试始终保留原 scheduledAt,不能用重试时的 Date.now() 替代。

小时为 0–23,分钟为 0–59,星期日为 0。daysOfWeek 和 daysOfMonth 同时存在时都必须匹配。夏令时跳过不存在的时刻,重复时刻只取第一次。默认错峰窗口为 30 分钟,偏移由租户、范围和 Routine 身份确定;设为 0 可关闭。

接续上面的变量。宿主提供 redis、openHarness 和用 Durable defineTask 定义的 inventoryTask:

import {
RedisRoutineLock,
RoutineRunner,
SqliteRoutineRunStore,
} from "@asm/durable-routine";
const store = new SqliteRoutineRunStore("./routine-runs.sqlite");
const runner = new RoutineRunner({
store,
lock: new RedisRoutineLock(redis),
openHarness,
});
try {
const result = await runner.run({
...definition,
scheduledAt: occurrence.scheduledAt,
task: inventoryTask,
input: null,
}, context);
console.log(result.kind);
} finally {
await store.close();
}

运行记录可换成 MemoryRoutineRunStore 或 PostgresRoutineRunStore。PostgreSQL 版本需要先由迁移角色执行导出的 POSTGRES_ROUTINE_SCHEMA,再传入连接池。运行记录库和 Durable 任务存储是两份不同职责的数据。

openHarness(run, context) 每次返回一个新的、暂停状态的 Harness,并重新注册任务定义。同一次运行必须重开同一份持久存储,且在 Harness 关闭前独占它。

例如两次不同日期的库存刷新使用不同运行键。它们可以并发,但不能仅凭“属于同一门店”就共用一个未加保护的 Durable 存储。PostgreSQL 可用 sessionKey: "routine:" + run.runKey 分开命名空间;同一运行重投时仍使用原键。

Runner 首次将会话、根任务和绑定关系写入同一次 Durable 提交。中断后重新读取绑定并等待原任务,不再新建根任务。Runner 会关闭它打开的 Harness;宿主关闭运行记录库、Redis 和数据库池。

result.kind 含义
succeeded Durable 任务已完成,结果在 run.outcome
failed Durable 任务到达失败或中止的终态
skipped 该次运行已是终态,无需再打开 Harness;检查 run.outcome
locked 同一运行仍持有 Redis 锁,保留投递以便稍后重试

基础设施异常会抛出,运行记录保留为 running。Redis 故障不会触发备用执行路径。RedisRoutineLock 接受 sendCommand(string[]) 适配器,默认 TTL 为 30 秒、命令超时为 5 秒。

检查点只保证已提交阶段的恢复。数据库之外的邮件、付款或其他外部副作用仍需宿主实现幂等请求。

源码依据:日历计算、执行器及结果类型、运行记录。