第 8 周 · Tether 源码课
期中链路实验:追踪一行 Plugin 的一生
学完你能做到
- 从 YAML stable id 追到真实 plugin class
- 用源码与测试区分 mounted、pending、active 和 disposed
- 列出一次 apply 发布的 effects 及其 owner
- 以证据表讲清 session row 的完整生命周期
课程进度
- 第 1 周
- 第 2 周
- 第 3 周
- 第 4 周
- 第 5 周
- 第 6 周
- 第 7 周
- 第 8 周
- 第 9 周
- 第 10 周
- 第 11 周
- 第 12 周
- 第 13 周
- 第 14 周
- 第 15 周
- 第 16 周
一句话先懂
读插件系统的可靠方法不是“看懂所有文件”,而是选一个 stable id,沿每次身份转换一路留下证据。
本周期中实验把一行 YAML 追成 factory、plugin instance、handle、activation scope、service registrations,再按相反方向追到 cleanup。
你已经分别学过 Plugin、Context、EffectScope、Service、Events 与 Composition。现在要证明它们不是六章互不相干的名词,而是同一条运行链上的六个观察点。
一张端到端追踪图
一个类比:快递全程追踪
快递单号不会告诉你包裹此刻在哪,但它让分拣、装车、签收这些身份转换可追踪。stable id 也一样:session 从 YAML slot 进入 Loader,变成 PluginSpec,再由 catalog 创建实例;每一步对象不同,但证据链不断。
类比边界:Plugin 不是只向前移动的包裹。依赖变化可让同一 handle 多次 active/pending,replacement 还能事务性回滚;类比只帮助保持追踪主语,不表达状态机细节。
期中实验规则
在 tools 与 session 两个 base row 中任选一个。不要一上来搜索它涉及的所有类型;严格按下表逐格推进,每格至少记录一个文件路径、类型名或测试名。
| 追踪格 | 要回答的问题 | 合格证据 |
|---|---|---|
| 1. YAML row | stable id、catalog name、config 是什么?位于哪层 include? | YAML 路径与原行 |
| 2. catalog factory | 谁注册这个 name?factory 产生什么? | catalog.Register(...) 所在文件 |
| 3. plugin instance | 具体 class 的职责是什么? | class 与 Apply / ApplyAsync |
| 4. Inject | 依赖 Type 有哪些?齐全前 active 还是 pending? | Inject 声明或 base class 默认值 |
| 5. activation | Context 在哪里 fork?handle 何时 active? | Context.Plugin、PluginHandle、PluginTests |
| 6. effects | apply 注册了哪些 service/listener/disposer?owner 是谁? | method body 与 Context API |
| 7. publication | dependent 何时能看见这些 service? | Registry publication 与 Completion 测试 |
| 8. disposal | 谁先 cleanup?哪些 registration 以什么顺序撤销? | EffectScope 与 Registry tests |
实验 A:独立完成你的追踪表
实验目标:为一个真实发行 row 建立从声明到回收的可复核证据链;答案必须指向源码,不使用“框架应该会……”作为证据。
1. 预测
任选一行后,先只看 src/Tether.Bundles/bundles/base.yaml:
- 它看起来会立即 active,还是会等待某个 seam?
- 它可能提供 service,还是消费 service?
- 若删掉这行,最先受影响的 downstream row 可能是谁?
把预测单独保存,不要边看答案边改写。预测错了是有价值的,它会暴露你把 row order、Inject 和 service lookup 混在了哪里。
2. 搜索
以下命令只定位证据,不替你解释:
rg -n 'id: (tools|session)|Register\("(tools|session)"' \
src/Tether.Bundles
rg -n 'class (ToolRegistryPlugin|InMemorySessionPlugin)|Provide<' \
src/Tether.Core.Tools src/Tether.Core.Session
rg -n 'Plugin_(without|waits|deactivates)|Provide_get_has|Dispose_runs_children' \
tests/Cordis.Tests
3. 验证底层规则
dotnet test tests/Cordis.Tests/Cordis.Tests.csproj \
--filter "FullyQualifiedName~PluginTests|FullyQualifiedName~RegistryTests|FullyQualifiedName~EffectScopeTests"
dotnet test tests/Cordis.Loader.Tests/Cordis.Loader.Tests.csproj \
--filter FullyQualifiedName~CompositionLoaderTests
4. 解释
把八格表改写成一段不超过 250 字的口头说明。每句话只允许一个主语,例如“Loader 解析 row”“catalog 创建 plugin”“Registry 发布 service”。若一句话同时出现五个系统名,通常表示你还没找准身份转换。
参考答案:完整追踪 session
展开完整证据链
- YAML row:
src/Tether.Bundles/bundles/base.yaml声明{ id: session, name: session };CLI profile depth-first include 它,没有为这行提供 config。 - catalog factory:
src/Tether.Bundles/BundleCatalogs.cs以catalog.Register("session", () => new InMemorySessionPlugin())登记名字。 - plugin instance:Loader 先把 enabled row 解析为
PluginSpec,MountedComposition 调 catalog factory 得到InMemorySessionPlugin,再交给Context.Plugin。 - Inject:
InMemorySessionPlugin : Plugin没有覆写Inject,所以继承空集合;Registry 无需等待 service,handle 可立即启动 activation。 - activation:
PluginHandle为这代 apply 创建 fresh fork。该 fork 与 root 共享 Registry/Events,但拥有独立 EffectScope。 - effects:
InMemorySessionPlugin.Apply依次通过 activation Context 发布SessionEventCatalog、ISession(实现为InMemorySession)和ISessionFactory。三项 registration 都属于这次 activation scope。 - publication:plugin apply 内的 service 先 reservation;三项都成功且 apply 完成后,publication batch 才 commit。依赖这些 Type 的 pending handles 随后激活;registration completion 等它们 settled。
- disposal:MountedComposition dispose 或 row replacement 会 dispose handle;activation scope 先使这些 services 不再可见并等待 dependent cleanup,再按 LIFO 撤销本 scope effects,因此 registration 的本地逆序是
ISessionFactory → ISession → SessionEventCatalog。终局 handle disposal 不会回 pending;若只是 Inject dependency 暂时离开,mounted handle 才可能回 pending。
注意:session row 的文档位置不证明它先于或后于 Consumer active。它无 Inject,所以本身可立即 apply;依赖它的其他 plugin 是否 active 由各自 Inject 集合决定。
自评量规
| 水平 | 表现 |
|---|---|
| 还需重走 | 只列文件名;把 mounted 当 active;无法指出 cleanup owner |
| 达标 | 八格都有真实证据;能区分 row order 与 Inject;能说出至少两项 effect |
| 扎实 | 能解释 publication completion、dependent cleanup 与终局 dispose / pending 的差别 |
| 可迁移 | 换成 tools row 后,仍能独立完成同样追踪并指出它捕获的 Events 与 optional services |
检查理解
1. catalog factory 已创建 plugin instance,能否说 plugin 已 active?
查看答案
不能。factory 只产生 IPlugin;Context.Plugin 创建 mounted handle,Registry 再根据 Inject 决定是否创建 activation fork 并 apply。pending handle 已 mounted 但不 active。
2. InMemorySessionPlugin.Apply 的第二次 Provide 失败,第一次会永久留在 Registry 吗?
查看答案
不会。plugin apply 内的 publications 先被 reservation 并绑定 activation scope;apply 失败会 abort batch、回收 scope,不能向其他 plugin 暴露一个半完成的 Provider。
3. row 从 composition 删除后,为什么不能只从 MountedComposition 的 list 里删掉 handle?
查看答案
handle 可能拥有 active scope、service、listener 和子 plugin。必须 await handle disposal,让 dependent cleanup 与 LIFO effects 全部 settled,再更新 composition owner;只删 list 会丢掉真正的 cleanup owner。
本单元带走
- 用 stable id 保持追踪主语,用 factory、handle、activation scope 等身份转换拆开链路。
- mounted、pending、active、disposed 是不同状态;row order 也不是 dependency order。
- 每次 apply 注册的一切都归 activation scope,publication 与 disposal 都会等待依赖图收敛。
你已经读懂 Tether 最底层的插件骨架。下一周开始沿第 1 周的 runtime 输出向上走,先研究 Session 为什么坚持记录 append-only events。需要复习时可回到Cordis 入门。