跳转至

架构

框架是普通 Python 类之上的一层薄封装。它把声明(写在类上的标记)与解释(读取 标记并驱动的引擎)分开。

分层

errors            异常,任何一层都可使用
   ▲
meta              声明层:phase / dep / introspect
   ▲
flow              运行层:scope / graph / invoke / enter / leave
   ▲
canary            门面:Canary 基类与 dep()

依赖方向严格单向:canary → flow → meta → errors。只有 canary 同时依赖两边, 那正是门面的职责。

这条约束不只是文档里的说法——tests/test_layering.py 用 ast 解析每个模块的 import 并断言方向,任何一次反向依赖都会让测试失败。

模块

模块 内容
core/errors.py 五个异常,全部继承 CanaryError
core/meta/phase.py Phase 与 init / start / stop
core/meta/dep.py Dep 描述符
core/meta/introspect.py 一次 MRO 遍历读出依赖与钩子,按类缓存
core/flow/scope.py Scope、每个单元在每个阶段上的状态、无参构造
core/flow/graph.py 构建依赖图、检测环
core/flow/invoke.py 调用一个钩子
core/flow/enter.py enter():在图上进入一个阶段,依赖在前
core/flow/leave.py leave():在图上离开一个阶段,本单元在前
core/canary.py Canary 基类与 dep()

标记,而非改造

装饰器只在方法上写一个标记,从不改造类。单元因此保持普通类的一切性质:可继承、可混入、 可嵌套。

标记 由谁写入 由谁读取
__canary_phases__ 阶段装饰器 introspect(哪些方法属于哪个阶段)
_canary_scope Scope.adopt Dep.__get__(从哪里取回依赖)

依赖不用标记表达,它就是一个类属性(Dep 描述符),因此持有的是类对象本身而不是名字。

钩子解析

一次 MRO 遍历同时收集依赖与带标记的属性名,结果按类缓存——声明在类创建之后不再变化。

钩子按属性名解析,因此语义与普通方法完全一致:

  • 子类覆盖同名钩子即替换,需要叠加时使用 super()。
  • 子类覆盖但不再标记时,该钩子不再运行。
  • 不同名的混入钩子各自成立,基类在前。

依赖图

一个单元第一次被进入时,连同它的全部依赖一起加入作用域的依赖图。节点是单元登记的类型, 边来自 dep() 声明,并按 Scope.provide() 的替换解析。图是静态的——dep() 是类上的 声明,生命周期开始之后不再接受替换——因此只构建一次,此后不变。

构建是一次迭代的深度优先遍历。环在这时被发现,早于任何钩子运行,并携带走到环上的 路径。一个节点在它的依赖都加入之后才加入,因此图的插入顺序本身就是一个拓扑序。

单元的状态

每个单元在每个阶段上有一个状态:

状态 含义
空闲 未进入,或已离开
进入中 已被一次 enter() 认领:在等依赖,或正在运行钩子
已进入 钩子全部成功
失败 进入时失败或被取消,尚未离开
离开中 正在运行离开钩子——此时它仍在使用自己的依赖

只要有依赖者处于进入中、已进入、失败或离开中,一个单元就正被使用。这一个定义同时决定 一个单元能否离开、以及一个依赖能否开始。等待仍然用 future——依赖者等待依赖的进入结果,对 同一个单元的第二次 leave() 等待第一次——但一个单元处于什么状态总是直接读它的状态, 不靠拼凑推断。

两条规则

进入依赖在前。enter() 认领本次调用涉及的空闲单元,每个一个任务:等它的依赖,再运行 它的钩子。互不依赖的单元因此同时进入,也没有任何递归。另一次调用正在进入的单元只被等待, 不会再运行一遍。

离开本单元在前。leave() 跳过正被使用的单元,否则运行它的离开钩子;无论哪种,随后都 以同样的方式尝试它的每个依赖,同时进行,各在一个任务里。被跳过或空闲的单元一次调用只穿过 一次;真正离开的单元每次都级联到依赖,因为某个依赖可能刚刚失去最后一个使用者。一次调用的 工作量因此与图的规模成正比。

两条规则在同一张图上互为镜像,正是这种对称让 stop() 与 init()、start() 一样成为 单元的动作。一个阶段用 leave 声明离开它时运行的阶段;start 的是 stop。

并发与失败

一个失败不会取消其他单元:每个单元各自完成。依赖未能全部进入的单元不运行钩子,以同一个 异常失败。阶段声明了 leave 时,失败或被取消的单元立即离开——不检查是否被使用,因为等待 它的依赖者从未用到它——再尝试它的依赖。因此一次失败的 enter() 返回时,它启动的一切都已 释放,仍被其他运行中单元使用的除外。没有声明 leave 的阶段,失败的单元在本次调用返回时 回到空闲,因此可以重试。

失败随后还原为真实的失败——经多条路径到达的同一个失败只报告一次。等待另一个单元不会取消 它。调用方取消 enter() 时,同样的回滚在取消传播之前完成;取消无法携带的离开钩子异常交给 事件循环的异常处理器。

不变量

  1. 单元一律由框架无参构造。
  2. 一个作用域内,每个类型只有一个实例;两个各自构造的根是两张互不相干的图。
  3. 装饰器只声明、不改造,单元保持普通类。
  4. 依赖在 @init 之后才可用。
  5. 全部 @init 完成之后,才有任何 @start 运行。
  6. stop() 是唯一的回收路径,正常结束与失败结束共用,重复调用幂等。回收同时撤销 start 的进入记录,因此图可以再次启动。
  7. 有依赖者正在启动、运行或停止时,一个单元不会被停止。
  8. 失败的 start() 在释放它启动的一切之后才返回。
  9. 环在任何钩子运行之前被报告。
  10. 框架不认识任何外壳,接入宿主只有上下文管理器与三个显式方法两条路。