任务持久化与执行身份
一个 Job 表示调用者希望完成的一项操作;Attempt 表示它的一次实际执行。重试会产生新的 Attempt,JobId 保持不变。Attempt Token 是每次领取生成的随机标记,例如第一次执行得到 A,重试得到 B;旧执行携带 A 的迟到进度或完成请求不得改变 B 的状态。
任务核心定义提交数据、生命周期契约和执行器。EF Core 适配负责持久化以及状态转换;它不运行处理器、不选择自动重试策略。执行器通过存储入口编排执行,不能自行修改表。
提交身份与 Scope
Jobs 的主键是 ScopeHash + Id,JobAttempts 使用相同键再加执行序号。Scope 是宿主提供的原始隔离标识,使用严格 UTF-8 编码后计算 SHA-256。这样查询不受数据库默认字符串排序规则影响:Owner 和 owner 属于不同 Scope。非法 Unicode 代理项必须拒绝,默认替换编码会把不同非法输入折成相同字节。
JobSubmission 持有不可变的 JSON 字符串,Create 在调用时立即序列化。重复提交按处理器名称/版本、JSON 原文、队列、最大执行次数和原始 NotBefore 比较。这里使用精确字符串比较,没有 JSON 语义归一化;调用者重试提交时应保留同一请求。
InitialNotBeforeUtcTicks 保存原始可空时间,NextRunAtUtcTicks 保存当前实际调度时间。首次未指定时间时,后者采用创建时刻,前者仍为空。重试可以改变当前调度时间;这些变化不会把相同原始提交误判为冲突。所有时间以 UTC ticks 存储,避免 SQLite 对 DateTimeOffset 的排序与精度限制。
独立 Submit 在新上下文中查询、添加和保存。唯一键竞争导致插入失败后,先释放失败的上下文,再用新上下文查询胜者:请求相同返回已有快照,不同则报告身份冲突;没有胜者时保留原数据库异常。
谁拥有事务
独立操作创建新的 DI scope,再解析同一宿主 DbContext 类型。请求作用域里尚未保存的业务实体因此不进入任务提交。复用同一实例、携带事务或预先带有待保存变更的新上下文会被拒绝;ambient transaction 也不能进入独立路径。
Stage 则只使用当前作用域的 DbContext,并要求已有显式事务。它检查已追踪和已保存的相同任务,然后仅 Add 新实体;不能替最外层业务操作调用 SaveChanges 或 Commit。消息 Handler 可以把任务创建与现有 Inbox 完成、业务数据、Outbox 一起提交,避免消息已经确认而任务尚未落盘。
Stage 的并发唯一键冲突由宿主事务处理,因为适配器不能在宿主事务中回滚后偷偷另开事务。失败后应让最外层操作回滚并清理上下文;任务不能脱离业务失败单独存活。
与现有 SelfManaged Messaging 组合时,同一个宿主上下文同时映射任务表、业务表和消息表,两个适配器注册相同的上下文类型。现有 IUnitOfWork 建立最外层事务并统一保存,Stage 不另造桥接工作单元。即使业务操作提前执行过 SaveChanges,之后的任务身份冲突仍应让整笔事务回滚;尚未落盘的 Outbox 也必须由现有工作单元清理。
稳定 JobId 的幂等只保护同 Scope 内任务创建。消息重投的业务副作用仍由 Inbox 与业务幂等控制;不同消息 ID 的 Outbox 记录不会因为携带同一 JobId 自动合并。
领取、进度与终态
GetDue 只查找已到时的 Queued 候选,不授予执行资格。TryStart 使用条件 UPDATE 同时检查状态、取消标记、到期时间和执行次数,再生成新 Token、增加次数、写入 Running 的 Attempt。任务更新与执行记录插入处于同一短事务;中途失败会一起回滚。
进度更新要求 Job 为 Running、Token 一致、尚未取消,以及新进度不小于原值;同事务更新对应 Attempt。结束更新同样验证当前 Token,然后一次性发布任务终态并更新 Attempt。历史记录缺失时整个转换失败,不能留下任务与执行记录不一致的数据。
取消与完成的先后由数据库中的条件更新确定。取消 Queued 任务直接发布 Cancelled,取消 Running 任务只设置请求标记。完成、失败或中断 SQL 在写入终态时读取这个标记:取消先提交则记录 Cancelled、隐藏结果引用;成功先提交则任务已离开 Running,后续取消不能覆盖它。存储不会强制终止实际工作,执行器必须把取消传给处理器并等待其结束。
Retry 仅接受 Failed、Cancelled、Interrupted 且尚有次数的 Job,将其重新排队并清除本次进度、错误、取消标记和结果引用,保留旧 Attempt。Running 不能重试;旧 Token 即使继续回报也无法更新新执行。最大次数约束的是实际领取执行的数量,排队时取消不消耗执行次数。
扩展边界
当前原子更新提供执行身份和数据库版本,没有分布式租约、续租或实例失联判定。单实例执行器启动时读取 Running 的执行令牌,再调用 RecoverAsync:同事务结束旧 Attempt,并在调用者明确允许、未取消且还有次数时将 Job 重新排队。这样进程不会在“已中断但尚未重排”的提交间隙丢失恢复机会。优雅停机也使用这一原子入口,安全任务留在队列供下次启动执行。
GetExecutionStatusAsync 只投影当前 Token 对应的取消状态,不加载 JSON 参数,用于持续取消观察。失效或结束的 Token 返回 Ended;调用者应停止工作。详细执行流程见 执行设计。单凭数据库记录不能保证邮件、外部请求等副作用恰好发生一次。
任务参数、结果引用和进度消息由宿主授权和生成。按 Scope 的读取/控制接口不会跨越该边界;跨 Scope 的候选查询和执行写入属于可信执行基础设施。组件不从用户输入解析任意 CLR 类型,也不会替宿主读取结果引用指向的文件。