Skip to content

生命周期与一致性契约

本页规定首版库嵌入应用时的任务、连接、快照、来源和订阅行为。接口见公共 API,以下状态和流程是首版生命周期契约,实际入口和签名见源码生成参考。

任务和资源归属

Client 的生命周期为 created → open → running → closing → closed。构造只有参数保存和同步校验;进入异步上下文执行依赖检查、数据库打开与恢复,退出上下文关闭资源。run 只能启动一次,重复启动、跨事件循环使用或关闭后使用均明确失败。

上下文准备尚未完成时也禁止重复进入。并发关闭会取消并等待准备过程、配置候选和已准入的查询触发重算;这些工作在线程中尚未退出时仍为 closing。某个资源关闭失败不阻止其余资源清理;保留失败资源以供再次 aclose,不能把它记为已释放。

宿主应用创建 AnyIO 任务组,通过 task_group.start 启动 run。Client 的来源、定时器、持久化和事件任务都必须是这棵任务树的子任务;不调用 anyio.run 或 asyncio.run,不创建脱离任务组的任务。

资源 拥有者和关闭规则
内置工厂返回的连接 会话拥有;重连前关闭旧连接
自定义工厂交付的连接 成功返回即移交会话;工厂失败前自行清理
已有注入流 owns_stream 明确决定是否关闭;无新连接工厂时不重连
注入持久化后端 进入 Client 上下文时接管使用生命周期;退出调用 close;禁止同一实例交给多个 Client
HTTP 客户端 内置创建者关闭;如支持注入须显式声明 owns_client
订阅 消费者退出 watch 上下文即注销并释放队列
不可变快照 持有者管理引用;持有不延长有效期,库不永久保留旧版本

调用 aclose 幂等:停止发起新连接与查询、通知订阅终止、取消来源子任务、丢弃暂存更新、尝试有限期持久化、关闭连接与后端。只对必要清理屏蔽取消,不屏蔽整个同步流程。cleanup_timeout 是超时报告阈值:超过后报告 cleanup_overdue、仍占用的资源及经过时间,Client 保持 closing,继续受管理地等待必要清理;不能假装 closed 或强制杀死宿主。自定义阻塞调用可能使实际关闭超过该阈值,不承诺硬性墙钟上限。

内置 RTR 与默认 HTTP 传输在 DNS、多地址连接竞赛及 TLS 握手期间保留每个 socket 的明确拥有者;失败、未胜出的连接和交接时的取消均在传播前释放未交付资源。DNS、连接与握手继续服从原有截止及取消,不对整个网络等待屏蔽取消。TCP 接收按调用读取非阻塞 socket,不建立无界后台接收队列;关闭同时唤醒读写等待者。实现使用 AnyIO 的公开 readiness/notify_closing 接口,不能改变宿主或其他依赖的全局网络行为。自定义传输仍须遵守自身的交付前清理责任。

默认 HTTP 传输还在发送任务内持有并关闭 request.stream.__aiter__() 返回的实际请求体迭代器,包括字节已产出、网络写入尚未完成时的取消或写入超时,以及服务器提前响应后 HTTPcore 接纳响应的路径。这里的归属只覆盖该外层迭代器,包装在其内部的调用方迭代器仍由调用方关闭。关闭 socket 不能替代该步骤,也不依赖 Trio/asyncio 的垃圾回收补做清理;次要迭代器清理错误不覆盖原始请求失败或取消,日志和传播的原始异常不附加其任意异常文本。已经取得响应而请求体清理失败时,传输先关闭响应、释放连接池预留,再传播该清理异常。

RemoteClient 对响应超限、解码失败和取消同样保留解码迭代器的归属。解码视图沿用 HTTPX 的压缩与 header 语义,原响应独占网络关闭;先关闭响应,再展开已缓冲的原始字节控制帧,不解压被丢弃的尾部。已知内置传输成功关闭连接后只剩有限的接收缓冲,完整展开这些控制帧属于必须完成的清理,不因清理时限或人工步数而遗弃。注入传输可能共享连接,关闭响应不保证终止底层读取;其恢复迭代最多一秒、65,536 步,每步都有取消检查点,随后关闭 SDK 持有的解码、原始字节及直接流迭代器。该额外等待也适用于守约但共享连接的传输。内置响应关闭失败时同样使用有限恢复预算。清理完成前请求仍占用其生命周期位置。注入传输仍负责自身嵌套资源,不能用 SDK 的有限恢复替代其 aclose 责任;自定义关闭过程的不可中断等待继续遵守前述清理约定。清理中的普通网络或自定义异常不覆盖原始响应失败;取消、KeyboardInterrupt 等控制异常在完成必要清理后继续传播。

内置 HTTP 响应的网络关闭由加入当前生命周期的受管理子任务执行,调用方等待该任务实际结束;这样原生 asyncio.Task.cancel() 在关闭期间到达时,也不会只中断等待者而留下池连接。必要关闭对 AnyIO 取消屏蔽,原生调用方取消在子任务结束后继续传播。解码和原始字节迭代器仍由原消费任务收尾。注入响应保持原任务归属,不把调用方流中的任务上下文转移给子任务。

SDK 默认传输的整个请求还在已加入调用方任务组的受管理工作任务中执行;请求登记、正文迭代与收尾均属于该工作任务。调用方原生取消转换为任务组取消并等待工作任务,因此后续原生取消不会直接打断 HTTPcore 在响应头或正文读取异常中自行执行的必要清理。正常 EOF 的解码视图关闭不直接关闭原响应,原响应统一在拥有者的 finally 中释放。注入传输继续在原调用任务中执行。有限恢复预算的到期由 AnyIO 自己判断;同时发生的原生取消不能被当作该局部期限而吞掉。

在线 HTTP JSON 来源也在关闭连接后完整展开内置传输有限的原始字节控制帧;展开过程不会继续解析或解压丢弃的内容。该清理没有一秒的放弃期限,来源仍占用构建和暂存字节名额直到清理完成。读取超限或取消同时遇到关闭错误时,保留原始失败或取消,次要错误仅记录固定文本,不发布部分候选,也不将关闭取消改记为同步失败。

持久化关闭阶段失败记录在状态和日志中,不把未持久化说成成功。要求落盘成功的调用方应先调用 flush 并检查回执;CLI 的持久化 sync 必须这样做。已有主体异常或取消时不由次要关闭错误覆盖原始原因。数据库工作线程中的事务必须结束后才能释放连接,不能取消等待者后把同一连接交给其他线程继续用。

已经确定的来源撤销属于必要清理,包括协议检查线程在取消或关闭开始后才报告的致命错误。Client 保护该撤销直到完整视图原子提交;它仍使用有限构建和维护排队额度,普通来源候选与普通维护在关闭时被拒绝。来源任务全部结束后才进行最终落盘,不能让关闭遗失已经确定的撤销。关闭后不再产生订阅事件或启动重同步;较早的完整视图在撤销提交之前仍可能被其他已准入工作使用。等待撤销可超过 cleanup_timeout,此时继续报告 closing 和清理逾期,不能遗弃线程。该等待计入原有最终 flush_timeout;期限耗尽或存储故障时旧的已确认落盘版本仍可能保留,必须报告未持久化,不能声称撤销已耐久保存。

撤销已生效的来源,在重连或再次失败时保留不可用状态,普通连接报告不触发重复撤销或抢占其他来源。只有接纳新的有效载荷才能解除该失效状态;适用的协议降级仍须移除对应载荷。订阅者通过 StatusEvent 观察这些连接变化,SnapshotEvent 继续对应实际快照提交。

未开始的可选最终落盘可在 flush_timeout 截止后放弃并记录;已经进入事务或不可中断 I/O 的工作必须确认结束/回滚,再关闭连接和释放 owner 锁。取消 flush 的等待者不等于取消事务。后端能够合作取消时使用其机制;不能使用 abandon-on-cancel 留下无主工作线程。此约束依据 AnyIO 4.11.0 线程取消说明,版本不替代本项目后续安装验收。

受管理的阻塞工作器在通知命令完成后、等待下一条命令前,释放自己对该命令的引用,避免空闲线程继续保留已经处理完的参数、结果或异常。执行中的命令仍保留到实际结束,返回对象由调用方拥有,工作器不会擅自关闭它们。释放引用不保证进程 RSS 立即下降;垃圾回收和分配器的内存归还另受运行时影响。

故障传播

来源网络不可达、合法 No Data Available、可重试 HTTP 失败等由该来源任务报告状态后重试,其他来源继续工作。格式错误保持旧有效状态,但该次更新失败;致命 RTR 错误按协议撤销该来源数据。

非法配置、缺少 extra、数据库模式不支持在启动网络前失败。内部断言失败、无法维持发布一致性等不可恢复异常终止 run 并传播到宿主任务组。AnyIO 的异常组允许保留,不转成一个无依据的“网络错误”。

wait_ready 的超时只结束本次等待,不停止后台同步。取消等待、取消订阅、取消整个 Client 分别作用于自己的生命周期;消费者异常不得在协议读取路径执行。

并发和阻塞工作

Client 绑定一次运行的事件循环及任务树;其异步方法可被同一循环中的多个任务调用。它不是跨线程、跨循环的共享客户端。应用在线程中工作时传递已取得的不可变 Snapshot,或使用宿主提供的线程桥接入口。

MemoryStore 的写入和到期发布由单一拥有者串行执行。冻结快照上的纯查询可并行读取,但不宣称自由线程 CPython 已经受支持。禁止在查询中修改可见缓存或共享可变索引。

大文件读取、JSON 解析、索引构建和数据库调用不得长时间占用事件循环。实现使用受管理线程或有界分块;线程不直接发布状态,构建完成后由拥有者重新核对提交条件。旧版本已被替换、来源已失效或任务被取消时,过时构建结果不得覆盖新状态。 原始输入未变的时间或连接健康更新可复用已构建组,但必须以最新已发布快照重算选择、事件和最终时效;这不允许被替换的来源 incarnation 或真正过时的原始输入提交。时效维护与必要撤销的有限准入、单槽等待边界见性能契约。

运行中的来源增删和配置修改遵循配置变更契约。配置版本、来源 incarnation 和候选所基于的数据版本都参与接纳检查,删除后重用相同来源 ID 不能接纳旧任务结果。

原子发布

一次完整发布包括原始来源状态、有效数据索引、来源支持、时效、能力、活动组、配置版本和策略身份。流程为:

  1. 在独立暂存状态中接收、检查并构建候选更新。
  2. 发布拥有者检查基础版本、来源身份和当前时间,必要时重新计算或丢弃过时结果。
  3. 在一个短临界区内交换当前快照指针并分配 generation;不在该临界区执行网络、SQL 或用户代码。
  4. 使用同一提交记录更新有界事件日志,唤醒订阅者和持久化工作器。

观察者在步骤 3 和 4 之间不能注册出一个缺事件的订阅:发布和订阅注册共享串行入口。事件只有发布成功后可见。数据相同但成功刷新了原始期限、session 或来源状态时可以发布新快照;单纯连接状态变化只增加状态事件序号。

有效 EOD 可以更新 RTR 的有效期,因为它是一次新的成功同步。读取同一旧 JSON、HTTP 304、数据库恢复、导出重载和创建新 generation 都不能更新原始期限。

时间与到期

在线同时记录 UTC 墙钟和单调时钟。协议定时、超时和进程内剩余寿命用单调时钟;外部绝对期限、导出及恢复记录用 UTC。两者共同检查,墙钟回拨不能延长已建立的寿命;明显跳变使时间状态不可信时撤销相应在线可用性并重新同步。

Client 管理的 RTR 新 EOD 使用共享时钟不倒退的有效 UTC 下界标记 generated_at、last_sync_at 和 expires_at;新同步取得完整 expire 间隔,旧数据及旧锚不重置。独立 RtrSession 使用原始 UTC 与 AnyIO 单调计时。JSON 的外部 generated_at 仍对当前原始墙钟加配置的 clock_tolerance 检查,不能借内部下界扩大外部时间信任。

时钟实现使用内部可替换的 UTC/monotonic 采样对象供测试注入,不新增公共 Clock API。接纳时固定墙钟与单调锚点,重建同一来源保留原锚点;检查绝对时刻与单调剩余寿命,两者都必须有效。偏离锚点推算的 UTC 超过 clock_tolerance 时,该在线来源进入时间不可信状态,等待重新同步;前跳、后跳、挂起均不得延寿。离线仅使用 reference_time。clock_tolerance 容忍测量偏差,不增加任何期限。

已准入候选在构建期间跨入失信 epoch 后,后续版本冲突重试保留其失效标记及原始锚点、期限,复用已校准的完整组;其他组提交不能使同一候选反复全量重建。候选即使完成提交也保持不可用,只有按当前时钟规则接纳的新原始输入才能恢复信任。

时钟失信后的 epoch 变化不重置进程内已知的有效时间下界;删除来源后重用 ID 也不能抹去已经消耗的寿命。相同原始生成时间和格式的输入重读保留原锚点;没有生成时间的输入按声明的载荷、原始记录及支持期限识别同一数据,单纯元数据或有效期策略变化不能恢复旧锚点。宿主在显式绝对期限下提供不同的完整数据,可作为重新同步重新接纳,仍使用不倒退的有效时间下界。

到期工作器为最早来源或逐条支持期限以及未来开始生效时间安排检查。删除某来源的到期支持后重新合并,其他来源仍支持的记录继续存在。若未到来源整体期限但所有记录都自然到期,可发布 ready_empty;来源本身整体期限到达则 expired。两种情况不能混淆。

旧 Snapshot 达到其某类载荷 usable_until 后,该类查询抛 SnapshotExpiredError;调用方重新向 store 或 Client 取得派生快照。即使后台工作器调度延迟,也不能让旧快照继续作有效判断。共享读取者执行相同检查,不依赖数据库写入者存活。

多源分组

一个来源仅属于一个组;组优先级数字越小越优先,禁止相同优先级造成隐式选择。组内对每类载荷合并有效来源,组之间不直接合并。活动组按载荷类型分别决定,快照同时记录这些选择,允许 VRP 和 ASPA 由不同组提供。

初始连接优先组全部来源;连接/查询失败或超过首次就绪等待窗口而未满足所需能力时启动下一组准备。已有活动组中,所有能提供某类载荷的来源都无法刷新时启动该类备用准备;旧组数据是否保留受原始期限和协议撤销规则约束。

startup_timeout 从每组第一次启动准备任务的单调时刻起算;每组具有独立窗口。窗口到期只启动后继组,不取消原组、授予尚未完整提交的能力或刷新数据期限。VRP、ASPA 的备用准备分别判断,即使默认 required 只有 VRP,也不能让已经就绪的 VRP 阻止 ASPA 启动备用。明确不支持某类的组无需为该类等完整窗口。配置 required 控制默认就绪和健康条件,不改变已有数据的可信性;显式 wait_ready 的 required 仅约束该次等待。修改 startup_timeout 只用于随后开始的准备窗口,不重置已经运行的截止时间。

备用准备由 Client 任务组内唯一的轻量调度任务检查状态变化与组启动截止,不等待快照构建或软维护取得重型工作额度。主组全部失败或首次窗口到期时,即使维护正在等待不可中断线程,仍启动后继组的受管理来源任务;这些来源的实际读取与构建继续遵守相同准入预算。调度任务只更新拥有事件循环中的启动状态,不并行发布快照;关闭 Client 时与来源和维护任务一同取消、等待退出。

备用组至少一个相应能力的来源完整提交后,才原子替换该类活动组;空但完整有效的数据也可以就绪。高优先级组在后台按各来源协议重试,重新就绪并经过 failback_delay 后回切。禁止把更低组“有更多记录”当作选择依据。

恢复观察起点属于拥有者的并发选择状态,即使健康维护暂时没有改变已发布视图,也必须使使用旧起点的在途构建重新核对。无关来源更新或配置提交不能覆盖较早的连续健康观察,从而重置 failback_delay;核对时可复用原始输入未变的组,不要求再次全量构建,也不延长数据期限。

一个来源恢复连接但未 EOD,不算恢复可用;v2 降到 v1 立即撤销该来源 ASPA 能力并重新选择 ASPA 组。故障、切换、回切和来源撤销都保留其他来源原始状态。

订阅和重新同步

SnapshotEvent 包含 event_id、before_id、after_id、before_config_revision、after_config_revision、reason、变化的 VRP/ASPA customer、来源变化、affected_prefixes、affected_customers、requires_full_revalidation。StatusEvent 包含 event_id、observed_snapshot_id、来源状态变化。event_id 是发布身份、epoch 和单调事件序号,状态事件不借用 generation 排序。

watch 的进入在一个无 await 的注册点取得 initial 快照、initial_status 当前状态报告,并注册该点之后的事件。initial.sources 保留快照发布时的元数据;initial_status 补齐在此之后发生的连接与持久化观测,不能通过改写同一 SnapshotId 的 payload 来刷新诊断。异步迭代不重复产生初始快照。每个订阅者独立消费,不能用分担消费的流克隆代替广播。

达到队列上限、所需快照或事件窗口被淘汰、发布 epoch 改变时,订阅进入终止性的 resync_required。该状态通过独立标志和唤醒路径交付,不能依赖已满队列再塞一条消息。后续 next 抛 ResyncRequiredError;消费者关闭旧订阅,重新进入 watch,使用新 initial 对相关路由重验。

变化条目超过事件大小限制时,不截断后假装完整 diff:使用 requires_full_revalidation 并明确 affected kind。正常事件的 VRP 影响范围包含变动前缀覆盖的所有路由,不按原 ASN 或 max_length 缩小。ASPA 范围包含路径或验证上下文涉及的 customer。

SQLite 轮询可合并跨代变化,标记 coalesced、起止版本和保守影响范围;无法可靠计算时要求重新同步。停止 Client 使订阅结束,关闭时丢弃尚未消费的普通事件;关闭不承诺排空队列。终止前已有遗漏仍须先告知 ResyncRequired,不能伪装成完整正常结束。