持久化与共享读取契约
首版同时实现可选 SQLite 和 DuckDB,默认只使用内存。本页规定完整状态、提交、恢复与共享边界。当前逻辑 schema 2 保存独立 ASPA 地址族范围;异步 Client/共享客户端的交付状态见源码 API 参考及相应验收记录。
后端接口
class PersistenceBackend(Protocol):
@property
def capabilities(self) -> BackendCapabilities: ...
def open(self) -> None: ...
def read_state(self) -> PersistedState | None: ...
def commit(
self,
state: PersistedState,
*,
expected: SnapshotId | None,
) -> PersistReceipt: ...
def close(self) -> None: ...
BackendCapabilities 声明 backend_name、schema_version、shared_read、single_owner、atomic_commit。PersistReceipt 包含实际 snapshot_id、config_revision、committed_at、schema_version、state_digest。自定义后端必须运行相同契约测试,不能仅实现同名方法就宣称兼容。
公开类型位于 rpkiparrot.persistence。SQLiteBackend(path, *, limits=None, busy_timeout=5.0) 与 DuckDBBackend(path, *, limits=None) 构造不访问文件、加载驱动或取得锁;在专用工作线程调用 open 时绑定线程,此后的 read_state、commit、close 必须在该线程执行。close 幂等,已关闭实例不可重新 open。SQLite 需要 sqlite extra 的 FileLock,DuckDB 需要 duckdb extra;缺依赖在打开时明确报 MissingExtraError,不影响核心导入。
连接同时绑定打开进程,fork 后继承的实例不能操作或释放原 owner 锁。Client 按配置自行创建的后端,在其工作线程开始下一事务前设置该快照固定的 Limits;正在执行的事务不会被热配置改预算。宿主注入的后端保留自己的限制,Client 不静默覆盖,因而可比客户端限制更严格。
state_from_snapshot 以 O(来源数) 捕获 PersistedState,共享不可变 ParsedDataset/SourceInfo 和记录元组,不复制整份 JSON。PersistedSource 保存 dataset、info、group_id、evaluated_at、invalidated;PersistedState 另保存发布/配置身份、发布时间、评价上下文、全部组/活动组、规范版本、policy_id 和上游引用。待提交状态 receipt 为 null,read_state 返回实际 PersistReceipt。observed_at 保存已观察的有效时间下界;它与 receipt 不进入固定 payload 摘要。state_digest 流式处理规范 JSON 字节并检查 max_export_bytes,排序只保留原始记录引用,不额外创建整份记录字典树。可执行例子 展示提交、幂等重试与重新打开。
摘要中的单条原始记录可在保守编码上界不超过 64 KiB 时一次严格编码,仍逐条生成和释放,不缓存完整载荷。较大记录、非原生字符串或非法 Unicode 均回到原逐字段路径;自由 metadata 保持通用处理。完整规范字节、支持顺序、重复条数、摘要与字节预算不变;非法 Unicode 与超限同时出现时,保留原字段顺序决定的错误。64 KiB 只限制该记录编码路径,不承诺全局内存上限,也不改变事务、来源时效或 SQL schema。
内置 SQLite/DuckDB 在工作器的同一次同步调用中,可以复用工作器已经完整校验的摘要;仅匹配同一个不可变 PersistedState 对象和相同 Limits。公开 commit 仍被调用,预算变化或状态对象变化均重新完整校验,宿主注入的更严格预算不能被绕过。自定义后端及子类保留自己的提交校验路径。复用引用仅存活到这次调用退出,成功、错误和控制流退出均清除,不形成跨提交缓存。直接调用公开后端 commit 仍自行完整校验;幂等重试和不确定提交恢复仍核实实际磁盘状态。
open/read_state/commit/close 都是同步阻塞调用;Client 用有固定连接归属的受管理工作器串行调用。不使用 DuckDB 全局连接,也不在不同线程之间随意迁移 sqlite3 连接。适配器可使用独占工作线程/命令队列实现;必须等待当前事务完成或回滚再关闭。
读取完整状态或探测头时,可识别的暂时性驱动故障返回 PersistenceError(code="persistence_failed", retryable=True):SQLite 的 busy/locked、锁协议失败、普通 I/O、内存不足、中断、空间不足及无法打开文件按原生错误码识别(包含扩展码);DuckDB 的内存不足、事务冲突及中断按具体异常类型识别。系统 OSError 和 MemoryError 同样作为运行期读取失败。SQLite 只读访问限制,以及 DuckDB 已关闭或已失效的连接,报告不可重试的 persistence_failed,不宣称在同一连接重试即可恢复。库不解析驱动错误文本或自动重试该同步调用。
这些诊断不授予新数据有效性:schema、严格类型、解码、摘要或明确损坏错误仍拒绝读取;SQLite 的数据/文件系统损坏型 I/O 扩展码、没有原生错误码的 UTF-8 解码失败,以及 DuckDB 的序列化错误不会被笼统当成暂时性故障。DuckDB 的通用 IOException 同时用于块校验和损坏与其他 I/O 故障,Python 驱动没有可靠子码;因此报告 persistence_failed、retryable=False,既不猜测文件已损坏,也不保证可重试。原文件保持不变。内部 AssertionError 和控制流 BaseException 在打开、读取及提交边界清理后原样传播,不伪装成数据损坏或可重试故障。共享读取者在暂时性失败期间仅保留仍在原期限内的旧视图,后续成功读取才清除错误。
expected 是读到的上一持久化版本,None 只匹配空数据库。提交之前核对,冲突报告 PersistenceError;即使已有进程锁也保留此检查。相同 snapshot_id 和相同摘要的重复提交幂等;相同 ID 不同内容为错误。
幂等重试返回原 committed_at,不把重试时间写成新提交。更晚观察到的时间下界须随新的完整发布版本提交;进程崩溃前尚未提交的时钟证据无法跨进程保留。后端不会修改同一 SnapshotId 的 payload 或伪造一次新的同步。
持久化状态
PersistedState 保存可重建完整来源状态的逻辑记录,不保存 Python 对象 pickle 或进程内索引:
| 逻辑表 | 必要列与约束 |
|---|---|
| store_meta | schema_version、当前 store_id/epoch/generation、written_at、包/规范版本、config_revision、配置身份、完整状态摘要;仅一个当前头 |
| sources | source_id 主键、kind、identity_digest、组/优先级、协议/格式、session/serial、完整标志、能力、原始同步/生成/失效时间、非敏感状态 |
| vrp_supports | source_id、规范化网络、prefix_length、max_length、ASN、支持标识、原始期限;唯一支持键 |
| aspa_supports | source_id、customer、provider、支持标识、原始期限;保留 AS0 和来源,不只保存最终 provider 并集 |
| router_keys | source_id、ASN、SKI、SPKI、原始期限;完整唯一键 |
| source_metadata | 来源的安全扩展元数据、格式识别和完整性诊断;不得保存凭据 |
| view_state | 每类载荷活动组、policy_id、上游快照引用;恢复后仍须按时间重新计算 |
网络存为明确地址族和固定长度网络序 bytes,ASN 使用能够完整表达无符号 32 位范围的 SQL 类型,时间使用 UTC 整数微秒;不能因 SQLite/DuckDB 默认类型不同改变语义。空集合由 sources 的完成与能力字段表示,不能靠“查不到载荷行”判断。
首版逻辑 schema_version=2。物理索引可优化,不改变逻辑状态与校验结果。源码提供精确 DDL、唯一约束、合法范围和后端参数化测试;schema 变更不得只在某个后端暗改。旧 schema 1 数据库拒绝直接打开,不自动迁移或清空;宿主选择新空文件重新同步。交换文件的版本 1 兼容导入属于独立显式路径,不允许数据库读取偷偷授予新的地址族范围。
精确 DDL 由 _sql_backend._DDL 定义,两后端使用同一表与字段,二进制长度函数分别绑定 SQLite length / DuckDB octet_length。schema_info 只有 singleton=1/schema_version=2 一行。store_meta 只有 singleton=1 的头:schema_version、store_id、epoch、generation、config_store_id、config_epoch、config_revision、published_at、written_at、mode、reference_time、upstream_store_id、upstream_epoch、upstream_generation、policy_id、versions_json、package_version、config_identity、state_digest、meta_digest。
| 表 | 精确字段与唯一规则 |
|---|---|
| source_groups | group_id 主键;priority 为唯一有符号 64 位整数,包含空组 |
| sources | source_id 主键;group_id、kind、identity_digest、format_id、protocol_version、connection、reason_code、last_sync_at、generated_at、expires_at、session_id、serial、complete、invalidated、evaluated_at、present、vrp_capability、aspa_capability、router_key_capability、error_json、aspa_families、aspa_v4_capability、aspa_v6_capability |
| vrp_supports | source_id、support_id、family、network、prefix_length、max_length、asn、valid_from、expires_at、upstream_label;主键 (source_id,support_id) |
| aspa_supports | source_id、support_id、customer、provider、valid_from、expires_at、upstream_label、afi;主键 (source_id,support_id,provider) |
| router_keys | source_id、support_id、asn、ski、spki、valid_from、expires_at、upstream_label;主键 (source_id,support_id) |
| source_metadata | source_id 主键;metadata_json、diagnostics_json |
| view_state | kind 主键,仅 vrp/aspa/router_key;active_group 可为 null |
| aspa_view_state | afi 非空主键,仅整数 1/2;active_group、capability、usable_until,后者为 UTC 整数微秒或 null |
support_id 是各来源、各载荷族中从 0 开始的原始支持位置,必须连续;相同载荷的重复支持仍占不同位置,不能按最终载荷去重。ASPA 同一 support_id 的所有 provider 行须具有相同 customer/afi/时间/标签,重组为一条原始记录。afi 在 SQL 中使用非空 0/1/2,0 明确编码 Python/交换格式的 None(双族),不是 IANA 地址族值;单族相同 customer/providers 仍是不同原始载荷。aspa_families 使用位 0/1 表示 IPv4/IPv6 完整覆盖,0 表示不支持 ASPA。所有来源必须且只能属于一个组,活动组须与固定评价时刻的来源能力一致;ASPA 分族活动组允许不同,聚合活动组仅在相同时保留。无头时所有载荷表必须为空。跨表覆盖、原始范围、能力与期限一致性由共同读取/提交校验执行,不依赖后端特有的外键时序行为。
ASN/customer/provider/serial 存 BIGINT,合法 ASN 上界 4294967295,ASPA customer 不得为 0;session_id 为 0–65535,协议为 null/1/2。时间用相对 1970-01-01 UTC 的有符号整数微秒,不经浮点 timestamp 转换;null 保留未知时间。complete/invalidated 仅 0/1,present 的位 0/1/2 分别为 VRP/ASPA/Router Key。network 必须为 IPv4 的 4 字节或 IPv6 的 16 字节网络序规范地址,前缀/最大长度按地址族检查;SKI 为 20 字节、SPKI 非空。SQLite 的动态类型不能使字符串或浮点 ASN/时间悄悄进入模型,读取还会核对实际存储类型。SQL CHECK、主键与应用层严格校验共同保护约束。
原始 metadata、diagnostics、ErrorInfo 和规范版本可用独立 JSON 字段;载荷、时间、ASN、组和头身份不能塞入单个快照 JSON blob。行写入和读取使用最多 256 行的批次;SQLite 使用 executemany。DuckDB 将一批严格检查类型的行作为单个 JSON 参数传输,再由内置 json_each 和显式 SQL CAST 写入上述规范列,二进制经十六进制无损还原;不保存整份 JSON 快照。传输缓冲另以 1 MiB 为拆批目标,超过目标的单一合法行独立发送。DuckDB 1.5.6 在关闭 external access、扩展自动加载及安装时也可执行该路径,不下载扩展或引入 Arrow/Pandas 依赖。读取先检查来源/支持计数和大字段字节量,再建立原始模型,不构建查询索引或授予在线可用性。
v2→v1 协商引起的 ASPA 撤销允许按交换格式的严格保留结构原子落盘。metadata.rtr.incremental_usable=false 与仍为 v2 的原格式、协议、同步和失效时间一同保存,ASPA 原始记录、覆盖和能力同步撤销;不将它伪造为新的 v1 EOD。两后端的提交和读取均检查该形状。恢复保留原 VRP/Router Key 剩余寿命并强制重新 Reset;该状态不能成为 Serial Query 的增量基础。旧 Snapshot 不被回写,但在线查询仍受期限和撤销事件重验约束。
状态摘要覆盖版本、来源、载荷和期限的确定性序列化,算法见交换格式。它用于检测损坏和错误提交,不是可信签名。文件不受信任时不能靠摘要就接受为可信缓存。
state_digest 与 schema 2 交换 payload 的 SHA-256 完全相同,包含原始 AFI 范围、来源覆盖、分族能力/活动组/期限及适配语义版本,不包含写入时刻。written_at 取当前 UTC、observed_at、published_at 和各来源 evaluated_at 的最大值,防止恢复或等待落盘掩盖已消耗的寿命。物理 meta_digest 是 store_meta 除自身之外的有序字段数组的规范 JSON SHA-256,覆盖 written_at、状态摘要和全部头身份;读取同时验证它与完整状态摘要。该额外校验检测合法形式的时间字段意外损坏,不把可重算摘要当作认证。
映射 IPv6 的摘要文本固定为 ::ffff:a.b.c.d/length,不随解释器版本变化;SQL 仍保存原 IPv6 的 16 字节网络地址。真实 CPython 3.11–3.14 间交叉提交/读取须保持摘要、回执、身份和原始期限一致。修复前的未发布开发数据库若使用十六进制映射前缀计算摘要,继续以 persistence_corrupt 拒绝,不改写回执或降低完整性检查;宿主保留旧文件,另选空文件重新同步。已有完整交换文件可通过文档明确的旧拼写导入路径重载,不能因此延长有效期。
提交顺序
- 来源更新通过校验,在内存中原子发布完整新快照。
- 发布后的完整状态交给持久化工作器。pending 只保留最新完整状态,可合并中间 generation;不得把增量差分当作可随意跳过的完整状态。
- 一个数据库事务内写入全部逻辑表及头信息,检查 expected;成功 commit 后才更新 persisted_snapshot_id 并发出持久化状态通知。
- 失败回滚,内存快照继续服务至原期限,记录 persistence_failed 和最后成功版本;重试始终使用合法完整状态。
SQLite 和 DuckDB 都允许用事务内替换当前表内容的简单首版实现;优化为 copy-on-write 表等方式时仍遵守同一原子可见性。不得先写“新 generation”再分多次事务补载荷。
进程在任一步骤被杀死,恢复时只能看到旧的完整提交或新的完整提交。数据库返回 commit 成功但调用方未收到确认时,读取头和摘要确认实际结果后再决定是否重试,不能假定失败。
flush 捕获当前目标版本并等待它或其后继持久化,返回实际回执;后继必须同一发布身份且确实覆盖完整状态。后台落盘合并不等于事件日志允许静默丢失变化。
关闭先终止普通候选,等待来源已经确定的撤销在同一有界构建预算内完成,再把最终完整状态交给持久化工作器。协议工作线程在关闭开始后才返回的致命错误也遵守此顺序;关闭期间不再发布事件。必要撤销所花时间计入原有最终 flush 期限,不重新起算。期限耗尽或数据库失败时,最后确认版本可能仍是撤销前的状态,必须通过 persistence_error 和 persisted_snapshot_id 如实报告,不能宣称撤销已持久化。撤销构建若出现意外内部异常,异常传递给宿主并开始关闭,丢弃尚未开始的数据库候选并拒绝新候选,不再将撤销前的内存候选作为新的最终数据库头;已经提交或正在执行的数据库事务仍遵守原子提交约定。关闭结果记录 persistence_failed,details.phase 为 close、cause 为 revocation_failed;未满足的 flush 等待者收到停止错误,不继续等到自己的超时。正常关闭则仍允许最终提交满足已有 flush 等待者。
启动和恢复
启动顺序为检查依赖/配置 → 获取写入所有权 → 打开并检查 schema → 读取一致完整状态 → 检查摘要与身份 → 检查时间 → 重建索引 → 发布恢复快照 → 启动网络。
- 空数据库允许创建 schema 2;shared_reader 不创建或修复数据库。
- 不支持的 schema 拒绝打开,不自动清空或降级。首版不自动执行 schema 或跨后端迁移;用户另选空文件重新同步。
- 损坏、摘要不符或来源身份不匹配的状态不进入在线有效视图。owner 可报告后从网络重新同步;原文件损坏到不能安全写入时启动失败,不能悄悄覆盖。
- 来源身份包含 kind、端点/路径、协议范围、格式、reader_profile_id、信任配置 ID、相关有效期策略;机密只通过非敏感身份标识参与,不保存机密本身。
- 数据库完整状态同时保留已配置但未加载的占位来源;只有已完成且通过身份/时效检查的来源可恢复为有效数据。逐条保留期限;任何启用载荷缺失都不能以旧 session/serial 发增量请求。过期部分使该 RTR 来源失去增量恢复资格,改用 Reset Query。
- 重启生成新 publisher epoch;恢复来源协议 session/serial 不代表沿用旧事件 epoch。记录 recovered_from 指向数据库旧发布版本。
- 墙钟早于 written_at 超出 clock_tolerance,或无法证明剩余期限时,不授予在线可用性,重新同步;绝不能把 expire 秒数从重启时重新起算。
恢复成功只表示获得当前仍有效的数据;连接状态仍从未连接开始。无有效数据的恢复不完成 wait_ready。
运行中配置变更
apply_config 发布的配置身份、config_revision、来源删除和新完整状态在同一事务持久化;无来源状态也必须落盘,不能因载荷为空跳过。pending 合并可跳过中间配置版本,但不能将旧配置的载荷与新配置头组合。失败时内存保持已提交的新配置,分别报告当前/持久化版本。
降低 Limits 时检查正在执行和尚未确认提交的预算。一次预算检查保留其已观察到的不可变未确认记录,即使数据库线程随后确认提交并清空该记录,也不在检查中丢掉预留或重复解引用已清空字段。当前检查可以保守拒绝降低预算;提交确认后重新检查不再保留已释放的预留。
启动配置由宿主显式提供,不从数据库反向覆盖宿主配置;只恢复仍配置且身份匹配的来源,已删除来源不得因旧持久化头再次启用。新 publisher epoch 具有自己的初始配置版本,recovered_from 保留磁盘来源。旧配置快照的固定语义和共享传播延迟见配置变更。
SQLite 共享
本机文件系统上使用 WAL,单一 owner 写入,多个只读进程读取。owner 锁覆盖 schema 检查、恢复、写入到 close 的完整期间;同一数据库的第二个 owner 明确失败。路径规范化后加锁,不能靠不同相对路径绕过。
rpkiparrot.shared.SharedSnapshotClient(SharedReaderConfig(...)) 构造不执行 I/O,进入 async context 时打开只读连接并恢复一次完整状态。宿主使用 await task_group.start(reader.run) 启动轮询;不启动来源、创建 schema 或取得 writer 锁。固定拥有者线程串行执行 SQLite 的打开、短事务读取和关闭,索引构建使用有界的受管理线程,取消与关闭都等待已准入工作真正结束。
只读共享端只需核心依赖与标准库 sqlite3;sqlite extra 中的 FileLock 用于写入拥有者,并非共享读取的额外门槛。核心安装可导入共享模块并读取已有 SQLite 库;创建/写入库的 owner 仍按后端契约检查对应 extra。
默认每秒查询版本(poll_interval 可配置);有变化时在同一读事务内取得头和所有记录,读完立即结束事务,再构建内存索引并发布。版本探测与随后完整读取可以看到不同提交,发布始终使用完整读取自身的头和回执;不能分别查询几张表后拼成跨代状态。未变化的头不重新解码全部载荷或重建时间锚点。读取失败通过 get_status().persistence_error 报告,保留尚未到期的旧视图,到期后必须报告不可用。
每个读取者具有自己的 store_id/epoch/generation,另保留 upstream_id 和 writer 的 config_revision。get_snapshot 不执行 SQL,只在需要时重新评价本地期限;get_status/get_metrics 为无 I/O 的诊断采样。wait_ready 的 required 必须显式为非空 VRP/ASPA 集合;aspa_afi 可显式选择 Afi.IPV4/IPV6,省略时要求两族分别就绪但不要求授权相同。get_status().ready 默认表示 VRP 可用,并独立输出两族可用性、期限和活动组。恢复及本地重新评价保留写入者的逐族活动组,不能通过聚合 ASPA 组猜测另一族的信任选择。watch 提供原子 initial 与独立有界队列,漏过同一 writer epoch 的中间 generation 时给出 coalesced 的完整端点差分,writer 发布身份变化时已有订阅收到粘性的 ResyncRequired,必须重新 watch。
配置变更没有改变组内原始数据时,读取者可复用组索引,但新快照、状态报告和发布事件的 config_revision 仍须与同次接纳的 writer 头一致,并与 upstream_id 一起推进。复用索引不复用旧配置身份,也不修改先前快照的版本或原始期限。
本地期限或活动组变化也发布自身派生快照;有效期定时器独立于数据库轮询,即使 writer 退出或某次 SQL 读取阻塞,旧快照查询仍拒绝过期数据。 数据库头候选与本地时效校准各自最多一个,实际受管理构建共同计入 SharedReaderConfig.limits.max_concurrent_builds;默认两个槽时本地校准可在头构建期间继续,显式一个槽则等待已准入工作真实结束。关闭及取消不提前释放线程占用的槽。新头提交须对期间已发布的本地版本重新计算完整端点事件,不能覆盖本地时间锚点或重复报告已发送的到期变化。恢复保留原始 expires_at,并以 written_at、observed_at、evaluated_at 作为不能回退的时钟下界;该下界在更换表示、删除及重新加入来源后继续按本地单调时钟推进。各来源与各族的下一变更点分别对照自身时间锚点,不能把某来源的有效时间套用到另一来源的期限。同一 JSON 表示或 RTR 原始同步状态在新元数据提交中保留本地时间锚点与失效状态,重新读取不能延寿。离线状态不能自动提升为在线可信状态,其任意参考时间也不能提高在线时钟下界。共享客户端不修改数据库,不提供 flush 或配置写入口。
aclose 终止订阅、取消轮询并等待读取/构建结束,再在原线程关闭连接和加入工作线程。关闭超过固定 5 秒报告 cleanup_overdue,状态保持 closing;关闭失败保留资源供再次 aclose,不假报 closed。主体异常优先于次要清理异常。可执行例子 使用实际 SQLite 提交、读取、验证和订阅。
busy、writer 退出、WAL 增长和 checkpoint 不改变一致性;reader 不持有长读事务来等待用户消费。网络文件系统不在共享文件模式的支持范围。
写入者使用 WAL、synchronous=FULL、wal_autocheckpoint=1000 页,journal_size_limit=64 MiB 只控制可回收 journal 的保留量。自动 checkpoint 为 PASSIVE,不等待卡住的 reader,也不强制 TRUNCATE;活跃长读可持续阻止回收,所以这些值不是硬磁盘上限。部署应监控数据库/WAL 文件和可用磁盘;空间不足或 busy 使本次事务失败,内存仍按原期限服务,持久化头只停留在最后完整成功状态。测试用真实 max_page_count 触发 SQLITE_FULL,并保留旧完整提交;持续更新和长读测试核对 WAL 增长及读者释放后的 checkpoint 进展。
共享连接使用 mode=ro、query_only,不以 immutable=1 打开正在变化的文件。SQLite 3.22+ 在已有可读 wal/shm 文件、允许创建这些文件,或真正不可变文件的条件下可只读打开 WAL 库;并非一律要求 shm 写权限。本项目共享实时读取不使用不可变条件,部署须保证主文件及必要 wal/shm 文件可访问,且整个生命周期中 SQLite 能满足其 WAL-index 条件;不因权限错误改用 owner 或修复文件。依据 SQLite WAL/只读与 checkpoint 说明及 Python 3.11 sqlite3 事务和 URI 接口。
DuckDB 拥有者
DuckDB 数据库由单一拥有者维护,所有读写串行进入该拥有者的连接。多个应用通过 HTTP 服务访问,不开放其他进程直接读取正在更新的文件。独立只读进程打开同一活动库属于启动配置错误,不能以“通常能读”作为支持。
适配器显式使用 duckdb.connect 创建独占连接,不使用模块全局连接;线程数为 1,关闭 external_access,不从数据库操作自动下载扩展或访问外部文件。API 依据 DuckDB 1.5 Python DB API和并发说明,读取时页面版本标识为 1.5 current;实际驱动验收版本另列 Issue/CI 证据。Context7 未提供固定 1.5 文档包,不能把其未标版本的 main 摘录当作版本证明。
两后端对原始状态、失败后的内存可用性、持久化回执和恢复时效给出相同结果。差异只通过 capabilities 和启动限制表达。
必需验证
共同测试覆盖完整与空状态、重复提交、expected 冲突、回滚、崩溃各阶段、未知 schema、损坏摘要、身份变化、时钟回拨、过期恢复和工作线程取消。SQLite 另测多进程一致读取与第二 owner 拒绝;DuckDB 另测单一拥有者和服务访问。具体案例编号见验收。
恢复与提交歧义的具体规则:组内 source_ids 在身份摘要中按 ID 排序,配置输入顺序不影响同一逻辑组;保留空组。持久化恢复作为当前 Client 的新发布取得新 generation,构造阶段已对外可见的未加载身份不复用于恢复后不同内容。正常的未加载占位来源仍是 not_loaded,不把它当作恢复校验错误。
提交可能成功但回执丢失、且立即读回又失败时,写入器保留有界的“尝试版本/配置身份/摘要”记录;接受后继完整状态前先重新核实该尝试。匹配尝试即推进已确认头,匹配原头即证明未提交,其他完整头继续报告冲突而不覆盖未知写入。此记录不额外保留第三份完整快照。后继状态可以合并 pending,但不能忘记未确认提交的 expected 边界。