← 返回目录

第 3 卷

第 3 / 6 卷日期:

有了这条边界,答案就很干脆:

出现在它之前的未闭合标记 → 属于一个已经结束的生命周期(不管是崩溃、被接续,还是从仍在运行的父会话 fork 出来的)→ 当它是死的;

出现在它之后的 → 本会话正在进行 → 是活的。

只有 Session 的构造函数能写它;已经以它结尾的 seed 不会再标记第二次,所以反复打开一个没动过的会话,日志不会一遍遍变长。配合 header 里的 seedLength = 8,日后的

resume 和重放就能分清哪段是父辈的历史、哪段是这条分支自己的工作。

  • 说明 boundary 是“含”的。传 4,就把 seq 0 到 4 都带走——正好是第一轮完整闭合的

五条,加上边界标记共 6 条。你可以从任意稳定的轮次间位置分叉,哪怕源会话此刻正在跑新一轮。

  • 是被拒的情形,也是本讲最值得记住的一条。 seq 2 卡在第一轮中间(turn/start 已

经写下,turn/end 还没有)。官方的做法是直接拒绝,而不是静默截断:

复制代码 1fork boundary 2 in session "parent" ends inside open turn 1

为什么不替你切一刀了事?因为切出来的前缀是一轮没做完的对话,模型可能已经说了“我去查一下”,却没有工具结果;也可能用户的问题还悬着没有回答。把这样一份历史交给模型,

它看到的是一个断在半空的回合,接下来的行为没法预期。

这跟上一讲那三条硬约束是同一个取向:拿不准就拒绝,绝不猜。上一讲叫它 fail closed,这里换了个说法——拒绝结束于开放轮次内的前缀,而不是静默截断,形状一模一样。源码里一

共四种拒绝:除了落在开放轮次内,还有 boundary 不存在(传了一个超过最后一条的 seq)、boundary 不是非负安全整数、以及子会话 id 已被占用。

崩溃修复:把没闭合的那一半补上

第四个问题:跑到一半进程死了,已经写进日志的那些还算吗?

上一节是主动的那一种,这一节换被动的,没人希望它发生,但它就是发生了。

能修多少,取决于写下去多少:持久化那一节的 flush 划定了那条线,没写进磁盘的,谁也找不回来。

上一讲聊“为什么不允许涂改”时,第一个理由就是这个:崩溃之后得能接着跑。当时只说了 “必须能”,这一节看它到底怎么做到的。

把日志从磁盘读回内存,这个动作上一讲给它起过名字,重建(官方叫 load)。这一节讲的就是重建时发生的一件事。

一个轮次开始时会先写 turn/start,结束时才写 turn/end。如果进程在中间死了,日志里就留下一个没有配对的 turn/start。这时候怎么办?

最省事的做法是截断,把这一轮没写完的部分扔掉。但不行:长周期任务里,一轮可能非常庞大(几十个步骤、大量工具输出),那些事件在崩溃前已经被真实地追加进日志了,是实打实

做过的工作。扔掉等于假装没干过。

官方的做法是配平:把开着的结构一层层补上(工具、step、turn),前后任何一条独立事件都不改动。

这不违背上一讲的“只追加”,补的这些是追加在尾巴上的新事件,崩溃前已经写下的那几条,一条都没被动过。所谓"改不动"指的是已有的,不是“不能再往后面写”。

复制代码 1--- ① 场景一:问到一半崩了(磁盘 3 条,turn 没闭合)---

2 3--- ② 新进程 load 场景一:5 条 ---

4seq0turn/start
5seq1step/start
6seq2user/message
7seq3step/end
8seq4turn/end {"turn":1,"reason":{"kind":"interrupted"}}

3 条进去,5 条出来。多的是 seq 3 的 step/end 和 seq 4 的 turn/end,一层层把开着的结构补闭合:

图 9-1
图 9-1

注意,turn/end 的结束原因:interrupted。官方的注脚很有意思——它是唯一一个不由循环发出的 TurnEndReason。其余原因(completed、aborted、blocked……)都是

程序自己跑完时写的,只有这个“被中断”是后端在恢复时替你补的。看到它,你就知道这一轮不是正常结束的。

全站分卷索引 · 6 卷
  1. 1第 1 卷
  2. 2第 2 卷
  3. 3第 3 卷
  4. 4第 4 卷
  5. 5第 5 卷
  6. 6第 6 卷