feat(session): bound persistence write batching
This commit is contained in:
+8
-6
@@ -4,15 +4,17 @@ Status: implemented
|
||||
|
||||
[English](2026-07-23-collapse-persistence-flush-state.md) | 中文
|
||||
|
||||
[有界写入批处理决策](../architecture/2026-08-08-bounded-session-persistence-write-batching.md)取代了本 Agent Note 中的即时调度节奏。单控制器归属、失败保留、按 id 串行化、退役和完全停稳的资源释放决策仍然有效。
|
||||
|
||||
## 问题
|
||||
|
||||
持久化协调器使用彼此独立的缓冲区、初始化容器和退役容器,以及按 id 划分的操作链,表示一个活跃会话的写入生命周期。这些结构反映的是同一个事实:该 `Session` 是否仍有初始化操作或事件必须完成,之后才能释放其状态。仅由检查点触发的排空还会让每个事件都停留在易失状态,直至另一个插件请求 `session/flush`,尽管后端可以在不阻塞同步生产方的情况下开始持久化工作。
|
||||
|
||||
## 决策
|
||||
|
||||
每个活跃的 `Session` 都有一个控制器,其中包含 `pending`、`init` 和可选的当前 `flush` promise。`session/event` 监听器将冻结的事件复制到 `pending`,并立即调度 `ensureFlush()`。活跃写入期间的调用复用同一个 promise。排空操作会对待处理事件中一个稳定的前缀生成快照,并且只在 `appendBatch` 提交后移除该前缀;写入期间接纳的事件保留在该前缀之后,并调度一个后续批次。
|
||||
每个活跃的 `Session` 都有一个生命周期条目,其中包含初始化和一个包私有写入控制器。该控制器负责 `pending`、固定批处理计时器、可选的活跃写入、自动重试暂停和共享 flush 屏障。`session/event` 监听器将冻结的事件复制到 `pending`;第一个事件设定固定截止时间,后续事件加入但不会重置。一次写入会取出一个稳定的待处理前缀;写入期间接纳的事件仍留在待处理队列中,并形成另一个独立有界的后续批次。
|
||||
|
||||
`session/flush` 是观测屏障。它等待初始化完成,并反复等待或启动控制器的刷新,直至当前 promise 和待处理事件均不存在。即时写入失败会被记录,但不会拒绝同步事件生产方;完整批次会保留下来,由下一次显式刷新、退役尝试或后端资源销毁重试。若该次重试仍失败,显式刷新和资源销毁仍会向调用方暴露失败。
|
||||
`session/flush` 是即时完全停稳屏障。它等待初始化完成、取消批处理计时器、等待任何活跃尝试完成,并排空待处理事件,包括屏障运行期间接纳的事件。后台写入失败会被记录,但不会拒绝同步事件生产方;系统会恢复完整且顺序不变的批次,并暂停自动重试。新事件会开启新的固定窗口;显式 flush、退役或后端资源销毁会立即重试,并在失败再次发生时向调用方暴露失败。
|
||||
|
||||
初始化只进入现有的按 id 操作链一次,并在占有该轮执行权时调用未串行化的核心操作。该操作链与活跃控制器保持分离,因为公共 `create`、`append`、`load` 调用即使没有 `Session` 对象仍可能发生竞态,依然需要按标识串行执行。
|
||||
|
||||
@@ -22,17 +24,17 @@ Status: implemented
|
||||
|
||||
## 备选方案
|
||||
|
||||
**保留仅由检查点触发的延后写入。** 这种方式可以形成更大的批次,但会让持久性依赖另行挂载的检查点策略,并使检查点之间因崩溃而丢失数据的窗口达到最大。即时调度仍会合并同步突发事件,以及活跃写入期间到达的事件。
|
||||
**保留仅由检查点触发的延后写入。** 这种方式可以形成更大的批次,但会让持久性依赖另行挂载的检查点策略,并使检查点之间因崩溃而丢失数据的窗口达到最大。有界后台调度仍会合并突发事件,并在强制屏障之间持久化进度。
|
||||
|
||||
**在整个协调器范围内使用一个刷新 promise。** 这种挂接方式适用于单个文件,但全局 promise 会串行化互不相关的会话。每个活跃会话各有一个控制器,既能让不同会话的后端操作独立推进,又由按 id 操作链保护同一标识的操作。
|
||||
|
||||
**永久锁存首次即时写入错误。** 这会让后续每次刷新都得到确定的结果,却会阻止现有的资源销毁重试从暂时性存储故障中恢复。保留批次但不锁存错误,可以同时保留可观测性和重试能力。
|
||||
**永久锁存首次后台写入错误。** 这会让后续每次刷新都得到确定的结果,却会阻止现有的资源销毁重试从暂时性存储故障中恢复。保留批次但不锁存错误,可以同时保留可观测性和重试能力。
|
||||
|
||||
**拒绝对所有活跃会话的加载。** 这样做很安全,但会让持久化消费方和测试无法再使用既有的闭合活跃会话快照。先生成快照再刷新,为调用提供了稳定的线性化点:刷新成功即可证明正是该快照已持久化,而活跃路径绝不调用崩溃修复。
|
||||
|
||||
## 验证
|
||||
|
||||
- 一个针对协调器的测试会阻塞第一次追加,在该次写入期间接纳另一个事件,并在不调用 `session/flush` 的情况下观测到自动执行的第二个持久批次。
|
||||
- 针对控制器的测试使用假时钟证明固定窗口不会重置,随后阻塞第一次追加,在该次写入期间接纳另一个事件,并在不调用 `session/flush` 的情况下观测到自动执行的第二个持久批次。
|
||||
- 共享协调器契约仍覆盖内存、JSONL 和 SQLite 后端上的活跃会话接管、冲突、崩溃修复,以及会话和后端的资源释放。
|
||||
- 失败和资源销毁测试会让写入失败的批次保持待处理,在关闭前重试这些批次,并证明尚在执行的控制器会延迟后端关闭。
|
||||
- 共享后端契约会持久化一个仍打开的活跃轮次,证明 `load` 会拒绝且不会写入合成闭合事件,随后完成该轮次并让其所有者退役,最后重新加载完全相同的已完成轮次。
|
||||
@@ -42,6 +44,6 @@ Status: implemented
|
||||
|
||||
## 后果
|
||||
|
||||
协调器有三个长生命周期容器:持久化的标识状态、活跃会话控制器和按 id 操作链。即时写入缩短了通常情况下因崩溃而丢失数据的窗口,并移除了彼此独立的缓冲区、初始化注册表和退役注册表。与仅由检查点触发的排空相比,这种方式可能产生更多后端批次;同一轮事件循环内的突发事件和一次写入期间接纳的事件仍会合并。
|
||||
活跃会话条目把初始化与一个控制器放在一起;该控制器负责待处理事件、计时器、活跃写入、重试暂停和 flush 屏障。协调器仍以独立容器保存已持久化的标识状态、准备好的冷态 Session、标识退役等待方和按 id 操作链,因为即使不存在可写的活跃 Session,这些生命周期仍然存在。有界写入缩短了通常情况下因崩溃而丢失数据的窗口,相比即时调度产生更少的后端批次,同时不改变强制屏障。
|
||||
|
||||
`session/flush` 不再决定普通持久化何时开始。它仍是循环和检查点策略使用的顺序与错误观测边界,因此检查点成功仍表示在其完成前接纳的每个事件都已持久化。
|
||||
|
||||
Reference in New Issue
Block a user