架构¶
框架是普通 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() 时,同样的回滚在取消传播之前完成;取消无法携带的离开钩子异常交给
事件循环的异常处理器。
不变量¶
- 单元一律由框架无参构造。
- 一个作用域内,每个类型只有一个实例;两个各自构造的根是两张互不相干的图。
- 装饰器只声明、不改造,单元保持普通类。
- 依赖在
@init之后才可用。 - 全部
@init完成之后,才有任何@start运行。 stop()是唯一的回收路径,正常结束与失败结束共用,重复调用幂等。回收同时撤销start的进入记录,因此图可以再次启动。- 有依赖者正在启动、运行或停止时,一个单元不会被停止。
- 失败的
start()在释放它启动的一切之后才返回。 - 环在任何钩子运行之前被报告。
- 框架不认识任何外壳,接入宿主只有上下文管理器与三个显式方法两条路。