定时任务与运行恢复
# 定时任务与运行恢复
# 目的
配置仅使用 Cron 的持久任务,查看实际触发点、运行与认领历史,并在失败后恢复原运行。租户任务内置「受理站内通知」;平台任务内置「平台基础设施目录统计」、「按租户拆分基础设施统计」和「维护统计结果」。计划保存成功只表示配置已提交,运行完成的含义由处理器决定。
平台目录统计完成后可以查看计数结果。按租户拆分的父运行完成只表示各目标子运行已持久受理,每个租户的成功、失败和恢复都在「租户子运行」中查看。通知任务完成表示通知已受理,实际送达继续查 站内通知。
# 前置条件
显式迁移到当前 schema v29,再启动匹配的 API、worker 和前端。迁移不自动启用任何平台任务;旧非 Cron 计划会停用待重新配置,见本页「历史计划升级」。在外层仓库运行,数据库和 Redis 参数由本机环境配置提供:
bash scripts/fago.sh migrate
bash scripts/fago.sh api
# 在另一个终端启动;只启动API不会调度或执行业务。
bash scripts/fago.sh worker
成功时 API 就绪检查通过,worker 启动租户任务、平台父任务和租户子运行循环。对象存储可以关闭;连接、schema 或必要依赖不符时启动失败,不降级为内存执行。
租户套餐必须开通 infra.jobs,通知处理器另需 infra.notifications。租户角色按需授予 infra:job:read/create/update/delete/run/replay;准备通知内容另需 infra:notification:send,页面选择模板需 infra:template:read。平台使用独立的 platform:infra:job:read/create/update/delete/run/replay;选择租户目标另需当前 platform:tenant:read。子运行执行时目标套餐必须仍有 infra.jobs 和 infra.configuration,租户必须启用且未到期。平台权限不能替代目标当前能力检查。
# 步骤与规则
# 1. 预览当前 Cron
打开租户「基础设施 → 定时任务」或平台「平台基础设施 → 平台系统任务」。填写名称、编码、处理器、准入起点、五段表达式、IANA 时区和错过策略,先点击「校验并预览执行时间」。页面显示服务端计算的未来最多五次时间;字段变化、预览失败或旧预览晚到都会使保存资格失效。
以下为开发环境明文预览。startAt 示例为 2030-01-01 UTC,只用于演示;实际使用本次需要的 UTC 毫秒准入起点。生产走统一 JWE,解密后的业务结构相同:
POST /api/admin/v1/infra/jobs/preview
Origin: http://127.0.0.1:5174
X-App-Request: admin
Authorization: Bearer <操作者提供的access>
Content-Type: application/json
{"kind":"cron","startAt":1893456000000,"cronExpression":"*/5 * * * *","timeZone":"Asia/Shanghai","misfirePolicy":"coalesce"}
成功响应中 times 是严格递增的 UTC 毫秒点,页面按响应的时区显示。平台预览使用 /api/platform/v1/infra/jobs/preview 和 X-App-Request: platform。
{"code":0,"data":{"times":[1893456000000,1893456300000,1893456600000,1893456900000,1893457200000],"timeZone":"Asia/Shanghai"},"message":"ok","requestId":"req_preview"}
Cron 格式为「分 时 日 月 周」,不接受秒、年、@every、描述符或表达式中的时区覆盖。每五分钟使用 */5 * * * *,每天凌晨两点使用 0 2 * * *,每周一九点使用 0 9 * * MON。这是日历边界计算,不是从上次业务完成后再等待一段时间。夏令时缺失分钟跳过,回拨重复分钟分别形成不同 UTC 触发点。
# 2. 保存租户通知计划
先准备启用的同租户通知模板与有效收件账号。创建时提交完整变量,服务在当前权限事务内渲染并保存私有内容快照;管理接口不会返回正文、原变量、摘要、租约 token 或运行随机键。
POST /api/admin/v1/infra/jobs
Origin: http://127.0.0.1:5174
X-App-Request: admin
Authorization: Bearer <操作者提供的access>
Content-Type: application/json
{
"code":"reminder.daily","name":"每日提醒","handler":"notification.inapp",
"schedule":{"kind":"cron","startAt":1893456000000,"cronExpression":"0 9 * * *","timeZone":"Asia/Shanghai","misfirePolicy":"coalesce"},
"status":"enabled","version":0,
"payload":{"templateId":"10","templateVersion":1,"recipientId":"20","variables":{"name":"同事"}}
}
variables 必须与所选模板一致;示例中的模板及账号 ID 需换成本租户真实资源。成功的计划 DTO 如下,实际 ID 与 nextDue 由服务计算:
{"code":0,"data":{"id":"30","code":"reminder.daily","name":"每日提醒","handler":"notification.inapp","schedule":{"kind":"cron","startAt":1893456000000,"cronExpression":"0 9 * * *","timeZone":"Asia/Shanghai","misfirePolicy":"coalesce"},"needsConfiguration":false,"status":"enabled","version":1,"nextDue":1893459600000},"message":"ok","requestId":"req_create"}
编码与处理器创建后不可变。普通编辑保留未返回页面的私有内容;只有勾选重新配置内容才发送新 payload 并重新检查模板版本、发送权限和收件状态。更新用 PUT /jobs/{id},携带当前正数 version;省略或 null 载荷保留原快照。后台推进游标不改变配置版本。
# 3. 保存平台统计与租户拆分计划
平台目录统计使用 handler: "platform.infrastructure.snapshot" 与 payload: {}。按租户统计使用下列固定处理器,目标来自平台租户管理中的规范 ID,最多100个且不重复。编辑已有计划默认保留原目标;只有显式重新选择才替换未来父运行的目标快照。
POST /api/platform/v1/infra/jobs
Origin: http://127.0.0.1:5174
X-App-Request: platform
Authorization: Bearer <平台操作者提供的access>
Content-Type: application/json
{
"code":"statistics.tenants","name":"租户目录统计","handler":"tenant.infrastructure.snapshot",
"schedule":{"kind":"cron","startAt":1893456000000,"cronExpression":"0 2 * * *","timeZone":"Asia/Shanghai","misfirePolicy":"skip"},
"status":"enabled","payload":{"tenantIds":["2","3"]}
}
保存成功后点击「执行一次」可以立即验证,不必等待准入起点。平台父事务只受理子集合,不获取全部租户锁;子运行使用真实租户 ID,分别检查当前状态、到期和能力,分别限流、重试与记录历史。某个租户不可用、队列满或暂时写入失败,不回滚其他租户已经完成的业务。
平台页面通过以下专用接口读取子历史,不能将相同数字的子运行 ID 送到父运行接口:
GET /api/platform/v1/infra/jobs/30/tenant-runs?pageNo=1&pageSize=20
Origin: http://127.0.0.1:5174
X-App-Request: platform
Authorization: Bearer <平台操作者提供的access>
GET /api/platform/v1/infra/tenant-job-runs/31/attempts?pageNo=1&pageSize=20
Origin: http://127.0.0.1:5174
X-App-Request: platform
Authorization: Bearer <平台操作者提供的access>
子运行 DTO 额外包含 tenantId 与 parentRunId,触发来源是 platform。计数结果分别由 GET /api/platform/v1/infra/snapshots/{id} 和 GET /api/platform/v1/infra/tenant-snapshots/{id} 读取;页面点击「统计结果」。两者只返回数量、目标和生成时间,没有参数原值。
# 平台全局维护
固定处理器 platform.infrastructure.maintenance 使用 payload: {},标签明确显示「清理90天前,每表100条」。它分批清理平台及租户目录统计结果,保留计划、运行、attempt、在线审计和业务参数。默认不会创建或启用维护计划,管理员需明确选择该处理器和Cron。
{"code":"statistics.maintenance","name":"统计结果保留期维护","handler":"platform.infrastructure.maintenance","schedule":{"kind":"cron","startAt":1893456000000,"cronExpression":"0 3 * * *","timeZone":"Asia/Shanghai","misfirePolicy":"skip"},"status":"enabled","payload":{}}
将上述正文提交到同一 POST /api/platform/v1/infra/jobs,认证/Origin/来源头同前。运行完成后点击「维护结果」,GET /api/platform/v1/infra/maintenance-results/{id} 返回实际 cutoffAt、platformDeleted、tenantDeleted 和 createdAt。每表最多100条,成功后的新命令可继续下一批;同键重试原已完成命令不会再清理一批。清理、结果和运行完成同事务,失败整体回滚。90天前的统计详情可能返回404,原任务历史仍可查看。
# 4. 人工命令与原死信恢复
「执行一次」是独立幂等命令,保留为手动操作,不是一次性调度类型,也不推进定时游标。页面在同一确认框失败重试时保留原键和计划版本。关闭后重新打开代表新执行意图;结果不确定时先查原运行,不能换键猜测失败。
POST /api/platform/v1/infra/jobs/30/run
Origin: http://127.0.0.1:5174
X-App-Request: platform
Authorization: Bearer <平台操作者提供的access>
Content-Type: application/json
{"version":1,"idempotencyKey":"statistics_manual_0001"}
同计划、操作者、键及版本返回原运行,即使计划随后修改、停用或删除;仍要通过当前认证和执行权限。同键换版本返回 409002。键为16–64个小写字母、数字、下划线或短横线。
死信恢复只恢复原 dead,要求当前计划启用、准入代次一致、尚无已受理资源,并同时核对计划与运行版本。恢复保留原身份和载荷、保留旧历史,重置本轮次数及24小时窗口;完成、取消或旧代次不能恢复。子运行使用独立恢复路径:
POST /api/platform/v1/infra/tenant-job-runs/31/replay
Origin: http://127.0.0.1:5174
X-App-Request: platform
Authorization: Bearer <平台操作者提供的access>
Content-Type: application/json
{"jobVersion":2,"runVersion":5}
成功返回 {"code":0,"data":null,"message":"ok","requestId":"req_replay"}。父运行恢复使用 /job-runs/{id}/replay;租户任务根路径改为 /api/admin/v1/infra。通知投递失败应恢复原通知,不能重新执行已经完成的任务生成第二份通知。
# 参数与执行约束
| 参数或限制 | 规则 |
|---|---|
schedule.kind | 只能为 cron;旧 once、interval 和 intervalSeconds 不再是输入合同 |
startAt | UTC毫秒,1至2100-01-01;新起点不能早于当前60秒;已保存起点可保留 |
cronExpression / timeZone | 五段、最多128字节;独立IANA时区最多64字节,拒绝 Local |
misfirePolicy | 默认 coalesce:执行最早到期点一次再推进未来;skip:超过一分钟抖动窗口则跳过;catch_up:只补最近24小时,每计划每轮最多一条 |
| 计划容量 | 每租户最多1000个有效计划;平台父计划同样有界 |
| 平台目标 | 每份快照1–100个;父受理失败整份回滚,不能只留下半组子集合 |
| 租户子运行配额 | 每租户最多100个活跃子运行;满目标保留 429001 死信,其他目标继续;恢复前也检查容量 |
| 租户子运行速率 | 每租户滚动一分钟最多60次认领,含失败历史;限流只推迟,不消耗业务重试次数 |
| 租约与截止 | 租约30秒、本地业务事务最多10秒;单批最多100、正式循环每批20;恢复窗口24小时 |
| 重试 | 本轮最多5次,临时故障按1/2/4/8/16秒退避;明确不可用目标保留各自失败历史 |
| 查询 | pageNo/pageSize,每页最多100;计划按名称关键词,运行/历史不接受关键词或租户选择器 |
# 历史计划升级
v27 保留旧调度字段和所有运行历史,将非 Cron 计划停用、清零下一触发点,推进配置版本、游标版本和准入代次。页面显示「历史计划已停用,需配置 Cron」,响应 needsConfiguration: true 且省略 schedule。旧61秒等间隔无法通用等价转换为五段日历表达式,所以不自动改写原执行语义。
重新编辑并预览合法 Cron、显式启用后才生成未来运行;旧准入代次的未完成运行仍不能复活。停用/删除与执行通过计划锁竞争,已经先提交的业务不会被撤回。SQL历史持续保留,不通过删除历史来消除积压。
# 失败处理
格式错误、已删除的调度字段通常是 400001;非法 Cron、时区、目标或起点为 400002。目标不存在为 404001。版本冲突必须关闭编辑器、刷新并核对,不能自动换版本覆盖:
HTTP/1.1 409 Conflict
Content-Type: application/json
{"code":409001,"data":null,"message":"记录已变更,请重试","requestId":"req_conflict"}
403001 是当前权限/目标能力拒绝;403004 是来源校验失败;409003 是状态或容量冲突;429001 可等待限额释放后重试。数据库/审计依赖故障返回 503001,业务与审计回滚。超时或提交结果未知先查询原运行,不直接换键。生产错误也使用JWE,解密后仍只含公开提示和稳定码。
# 边界
Cron库只计算日期,持久计划/运行、去重、租约、fencing和恢复由同库执行引擎负责。平台父表不含租户外键,不使用虚假零租户;子运行只从已受理父运行产生,并在业务事务中获取真实租户锁。在线操作审计有真实平台操作者,后台不冒充操作者,以运行/attempt保留执行历史。
处理器只由代码白名单注册,最多32个,必须遵守传入context并只使用传入事务;不支持用户脚本、任意URL、SQL、命令、动态处理器或在回调进行网络副作用。新增业务处理器仍须单独实现授权和幂等。工作流定时器、自动SQL历史归档、生产容量/恢复和达梦v29尚未认证,不因本页可以管理Cron就宣称这些能力已完成。
# 关联
完整合同见仓库 api/openapi.yaml。迁移见 数据库迁移,平台参数见 字典与参数配置,通知见 站内通知,实际检查及未认证边界见 M5增量实施记录。