⏰ 任务调度与后台任务
Microi.Job 基于 Quartz 调度接口引擎或定制 .NET Job。可靠任务必须同时设计多节点租约、业务幂等、重试和重启恢复。
能力选择
| 需求 | 推荐 |
|---|---|
| 周期扫描、对账、补偿、归档 | Microi.Job |
| 用户触发的安装、导入、批量同步 | 菜单后台任务 |
| 可靠跨服务异步 | MQ + outbox/inbox |
| 请求内等待外部结果 | await |
后端 setTimeout、Task.Run、static bool、普通 lock 和本机定时器不能承载可靠业务。
管理能力
平台支持查询、添加、更新、暂停、恢复和删除任务。任务配置、接口引擎和执行日志属于控制面,只允许 Level >= 9999 维护。日常管理使用任务调度页面及官方 platform-schedule-job 接口引擎,不直接修改 Quartz 表。
任务 Key 支持 test111、daily_sync、task-01、task.v2:以英文字母开头,可含数字、点、下划线及短横线,标准任务表最多 50 个字符。已有任务的 Key 保持只读;仅保存了 Cron 的历史任务可直接编辑保存,不必重新选择周期类别。
任务表单的服务器提交前事件直接调用 V8.Method.SaveScheduleJob,传入 RuntimeOnly:true,只同步并回读 Quartz,由原表单事务保存任务元数据。这样既避免再次写当前表造成锁等待,也避免把业务 ApiEngineKey 作为另一个接口引擎的路由参数传递时被覆盖。事件先检查 ManageScheduleJob({Action:'Capabilities'});旧后端不具备该能力时立即提示升级,不产生调度副作用。普通 MCP/商城保存仍负责 Quartz 与任务元数据两部分。
按月查看运行日志
新版任务表单通过“运行日志 / 历史日志”两个页签查看记录。运行日志写入 MongoDB 的租户月度集合,历史关系库 diy_schedule_job_log 保留查询;请选择月份,使用上一页/下一页浏览,不再统计多年日志总数。只有显示的页签会加载数据。Mongo 连接失败会明确报错,不能据此判断任务没有执行。
日志通过后台队列写入,日志存储故障不会使已完成的业务变成失败。为 spool 配置持久卷;API 镜像更新不会自动升级 MongoDB。新版镜像基于驱动 3.11.2 的安全修复提供平台 MongoDB 3.6 协议兼容构建,避免直接降级旧驱动;这不等于修复旧数据库服务端自身的问题。升级任务调度应用可取得双页签和历史表 (JobName,CreateTime,Id) 索引声明,存量大表安装后仍应回读索引和查询耗时。
管理员可通过 MCP microi_query_job_runtime 查询 Diagnostics、Logs、HistoryLogs,或调用 platform-schedule-job 对应动作。日志查询传 JobName、SearchMonth=yyyyMM;返回的 HasMore/BeforeLogTime/BeforeLogId 用于继续翻页。诊断同时显示当前节点启动状态、执行数、领取进展和设置读取失败,不能把元数据“正常”或 Quartz 已启动当作任务执行成功。
运行时间刷新不生成配置版本
更新包含此修复的后端后,Quartz 每分钟刷新 LastTime/NextTime 时,只在时间确实变化时向当前租户主库条件更新这两列。并行节点或暂停、删除、重命名后的旧观测不会覆盖新状态。运行时间刷新不再触发通用表单更新、配置版本和数据日志;用户修改任务配置时,原有事件和版本追溯继续生效,历史版本也不会被删除。
旧库还应通过索引管理确认 mic_data_version 存在 (TableId,TableRowId,CreateTime) 联合索引。【表单引擎】应用已同时声明新装和存量库的索引;仅看到商城包中有索引,不代表当前数据库已经安装。缺索引时,每次版本号读取都可能扫描全部历史,拖慢同机其它服务。可通过吾码 MCP 的索引查询与系统监控核验,无需 NAS SSH。
任务不执行(领取不到触发器)
调度器“已启动”不代表任务在运行。用 MCP microi_query_system_observability 或 platform-schedule-job 的 action=diagnostics 读取 Scheduler.IsStarted、Scheduler.NumberOfJobsExecuted 与 Acquisition.LastAcquisitionError,不要用 diy_schedule_job.Status=正常 或 Quartz 已启动推断任务成功执行。
NumberOfJobsExecuted 长时间为 0 且领取错误包含 Key 'IDX_...\' doesn't exist 时,是 Quartz 3.19 的 MySQL 方言在领取语句里使用了索引提示,而触发器表缺少对应索引:
CREATE INDEX IDX_microi_job_T_NFT_ST ON microi_job_triggers (SCHED_NAME, NEXT_FIRE_TIME, TRIGGER_STATE);
CREATE INDEX IDX_microi_job_T_NFT_ST_MISFIRE ON microi_job_triggers (SCHED_NAME, NEXT_FIRE_TIME, MISFIRE_INSTR, TRIGGER_STATE);索引名大小写不敏感;官方空库模板已包含这两个索引,历史租户库仍可能只有旧 QRTZ_ 前缀的索引名。包含本次修复的后端会在调度器存储初始化时按实际表前缀幂等补齐(多节点并发启动不会报错,失败只告警不阻断启动),完成后无需重启任务或重建 Cron。
平台自动升级进度
启动门禁和后台租户升级的每条升级日志都会显示百分比、当前租户/租户总数、当前升级点/升级点总数、已完成点数、耗时及剩余时间估算。进度按实际完成或版本已覆盖的检查点计算;正在执行、等待租约或失败不会自动推进。剩余时间按已完成点的平均耗时估算,步骤耗时不均时可能变化。
已达到当前版本的租户只读版本后快速跳过;整批末尾输出“平台自动升级已结束”、成功/已是最新数、失败数、未处理数及总耗时。失败或取消保留实际完成进度;批次结束不代表失败租户已修复,仍按日志定位失败点后重试。升级进度日志由平台后端提供,任务表单事件通过独立“任务调度”应用(app.microi.job)交付,两者需一起更新。SaaS 基础包中的初始化副本同时保持一致,不能替代存量租户更新“任务调度”应用。
多节点与幂等
新旧版本共库时停用新版任务调度
更新后端与“系统设置”应用 v6.4.2 后,在【系统设置 → 开发配置 → 接口、文件与运行环境】开启【停用任务调度】(DisableTaskScheduling)。默认关闭;字段尚未安装或为空时保持原有调度行为。SaaS 空库基础包 v8.3.4 同步交付此字段,但安装不会覆盖租户已经保存的开关值。
开启后,仅支持该能力的新版后端停止领取当前租户的 Quartz 任务,不逐条暂停任务,不改变共享 Cron 的执行时间,旧版仍可运行。领取之后才打开开关的任务,在真正触发前还会复核并归还领取权。不能仅在业务 V8 开头 return:Quartz 在执行业务前已经推进触发器,旧版可能因此漏执行。
新版也不会替被停用租户进行集群故障补偿。由于 Quartz 会按故障节点整组清理在途记录,若一个故障节点同时承载正常与停用租户,新版会推迟该节点整组恢复,交由旧节点或关闭开关后处理;其它健康节点的正常领取不受影响。
保存系统设置后,提交成功的缓存失效在下一次调度检查生效,无需重启。正常调度轮询约 1 秒,但不承诺网络故障或缓存通知异常下硬实时;已开始的任务不会被强制中断。关闭开关后恢复领取;错过的触发按任务原有 Misfire 策略处理,不额外补跑业务。
新版按被停用任务的计划时间记录“系统设置中已经手动停用了任务调度,本次只做记录,未执行相关的业务”。记录包含 Status=Skipped、Executed=false、Reason=SystemTaskSchedulingDisabled、节点和计划时间;多个新版节点按确定性日志 Id 去重。此记录只表示新版未执行,不能据此判断旧版未执行。观察器只读共享计划,进程停机期间不补写历史;新增或修改计划的日志目录约 10 秒内刷新。
推荐交接顺序:先安装字段并开启开关,再启动新版调度节点;旧版所有调度进程停止、在途任务完成后,关闭开关交给新版。 五套独立业务库需分别设置。普通 HTTP 接口、MQ 消费、平台升级和应用安装等持久后台任务不受此开关影响;通过 Quartz 手动触发任务仍受门禁约束。
Quartz 集群共享持久化任务库,每个节点使用自动生成的唯一 SchedulerId,不能让多个节点共用默认的 NON_CLUSTERED 标识。节点时钟应保持同步;任务运行态查询和后台执行时间同步都按 OsClient 选择租户分组,同名任务不会借用其它租户的上次或下次执行时间。
调度节点将空闲轮询和预取窗口限制为 1 秒,及时发现其它节点新增或解除阻塞的任务,避免默认 30 秒等待窗口放大秒级任务延迟。节点仍按共享持久化库原子认领触发器;这不会替代业务幂等,也不承诺过载或外部服务故障下的硬实时执行。
节点可能使用不同的 SaaS 运行配置。MySQL、SQL Server 调度驱动在抢占和处理错过的触发时间前,按本节点已经加载的租户选择分组,避免本地测试节点抢走仅线上可访问的子租户任务。历史 default_group 保留兼容,新建和修改任务使用租户分组。ManageScheduleJob({Action:'GetByNames',Names:[]}) 的 DataAppend.Scheduler 可查看当前节点是否启动、是否待机、已执行次数及集群状态;元数据中的“下次执行时间”不能代替真实执行日志。
同一任务可能在每个 API/Worker 节点同时到点。每个任务必须具备:
- 分布式租约:Key 包含
OsClient + JobKey + 计划时间/分片,有 TTL、唯一持有者、续租和仅持有者释放。 - 稳定幂等键:例如
JobRunId、业务日期、消息EventId。 - 数据库唯一约束或条件状态迁移,保证副作用仅一次。
- 共享 checkpoint:待处理、处理中、成功、失败、重试次数和下次重试时间。
锁只能减少并发,不能代替幂等。资金、库存、积分和流水需要版本/条件更新,防止锁过期后的旧持有者继续写入。
接口引擎任务
任务调用稳定的 ApiEngineKey,调度层应传 JobRunId 与触发时间。接口引擎先以唯一约束抢占执行记录,再分页处理;不能使用“先查询、再新增”的非原子去重。
var runId = String(V8.Param.JobRunId || '');
if (!runId) return { Code: 0, Msg: '缺少 JobRunId' };
// 实际项目由专用执行表唯一索引或接口引擎原子能力完成 claim。
// 每项业务副作用还要有自己的幂等键。
return { Code: 1, Data: { JobRunId: runId } };失败与恢复
- 外部调用设置超时;无法确认对方是否成功时按业务幂等号查询,不能盲目重发。
- 重试使用有上限退避;永久错误进入人工处理。
- 服务停机先停止接单,再在有限宽限期排空或持久化;重启后扫描未完成任务。
- 要求
kill -9窗口零丢失时,业务成功响应前必须取得共享 outbox/MQ/WAL 的持久化确认。
后台按钮
以下任一条件成立时按后台任务设计:预计超过 2 分钟、500 条以上、1000 个以上扇出子操作、100 次以上外部调用、总量未知且可能持续运行,或属于安装、初始化、批量导入/生成、全量同步、迁移、备份。
长耗时菜单按钮设置 RunBackground/BackgroundTask/IsBackgroundTask=true 并绑定 ApiEngineKey。BackgroundTaskOptions 至少配置稳定 IdempotencyKey;需要串行执行的 DDL/安装配置 ConcurrencyKey;关联业务数据时配置 BusinessTable/BusinessId/BusinessStatusField/BusinessTaskIdField,业务记录至少写“后台处理中”和任务 Id,用户即可去通知中心查看详情。
按钮提交成功后,前端通过当前用户正常的 V8.FormEngine 权限写入上述状态;通用后台服务不会按客户端传入的任意表名/字段名直接写库。接口引擎需要在完成、失败或取消补偿路径更新业务记录的最终状态。专用无人值守任务应在接口引擎中固定表名和字段名,不能把任意写库权限交给请求参数。
接口通过 V8.Method.UpdateBackgroundTask({Current,Total,Msg}) 上报已提交的真实工作量。平台根据采样吞吐计算预计结束时间并标记可信度:
- 已知总量:百分比只由
Current/Total推导。 - 未知总量:不要传
Total=100,界面显示“不定进度/积累真实样本后估算”。 - 失败或取消:停在最后真实进度,只有最终成功显示 100%。
- SignalR 负责进度实时推送;页面只在启动、打开通知中心、重连、页面恢复或用户手动刷新时单次回读共享数据库,不再每几秒轮询任务列表。事件丢失由下一次上述权威回读恢复。
通知中心的后台任务“创建时间”统一显示 yyyy-MM-dd HH:mm:ss,不能只显示时分秒;跨日任务必须能直接看出具体日期。
预计超过 10 分钟的任务必须分片。每片在短事务内提交一批,仍有后续时返回 Data.BackgroundTask:
return {
Code: 1,
Data: {
BackgroundTask: {
HasMore: true,
Checkpoint: { LastId: lastId },
Current: committedCount,
Total: totalCount,
NextDelaySeconds: 1,
Msg: '本批已提交,等待下一批'
}
}
};平台持久化 checkpoint 后重新入队;最后一片返回普通 Code:1。节点异常时租约过期后由其它节点恢复,旧节点的写入由 fencing token 和业务唯一约束拒绝。
最低验收
至少启动两个节点连接同一数据库/Redis,覆盖:同时到点、重复投递、锁持有者中途退出、Redis 短暂故障、写入后响应前重启和滚动升级。最终断言业务副作用仅一次、无永久死锁、未完成任务可恢复,并核对通知中心的真实分子/分母、ETA、日志以及失败/取消不显示 100%。
完整规范见 microi.skills/job-engine/SKILL.md 与平台安全与兼容基线。