diff --git a/GFramework.Cqrs/README.md b/GFramework.Cqrs/README.md index 0e4c0891..2d205335 100644 --- a/GFramework.Cqrs/README.md +++ b/GFramework.Cqrs/README.md @@ -125,9 +125,14 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu - 未找到处理器会抛出异常。 - 通知分发 - 通知会分发给所有已注册 `INotificationHandler<>`;零处理器时默认静默完成。 - - 默认通知发布器会按容器解析顺序逐个执行处理器,并在首个处理器抛出异常时立即停止后续分发。 - - 若需要等待所有处理器并行完成,可以在创建 runtime 时显式传入 `TaskWhenAllNotificationPublisher`;该策略不保证执行顺序,并会在全部处理器结束后聚合异常或取消结果。 - 若容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置 `SequentialNotificationPublisher`。 + - 内置 notification publisher 的推荐选择如下: + + | 策略 | 推荐场景 | 执行顺序 | 失败语义 | 备注 | + | --- | --- | --- | --- | --- | + | `SequentialNotificationPublisher` | 需要保持容器顺序,且希望首个失败立即停止后续分发 | 保证按容器解析顺序逐个执行 | 首个处理器抛出异常时立即停止 | 也是默认回退策略 | + | `TaskWhenAllNotificationPublisher` | 需要让全部处理器并行完成,并在结束后统一观察失败或取消 | 不保证顺序 | 不会在首个失败时停止其余处理器;会聚合最终异常或取消结果 | 更适合语义补齐,不是性能开关 | + | `UseNotificationPublisher(...)` 自定义实例 | 需要接入仓库外的自定义策略或第三方策略 | 取决于具体实现 | 取决于具体实现 | 仅在内置顺序 / 并行策略都不满足时使用 | - 若只是为了降低 fixed fan-out publish 的 steady-state 成本,当前 benchmark 并不表明 `TaskWhenAllNotificationPublisher` 会优于默认顺序发布器;它更适合你需要“等待全部处理器完成并统一观察失败”的场景。 如果你需要显式保留默认顺序语义,也可以在组合根里直接声明: diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md index 424b8444..7de71bf2 100644 --- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md +++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md @@ -7,10 +7,13 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-116` +- 恢复点编号:`CQRS-REWRITE-RP-117` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #341` - 当前结论: + - 当前 `RP-117` 继续沿用 `$gframework-batch-boot 50`,但没有继续把 batch 推回 request dispatch 热路径:本轮先试了一刀“按运行时类型缓存 `IContextAware` 判定”的 dispatcher 微优化,随后按 `RequestBenchmarks` / `RequestLifetimeBenchmarks` 复跑确认 steady-state request 反而回落到约 `71.824 ns`,因此这组运行时代码已在同轮完全撤回,不保留负收益热点实验 + - 这一批改为只收口 notification publisher 的采用文档:`GFramework.Cqrs/README.md` 与 `docs/zh-CN/core/cqrs.md` 现在把 `Sequential` / `TaskWhenAll` / 自定义 publisher 三条策略放进同一张选择矩阵,明确 `TaskWhenAll` 的价值是“并行完成 + 聚合失败”,而不是 fixed fan-out publish 的性能升级开关 + - 当前分支相对 `origin/main`(`7ca21af9`, `2026-05-08 16:12:20 +0800`)的累计 branch diff 仍约为 `12 files`,远低于 `$gframework-batch-boot 50` 的停止阈值;因此这批继续保持 notification 采用边界内的低风险、可评审文档切片 - 当前 `RP-116` 已继续沿用 `$gframework-batch-boot 50`,并把刚收口的 notification publisher 配置面补成对称的内置策略集合:`SequentialNotificationPublisher` 现在作为公开类型提供,组合根新增 `UseSequentialNotificationPublisher()`,不再只存在“一个显式并行策略 + 一个隐式默认回退” - 这一批让用户能够在文档、配置和测试里显式表达“我要顺序失败即停”与“我要并行等待全部完成”这两条内置语义,而不需要把默认顺序策略理解成 runtime 内部细节;这进一步降低了 notification publisher seam 的心智负担 - 当前分支相对 `origin/main` 的累计 branch diff 提交 `RP-115` 后约为 `11 files`,仍明显低于 `$gframework-batch-boot 50` 的停止阈值;因此这批继续保持 notification 配置面内的低风险、可评审切片 @@ -375,8 +378,8 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 -1. 既然 `RP-116` 已把顺序 / 并行两条内置策略都收口成显式组合根入口,下一轮若继续留在 notification 线,优先评估是否需要补更细的采用文档或第三种策略,而不是再让用户直接依赖裸 `INotificationPublisher` 注册细节 -2. 当前 benchmark 已证明 `TaskWhenAllNotificationPublisher` 的价值主要在并行完成与异常聚合语义,而不是吞吐收益;若 notification 配置面已经足够,下一轮优先回到 request dispatch 常量开销,而不是新增 generated notification invoker/provider 这类 steady-state 收益信号偏弱的 runtime seam +1. 既然 `RP-117` 已把 notification publisher 的采用路径收口成显式策略矩阵,下一轮若继续留在 notification 线,优先评估是否需要补第三种仓库内置策略或更贴近示例代码的采用文档,而不是再重复翻写同一套边界说明 +2. 当前 benchmark 仍证明 `TaskWhenAllNotificationPublisher` 的价值主要在并行完成与异常聚合语义,而不是吞吐收益;若 notification 文档已经足够,下一轮再回到 request dispatch 常量开销时,应先避开“类型级 `IContextAware` 判定缓存”这条已验证无收益的热点假设 3. 若 benchmark 对照需要继续贴近 `Mediator` 官方设计,再评估 `Mediator` 的 compile-time lifetime / stream 对照矩阵,或给 stream 引入 scoped host 基线,而不是回头重试已被 benchmark 否决的 `GetAll(Type)` 零行为探测方案 ## 活跃文档 diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md index d0c4f41e..f01a947a 100644 --- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md +++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md @@ -2,6 +2,35 @@ ## 2026-05-08 +### 阶段:notification publisher 采用矩阵文档收口(CQRS-REWRITE-RP-117) + +- 延续 `$gframework-batch-boot 50`,本轮没有继续把自动批处理推到新的 runtime seam,而是先按 tracking 建议复核“notification 线是否还缺采用边界文档”: + - 当前分支相对 `origin/main`(`7ca21af9`, `2026-05-08 16:12:20 +0800`)的累计 branch diff 约为 `12 files`,仍明显低于 `50` 文件阈值 + - 主线程先短试了一刀 request dispatch 热路径微优化:把 dispatcher 中“运行时类型是否实现 `IContextAware`”改成弱键缓存,并按性能治理规则复跑 `RequestBenchmarks` 与 `RequestLifetimeBenchmarks` + - 复跑结果表明这条假设没有正收益:默认 steady-state request 回到约 `71.824 ns / 32 B`,`Singleton / Transient` lifetime 约为 `73.191 ns / 32 B` 与 `80.468 ns / 56 B`,因此本轮在同一提交前已完全撤回该运行时代码实验,不把负收益热点带进后续恢复点 +- 本轮主线程决策: + - 保持 `GFramework.Cqrs` runtime 与测试代码不变,只更新 `GFramework.Cqrs/README.md` 与 `docs/zh-CN/core/cqrs.md` + - 把 `SequentialNotificationPublisher`、`TaskWhenAllNotificationPublisher` 与 `UseNotificationPublisher(...)` 自定义实例三条路径收口到同一张策略矩阵 + - 在用户文档里明确 `TaskWhenAllNotificationPublisher` 是“并行完成 + 聚合失败”语义策略,而不是 fixed fan-out publish 的性能开关 +- 本轮权威验证: + - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsDispatcherCacheTests|FullyQualifiedName~CqrsDispatcherContextValidationTests"` + - 结果:通过,`17/17` passed + - 备注:首轮与 build 并行触发时出现 `MSB3026` 单次复制重试告警,但同一命令最终稳定通过,未形成代码失败 + - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:用于否决本轮已撤回的热点假设;默认 steady-state request 对照约为 baseline `5.853 ns / 32 B`、`Mediator` `6.256 ns / 32 B`、`MediatR` `53.401 ns / 232 B`、`GFramework.Cqrs` `71.824 ns / 32 B` + - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:用于否决本轮已撤回的热点假设;`Singleton` 下 baseline / `MediatR` / `GFramework.Cqrs` 约 `5.259 ns / 58.415 ns / 73.191 ns`,`Transient` 下约 `4.914 ns / 57.150 ns / 80.468 ns` +- 本轮结论: + - notification publisher 公开入口现在不仅有显式顺序 / 并行 API,也有更直接的策略选择矩阵;读者不再需要从分散段落里拼装“什么时候该选哪条策略” + - request dispatch 热路径的下一轮探索应显式绕开“类型级 `IContextAware` 判定缓存”这一条已验证无收益的方向,把 context budget 留给更可能影响 steady-state 的热点 + - 当前仍可继续自动推进,但若再开一批 runtime 性能实验,应放在新的自然批次里,避免把已否决假设和新热点混在同一评审单元中 + ### 阶段:公开顺序 notification publisher 策略(CQRS-REWRITE-RP-116) - 延续 `$gframework-batch-boot 50`,本轮继续留在 notification publisher 配置面,但不再新增第三方 benchmark 或 runtime seam: diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 866fc706..7f9612fa 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -119,6 +119,14 @@ var playerId = await architecture.Context.SendRequestAsync( - 首个处理器抛出异常时立即停止后续分发 - 如果容器在 runtime 创建前已显式注册 `INotificationPublisher`,默认 runtime 会复用该策略;未注册时回退到内置 `SequentialNotificationPublisher` +如果你需要在组合根里明确表达“为什么选这条策略”,可以按下面的矩阵判断: + +| 策略 | 适用场景 | 顺序语义 | 失败语义 | 备注 | +| --- | --- | --- | --- | --- | +| `UseSequentialNotificationPublisher()` | 需要保持容器顺序,且希望首个失败立即停止 | 保证按容器顺序执行 | 首个处理器异常会中断后续处理器 | 这也是默认回退策略 | +| `UseTaskWhenAllNotificationPublisher()` | 需要让全部处理器并行完成,再统一观察异常或取消 | 不保证顺序 | 不会在首个失败时中断其余处理器;全部结束后统一暴露结果 | 更适合语义补齐,不是性能优化开关 | +| `UseNotificationPublisher(...)` | 需要接入自定义或第三方 publisher 策略 | 取决于实现 | 取决于实现 | 仅在内置顺序 / 并行策略都不满足时使用 | + 如果你想在组合根里显式保留默认顺序语义,也可以直接写成: ```csharp