Skip to content
工程案例研究
待周评
回执坏了,为什么任务没有再跑一遍?一次真实调度链的反例实验
开源工程观察 · 受控研究

回执坏了,为什么任务没有再跑一遍?一次真实调度链的反例实验

损坏回执让命令再次进入执行回调,却没有让实际调度模块再启动一次执行。我们用两轮实验追到会话、租约和持久化幂等层,区分读取异常、重复调用与重复效果,也收窄了真正值得评审的缺口。

2026-09-08English →

查看题图原图

回执坏了,为什么任务没有再跑一遍?一次真实调度链的反例实验

我们故意损坏一条命令的完成记录,再提交同一条命令。系统果然再次调用了负责执行的函数。

但函数被调用两次,任务就一定会启动两次吗?接上产品的真实调度模块后,我们发现:没有。实验中的模拟执行接口仍只启动了一次,也只留下了一条执行尝试记录。

如果只数函数被调用了几次,很容易过早得出“任务重复执行”的结论。

这里负责执行的函数,后文称为“执行回调”;模拟执行接口则代替 SDK,也就是运行时连接模型的接口。它只在内存中模拟启动,没有连接真实模型,更没有付款、发信或代码推送。

再次调用,不等于再次启动;再次启动,也还不是外部效果已经重复的证据。

这篇文章讲的不是一次生产重复派单事故,而是我们怎样通过反例,收回一个过早的工程判断。

1. 一张回执,为什么会影响下一次执行?

CodeFlowMu 是我们正在开发的本地多 Agent 协作系统。任务身份与业务记录由 FCoP 组织,运行时负责调度、会话和技术执行。

用户提交“重试这项任务”时,系统不仅要执行命令,还要留下命令处理记录。客户端可能丢失响应,进程也可能重新启动;之后收到相同请求,系统需要判断:这是新命令,还是已经处理过的命令又来了?

为此,请求携带幂等键,也就是标识“同一次意图”的稳定编号。命令回执通常先记为 pending,表示正在处理;完成后追加 completed,表示已有结果。再次收到同一个键时,可以直接返回旧结果,而不是把操作做一遍。

我们先检查了选定的真实历史文件,避免把日志行数误当事故数量。

9 月 8 日读取的历史样本数量
任务命令回执行10
不同幂等键5
pending / completed5 / 5
解析坏行 / 完全重复行0 / 0

这些记录来自 9 月 5 日。十行是五条命令各自的状态事件,不是重复派发了五次,更没有观察到我们后来人为注入的损坏。

因此,接下来的实验是在隔离目录里提出一个问题:假如已完成的回执不能再被读取,系统会怎样判断同一条重试请求?

2. 第一轮:文件没删,读取结论却退回了旧状态

当前回执读取器逐行倒序查找。遇到无法解析的行,或者无法识别其格式版本的记录,它会跳过,继续寻找可读的匹配项。

这是一种容错方式:一条无关坏行,不应必然让整份文件都不能读取。但它也有一个后果。如果最新的完成回执坏了,读取器可能找到更早的 pending;如果两条目标记录都坏了,它可能返回 null,即没有找到可用记录。

本轮源码与实验没有发现读取过程删除原始字节。变化发生在“调用方能得到什么判断依据”,不是“磁盘上还剩什么”。

为了单独测这层语义,我们调用真实命令内核和回执存储,固定一份允许重试的治理状态,并使用只计数的模拟执行器。每次重试创建新内核实例,排除旧实例内存缓存的影响。

读下面的表,可以先抓住三件事:完整回执会复用;目标回执损坏后可能再次调用;即使再次调用,下层去重仍可能合并效果。 其余对照用于检查这三个判断的适用条件。

受控条件第二次读取或处理结果两次请求累计回调 / 模拟效果
完成回执完整复用完成结果1 / 1
仅插入无关坏行仍复用目标完成结果1 / 1
完成回执损坏,旧 pending 完整再次进入回调2 / 2
两条目标回执都损坏未找到可用回执,再次进入回调2 / 2
两条损坏,但当前状态版本已改变拒绝旧版本请求1 / 1
读取器明确抛错存储不可用,不再调用1 / 1
完成回执改成不支持的版本找到旧 pending,再次进入回调2 / 2
完成回执损坏,pending 完整,状态版本已改变识别到状态已推进,不再调用1 / 1
完成回执损坏,模拟执行器按相同键去重回调再次进入,但效果合并2 / 1

表中的显式读取错误是研究注入的反例,不是说当前读取器已经会报告这种损坏;不支持的版本也是人为构造。最后一行的去重集合位于实验进程内,不是持久化数据库。

因此,第一轮的结论应停在调用层:读取异常没有被明确表达时,真实内核可能再次调用执行器;重复效果是否发生,还取决于执行器。 现有内核本来就允许在 pending、状态未变的条件下重试,并要求下层按同一幂等键合并工作。

3. 第二轮:把“实际会不会再启动”测出来

第二轮不再让执行回调只增加一个计数,而是让它进入产品真正安排任务启动的流程。负责调度、保存执行尝试、管理任务状态和会话的组件,都使用当前产品实现。

实验固定了一份允许重试的治理状态,让请求能够进入下层;最后启动的仍是前面介绍的内存模拟接口。我们没有从网页发起请求,而是用研究代码按照网页服务的重试处理逻辑,传入同一个幂等键和调度参数。这测到了实际调度组件,不是整个网页入口的端到端验证。

这使我们能够回答一个更窄、但比第一轮更接近实际执行的问题:真实调度链是否会把这次重复调用变成第二个执行尝试、第二次 SDK 启动?

每组先验证第一次确实启动,再破坏隔离回执、执行第二步。下面的数量都包含第一次执行。先看两种结果:使用原命令键的场景没有第二次启动;换成新键的对照能够启动。 再看中间各行怎样区分取消、状态变化与并发。

场景变化条件第二次结果累计 SDK 启动 / 执行尝试
I0完整回执回执层直接复用1 / 1
I1完成回执损坏调度器返回已派发1 / 1
I2两条目标回执都损坏调度器返回已派发1 / 1
I3同 I2,新进程读取原磁盘状态调度器返回已派发1 / 1
I4同 I2,先取消原会话,再用原键重试调度器返回已派发1 / 1
I5同 I2,治理状态版本已改变内核拒绝旧版本请求1 / 1
I6同 I2,八个内核并发调用同一调度器八次均返回已派发1 / 1
I7取消后使用新命令键新建执行尝试并启动2 / 2

I2 场景中同键请求累计两次进入回调、一次启动合成 SDK;I7 改用新命令键后累计两次启动。

图 1:I2 的分层计数与 I7 的区分对照,均包含第一次调用。中间列出调度模块的保护层,不表示 I2 依次执行了所有检查;合成 SDK 启动也不等于真实外部效果。来源:正式观测与证据说明。点击图片可查看原图。

I1、I2 直接修正了第一轮最危险的外推:回执确实没能告诉内核“已经完成”,但会话和租约仍告诉调度器“不要再启动一次”。

I4 则进一步说明,相同命令不会仅因旧会话已取消,就自动变成一个新命令。实际对账把旧尝试收敛到终结状态;相同幂等键仍复用这条尝试,没有再次启动。内部结果里的“已派发”在这里不能翻译成“旧进程仍在运行”。

I7 是不能删掉的对照。如果只展示前七行,读者可能以为我们把系统测试成了“任何时候都不执行”。换成不同命令键后,合成治理输入允许的新命令确实建立了第二条尝试。它不是同键重复效果,更不代表“随便改键就获得真人授权”;本实验没有测试完整认证与批准界面。

4. 挡住第二次启动的,不是一张万能回执

源码给出了与实验一致的解释。

首先,调度器会检查当前任务、当前工作轮次的会话与有效执行租约。租约在这里表示执行占用关系:谁持有哪项任务的哪次执行,不是任务已经完成的业务结论。有会话或有效占用,重复进入调度不应自然获得另一次启动资格。

其次,执行尝试存储会按幂等键复用已有记录。offer() 负责查找或提出执行尝试,claim() 负责争取执行占用;两者不能合并成“调用一次就启动一次”。终结的尝试和冲突的租约,还有各自的拒绝条件。

最后,单个调度器会串行处理同一任务的重新派发过程。仅保证尝试记录不重复创建,并不足以阻止两次流程同时修改任务位置,因此 I6 测的是整段过程的单实例并发保护。

I3 还揭示了一个必须解释的细节:新进程没有旧的内存运行句柄,但会话列表读取的是磁盘上的 running 记录,租约也仍然存在。这足以保守地挡住第二次启动,却不证明原执行者仍活着,也不证明工作已经恢复

我们没有在这里跑完整启动恢复,也没有模拟硬件掉电。把这个结果叫作“新进程没有丢弃已有占用证据”是准确的,叫作“断电恢复通过”就越过了实验范围。

5. 外部日志修复,为什么不能直接套成本地缺陷?

这项研究的外部起点之一,是 Orca 的会话日志修复提案。它处理的是另一种风险:损坏位置后面的日志被修复流程删除,而模型服务端的对话记录又不能重建应用自身的回执。

截至 9 月 8 日,该提案仍为 Draft。方案是在保留原始日志的前提下,把可读部分发布为新一代记录,并对不支持的版本限制写入。它尚不是本文核验过的正式发布能力。Orca #19399

两者不能简单画等号。Orca 提案要消除破坏性的修复路径;本地实验里的读取器没有删原文。相同的问题意识是:恢复需要自己的证据,不能因为“能重新打开”或“模型还记得”,就认为恢复判断的依据没有变化。

本地也并非所有存储都采用跳过坏行的方式。我们检查的会话单记录存储,会区分“找不到文件”和“文件损坏”;操作审批记录损坏时,也会返回明确错误。它们同样保留原文。这些反例阻止我们把一个 JSONL 回执读取行为概括成“整个系统把损坏当不存在”。

6. 工程落点:先让异常可见,而不是宣判重复执行

第二轮没有让读取层的异常消失。完成回执坏了之后,调用方仍可能只看到旧 pending 或没有可用记录,却不知道这两种返回背后存在损坏。

已有执行保护在本轮场景中仍然有效,所以值得评审的是一个更窄的问题:读取结果能否同时表达“找到什么记录”与“读取过程是否健康”?找不到、内容损坏、格式版本不支持,是否需要不同的诊断信息?这些信息应该影响哪类命令,哪些只用于提示和对账?

这不是立即规定“有一条坏行就禁止整个系统工作”。无关坏行、仍可用的目标回执、版本兼容与真实执行占用,必须分别处理。粗暴阻断可能把原本有效的恢复路径也切断。

一个可复用的研究方法,是沿链逐层计数:保留了哪些原文,读取了哪份结果,进入了几次回调,形成了几条执行尝试,启动了几次执行,最后实际产生了几个外部效果。只有测到哪一层,才对哪一层下结论。

可靠性研究的价值,不是尽快证明系统有问题,而是把问题定位到不能再被下一层反例推翻的位置。

这一次,执行回调来了两次,任务没有因此再启动一遍。应该保留的是已经起作用的保护,应该补清的是尚未被表达的读取异常。

研究与证据说明

第一方源码固定于 2026 年 9 月 8 日提交 c008d9db91a2,完整值见文稿元数据。第一轮模块对照包含 18 项,第二轮包含 8 组调度与 4 组身份探针,各自正式复跑两轮;本文只使用相关子集,不把重复运行次数当事故样本或可靠性百分比。

所有损坏注入都发生在隔离夹具。内核、存储、调度和会话组件来自实际产品,治理输入与 SDK 为合成部分;没有真实外部效果实验。八路并发限于同一进程共享调度器,跨进程场景是顺序交接,未验证跨进程写锁、硬件掉电、所有命令或所有执行平台,也不是独立 QA。

逐轮脱敏观测、源码摘要、历史聚合与记录校验器已随本文公开:中文证据说明 · 英文说明。公开包核对已保存的结果,不等于重新执行产品实验;原始运行文件、完整夹具及产品复跑仍属于访问受限材料。研究没有修改产品代码,也没有授权新的恢复改造。

Last updated: