第 8 周 · Tether 源码课

期中链路实验:追踪一行 Plugin 的一生

预计 110–130 分钟 先修:第 3–7 周:完整 Cordis 单元、已成功运行 Cordis 与 Loader 定向测试

学完你能做到

  • 从 YAML stable id 追到真实 plugin class
  • 用源码与测试区分 mounted、pending、active 和 disposed
  • 列出一次 apply 发布的 effects 及其 owner
  • 以证据表讲清 session row 的完整生命周期

课程进度

  1. 第 1 周
  2. 第 2 周
  3. 第 3 周
  4. 第 4 周
  5. 第 5 周
  6. 第 6 周
  7. 第 7 周
  8. 第 8 周
  9. 第 9 周
  10. 第 10 周
  11. 第 11 周
  12. 第 12 周
  13. 第 13 周
  14. 第 14 周
  15. 第 15 周
  16. 第 16 周

一句话先懂

读插件系统的可靠方法不是“看懂所有文件”,而是选一个 stable id,沿每次身份转换一路留下证据。

本周期中实验把一行 YAML 追成 factory、plugin instance、handle、activation scope、service registrations,再按相反方向追到 cleanup。

你已经分别学过 Plugin、Context、EffectScope、Service、Events 与 Composition。现在要证明它们不是六章互不相干的名词,而是同一条运行链上的六个观察点。

一张端到端追踪图

进入系统 YAML rowid + name PluginSpec已验证 row catalog factoryplugin instanceconfig 已绑定 PluginHandlemounted Inject?Registry 缺失 → pending fresh forkactivation scope ApplyAsyncregister effects publication commitactive唤醒 dependents 离开系统 · 方向相反 Provider 消失或 owner dispose dependents先 cleanup activation scopechildren + LIFO全部 settled pending可再激活 disposed终局卸载 每个箭头都要有一种证据 YAML 行 · factory 注册 · 类型声明 · method body · test assertion · runtime output
期中目标不是背诵这张图,而是能为每一支箭头指出证据来源。

一个类比:快递全程追踪

快递单号不会告诉你包裹此刻在哪,但它让分拣、装车、签收这些身份转换可追踪。stable id 也一样:session 从 YAML slot 进入 Loader,变成 PluginSpec,再由 catalog 创建实例;每一步对象不同,但证据链不断。

类比边界:Plugin 不是只向前移动的包裹。依赖变化可让同一 handle 多次 active/pending,replacement 还能事务性回滚;类比只帮助保持追踪主语,不表达状态机细节。

期中实验规则

在 tools 与 session 两个 base row 中任选一个。不要一上来搜索它涉及的所有类型;严格按下表逐格推进,每格至少记录一个文件路径、类型名或测试名。

追踪格要回答的问题合格证据
1. YAML rowstable 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. activationContext 在哪里 fork?handle 何时 active?Context.Plugin、PluginHandle、PluginTests
6. effectsapply 注册了哪些 service/listener/disposer?owner 是谁?method body 与 Context API
7. publicationdependent 何时能看见这些 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

展开完整证据链
  1. YAML row:src/Tether.Bundles/bundles/base.yaml 声明 { id: session, name: session };CLI profile depth-first include 它,没有为这行提供 config。
  2. catalog factory:src/Tether.Bundles/BundleCatalogs.cs 以 catalog.Register("session", () => new InMemorySessionPlugin()) 登记名字。
  3. plugin instance:Loader 先把 enabled row 解析为 PluginSpec,MountedComposition 调 catalog factory 得到 InMemorySessionPlugin,再交给 Context.Plugin。
  4. Inject:InMemorySessionPlugin : Plugin 没有覆写 Inject,所以继承空集合;Registry 无需等待 service,handle 可立即启动 activation。
  5. activation:PluginHandle 为这代 apply 创建 fresh fork。该 fork 与 root 共享 Registry/Events,但拥有独立 EffectScope。
  6. effects:InMemorySessionPlugin.Apply 依次通过 activation Context 发布 SessionEventCatalog、ISession(实现为 InMemorySession)和 ISessionFactory。三项 registration 都属于这次 activation scope。
  7. publication:plugin apply 内的 service 先 reservation;三项都成功且 apply 完成后,publication batch 才 commit。依赖这些 Type 的 pending handles 随后激活;registration completion 等它们 settled。
  8. 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 入门。

在 GitHub 上编辑此页