Skip to content
bitzorcas
中EN

Concept

后台任务

在独立 JobHost 中设计可重试、可审计、可取消的后台任务,并正确处理租户、幂等和失败。

Last updated

BitzOrcas 把定时和批处理工作放在独立 BitzOrcas.JobHost 进程中。API Host 保持裁剪安全,不引用 Quartz;JobHost 负责调度,模块拥有实际业务执行器。

模块声明与执行器 → JobHost 描述符 → Quartz Trigger
↓
审计信封与 OpenTelemetry
↓
owner-local 业务端口

关键路径图

下图展示了后台任务在 Quartz 集群中的持久化调度与执行安全模型:调度器从数据库抢占排他租约,并通过类型化执行器安全执行业务逻辑。

调度器或消息

租约或幂等

可信租户范围

有界执行

结果与重试

什么时候使用后台任务

适合:周期清理、聚合、备份验证、密钥轮换、超时扫描,以及已经接受后异步执行的导出等工作。不适合:必须与当前数据库事务原子完成的动作,或用户正在等待的短命令。

当前 JobHost 不是只有审计清理。它装配 Audit、Workflow、Identity、Backup、Export、DataLifecycle、Website、Payment 等 owner 的任务,包括导出执行、工作流定时器、孤儿文件清理、CAP 消息清理、自动续费和备份保留。

一个任务的三个部分

  1. BackgroundJobDeclaration:稳定名称、Cron 或间隔、模块、启用状态、关键性、配置来源和说明。
  2. IJobExecutor:不依赖 Quartz 的业务执行入口,预期业务失败返回 Result.Failure。
  3. JobHost 适配器:把 Quartz 触发映射到执行器,并套上统一审计信封。

启用任务必须且只能配置 Cron 或正数间隔。禁用任务可以没有调度,但 Job 类型仍注册,因此运维入口可以受控地手动触发。

新增任务的推荐顺序

  1. 在 owner 模块定义稳定任务身份与声明。
  2. 实现 IJobExecutor<TIdentity>,只依赖模块端口和 CancellationToken。
  3. 在该模块的 JobHost 注册扩展中绑定声明、执行器和 Quartz 适配器。
  4. 为成功、预期失败、异常、取消、重复执行和并发执行编写测试。
  5. 检查 Operations 调度报告与 JobHost 健康检查是否出现该任务。

不要直接修改 Program.cs 添加一串 AddJob<T>()。组合根通过 AddJobHostBackgroundJobs 汇总 owner-local 扩展,并在启动时校验 Catalog、Handler 和 Executor 是否完整且唯一。

幂等与并发

调度系统通常提供至少一次执行条件:进程可能在完成业务写入后、确认调度结果前退出;运维也可能手动重跑。因此执行器要能识别已经完成的批次或业务键。

常用做法包括唯一约束、状态机条件更新、处理游标和幂等记录。不要只靠“Cron 不会重叠”的假设。多实例 JobHost 使用持久化 Quartz 集群存储,但业务层仍需抵抗重复触发。

租户边界

批量处理多租户数据时,先取得明确的租户列表,再为每个租户建立可信上下文。查询必须包含租户过滤;游标、锁和幂等键也要带租户维度。后台进程没有浏览器用户,不能伪造普通用户上下文来绕过授权。

失败、审计与取消

QuartzJobExecutionAuditor 为每次执行创建 Activity,并记录任务名、Quartz FireInstanceId 或生成的关联标识、耗时和成功/失败状态。Result.Failure 在 Host 边界转为失败,使 Quartz 与审计能看到真实结果;系统异常和取消继续抛出。

审计写入失败只记录警告,不会把业务成功改成失败。相反,业务异常不能吞掉,否则调度器会误判成功。耗时循环应定期检查取消令牌,让部署关闭能够收敛。

声明、配置与运行态报告

任务身份和默认调度属于模块契约,环境是否启用及 Cron/间隔覆盖属于部署配置。稳定名称一旦进入告警、运维 API 与审计查询,就不能随类名重构而改变。

// 声明只描述稳定任务身份和调度元数据,不承载业务执行逻辑。
public static readonly BackgroundJobDeclaration Cleanup = new(
// 稳定名称供配置、审计和运维 API 共同引用。
Name: "files.orphan-cleanup",
Module: "Files",
CronExpression: "0 0/15 * * * ?",
Interval: null,
Enabled: true,
Critical: false,
ConfigurationSource: "Files:Jobs:OrphanCleanup");

Operations 报告应能看到声明、实际 Trigger、下一次触发、最近结果和不一致原因。声明已启用但没有 Trigger、重复名称、缺少 Executor 或配置同时给 Cron/Interval 都属于启动或 GA 阻断。

批处理游标与事务边界

长任务按有界批次读取并提交游标,避免一个小时级数据库事务。游标要表达已提交的事实;先推进游标再提交业务写入会丢数据,先提交写入再推进游标则可能重做,因此处理动作必须幂等。

// 每批只在业务写入成功后推进游标;重复读取仍由业务键兜底。
while (!cancellationToken.IsCancellationRequested)
{
// 小批量读取限制单次事务、内存和关闭等待时间。
var batch = await source.ReadAsync(tenantId, cursor, batchSize: 200, cancellationToken);
if (batch.Count == 0) break;
await processor.ApplyIdempotentlyAsync(batch, cancellationToken);
cursor = await cursors.CommitAsync(tenantId, batch[^1].Sequence, cancellationToken);
}

外部调用不要和数据库事务假装原子。写库后发消息使用 Outbox;调用外部 Provider 时保存可恢复状态与供应商幂等键,让下一次执行从确定状态继续。

重试、错过触发与重叠

区分调度器重试、下次 Cron 扫描和人工重跑。Misfire 策略必须按任务语义选择:结算类可能需要补跑,周期刷新类通常只需最近一次。

Quartz 锁只能约束调度实例;业务侧仍要用租约、唯一批次或条件更新防止手动入口、旧实例和消息消费者并发执行。租约需要 owner、到期和 fencing token,不能用永久布尔锁。

测试与故障注入

  • 在业务写入成功、游标提交前终止进程,重启后不产生重复副作用;
  • 同一 Trigger 并发两次时只有一个业务批次生效;
  • 一个租户失败不阻断其他租户,且失败租户可独立重跑;
  • 取消会停止新批次并让当前批次安全收敛;
  • 审计/OTLP 故障不改变业务结果,但能产生运维告警;
  • Cron 时区、夏令时、misfire 和未来触发时间按部署地区验证。

发布门禁

  • 生产环境有数据库配置,JobHost 的运行时守卫通过。
  • 关键任务已启用且存在未来触发时间,Scheduler 健康检查正常。
  • 同一任务重跑不会重复收费、发信或删除数据。
  • 多租户扫描没有跨租户读取或共享游标。
  • 失败记录、Activity、告警和受控手动重跑路径可用。

类型与组合方式见后台任务构建块。

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%