# No-Resident Leaf Design

## 1. 目标与非目标

### 目标

1. 去掉 leaf runtime 的 `Resident` 主路径，让 leaf `compact/split/merge` 直接产出 `Addressable(LocalPtr)` 结构。
2. 保留 `bwe-opt` 已落地收益：
   - per-pid durable cutoff
   - published cutoff handoff
   - append-only incremental checkpoint
   - structural rewrite fallback
3. 继续保持 `replace-interval` 已落地语义：
   - runtime `LocalPtr` 与 durable `DurableAddr` 分离
   - checkpoint 统一做 `LocalPtr -> DurableAddr` rewrite
   - `lid` ownership 作为 durable exact source-of-truth
4. 明确 `Pool::pages` 的闭包清理模型，防止 addressable-only 模式下池子无限膨胀。
5. 保持 `data-first, meta-last`、`data_junk/blob_junk -> apply_*_junks`、GC/recovery 闭包不变。

### 非目标

1. 本方案不改事务、MVCC、WAL rollback、`CommitTree::compact` 语义。
2. 本方案不改 `Options::sync_on_write` 语义。
3. 本方案不引入新的 durable page 格式，不修改 `replace-interval` 的 `lid`/`addr` 持久化约束。
4. 本方案不在首阶段删除所有 resident 代码；先把 resident 从主流程摘掉，再做收尾删除。

## 2. 当前实现的关键问题

当前实现已经不再依赖“中间态提前拿 durable addr”：

1. `Pool::alloc()` 分配的是 `alloc_local`，先进入 `Pool.pages`，不是 durable addr  
   - `src/map/buffer.rs`
2. `CheckpointTask::needs_flush()` 以 `is_local_ptr() || lsn > checkpoint_lsn` 判定刷盘  
   - `src/map/data.rs`
3. durable addr 在 flush 阶段统一 `pack_durable_addr + rewrite` 才生成  
   - `src/map/flush.rs`

但当前仍把 resident 作为 leaf 主路径，造成两类问题：

1. 运行时复杂度冗余
   - leaf 双态（resident/addressable）让 checkpoint/evict/read-path 长期维护分叉逻辑
   - `resident_seq` / `sealed_head` / resident owner 的维护成本高
2. addressable-only 合同未成文
   - 如果直接撤 resident，而不先明确 `Pool.pages` 的清理闭包，存在“写入快于checkpoint清理”的积压风险
   - `snapshot/done` 的 exact-remove 模型需要显式绑定到新主路径，不能依赖历史语义推断

## 3. 必守不变量

### 持久化不变量

1. 必须保持 `data-first, meta-last`。
2. manifest 中 `page_table`、`lid_map`、junk apply、frontier 必须同 txn 提交。
3. recovery 只认 durable manifest/file，不依赖 runtime handoff/resident state。

### checkpoint/ownership 不变量

1. runtime page 可持有 `LocalPtr`，但 `LocalPtr` 不得进入 manifest durable 字段。
2. `LocalPtr -> DurableAddr` rewrite 必须由同一批 snapshot 统一驱动，禁止多套映射。
3. `published_cutoff + typed_rewrite_registry` 必须覆盖 ack miss 窗口。
4. append-only checkpoint 只允许复用已确认 durable cutoff 的 suffix；否则必须 structural rewrite。

### `Pool.pages` 生命周期不变量

1. 所有 `Pool.pages` 地址必须可归类为：
   - `replace immediate dealloc`
   - `checkpoint durable_pool_addrs remove`
2. `CheckpointTask::done()` 只能按 `durable_pool_addrs` 精确删，禁止 frontier 全表清理。
3. `dealloc(junks)` 只能删除本地页地址，不得影响 durable owner 视图。
4. 当 checkpoint budget 命中时，deferred dirty pid 必须回灌，保证最终可刷。

### crash-closure 不变量

1. `after_data_sync` 前崩溃：新文件可被 orphan 清理，不可见于恢复。
2. `before_manifest_commit` 崩溃：本轮 checkpoint 对恢复不可见。
3. `after_manifest_commit` 崩溃：本轮 durable 结果必须可恢复且自洽。
4. `retire_after_apply_before_clear` 必须允许幂等重放，不得 double-retire 或漏 retire。

## 4. 控制模型总览

### 4.1 模型核心

leaf 统一为 `Addressable(LocalPtr)` 主模型，去掉 resident 主流程，保留以下 runtime 状态：

1. `addr_head`：本地 addressable 链头
2. `durable_head_addr + durable_frontier + durable_publish_seq`
3. `artifact_retired_{local,durable}_addrs`
4. `published_cutoff` handoff + typed rewrite registry

### 4.2 与 `bwe-opt` 的对齐方式

1. `AppendOnly` 模式
   - 条件：存在有效 `durable_head_addr` 且本地链仍锚定 cutoff
   - 行为：checkpoint 只收集 suffix（`addr_head -> durable_head`）
2. `StructuralRewritePending` 模式
   - 触发：compact/split/merge 或锚定丢失/前沿未知
   - 行为：下一轮对该 pid 做完整 closure 输出
3. publish 后仍走 handoff 吸收
   - direct ack 命中：立即前推 cutoff
   - direct ack miss：保留 handoff，后续写入/collect 吸收

### 4.3 `Pool.pages` 闭包

地址只走两种退出路径：

1. 前台 replace 成功后：`dealloc(junks)` 立即删除旧本地页
2. checkpoint publish 成功后：`done(durable_pool_addrs)` 删除本轮持久化页

两者组合保证“本地页不会因 resident 移除而失去清理渠道”。

## 5. 关键时序

### 5.1 前台写入

1. `alloc_local` 分配 delta/base/sibling/remote（如需）
2. `Pool.pages.insert` + `active_bytes += size`
3. `mark_dirty_pid(pid)`
4. 如超过阈值，`try_checkpoint()` 触发异步 checkpoint

### 5.2 replace / merge / split / compact

1. leaf 直接产 addressable node，不再产 resident node
2. 产出完整 `junks`（旧 base/delta/sibling/remote 中的本地页）
3. `publish.replace()` CAS 成功后立刻 `pool.dealloc(junks)`
4. `touch_pid` 让新 head 进入下一轮 checkpoint

### 5.3 checkpoint snapshot

1. `swap(dirty_pid, empty)` 冻结 pid 集
2. pointer-swip page：
   - 锁内冻结可达源（addressable snapshot）
   - 合并 node-local cutoff 与 handoff 为 `effective_cutoff`
   - `AppendOnly`：增量 collect suffix
   - `StructuralRewritePending`：完整 collect closure
3. tagged-swip page：
   - 继续从 `ArtifactRetireState + durable_cutoff` collect
4. 形成 `pages + durable_pool_addrs + rewrites + acks + junk`

### 5.4 checkpoint flush/publish/done

1. flush 阶段：
   - 为 snapshot 中 local object 分配 durable addr
   - rewrite page/link/val 内嵌地址
2. publish 阶段：
   - 持久化 `map/lid/junk/frontier`
   - 安装 published cutoff 与 rewrite registry
3. done 阶段：
   - 仅删除 `durable_pool_addrs` 指定地址
   - 回收 inflight protected
   - 推进 `next_addr` 与 `last_chkpt_lsn`

### 5.5 crash matrix

| 窗口 | 运行时状态 | 恢复结果 |
| :--- | :--- | :--- |
| `after_data_sync` | 新文件已写，manifest 未提交 | 结果不可见，走 orphan 清理 |
| `before_manifest_commit` | rewrite 已完成，metadata 未提交 | 结果不可见，runtime state 丢弃 |
| `after_manifest_commit` | metadata 已提交 | 新 durable head 可见，recovery 自洽 |
| `retire_after_apply_before_clear` | apply 完成，runtime clear 未完成 | 重启后可幂等重放，不重复破坏统计 |

## 6. 代码接入点

### `src/types/node.rs`

1. leaf `compact/split/merge` 改为 addressable 输出
2. 引入/固化 `CheckpointRewriteMode`（`AppendOnly|StructuralRewritePending`）
3. `incremental_checkpoint` 判定从 `is_resident_leaf()` 解耦到 mode + cutoff 锚定
4. `checkpoint_*_rewrite_addrs()` 统一走 addressable 主路径

### `src/index/tree.rs`

1. 维持写入流程，不再假设 leaf compact 产 resident
2. 保持 published cutoff 吸收路径

### `src/map/publish.rs`

1. `replace()` 继续作为 immediate dealloc 主入口
2. 强化 `junks` 完整性断言（调试模式）

### `src/map/data.rs`

1. `snapshot()` 去掉 resident 分支，统一 pointer-swip collect
2. collect 按 mode 分流 append-only vs structural
3. `done()` 继续 exact-remove `durable_pool_addrs`，不做全表/frontier purge

### `src/map/flush.rs`

1. 保持统一 rewrite pipeline
2. 对 no-resident 模式新增断言：snapshot 不应依赖 resident-only carrier

### `src/map/buffer.rs`

1. 保持 `alloc/dealloc/try_checkpoint/timer` 机制
2. 增加 pool backlog 观测与阈值告警

## 7. 与现有机制关系

1. 与 `replace-interval`：正交
   - durable ownership 仍由 `lid + durable addr` 管理
   - no-resident 只改 runtime 组织与 collect 路径
2. 与 `bwe-opt`：兼容并保留
   - per-pid cutoff/handoff 继续作为增量 checkpoint核心
3. 与 GC：
   - GC 仍依赖 manifest owner 路径
   - runtime rewrite/protected set 继续防止 publish 过渡窗口误删

## 8. 配置项建议

1. 新增 `no_resident_leaf_mode = off | shadow | on`
   - `off`：当前行为
   - `shadow`：双算不切流，只做一致性比对
   - `on`：完全切到 addressable-only leaf
2. 新增 `checkpoint_force_structural_ratio`（调试/灰度）
   - 控制按比例强制 structural rewrite，验证退化安全
3. 禁止开关
   - 禁止“关闭 dealloc 即时回收”
   - 禁止“done 使用 frontier 批量清理”

## 9. 可观测性

1. `pool_pages_count`, `pool_active_bytes`
2. `pool_dealloc_bytes_immediate`, `pool_dealloc_bytes_done`
3. `checkpoint_append_only_pid_count`, `checkpoint_structural_pid_count`
4. `checkpoint_deferred_pid_count`
5. `published_cutoff_apply_hit`, `published_cutoff_apply_miss`
6. `rewrite_registry_data_size`, `rewrite_registry_blob_size`
7. failure metric：
   - `pool_pages_growth_without_checkpoint_done_rounds`

## 10. 性能影响与约束

### 热路径

1. 去掉 resident 读分支可降低 leaf 双态分叉复杂度
2. leaf rebuild 直接 addressable 会增加 `Pool.pages` 分配与 active_bytes 压力

### 慢路径

1. append-only pid 继续享受增量 checkpoint，避免整页重刷
2. structural rewrite pid 成本上升，但由 mode 精确控制，不全局退化

### 验收阈值

1. 同 workload 下 `ckpt_io_avg_bytes` 不得劣化超过 10%
2. `pool_active_bytes` 峰值不高于当前基线 15%
3. `ack_miss` 不得导致 repeated serialization 回升到 pre-bwe 水平

## 11. 失败与边界场景

1. checkpoint budget 打满
   - deferred pid 必须回灌，下一轮继续
2. publish 成功但 direct ack miss
   - handoff 保留，后续 absorb，不丢 cutoff
3. CAS 失败替换
   - 不执行 dealloc，交由重试路径持有并最终清理
4. structural rewrite 长期偏高
   - 触发观测告警，回滚到 `shadow/off`

## 12. 分阶段落地计划

### Phase A: state and mode preparation

1. 增加 `CheckpointRewriteMode`
2. addressable 节点补齐 mode 继承逻辑
3. 增加观测指标，不改行为

退出条件：

1. 全量测试通过
2. 指标稳定，无行为变化

### Phase B: snapshot unification (shadow)

1. `snapshot` 增加 no-resident shadow collect
2. 对比当前 collect 输出（page/junk/ack/frontier）一致性

退出条件：

1. shadow 对比无系统性偏差
2. failpoint 窗口一致性通过

### Phase C: addressable-only leaf on

1. leaf compact/split/merge 切 addressable
2. resident 分支退出主流程
3. `replace` immediate dealloc 合同收紧并加断言

退出条件：

1. 长测无 `pool_pages` 单调失控
2. checkpoint append-only 比例与基线相当

### Phase D: resident code retirement

1. 删除不再可达 resident 逻辑和测试
2. 清理遗留 metric/config

退出条件：

1. 无 resident 主流程调用
2. 文档与实现一致

## 13. 测试矩阵建议

### 单元测试

1. `compact/split/merge` 输出必须是 addressable leaf
2. `replace` 后 `dealloc(junks)` 精确减少 `pages/active_bytes`
3. `done` 仅删除 `durable_pool_addrs`，不误删未刷页
4. append-only collect 仅输出 cutoff 之后 suffix
5. structural collect 输出完整 closure
6. handoff miss 后下一次 absorb 能恢复 append-only

### 集成测试

1. 高频 update 下 repeated serialization 不回退
2. evict + checkpoint + reload 链路保持正确
3. gc_data/gc_blob/recovery 回归
4. 长时间低压写入 + timer checkpoint 场景无池子积压

### failpoint/crash

1. `after_data_sync`
2. `before_manifest_commit`
3. `after_manifest_commit`
4. `retire_after_apply_before_clear`
5. 断点后重启验证 `lid_map/page_table/junk` 幂等

## 14. 与替代方案对比

### 方案 A：保留 resident，只继续补洞

优点：

1. 短期改动小

缺点：

1. 双态长期维护成本高
2. checkpoint/evict/read-path分支持续膨胀

### 方案 B：直接一次性删除 resident 全量重写

优点：

1. 最终形态收敛快

缺点：

1. 变更面过大，回归风险高

### 方案 C：本方案（分阶段 no-resident）

优点：

1. 保持 bwe-opt/replace-interval 核心收益
2. 风险可控，可灰度回退

缺点：

1. 迁移期需要 shadow 与双路径校验

## 15. 实施顺序建议

1. 先补 `CheckpointRewriteMode` 与观测
2. 再做 `snapshot` shadow 对比
3. 然后切 leaf rebuild 到 addressable-only
4. 收紧 `replace`/`done` 的池子清理断言
5. 最后删除 resident 主流程代码和相关测试

