Table of Contents

段表使用指南(SegmentTable)

给谁看:IO 引擎层(Storage/)开发者、lease 消费侧、恢复/Checkpoint 接入者。 回答什么:段表持有什么、我能碰什么、事件怎么接、回调怎么发、什么绝对不要做。


0. 一句话总纲

段表 = 地址空间的唯一真相之源:它持有三支柱(水位线 / 段 / 段区间),只发事件、只收回调, 不等待、不做 IO、不关心物理文件。物理段是 IO 层对段表统一事件的实现。

                    统一事件(ISegmentHandler,段表 → IO 层)
段表(真相)  ────────────────────────────────────────────→  IO 层(物理资产)
  三支柱        ←────────────────────────────────────────────  worker 建段 / 预备池 /
                 建段回调(CreateSegmentCallback,IO 层 → 段表)  meta / 删文件

1. 三支柱(调用者必须建立的心智模型)

支柱 内容 关键规则
水位线 MinAddress(头)/ CommittedTail(终态前缀)/ AllocatedTail(分配) 架在区间表上的粗游标;外部只读;不变量链 MinAddress ≤ CommittedTail ≤ AllocatedTail
每段 MinOffset/MaxOffset 两条段内水位 + StableState + GrowthLimit RealSize = MaxOffset − MinOffset 是数据大小,GrowthLimit 是容量——两者不是一回事;IsFull ⟺ MaxOffset ≥ GrowthLimit(派生属性)
段区间 每段一份区间表(ExtentStateCode:在途带 Src → 终态) lease 协议的核心状态机;可占性 IsOccupiable = Committed || Wasted

读门:区间的可读性看区间表IsRangeFullyReadable),不是看水位游标——游标是粗裁剪, 区间表才是逐字节真相(游标 vs 区间双轨,勿混用)。

2. 段稳态(StableState)——准入语义

稳态 含义 进入 离开
Empty 逻辑已注册、物理未建——物理门关(不接受 chunk IO) 注册即 Empty(AppendSegmentRaw 唯一出口:建段回调
Ready 物理就绪,chunk IO 合法 回调成功 TryMarkReady 到顶自动→Full;→Compacting;→Invalid
Full MaxOffset ≥ GrowthLimit:不可扩容、可覆写 AdvanceOffset 到顶(仅 Ready→Full →Compacting;→Invalid
Compacting 整理排他 overlay Compact 进入 Compact 完成
Broken 建段失败终态——物理门永久关,不重试不可分配:分配区间不得含其任何字节,尾在 Broken 段内/请求跨段时烧洞跳过 回调失败 TryMarkBroken 终态(地址空间墓碑)
Invalid 已删除:准入吊销、文件不存在 ReclaimHead/Compact/启动修正 终态(幂等)

三条设计决策(勿当缺陷):

  • 运行期 Full 只经 Ready→Full。Empty 期被 Allocate 占位推满的段不转 Full 态——满语义由派生 IsFull 承担;Empty→Full 直达只存在于启动恢复装配。
  • 回调双分支幂等:成功/失败均 CAS,非 Empty 一律 no-op——重复/迟到回调不打断已迁移段。
  • Compact 后的段槽复用是设计路径:整理压缩空洞 → 中间段 Invalid 出服务 → 退尾再分配时重注册 同 segId——这是地址空间回收复用机制。

3. 关键 API(调用者视角)

类别 成员 说明
只读查询 AllocatedTail / CommittedTail / MinAddress / SegCount / MaxSegId / GetSegment(segId) / TryGetSegment / IsRangeFullyReadable / GetExtentRanges 全部无锁安全;GetSegment 返回只读 SegmentView(不存在返回 Hollow
地址算术 AdvanceAddress / RetreatAddress / GetDistance(+ SegmentGrowthLimit 纯计算,不改状态——语义见 §3.1
lease 入口 AppendLease / AllocateLease / WriteLease / ReclaimLease / ReclaimHeadLease / ReclaimTailLease / CompactLease 外部改状态的唯一合法通道,见 lease-protocol.md
协调(IO 层专用) WaitSegmentReady / CreateSegmentCallback / PulseAllSegmentsReady 建段协调;WaitSegmentReady唯一调用方是 lease 协议(物理门)
持久化 SaveAddressTable / LoadAddressTable(一对)+ SetStartupTails(启动水位) 恢复/落盘专用,见 §3.2
锁开放面 ExecuteUnderLock / TryGetLock 三级锁中必须开放的两级,见 §4

3.1 地址算术(三个接口,纯计算无状态)

接口 语义 边界规则
AdvanceAddress(start, length) 前进 length 字节,跨段进位 只前进(回退调 Retreat);返回的 Extension 恒为 0——调用方须显式保留 start.Extension;恰好填满一段时停驻段末边界 (segId, GrowthLimit)
RetreatAddress(start, length) 回退 length 字节,跨段借位 只回退;回退到 MinAddress 之前返回 LogicalAddress.Invalid;落点恒为真实字节位置
GetDistance(from, to) from→to 字节距离,跨段累加 from > to 返回负值;不做 AllocatedTail 上界校验(调用方保证合法)
SegmentGrowthLimit(segId) 取段生长上限 段存在用段的;Hollow 段用生命周期上限——地址空间连续,被回收段在逻辑地址上仍占位

三个算术共享同一条不变量:跨段进位/借位按每段各自的 GrowthLimit 计算(Compact 后段大小可能 不同,不用全局值)。

区间表示规范(半开 [start, end)——统一规范)

  • 一切区间都是半开:覆盖 start ≤ b < endend 是"已占用字节之后第一个位置"。
  • 边界规范形:恰好填满一段时,边界停驻段末 (seg, GrowthLimit)——旧哨兵形态 (seg+1, 0) (把段末写成"下一段头")已废除(存量盘兼容接受,新算术永不产出);(N, 0) 保留唯一身份 = 段首字节 / 地址空间原点
  • 半开的理由(定稿):空区间天然是 start == end(零特判);Append 写起点就是 end 本身(热路径 零 +1);全库水位比较按半开格建设。

3.2 持久化(一对读写 + 一个水位修正)

接口 语义 时机
SaveAddressTable(IAddressTableWriter) 落盘段表:逐段 SegmentSpec + footer(双尾水位是权威 Checkpoint / 关闭
LoadAddressTable(IAddressTableReader) 装配:三段式(头部 → 段载荷循环 → 尾部直读) 恢复期,一次性
SetStartupTails(StartupParameters) 启动期双尾水位设定(可大可小:小=截断回收;大=覆盖旧数据推水位)。单值=双尾同址;双值=扫盘恢复形态 仅启动阶段(首次 Allocate 之前,之后调用直接抛)

恢复顺序硬性要求:LoadAddressTableSetStartupTails(若扫盘结果与持久化水位不一致)→ 首次 Allocate 锁定运行期。无持久化启动:构造 → SetStartupTails 定双尾 → 直接 Allocate——LoadAddressTable 全程可选。(IO 引擎侧的 EngineRecoveryHints 由引擎恢复流程翻译为 SetStartupTails;两层类型不共用。)

4. 锁模型(三级——开放两级,区间锁不开放)

保护对象 开放面 谁用
表级 _mutationLock(Monitor) 段数组/索引结构变更 ExecuteUnderLock(Action) 外部批量自洽结构操作
段级 SegmentLock(SpinRWLock 写偏向) 段级读写互斥 / Compact/截断排他 TryGetLock(segId, out SpinRWLock?)(裸锁) 读者与写者互斥、Compact 与一切互斥
段内 区间锁(struct SpinLock,微秒级临界区) 段内区间表 不开放——lease 协议封装 只有 lease 协议

为什么区间锁不开放:区间表的每次变更都是 lease 三阶段协议的一部分(占住→锁外 IO→提交/回滚)—— 开放区间锁 = 外部绕过协议直改区间表 = 状态机与事件契约脱钩。需要区间排他?拿一个 lease,协议替你 持锁。

锁序(必须遵守)_mutationLock > 段 SpinRWLock > 区间 SpinLock——只能从上往下拿,不可逆序 (逆序 = 死锁)。SpinRWLock 排他临界区内绝对禁止 await(线程关联原语,跨 await 释放即泄漏; 共享锁可跨 await 长持,读计划锁即此用法)。

与物理门的分工:锁管互斥(谁在改),单向状态闩(§2 Empty→Ready/Broken/Invalid 的 _physicalReady)管协调(等谁就绪)——纯状态协调不用锁。

5. 事件契约(段表 → IO 层,ISegmentHandler

事件 时机 IO 层职责
OnSegmentCreate(segId, growthLimit, isHighPriority) 注册时 / 段满预建下一段 正式建段或池预建(build-gate single-flight:同一 segId 恰好一次物理构建
OnSegmentFull(segId, finalSize, growthLimit) 段满(含占位推满——派生 IsFull 判定) 写 Full-meta + 补池
OnSegmentDelete / OnSegmentReplace / OnSegmentReclaim 删段/Compact 替换/回收通知 物理联动(引擎子系统自管)
SubmitBackgroundWork(work) 段表自洽低频任务 worker 顺序执行

回调契约(IO 层 → 段表):物理构建完成后调 CreateSegmentCallback(segId, success)。建段任务唯一 职责 = 为「已注册且 Empty」的段完成物理构建并回调;非 Empty / 已摘索引 → 作废绝不重建

6. 生命周期与阶段门禁

  • 构造(SegmentTableSettingsGrowthLimit/MinSegId/IndexCapacity/SpinMilliseconds/WarnEvery/ EnableSingleSegment)→ 启动阶段(LoadAddressTable(可选)+ SetStartupTails仅 Allocate 之前 可调)→ 运行期(首次 Allocate 成功即 CAS 锁定,不可逆)。
  • handler 传入与否决定出生态:带 handler 出生 Empty(等物理回调);null(纯内存/测试)出生 Ready。

7. 硬性要求(违反 = 数据丢失 / 死锁 / 复活已删段)

  1. 外部永远拿 SegmentView,不碰 Segment 引用;改状态唯一入口 = lease 协议。
  2. 段表不等待、不做 IO。任何"等建段完成再…"的诉求都是 lease 协议的事(物理门),绝不往段表/ worker 里加等待。
  3. 建段 single-flight:同一 segId 的物理构建恰好一次(取用、守卫、声明三者原子同临界区)。
  4. 索引/数组扩容 build-then-publish(先填 -1 后 Volatile.Write 单点发布),禁用 Array.Resize—— 其零填充窗口会被无锁读者读到幽灵索引。
  5. 发布-读取 acquire/release 全对称(ARM 弱序合规):写侧全 Volatile.Write 发布;读侧对索引 字段与槽位Volatile.Read(任一环 plain load 在 ARM 上可见中间态;x64 TSO 恰安全但不得依赖)。
  6. WaitSegmentReady 唯一调用方 = lease 协议;有界放弃(预算/告警走 Settings)。

8. 反模式(禁止重蹈)

  • Invalid 段重建:持过期复查的任务去 claim 已回收段 = 复活已删文件 + 容量重计。守卫与声明 同临界区,四态(Consumed/Claimed/InFlight/Abandoned)一锤定音。
  • 锁内的"正确"证明不了无锁读者的中间态:共享数组变更先想读者看到什么。
  • StableState.Full 当满判据:满 = 派生 IsFullStableState 是准入/物理语义,不是容量语义。
  • 运行期调 SetStartupTails / 在 Allocate 后裸写水位——直接抛。
  • 给段表加物理概念(文件路径、句柄、池深度……)——物理是 IO 层的事。

9. 最小正确范式(IO 引擎接线)

// ① 引擎构造段表:带 handler(出生 Empty),等待参数走 Settings
_table = new SegmentTable(
    new SegmentTableSettings(growthLimit: SegmentGrowthLimit, SpinMilliseconds: 30_000),
    SegmentHandler,          // ISegmentHandler 实现(事件 → worker/池)
    Logger);

// ② handler 收到注册事件 → 入队正式建段(或池命中同步转正)
public void OnSegmentCreate(int segId, long growthLimit, bool isHighPriority)
{
    if (TryConsumePooledSegment(segId))          // 池命中:物理现成
        _table.CreateSegmentCallback(segId, success: true);   // 同步转正(幂等)
    else
        EnqueueCreateTask(segId, growthLimit, isHighPriority); // worker 正式建
}

// ③ worker 建段任务体:只对「已注册且 Empty」构建,成败都回调,绝不等待/重试
private async ValueTask EnsureSegmentPhysicalAsync(int segId, long growthLimit, CancellationToken ct)
{
    switch (TryConsumeOrClaimPhysicalBuild(segId, out var gate))   // 取用/守卫/声明同临界区
    {
        case PhysicalBuildClaim.Consumed:
            _table.CreateSegmentCallback(segId, success: true);  return;
        case PhysicalBuildClaim.Claimed:
            try { await CreateSegmentCoreAsync(segId, growthLimit, ct);
                  _table.CreateSegmentCallback(segId, success: true); }
            catch (Exception ex) { Logger.LogError(ex, "建段失败 {SegId}", segId);
                                  _table.CreateSegmentCallback(segId, success: false); } // 幂等,重复不抛
            finally { CompletePhysicalBuild(segId, gate, pooled: false); }
            return;
        case PhysicalBuildClaim.InFlight:   // 池在途——其完成者代执行同一回调
        case PhysicalBuildClaim.Abandoned:  // 非 Empty/已摘索引——绝不重建
        default: return;
    }
}

纯内存/测试handler: null → 段出生即 Ready,无物理协调(SegmentTableLeaseProtocolTests 全套示例)。

10. 决策速查

我要…
查某段是否存在/多大 GetSegment/TryGetSegmentSegmentView(只读)
判断区间可读 IsRangeFullyReadable(读门 = 区间表,不是游标)
地址前进/回退/距离 AdvanceAddress / RetreatAddress / GetDistance(§3.1——注意返回 Extension 恒 0)
跨段区间切分 GetExtentRanges
改任何状态(写/收/删) lease 入口 → lease-protocol.md
段表落盘/装配/启动水位 SaveAddressTable / LoadAddressTable / SetStartupTails(§3.2,仅启动阶段)
批量结构变更对外不可见 ExecuteUnderLock(表级,§4)
段级读写/Compact 互斥 TryGetLock(段级裸锁,§4)
区间排他 不直接拿锁——走 lease 协议(§4 锁模型)
诊断 GetActiveLeases/SnapshotSegmentExtents(只读;ForceRelease 仅受控回滚)