Table of Contents

Lease 协议使用指南(六类型 · 三阶段 · 三态迭代)

给谁看:IO 引擎层(Storage/IO)开发者、所有调 SegmentTable.{X}Lease(...) 的人。 回答什么:我要做追加/覆写/回收/截断/整理——用哪个 lease、怎么遍历 chunk、什么时候 Commit、 失败怎么办。


0. 一句话总纲与三条硬性要求

lease = IO 层与段表之间的唯一交互协议。每个操作类型是一个独立的类型化 lease, 对段表 / 物理段 / 稳态的要求由类型自身表达。

  1. 等待只有一个合法宿主:lease 协议。物理门(Empty→Ready)的等待点 = chunk 流水线第一拍 + 类型声明的提交扫尾。段表不等待、worker 不等待、池不等待。
  2. 类型即协议。禁止 kind 字节路由、禁止合并协议、禁止把某类型的要求放进共享路径。
  3. 段表是核心,物理段 = IO 层对段表统一事件的实现(见 segment-table.md)。

1. 选型表(需求 → lease 类型)

我要… 关键语义
追加写新数据(地址由段表分配) AppendLease(length) 推双尾;物理门有
只分配地址不写(预留空间) AllocateLease(length) 无 lease 对象,返回 (Start, End);占位 Committed+sparse
覆写已提交区间(地址已知) WriteLease(start, length) 不动水位;物理门有(快路径)
回收中间区间(打洞归零抹数据) ReclaimLease(from, to) [from,to) 可跨段——按段切 chunk 逐块 PunchHole;水位不动;无门
回收头部(删整段) ReclaimHeadLease(to) 跨段删除 + 尾段打洞;MinAddress 推进;无门
截断尾部(退水位) ReclaimTailLease(newTail) 仅退双尾(无打洞);独占双尾(不并发);无门
整理空洞(搬移压缩) CompactLease(from, to) overlay 原子替换段(一把锁内 invalidate+replace)

主推模型(性能最优,实测快 ~23%):一次 AllocateLease 大空间定地址 + 批量 WriteLease 写 (Write 不推 CommittedTail)。


2. 通用协议结构(全部类型共享,LeaseBase

三阶段生命周期:

① 占住(构造即完成)          ② 锁外 IO(你写数据的地方)        ③ 终态(Commit/Rollback/Dispose)
  AcquireExtentsForLease        遍历 chunk → 每块 IO →             整体 Commit() / Rollback()
  逐 chunk 占区间(段表切分)    chunk.Commit()/Rollback()           最后增量者触发 FinalizeTerminalCore
  • chunk 终态仲裁 doneMask 位掩码 exactly-once:部分/整体 × 提交/回滚四路径共用单一仲裁—— 每个 chunk 恰好一次终态,重复 Commit/Rollback 是 no-op,不会重复扣账

区间终态的分界线 = 物理真相(不是"成功/失败"——这是最容易记错的一点):

终态 适用场景 读门 为什么
Committed+sparse 打洞成功 / Allocate 占位 ✅ 可读(读零) 物理上确实全是零
Committed Append/Write chunk 提交成功 ✅ 可读(真实数据) 内容为真
Wasted Append/Write chunk 回滚 ❌ 阻断(可被 Write 覆写) 地址已消费,但内容物理未知——宣称读零 = 读出垃圾
Aborted Reclaim 打洞失败 ❌(只 Compact 修) 永久洞

判定口诀:物理上真是零 → Committed+sparse;内容未知 → Wasted。"失败 = 提交 0"只在物理上真是零 的场景成立;回滚覆写旧数据的场景不成立。

  • Dispose 语义:未终态 chunk 的回滚扫尾 + 终态收敛——using 忘了显式 Commit 不会泄漏区间。
  • 物理门EnterChunkPhysicalGate):基类无门;Append/Write override 等 Empty→Ready;Reclaim 系无门。门在迭代器/索引器里自动过,调用者无需手动调。

3. chunk 遍历(API:ChunkScope——三态模式)

每个 lease 的区间被段表切成 ≥1 个 chunk(跨段必多块)。ChunkScope 是只读几何 + 分段终态: Commit()(本块提交)/ Rollback()(本块回滚)。

using var lease = table.AppendLease(length);

// foreach——完整迭代器(推荐默认;自动过物理门)
foreach (var chunk in lease)
{
    await WriteAsync(chunk.SegId, chunk.SegOff, data);   // 锁外 IO(你的部分)
    chunk.Commit();                                       // 本块终态
}
lease.Commit();                                           // 整体:推 CommittedTail
  • 流水线规则:chunk 逐块提交可以乱序(doneMask 仲裁),但整体 lease.Commit() 前所有块必须已 终态——Append 的扫尾会对未提交块补物理门再收尾。整块失败 → lease.Rollback()(或直接 Dispose)。
  • 需要随机访问/重试单块时用下标 lease[i](索引器含物理门);需要操作枚举器本身时用显式迭代器 (MoveNext 已过物理门,iter.CommitCurrent())。

4. 各类型速查卡(权威表压缩版)

AppendLease(追加——最频繁)

维度 语义
范围 AllocatedTail 分配 [start,end);Empty 段合法(注册即 Empty,不阻塞分配)
物理门 ——每个 chunk IO 前等 Empty→Ready
chunk 提交 Leased→Committed,AdvanceOffset;到顶自动 Ready→Full + OnSegmentFull + 预建下一段
chunk 回滚 →Wasted(可覆写空洞),水位照推(地址已消费不可逆)
整体收敛 AppendFinalize(End):推 CommittedTail(precise-prefix,max-CAS 幂等)

WriteLease(覆写)

维度 语义
范围 覆写 [start,start+length);目标区间必须 ≤ CommittedTail 且可占(Committed/Wasted)
物理门 (显式声明;常态走快路径零开销)
chunk 提交 Leased→Committed(去 sparse,落真实覆写)
chunk 回滚 →Wasted
整体收敛 (目标区间本就 ≤ CommittedTail,推尾即错误前进)

ReclaimLease(中间回收 = 段上打洞)

维度 语义
范围 [from,to) 打洞(可跨段——逐块 PunchHole 物理归零抹除数据);区间保持 Committed+sparse
物理门 (作用于已提交区间,物理已存在)
chunk 提交 →Committed+sparse(打洞成功终态记录)
chunk 回滚 →Aborted(永久洞,只 Compact 修
三支柱 只动段区间;段/水位不变

ReclaimHeadLease(头部回收 = 打洞 + 跨段删除 + 推头)

维度 语义
范围 [MinAddress, to):to 前整段删除 + to 段 [0,to.Offset) 打洞
物理门 (删除不依赖物理就绪)
Commit/Rollback 都推 MinAddress(物理删段不可逆)
物理联动 删段文件由引擎在 lease 外执行(先 meta Remove+Flush 再删);lease 不做 IO

ReclaimTailLease(尾截断 = 仅退双尾)

维度 语义
范围 截断 [newTail, AllocatedTail)——无跨段打洞,段区间状态机不参与(游标即裁决)
前置 MinAddress ≤ newTail < AllocatedTail独占双尾(与其它 ReclaimTail 不并发;Append 退避不阻塞)
Commit/Rollback 都退水位(物理截断不可逆)
物理联动 IO 层跟随 SetLength;段表侧到此为止

CompactLease(整理 = overlay 拷贝 + 段表原子替换)

使用契约创建(独占占住全部目标区间)→ 完成全部物理 IO/搬移 → 最后一次性 Commit()。 区间码 CompactLeased 是独占模式——占住持续整个整理期间,不支持 chunk 级提交/释放(部分释放 会破坏"一把锁内全部段原子替换"的不变量);物理 IO 未全部完成不得提交(完整性绊线拦截 Pending chunk)。 代价模型:目标区间在整理全程对写者关闭(引擎侧 Compacting 排他)——大范围整理用 RangeCompact 分片。

独立协议sealed class CompactLease : IDisposable——不继承 LeaseBase(无 chunk 流水线/ doneMask/物理门,模型是"占住 → 搬移 → 整体替换"两阶段)。调用方是引擎 Compact 子系统(compactor), 业务侧入口是引擎的 StartCompact() / StartRangeCompact()

维度 语义
范围 [from,to)——全量 Compact = [MinAddress, CommittedTail)(未写满的尾段不含未提交区);可跨段
区间码 CompactLeased(Src=Compact,排他占住)
物理门 ——overlay 拷贝的是已提交数据(物理已存在)
阶段①(搬移) 每个 chunk 恰好调一次 SetReplacement(newMin, newMax, newGrowthLimit, state)(压缩后重建)或 MarkInvalid()(全部搬空删除)——互斥、一次性流转,未填 = Pending
阶段②(提交) Commit():先过完整性绊线(任何 chunk 仍 Pending → 拒绝提交抛异常)→ CAS Active→Committed → AtomicCompactReplace(一把 _mutationLock 内:invalidate 段 + 替换段槽位 Volatile.Write 发布)——中间状态不可见
整体终态 Rollback()/Dispose():全部 chunk 释放占住 + CompactRollback;Dispose = 未终态自动回滚

5. Reclaim 家族辨析(三兄弟只差三件事——"跨段"不是差异点,三者区间都可跨段)

段上打洞 整段删除 水位线
ReclaimLease(中间) ✓ 逐块打洞归零 ✗(段都保留) 不动
ReclaimHeadLease(头) ✓ 尾段部分区间 ✓ to 之前的整段 MinAddress 推进
ReclaimTailLease(尾) 尾后段随尾消失 仅退双尾(回退≠打洞)

6. 硬性要求与反模式

  1. 物理门不手动调——它在迭代器/索引器里。绕过迭代器直摸 ExtentsInternal = 绕过门 + 绕过仲裁。
  2. 禁止合并协议 / kind 路由——任一类型的要求泄漏成其它类型的隐藏前提。
  3. 跨段 lease 逐块处理,不要假设单 chunk(growthLimit 边界必然切块)。
  4. 对 Reclaim 打洞失败置之不理——Aborted 永久洞只 Compact 能修,洞多了读门会拦。
  5. ReclaimTail 当打洞用——运行期退尾不删区间记录;真要删数据走 Reclaim(中间)/启动修正。
  6. 把 lease 对象跨线程共享——lease 非线程安全;并发写各拿各的 lease(地址唯一性由 CAS 保证)。
  7. Compact 整体提交CompactLease 没有 chunk 级部分提交语义。

7. 最小正确范式(含失败路径)

// 追加写(生产范式:try/using 覆盖失败路径)
try
{
    using var lease = _table.AppendLease(data.Length, ct);
    var off = 0L;                                  // lease 内数据游标(chunk.SegOff 是段内相对量,跨段会归零——勿用它算 lease 内偏移)
    foreach (var chunk in lease)
    {
        await WriteChunkAsync(chunk.SegId, chunk.SegOff, data.Slice((int)off, (int)chunk.Length), ct);
        chunk.Commit();                            // 成功一块提一块(乱序合法)
        off += chunk.Length;
    }
    lease.Commit();                                // 整体:推 CommittedTail(内部最后增量者收敛)
}
catch (OperationCanceledException) { throw; }
catch (Exception)
{
    // 未 Commit 的 lease 经 Dispose 自动回滚扫尾(Wasted 空洞,地址不回收)——无需手动 Rollback
    throw;
}

// 主推批量模型:Allocate 定地址 + Write 批量写(快 ~23%)
var (start, end) = _table.AllocateLease(totalLength, ct);          // 纯地址(Committed+sparse 占位)
for (var off = 0L; off < totalLength; off += chunkSize)
{
    using var w = _table.WriteLease(start + off, chunkSize, ct);   // 覆写占位区间
    foreach (var chunk in w) { await WriteChunkAsync(chunk.SegId, chunk.SegOff, ...); chunk.Commit(); }
    w.Commit();   // Write 整体 Commit 无段表副作用(不推尾)——终态收敛在 lease 内部完成
}

// Compact(compactor 视角——业务侧经引擎 StartCompact()/StartRangeCompact(),不直接拿 lease)

using var lease = _table.CompactLease(from, to);
foreach (var chunk in lease.Chunks)
{
    var packed = PlanPacking(chunk);                     // 搬移规划:数据前移压缩
    if (packed.NewMaxOffset == 0)
        chunk.MarkInvalid();                             // 该段全部搬空 → 整段删除
    else
        chunk.SetReplacement(packed.NewMin, packed.NewMax,  // 压缩后重建(新容量/水位)
                             packed.NewGrowthLimit, StableState.Ready);
    CopyData(chunk, packed);                             // 物理搬移(overlay,锁外)
}
lease.Commit();   // 完整性绊线:有 chunk 漏填 SetReplacement/MarkInvalid → 此处抛,lease 仍 Active 可回滚
// 失败路径:异常未提交 → Dispose 自动回滚(全部占住释放,段表零改动)

8. 性能画像(速览)

单次 lease 全协议 ~1.5µs / 107-112B(纯地址 438ns;批量 WriteLease 最优);10 万次 Gen0=1-2、 区间表保持 1 条、p50 无退化;2 线程近线性零 Monitor 争用;IO 占比 Buffered ~7%、WriteThrough ~0.4% ——lease 不是写路径瓶颈,优化预算在 IO 引擎层。池化:LeaseFactory.Default = 每次 new(默认不池化 ——对象小,池化成本更高);Pooled 仅诊断/对比用。

9. 决策速查

场景 做法
高频小写 AppendLease 逐条(便捷)或 Allocate+Write 批量(最快)
大块顺序写 Allocate 大空间 + WriteLease 批量
覆写已知地址 WriteLease(先确认区间 ≤ CommittedTail)
释放空间不缩边界 ReclaimLease 打洞
删最老数据 ReclaimHeadLease
truncate 语义 ReclaimTailLease
空洞压缩 CompactLease(整体提交)
写失败 块级 chunk.Rollback() 或整体 Dispose(自动扫尾)