mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-10 02:59:02 +08:00
Merge pull request #345 from GeWuYou/feat/cqrs-optimization
Feat/cqrs optimization
This commit is contained in:
commit
699d0b4896
@ -1,6 +1,6 @@
|
||||
# GFramework Skills
|
||||
|
||||
公开入口目前包含 `gframework-doc-refresh` 与 `gframework-batch-boot`。
|
||||
公开入口目前包含 `gframework-doc-refresh`、`gframework-batch-boot` 与 `gframework-multi-agent-batch`。
|
||||
|
||||
## 公开入口
|
||||
|
||||
@ -66,6 +66,30 @@
|
||||
/gframework-batch-boot keep refactoring repetitive source-generator tests in bounded batches
|
||||
```
|
||||
|
||||
### `gframework-multi-agent-batch`
|
||||
|
||||
当用户希望主 Agent 负责拆分任务、派发互不冲突的 subagent 切片、核对进度、维护 `ai-plan`、验收结果并持续推进时,使用该入口。
|
||||
|
||||
适用场景:
|
||||
|
||||
- 复杂任务已经明确可以拆成多个互不冲突的写面
|
||||
- 主 Agent 需要持续 review / integrate,而不是把执行权完全交给单个 worker
|
||||
- 需要把 delegated scope、验证结果与下一恢复点同步写回 `ai-plan`
|
||||
- 任务仍要受 branch diff、context budget 与 reviewability 边界约束
|
||||
|
||||
推荐调用:
|
||||
|
||||
```bash
|
||||
/gframework-multi-agent-batch <task>
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
/gframework-multi-agent-batch continue the current cqrs optimization by delegating non-conflicting benchmark and runtime slices
|
||||
/gframework-multi-agent-batch coordinate parallel subagents, keep ai-plan updated, and stop when reviewability starts to degrade
|
||||
```
|
||||
|
||||
## 共享资源
|
||||
|
||||
- `_shared/DOCUMENTATION_STANDARDS.md`
|
||||
|
||||
@ -11,6 +11,9 @@ Use this skill when `gframework-boot` is necessary but not sufficient because th
|
||||
batches until a clear stop condition is met.
|
||||
|
||||
Treat `AGENTS.md` as the source of truth. This skill extends `gframework-boot`; it does not replace it.
|
||||
If the task's defining requirement is that the main agent must keep acting as dispatcher, reviewer, `ai-plan` owner,
|
||||
and final integrator for multiple parallel workers, prefer `gframework-multi-agent-batch` and use this skill's stop
|
||||
condition guidance as a secondary reference.
|
||||
|
||||
Context budget is a first-class stop signal. Do not keep batching merely because a file-count threshold still has
|
||||
headroom if the active conversation, loaded repo artifacts, validation output, and pending recovery updates suggest the
|
||||
@ -27,6 +30,7 @@ agent is approaching its safe working-context limit.
|
||||
- the work is repetitive, sliceable, or likely to require multiple similar iterations
|
||||
- each batch can be given an explicit ownership boundary
|
||||
- a stop condition can be measured locally
|
||||
- the task does not primarily need the orchestration-heavy main-agent workflow captured by `gframework-multi-agent-batch`
|
||||
3. Before any delegation, define the batch objective in one sentence:
|
||||
- warning family reduction
|
||||
- repeated test refactor pattern
|
||||
|
||||
@ -9,6 +9,8 @@ description: Repository-specific boot workflow for the GFramework repo. Use when
|
||||
|
||||
Use this skill to bootstrap work in the GFramework repository with minimal user prompting.
|
||||
Treat `AGENTS.md` as the source of truth. Use this skill to enforce a startup sequence, not to replace repository rules.
|
||||
If the task clearly requires the main agent to keep coordinating multiple parallel subagents while maintaining
|
||||
`ai-plan` and reviewing each result, switch to `gframework-multi-agent-batch` after the boot context is established.
|
||||
|
||||
## Startup Workflow
|
||||
|
||||
@ -45,6 +47,8 @@ Treat `AGENTS.md` as the source of truth. Use this skill to enforce a startup se
|
||||
- Use `explorer` with `gpt-5.1-codex-mini` for narrow read-only questions, tracing, inventory, and comparisons
|
||||
- Use `worker` with `gpt-5.4` only for bounded implementation tasks with explicit ownership
|
||||
- Do not delegate purely for ceremony; delegate only when it materially shortens the task or controls context growth
|
||||
- If the user explicitly wants the main agent to keep orchestrating multiple workers through several review/integration
|
||||
cycles, prefer `gframework-multi-agent-batch` over ad-hoc delegation
|
||||
13. Before editing files, tell the user what you read, how you classified the task, whether subagents will be used,
|
||||
and the first implementation step.
|
||||
14. Proceed with execution, validation, and documentation updates required by `AGENTS.md`.
|
||||
|
||||
114
.agents/skills/gframework-multi-agent-batch/SKILL.md
Normal file
114
.agents/skills/gframework-multi-agent-batch/SKILL.md
Normal file
@ -0,0 +1,114 @@
|
||||
---
|
||||
name: gframework-multi-agent-batch
|
||||
description: Repository-specific multi-agent orchestration workflow for the GFramework repo. Use when the main agent should keep coordinating multiple parallel subagents, maintain ai-plan recovery artifacts, review subagent results, and continue bounded multi-agent waves until reviewability, context budget, or branch-diff limits say to stop.
|
||||
---
|
||||
|
||||
# GFramework Multi-Agent Batch
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill when `gframework-boot` has already established repository context, and the task now benefits from the
|
||||
main agent acting as the persistent coordinator for multiple parallel subagents.
|
||||
|
||||
Treat `AGENTS.md` as the source of truth. This skill expands the repository's multi-agent coordination rules; it does
|
||||
not replace them.
|
||||
|
||||
This skill is for orchestration-heavy work, not for every task that merely happens to use one subagent. Prefer it when
|
||||
the main agent must keep splitting bounded write slices, monitoring progress, updating `ai-plan`, validating accepted
|
||||
results, and deciding whether another delegation wave is still safe.
|
||||
|
||||
## Use When
|
||||
|
||||
Adopt this workflow only when all of the following are true:
|
||||
|
||||
1. The task is complex enough that multiple parallel slices materially shorten the work.
|
||||
2. The candidate write sets can be kept disjoint.
|
||||
3. The main agent still needs to own review, validation, integration, and `ai-plan` updates.
|
||||
4. Another wave is still likely to fit the branch-diff, context-budget, and reviewability budget.
|
||||
|
||||
Prefer `gframework-batch-boot` instead when the task is mainly repetitive bulk progress with a single obvious slice
|
||||
pattern and little need for continuous multi-worker orchestration.
|
||||
|
||||
## Startup Workflow
|
||||
|
||||
1. Execute the normal `gframework-boot` startup sequence first:
|
||||
- read `AGENTS.md`
|
||||
- read `.ai/environment/tools.ai.yaml`
|
||||
- read `ai-plan/public/README.md`
|
||||
- read the mapped active topic `todos/` and `traces/`
|
||||
2. Confirm that the active topic and current branch still match the work you are about to delegate.
|
||||
3. Define the current wave in one sentence:
|
||||
- benchmark-host alignment
|
||||
- runtime hotspot reduction
|
||||
- documentation synchronization
|
||||
- other bounded multi-slice work
|
||||
4. Identify the critical path and keep it local.
|
||||
5. Split only the non-blocking work into disjoint ownership slices.
|
||||
6. Estimate whether one more delegation wave is still safe:
|
||||
- include current branch diff vs baseline
|
||||
- loaded `ai-plan` context
|
||||
- expected validation output
|
||||
- expected integration overhead
|
||||
|
||||
## Worker Design Rules
|
||||
|
||||
For each `worker` subagent, specify:
|
||||
|
||||
- the concrete objective
|
||||
- the exact owned files or subsystem
|
||||
- files or areas the worker must not touch
|
||||
- required validation commands
|
||||
- expected output format
|
||||
- a reminder that other agents may be editing the repo
|
||||
|
||||
Prefer `explorer` subagents when the result is read-only ranking, tracing, or candidate discovery.
|
||||
|
||||
Do not launch two workers whose write sets overlap unless the overlap is trivial and the main agent has already decided
|
||||
how to serialize or reconcile that overlap.
|
||||
|
||||
## Main-Agent Loop
|
||||
|
||||
While workers run, the main agent should only do non-overlapping work:
|
||||
|
||||
- inspect the next candidate slices
|
||||
- recompute branch-diff and context-budget posture
|
||||
- review finished worker output
|
||||
- queue follow-up validation
|
||||
- keep `ai-plan/public/**` current when accepted scope or next steps change
|
||||
|
||||
After each completed worker task:
|
||||
|
||||
1. Review the reported ownership, validation, and changed files.
|
||||
2. Confirm the worker stayed inside its boundary.
|
||||
3. Run or rerun the required validation locally if the slice is accepted.
|
||||
4. Record accepted delegated scope, validation milestones, and the next recovery point in the active `ai-plan` files.
|
||||
5. Reassess whether another wave is still reviewable and safe.
|
||||
|
||||
## Stop Conditions
|
||||
|
||||
Stop the current multi-agent wave when any of the following becomes true:
|
||||
|
||||
- the next wave would likely push the main agent near or beyond a safe context budget
|
||||
- the remaining work no longer splits into clean disjoint ownership slices
|
||||
- branch diff vs baseline is approaching the current reviewability budget
|
||||
- integrating another worker would degrade clarity more than it would save time
|
||||
- validation failures show that the next step belongs on the critical path and should stay local
|
||||
|
||||
If a branch-size threshold is also in play, treat it as a coarse repository-scope signal, not the sole decision rule.
|
||||
|
||||
## Task Tracking
|
||||
|
||||
When this workflow is active, the main agent must keep the active `ai-plan` topic current with:
|
||||
|
||||
- delegated scope that has been accepted
|
||||
- validation results
|
||||
- current branch-diff posture if it affects stop decisions
|
||||
- the next recommended resume step
|
||||
|
||||
The main agent should keep active entries concise enough that `boot` can still recover the current wave quickly.
|
||||
|
||||
## Example Triggers
|
||||
|
||||
- `Use $gframework-multi-agent-batch to coordinate non-conflicting subagents for this complex CQRS task.`
|
||||
- `Keep delegating bounded parallel slices, update ai-plan, and verify each worker result before continuing.`
|
||||
- `Run a multi-agent wave where the main agent owns review, validation, and integration.`
|
||||
@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "GFramework Multi-Agent Batch"
|
||||
short_description: "Coordinate bounded parallel subagents with ai-plan tracking"
|
||||
default_prompt: "Use $gframework-multi-agent-batch to coordinate multiple bounded parallel subagents in this GFramework repository while the main agent owns ai-plan updates, validation, review, and integration."
|
||||
52
AGENTS.md
52
AGENTS.md
@ -102,9 +102,13 @@ All AI agents and contributors must follow these rules when writing, reviewing,
|
||||
|
||||
## Repository Boot Skill
|
||||
|
||||
- The repository-maintained Codex boot skill lives at `.codex/skills/gframework-boot/`.
|
||||
- The repository-maintained Codex boot skill lives at `.agents/skills/gframework-boot/`.
|
||||
- The repository-maintained multi-agent coordination skill lives at `.agents/skills/gframework-multi-agent-batch/`.
|
||||
- Prefer invoking `$gframework-boot` when the user uses short startup prompts such as `boot`、`continue`、`next step`、
|
||||
`按 boot 开始`、`先看 AGENTS`、`继续当前任务`.
|
||||
- Prefer invoking `$gframework-multi-agent-batch` when the user explicitly wants the main agent to delegate bounded
|
||||
parallel work, track subagent progress, maintain `ai-plan`, verify subagent output, and keep coordinating until the
|
||||
current multi-agent batch reaches a natural stop boundary.
|
||||
- The boot skill is a startup convenience layer, not a replacement for this document. If the skill and `AGENTS.md`
|
||||
diverge, follow `AGENTS.md` first and update the skill in the same change.
|
||||
- The boot skill MUST read `AGENTS.md`、`.ai/environment/tools.ai.yaml`、`ai-plan/public/README.md` and the relevant
|
||||
@ -131,6 +135,52 @@ All AI agents and contributors must follow these rules when writing, reviewing,
|
||||
- The main agent remains responsible for reviewing and integrating subagent output. Unreviewed subagent conclusions do
|
||||
not count as final results.
|
||||
|
||||
### Multi-Agent Coordination Rules
|
||||
|
||||
The terms below describe the default guardrails for multi-agent batches and how they affect worker-launch decisions.
|
||||
|
||||
- `branch-diff budget`: the maximum acceptable branch diff size in files or lines before another worker wave becomes
|
||||
harder to review as a single PR.
|
||||
- `reviewability budget`: the cumulative complexity limit beyond which accepting more parallel slices would materially
|
||||
reduce review quality, even if the raw file count still looks acceptable.
|
||||
- `context-budget`: the main agent's remaining capacity to track active workers, validation, and integration state
|
||||
without losing critical execution context.
|
||||
- When any of these budgets approaches its safe limit, the main agent SHOULD stop launching more workers and close the
|
||||
current wave first.
|
||||
- `$gframework-multi-agent-batch` contains the fuller workflow and stop-condition guidance for applying these budgets in
|
||||
practice.
|
||||
|
||||
- Prefer the repository's multi-agent coordination mode when the user explicitly wants the main agent to keep
|
||||
orchestrating parallel subagents, or when the work naturally splits into `2+` disjoint write slices that can proceed
|
||||
in parallel without blocking the next local step.
|
||||
- In that mode, the main agent MUST keep ownership of:
|
||||
- critical-path selection
|
||||
- baseline and stop-condition tracking
|
||||
- `ai-plan` updates
|
||||
- validation planning and final validation
|
||||
- review and acceptance of every subagent result
|
||||
- the final integration and completion decision
|
||||
- Before spawning any `worker` subagent, the main agent MUST:
|
||||
- identify the immediate blocking step and keep it local
|
||||
- define disjoint file or subsystem ownership for each worker
|
||||
- state the required validation commands and expected output format
|
||||
- check that the expected write set still fits the current branch-diff and reviewability budget
|
||||
- While workers run, the main agent MUST avoid overlapping edits and focus on non-conflicting work such as:
|
||||
- ranking the next candidate slices
|
||||
- reviewing completed worker output
|
||||
- recomputing branch-diff and context-budget posture
|
||||
- keeping `ai-plan/public/**` recovery artifacts current
|
||||
- Before accepting a worker result, the main agent MUST confirm:
|
||||
- the worker stayed within its owned files or subsystem
|
||||
- the reported validation is sufficient for that slice
|
||||
- any accepted findings or follow-up scope are recorded in the active `ai-plan` todo or trace when the task is
|
||||
complex or multi-step
|
||||
- Do not continue launching workers merely because a file-count threshold still has room. Stop the current wave when
|
||||
ownership boundaries start to overlap, reviewability materially degrades, or the context-budget signal says the main
|
||||
agent should close the batch.
|
||||
- When a complex task uses multiple workers, the main agent SHOULD prefer the public workflow documented by
|
||||
`$gframework-multi-agent-batch` unless a more task-specific skill already provides stricter rules.
|
||||
|
||||
## Commenting Rules (MUST)
|
||||
|
||||
All generated or modified code MUST include clear and meaningful comments where required by the rules below.
|
||||
|
||||
@ -0,0 +1,112 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
|
||||
typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedRequestLifetimeBenchmarkRegistry))]
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 为 request 生命周期矩阵 benchmark 提供 hand-written generated registry,
|
||||
/// 以便在默认 generated-provider 宿主路径上比较不同 handler 生命周期的 dispatch 成本。
|
||||
/// </summary>
|
||||
public sealed class GeneratedRequestLifetimeBenchmarkRegistry :
|
||||
GFramework.Cqrs.ICqrsHandlerRegistry,
|
||||
GFramework.Cqrs.ICqrsRequestInvokerProvider,
|
||||
GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors
|
||||
{
|
||||
private static readonly GFramework.Cqrs.CqrsRequestInvokerDescriptor Descriptor =
|
||||
new(
|
||||
typeof(IRequestHandler<
|
||||
RequestLifetimeBenchmarks.BenchmarkRequest,
|
||||
RequestLifetimeBenchmarks.BenchmarkResponse>),
|
||||
typeof(GeneratedRequestLifetimeBenchmarkRegistry).GetMethod(
|
||||
nameof(InvokeBenchmarkRequestHandler),
|
||||
BindingFlags.Public | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("Missing generated request lifetime benchmark method."));
|
||||
|
||||
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> Descriptors =
|
||||
[
|
||||
new GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(
|
||||
typeof(RequestLifetimeBenchmarks.BenchmarkRequest),
|
||||
typeof(RequestLifetimeBenchmarks.BenchmarkResponse),
|
||||
Descriptor)
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 参与程序集注册入口,但不在这里直接写入 handler 生命周期。
|
||||
/// </summary>
|
||||
/// <param name="services">当前 generated registry 拥有的服务集合。</param>
|
||||
/// <param name="logger">用于记录 generated registry 注册行为的日志器。</param>
|
||||
/// <remarks>
|
||||
/// 生命周期矩阵需要让 benchmark 主体显式控制 `Singleton / Transient` 变量。
|
||||
/// 因此 registry 只负责暴露 generated descriptor,不在这里抢先注册 handler,避免把默认单例注册混入比较结果。
|
||||
/// </remarks>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
logger.Debug("Registered generated request lifetime benchmark descriptors.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前 provider 暴露的全部 generated request invoker 描述符。
|
||||
/// </summary>
|
||||
/// <returns>当前 benchmark 需要的 request invoker 描述符集合。</returns>
|
||||
public IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return Descriptors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为目标请求/响应类型对返回 generated request invoker 描述符。
|
||||
/// </summary>
|
||||
/// <param name="requestType">待匹配的请求类型。</param>
|
||||
/// <param name="responseType">待匹配的响应类型。</param>
|
||||
/// <param name="descriptor">命中时返回的 generated descriptor。</param>
|
||||
/// <returns>命中当前 benchmark 的请求/响应类型对时返回 <see langword="true" />。</returns>
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out GFramework.Cqrs.CqrsRequestInvokerDescriptor? descriptor)
|
||||
{
|
||||
if (requestType == typeof(RequestLifetimeBenchmarks.BenchmarkRequest) &&
|
||||
responseType == typeof(RequestLifetimeBenchmarks.BenchmarkResponse))
|
||||
{
|
||||
descriptor = Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 generated request invoker provider 为生命周期矩阵 benchmark 产出的开放静态调用入口。
|
||||
/// </summary>
|
||||
/// <param name="handler">当前请求对应的 handler 实例。</param>
|
||||
/// <param name="request">待分发的 request。</param>
|
||||
/// <param name="cancellationToken">调用方传入的取消令牌。</param>
|
||||
/// <returns>交给目标 request handler 处理后的响应任务。</returns>
|
||||
public static ValueTask<RequestLifetimeBenchmarks.BenchmarkResponse> InvokeBenchmarkRequestHandler(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var typedHandler = (IRequestHandler<
|
||||
RequestLifetimeBenchmarks.BenchmarkRequest,
|
||||
RequestLifetimeBenchmarks.BenchmarkResponse>)handler;
|
||||
var typedRequest = (RequestLifetimeBenchmarks.BenchmarkRequest)request;
|
||||
return typedHandler.Handle(typedRequest, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -26,8 +26,8 @@ public sealed class GeneratedStreamLifetimeBenchmarkRegistry :
|
||||
private static readonly GFramework.Cqrs.CqrsStreamInvokerDescriptor Descriptor =
|
||||
new(
|
||||
typeof(IStreamRequestHandler<
|
||||
StreamLifetimeBenchmarks.BenchmarkStreamRequest,
|
||||
StreamLifetimeBenchmarks.BenchmarkResponse>),
|
||||
StreamLifetimeBenchmarks.GeneratedBenchmarkStreamRequest,
|
||||
StreamLifetimeBenchmarks.GeneratedBenchmarkResponse>),
|
||||
typeof(GeneratedStreamLifetimeBenchmarkRegistry).GetMethod(
|
||||
nameof(InvokeBenchmarkStreamHandler),
|
||||
BindingFlags.Public | BindingFlags.Static)
|
||||
@ -36,8 +36,8 @@ public sealed class GeneratedStreamLifetimeBenchmarkRegistry :
|
||||
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> Descriptors =
|
||||
[
|
||||
new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(
|
||||
typeof(StreamLifetimeBenchmarks.BenchmarkStreamRequest),
|
||||
typeof(StreamLifetimeBenchmarks.BenchmarkResponse),
|
||||
typeof(StreamLifetimeBenchmarks.GeneratedBenchmarkStreamRequest),
|
||||
typeof(StreamLifetimeBenchmarks.GeneratedBenchmarkResponse),
|
||||
Descriptor)
|
||||
];
|
||||
|
||||
@ -78,8 +78,8 @@ public sealed class GeneratedStreamLifetimeBenchmarkRegistry :
|
||||
Type responseType,
|
||||
out GFramework.Cqrs.CqrsStreamInvokerDescriptor? descriptor)
|
||||
{
|
||||
if (requestType == typeof(StreamLifetimeBenchmarks.BenchmarkStreamRequest) &&
|
||||
responseType == typeof(StreamLifetimeBenchmarks.BenchmarkResponse))
|
||||
if (requestType == typeof(StreamLifetimeBenchmarks.GeneratedBenchmarkStreamRequest) &&
|
||||
responseType == typeof(StreamLifetimeBenchmarks.GeneratedBenchmarkResponse))
|
||||
{
|
||||
descriptor = Descriptor;
|
||||
return true;
|
||||
@ -102,9 +102,9 @@ public sealed class GeneratedStreamLifetimeBenchmarkRegistry :
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var typedHandler = (IStreamRequestHandler<
|
||||
StreamLifetimeBenchmarks.BenchmarkStreamRequest,
|
||||
StreamLifetimeBenchmarks.BenchmarkResponse>)handler;
|
||||
var typedRequest = (StreamLifetimeBenchmarks.BenchmarkStreamRequest)request;
|
||||
StreamLifetimeBenchmarks.GeneratedBenchmarkStreamRequest,
|
||||
StreamLifetimeBenchmarks.GeneratedBenchmarkResponse>)handler;
|
||||
var typedRequest = (StreamLifetimeBenchmarks.GeneratedBenchmarkStreamRequest)request;
|
||||
return typedHandler.Handle(typedRequest, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,14 +85,18 @@ public class RequestLifetimeBenchmarks
|
||||
MinLevel = LogLevel.Fatal
|
||||
};
|
||||
Fixture.Setup($"RequestLifetime/{Lifetime}", handlerCount: 1, pipelineCount: 0);
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
|
||||
_baselineHandler = new BenchmarkRequestHandler();
|
||||
_request = new BenchmarkRequest(Guid.NewGuid());
|
||||
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedRequestLifetimeBenchmarkRegistry>(container);
|
||||
RegisterGFrameworkHandler(container, Lifetime);
|
||||
});
|
||||
// 容器内已提前保留默认 runtime 以支撑 generated registry 接线;
|
||||
// 这里额外创建带生命周期后缀的 runtime,只是为了区分不同 benchmark 矩阵的 dispatcher 日志。
|
||||
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_container,
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestLifetimeBenchmarks) + "." + Lifetime));
|
||||
@ -111,7 +115,14 @@ public class RequestLifetimeBenchmarks
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
|
||||
try
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -146,6 +157,10 @@ public class RequestLifetimeBenchmarks
|
||||
/// </summary>
|
||||
/// <param name="container">当前 benchmark 拥有并负责释放的容器。</param>
|
||||
/// <param name="lifetime">待比较的 handler 生命周期。</param>
|
||||
/// <remarks>
|
||||
/// 先通过 generated registry 提供静态 descriptor,再显式覆盖 handler 生命周期,
|
||||
/// 可以把比较变量收敛到 handler 解析成本,而不是 descriptor 发现路径本身。
|
||||
/// </remarks>
|
||||
private static void RegisterGFrameworkHandler(MicrosoftDiContainer container, HandlerLifetime lifetime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
@ -9,6 +9,7 @@ using BenchmarkDotNet.Jobs;
|
||||
using BenchmarkDotNet.Order;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
@ -21,22 +22,29 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 对比 stream 完整枚举在不同 handler 生命周期下的额外开销。
|
||||
/// 对比 stream 在不同 handler 生命周期与观测方式下的额外开销。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 当前矩阵只覆盖 `Singleton` 与 `Transient`。
|
||||
/// `Scoped` 仍依赖真实的显式作用域边界;在当前“单根容器最小宿主”模型下直接加入 scoped 会把枚举宿主成本与生命周期成本混在一起,
|
||||
/// 因此保持与 request 生命周期矩阵相同的边界,留待后续 scoped host 基线具备后再扩展。
|
||||
/// <see cref="StreamObservation" /> 当前只保留 <see cref="StreamObservation.FirstItem" /> 与
|
||||
/// <see cref="StreamObservation.DrainAll" /> 两种模式,分别用于观察建流到首个元素的固定成本与完整枚举的总成本,
|
||||
/// 以避免把更多观测策略与 <see cref="StreamLifetimeBenchmarks" /> 的生命周期对照目标混在一起。
|
||||
/// </remarks>
|
||||
[Config(typeof(Config))]
|
||||
public class StreamLifetimeBenchmarks
|
||||
{
|
||||
private MicrosoftDiContainer _container = null!;
|
||||
private ICqrsRuntime _runtime = null!;
|
||||
private MicrosoftDiContainer _reflectionContainer = null!;
|
||||
private ICqrsRuntime _reflectionRuntime = null!;
|
||||
private MicrosoftDiContainer _generatedContainer = null!;
|
||||
private ICqrsRuntime _generatedRuntime = null!;
|
||||
private ServiceProvider _serviceProvider = null!;
|
||||
private IMediator _mediatr = null!;
|
||||
private BenchmarkStreamHandler _baselineHandler = null!;
|
||||
private BenchmarkStreamRequest _request = null!;
|
||||
private ReflectionBenchmarkStreamHandler _baselineHandler = null!;
|
||||
private ReflectionBenchmarkStreamRequest _reflectionRequest = null!;
|
||||
private GeneratedBenchmarkStreamRequest _generatedRequest = null!;
|
||||
private MediatRBenchmarkStreamRequest _mediatrRequest = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 控制当前 benchmark 使用的 handler 生命周期。
|
||||
@ -44,6 +52,12 @@ public class StreamLifetimeBenchmarks
|
||||
[Params(HandlerLifetime.Singleton, HandlerLifetime.Transient)]
|
||||
public HandlerLifetime Lifetime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 控制当前 benchmark 观察“只推进首个元素”还是“完整枚举整个 stream”。
|
||||
/// </summary>
|
||||
[Params(StreamObservation.FirstItem, StreamObservation.DrainAll)]
|
||||
public StreamObservation Observation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可公平比较的 benchmark handler 生命周期集合。
|
||||
/// </summary>
|
||||
@ -60,6 +74,22 @@ public class StreamLifetimeBenchmarks
|
||||
Transient
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于拆分 stream dispatch 与后续枚举成本的观测模式。
|
||||
/// </summary>
|
||||
public enum StreamObservation
|
||||
{
|
||||
/// <summary>
|
||||
/// 只推进到首个元素后立即释放枚举器。
|
||||
/// </summary>
|
||||
FirstItem,
|
||||
|
||||
/// <summary>
|
||||
/// 完整枚举整个 stream,保留原有 benchmark 语义。
|
||||
/// </summary>
|
||||
DrainAll
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 stream 生命周期 benchmark 的公共输出格式。
|
||||
/// </summary>
|
||||
@ -76,7 +106,7 @@ public class StreamLifetimeBenchmarks
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建当前生命周期下的 GFramework 与 MediatR stream 对照宿主。
|
||||
/// 构建当前生命周期下的 GFramework reflection、GFramework generated 与 MediatR stream 对照宿主。
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
@ -88,24 +118,34 @@ public class StreamLifetimeBenchmarks
|
||||
Fixture.Setup($"StreamLifetime/{Lifetime}", handlerCount: 1, pipelineCount: 0);
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
|
||||
_baselineHandler = new BenchmarkStreamHandler();
|
||||
_request = new BenchmarkStreamRequest(Guid.NewGuid(), 3);
|
||||
_baselineHandler = new ReflectionBenchmarkStreamHandler();
|
||||
_reflectionRequest = new ReflectionBenchmarkStreamRequest(Guid.NewGuid(), 3);
|
||||
_generatedRequest = new GeneratedBenchmarkStreamRequest(Guid.NewGuid(), 3);
|
||||
_mediatrRequest = new MediatRBenchmarkStreamRequest(Guid.NewGuid(), 3);
|
||||
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
_reflectionContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
RegisterReflectionHandler(container, Lifetime);
|
||||
});
|
||||
_reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_reflectionContainer,
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Reflection." + Lifetime));
|
||||
|
||||
_generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedStreamLifetimeBenchmarkRegistry>(container);
|
||||
RegisterGFrameworkHandler(container, Lifetime);
|
||||
RegisterGeneratedHandler(container, Lifetime);
|
||||
});
|
||||
// 容器内已提前保留默认 runtime 以支撑 generated registry 接线;
|
||||
// 这里额外创建带生命周期后缀的 runtime,只是为了区分不同 benchmark 矩阵的 dispatcher 日志。
|
||||
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_container,
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + "." + Lifetime));
|
||||
_generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_generatedContainer,
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Generated." + Lifetime));
|
||||
|
||||
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
configure: null,
|
||||
typeof(StreamLifetimeBenchmarks),
|
||||
static candidateType => candidateType == typeof(BenchmarkStreamHandler),
|
||||
static candidateType => candidateType == typeof(MediatRBenchmarkStreamHandler),
|
||||
ResolveMediatRLifetime(Lifetime));
|
||||
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
|
||||
}
|
||||
@ -118,7 +158,7 @@ public class StreamLifetimeBenchmarks
|
||||
{
|
||||
try
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
|
||||
BenchmarkCleanupHelper.DisposeAll(_reflectionContainer, _generatedContainer, _serviceProvider);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -127,52 +167,57 @@ public class StreamLifetimeBenchmarks
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接调用 handler 并完整枚举,作为不同生命周期矩阵下的 dispatch 额外开销 baseline。
|
||||
/// 直接调用 handler,并按当前观测模式消费 stream,作为不同生命周期矩阵下的 dispatch 额外开销 baseline。
|
||||
/// </summary>
|
||||
[Benchmark(Baseline = true)]
|
||||
public async ValueTask Stream_Baseline()
|
||||
public ValueTask Stream_Baseline()
|
||||
{
|
||||
await foreach (var response in _baselineHandler.Handle(_request, CancellationToken.None).ConfigureAwait(false))
|
||||
{
|
||||
_ = response;
|
||||
}
|
||||
return ObserveAsync(_baselineHandler.Handle(_reflectionRequest, CancellationToken.None), Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 GFramework.CQRS runtime 创建并完整枚举 stream。
|
||||
/// 通过 GFramework.CQRS reflection stream binding 路径创建 stream,并按当前观测模式消费。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public async ValueTask Stream_GFrameworkCqrs()
|
||||
public ValueTask Stream_GFrameworkReflection()
|
||||
{
|
||||
await foreach (var response in _runtime.CreateStream(BenchmarkContext.Instance, _request, CancellationToken.None)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
_ = response;
|
||||
}
|
||||
return ObserveAsync(
|
||||
_reflectionRuntime.CreateStream(
|
||||
BenchmarkContext.Instance,
|
||||
_reflectionRequest,
|
||||
CancellationToken.None),
|
||||
Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 MediatR 创建并完整枚举 stream,作为外部对照。
|
||||
/// 通过 generated stream invoker provider 预热后的 GFramework.CQRS runtime 创建 stream,并按当前观测模式消费。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public async ValueTask Stream_MediatR()
|
||||
public ValueTask Stream_GFrameworkGenerated()
|
||||
{
|
||||
await foreach (var response in _mediatr.CreateStream(_request, CancellationToken.None).ConfigureAwait(false))
|
||||
{
|
||||
_ = response;
|
||||
}
|
||||
return ObserveAsync(
|
||||
_generatedRuntime.CreateStream(
|
||||
BenchmarkContext.Instance,
|
||||
_generatedRequest,
|
||||
CancellationToken.None),
|
||||
Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按生命周期把 benchmark stream handler 注册到 GFramework 容器。
|
||||
/// 通过 MediatR 创建 stream,并按当前观测模式消费,作为外部对照。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ValueTask Stream_MediatR()
|
||||
{
|
||||
return ObserveAsync(_mediatr.CreateStream(_mediatrRequest, CancellationToken.None), Observation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按生命周期把 reflection benchmark stream handler 注册到 GFramework 容器。
|
||||
/// </summary>
|
||||
/// <param name="container">当前 benchmark 拥有并负责释放的容器。</param>
|
||||
/// <param name="lifetime">待比较的 handler 生命周期。</param>
|
||||
/// <remarks>
|
||||
/// 先通过 generated registry 提供静态 descriptor,再显式覆盖 handler 生命周期,
|
||||
/// 可以把比较变量收敛到 handler 解析成本,而不是 descriptor 发现路径本身。
|
||||
/// </remarks>
|
||||
private static void RegisterGFrameworkHandler(MicrosoftDiContainer container, HandlerLifetime lifetime)
|
||||
private static void RegisterReflectionHandler(MicrosoftDiContainer container, HandlerLifetime lifetime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
@ -180,14 +225,46 @@ public class StreamLifetimeBenchmarks
|
||||
{
|
||||
case HandlerLifetime.Singleton:
|
||||
container.RegisterSingleton<
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>,
|
||||
BenchmarkStreamHandler>();
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<ReflectionBenchmarkStreamRequest, ReflectionBenchmarkResponse>,
|
||||
ReflectionBenchmarkStreamHandler>();
|
||||
return;
|
||||
|
||||
case HandlerLifetime.Transient:
|
||||
container.RegisterTransient<
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>,
|
||||
BenchmarkStreamHandler>();
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<ReflectionBenchmarkStreamRequest, ReflectionBenchmarkResponse>,
|
||||
ReflectionBenchmarkStreamHandler>();
|
||||
return;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按生命周期把 generated benchmark stream handler 注册到 GFramework 容器。
|
||||
/// </summary>
|
||||
/// <param name="container">当前 benchmark 拥有并负责释放的容器。</param>
|
||||
/// <param name="lifetime">待比较的 handler 生命周期。</param>
|
||||
/// <remarks>
|
||||
/// generated registry 只负责暴露静态 descriptor;
|
||||
/// 生命周期矩阵仍由 benchmark 主体显式覆盖 handler 注册,避免把 descriptor 发现与实例解析混在一起。
|
||||
/// </remarks>
|
||||
private static void RegisterGeneratedHandler(MicrosoftDiContainer container, HandlerLifetime lifetime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
switch (lifetime)
|
||||
{
|
||||
case HandlerLifetime.Singleton:
|
||||
container.RegisterSingleton<
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<GeneratedBenchmarkStreamRequest, GeneratedBenchmarkResponse>,
|
||||
GeneratedBenchmarkStreamHandler>();
|
||||
return;
|
||||
|
||||
case HandlerLifetime.Transient:
|
||||
container.RegisterTransient<
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<GeneratedBenchmarkStreamRequest, GeneratedBenchmarkResponse>,
|
||||
GeneratedBenchmarkStreamHandler>();
|
||||
return;
|
||||
|
||||
default:
|
||||
@ -211,69 +288,192 @@ public class StreamLifetimeBenchmarks
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark stream request。
|
||||
/// 按观测模式消费 stream,便于把“建流/首个元素”和“完整枚举”分开观察。
|
||||
/// </summary>
|
||||
/// <param name="Id">请求标识。</param>
|
||||
/// <param name="ItemCount">返回元素数量。</param>
|
||||
public sealed record BenchmarkStreamRequest(Guid Id, int ItemCount) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest<BenchmarkResponse>,
|
||||
MediatR.IStreamRequest<BenchmarkResponse>;
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark stream response。
|
||||
/// </summary>
|
||||
/// <param name="Id">响应标识。</param>
|
||||
public sealed record BenchmarkResponse(Guid Id);
|
||||
|
||||
/// <summary>
|
||||
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 stream handler。
|
||||
/// </summary>
|
||||
public sealed class BenchmarkStreamHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>,
|
||||
MediatR.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待观察的异步响应序列。</param>
|
||||
/// <param name="observation">当前 benchmark 选定的观测模式。</param>
|
||||
/// <returns>异步消费完成后的等待句柄。</returns>
|
||||
private static ValueTask ObserveAsync<TResponse>(
|
||||
IAsyncEnumerable<TResponse> responses,
|
||||
StreamObservation observation)
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 GFramework.CQRS stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 benchmark stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>完整枚举所需的低噪声异步响应序列。</returns>
|
||||
public IAsyncEnumerable<BenchmarkResponse> Handle(
|
||||
BenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return EnumerateAsync(request, cancellationToken);
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(responses);
|
||||
|
||||
/// <summary>
|
||||
/// 处理 MediatR stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 benchmark stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>完整枚举所需的低噪声异步响应序列。</returns>
|
||||
IAsyncEnumerable<BenchmarkResponse> MediatR.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>.Handle(
|
||||
BenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
return observation switch
|
||||
{
|
||||
return EnumerateAsync(request, cancellationToken);
|
||||
}
|
||||
StreamObservation.FirstItem => ConsumeFirstItemAsync(responses, CancellationToken.None),
|
||||
StreamObservation.DrainAll => DrainAsync(responses),
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(observation),
|
||||
observation,
|
||||
"Unsupported stream observation mode.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为生命周期矩阵构造稳定、低噪声的异步响应序列。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 benchmark 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>按固定元素数量返回的异步响应序列。</returns>
|
||||
private static async IAsyncEnumerable<BenchmarkResponse> EnumerateAsync(
|
||||
BenchmarkStreamRequest request,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
/// <summary>
|
||||
/// 只推进到首个元素后立即释放枚举器,用来近似隔离建流与首个 `MoveNextAsync` 的固定成本。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待观察的异步响应序列。</param>
|
||||
/// <param name="cancellationToken">用于向异步枚举器传播取消的令牌。</param>
|
||||
/// <returns>消费首个元素后的等待句柄。</returns>
|
||||
private static async ValueTask ConsumeFirstItemAsync<TResponse>(
|
||||
IAsyncEnumerable<TResponse> responses,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var enumerator = responses.GetAsyncEnumerator(cancellationToken);
|
||||
await using (enumerator.ConfigureAwait(false))
|
||||
{
|
||||
for (var index = 0; index < request.ItemCount; index++)
|
||||
if (await enumerator.MoveNextAsync().ConfigureAwait(false))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
yield return new BenchmarkResponse(request.Id);
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
_ = enumerator.Current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 完整枚举整个 stream,保留原 benchmark 的总成本观测口径。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前 stream 的响应类型。</typeparam>
|
||||
/// <param name="responses">待完整枚举的异步响应序列。</param>
|
||||
/// <returns>完整枚举结束后的等待句柄。</returns>
|
||||
private static async ValueTask DrainAsync<TResponse>(IAsyncEnumerable<TResponse> responses)
|
||||
{
|
||||
await foreach (var response in responses.ConfigureAwait(false))
|
||||
{
|
||||
_ = response;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reflection runtime stream request。
|
||||
/// </summary>
|
||||
/// <param name="Id">请求标识。</param>
|
||||
/// <param name="ItemCount">返回元素数量。</param>
|
||||
public sealed record ReflectionBenchmarkStreamRequest(Guid Id, int ItemCount) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest<ReflectionBenchmarkResponse>;
|
||||
|
||||
/// <summary>
|
||||
/// Reflection runtime stream response。
|
||||
/// </summary>
|
||||
/// <param name="Id">响应标识。</param>
|
||||
public sealed record ReflectionBenchmarkResponse(Guid Id);
|
||||
|
||||
/// <summary>
|
||||
/// Generated runtime stream request。
|
||||
/// </summary>
|
||||
/// <param name="Id">请求标识。</param>
|
||||
/// <param name="ItemCount">返回元素数量。</param>
|
||||
public sealed record GeneratedBenchmarkStreamRequest(Guid Id, int ItemCount) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest<GeneratedBenchmarkResponse>;
|
||||
|
||||
/// <summary>
|
||||
/// Generated runtime stream response。
|
||||
/// </summary>
|
||||
/// <param name="Id">响应标识。</param>
|
||||
public sealed record GeneratedBenchmarkResponse(Guid Id);
|
||||
|
||||
/// <summary>
|
||||
/// MediatR stream request。
|
||||
/// </summary>
|
||||
/// <param name="Id">请求标识。</param>
|
||||
/// <param name="ItemCount">返回元素数量。</param>
|
||||
public sealed record MediatRBenchmarkStreamRequest(Guid Id, int ItemCount) :
|
||||
MediatR.IStreamRequest<MediatRBenchmarkResponse>;
|
||||
|
||||
/// <summary>
|
||||
/// MediatR stream response。
|
||||
/// </summary>
|
||||
/// <param name="Id">响应标识。</param>
|
||||
public sealed record MediatRBenchmarkResponse(Guid Id);
|
||||
|
||||
/// <summary>
|
||||
/// Reflection runtime 的最小 stream request handler。
|
||||
/// </summary>
|
||||
public sealed class ReflectionBenchmarkStreamHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<ReflectionBenchmarkStreamRequest, ReflectionBenchmarkResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 reflection benchmark stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 reflection benchmark stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>完整枚举所需的低噪声异步响应序列。</returns>
|
||||
public IAsyncEnumerable<ReflectionBenchmarkResponse> Handle(
|
||||
ReflectionBenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return EnumerateAsync(
|
||||
request.Id,
|
||||
request.ItemCount,
|
||||
static id => new ReflectionBenchmarkResponse(id),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generated runtime 的最小 stream request handler。
|
||||
/// </summary>
|
||||
public sealed class GeneratedBenchmarkStreamHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<GeneratedBenchmarkStreamRequest, GeneratedBenchmarkResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 generated benchmark stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 generated benchmark stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>完整枚举所需的低噪声异步响应序列。</returns>
|
||||
public IAsyncEnumerable<GeneratedBenchmarkResponse> Handle(
|
||||
GeneratedBenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return EnumerateAsync(
|
||||
request.Id,
|
||||
request.ItemCount,
|
||||
static id => new GeneratedBenchmarkResponse(id),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MediatR 对照组的最小 stream request handler。
|
||||
/// </summary>
|
||||
public sealed class MediatRBenchmarkStreamHandler :
|
||||
MediatR.IStreamRequestHandler<MediatRBenchmarkStreamRequest, MediatRBenchmarkResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 MediatR benchmark stream request。
|
||||
/// </summary>
|
||||
/// <param name="request">当前 MediatR benchmark stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
|
||||
/// <returns>完整枚举所需的低噪声异步响应序列。</returns>
|
||||
public IAsyncEnumerable<MediatRBenchmarkResponse> Handle(
|
||||
MediatRBenchmarkStreamRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return EnumerateAsync(
|
||||
request.Id,
|
||||
request.ItemCount,
|
||||
static id => new MediatRBenchmarkResponse(id),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为生命周期矩阵构造相同形状的低噪声异步枚举,避免不同口径的枚举体差异干扰 dispatch 对照。
|
||||
/// </summary>
|
||||
private static async IAsyncEnumerable<TResponse> EnumerateAsync<TResponse>(
|
||||
Guid id,
|
||||
int itemCount,
|
||||
Func<Guid, TResponse> responseFactory,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
for (var index = 0; index < itemCount; index++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
yield return responseFactory(id);
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,9 +17,9 @@
|
||||
- `Messaging/RequestBenchmarks.cs`
|
||||
- direct handler、NuGet `Mediator` source-generated concrete path、已接上 handwritten generated request invoker provider 的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
|
||||
- `Messaging/RequestLifetimeBenchmarks.cs`
|
||||
- `Singleton / Transient` 两类 handler 生命周期下,direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
|
||||
- `Singleton / Transient` 两类 handler 生命周期下,direct handler、已对齐 generated-provider 宿主接线的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
|
||||
- `Messaging/StreamLifetimeBenchmarks.cs`
|
||||
- `Singleton / Transient` 两类 handler 生命周期下,direct handler、已接上 handwritten generated stream invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream 完整枚举对比
|
||||
- `Singleton / Transient` 两类 handler 生命周期下,direct handler、`GFramework.Cqrs` reflection stream binding、接上 generated stream registry 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream 完整枚举分层对照
|
||||
- `Messaging/RequestPipelineBenchmarks.cs`
|
||||
- `0 / 1 / 4` 个 pipeline 行为下,direct handler、已接上 handwritten generated request invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
|
||||
- `Messaging/RequestStartupBenchmarks.cs`
|
||||
@ -37,22 +37,42 @@
|
||||
|
||||
## 最小使用方式
|
||||
|
||||
```bash
|
||||
dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release
|
||||
```
|
||||
|
||||
上面的命令只验证 benchmark 工程当前可以正常编译。
|
||||
|
||||
如需实际运行 benchmark,再执行:
|
||||
|
||||
```bash
|
||||
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release
|
||||
```
|
||||
|
||||
也可以通过 `BenchmarkDotNet` 过滤器只运行某一类场景。
|
||||
如需只复核某一类场景,可把 `BenchmarkDotNet` 参数放在 `--` 之后,例如:
|
||||
|
||||
```bash
|
||||
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestLifetimeBenchmarks.SendRequest_*"
|
||||
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*StreamLifetimeBenchmarks.Stream_*"
|
||||
```
|
||||
|
||||
## 当前约束
|
||||
|
||||
- `BenchmarkDotNet.Artifacts/` 属于本地生成输出,默认加入仓库忽略,不作为常规提交内容
|
||||
- 只要变更影响 `GFramework.Cqrs` request dispatch、DI 解析热路径、invoker/provider、pipeline 或 benchmark 宿主,就必须至少复跑:
|
||||
- `RequestLifetimeBenchmarks` 现在复用与默认 generated-provider 路径一致的 benchmark 宿主接线;它比较的是生命周期切换后的 handler 解析与 dispatch 成本,不单独引入另一套 runtime 发现口径
|
||||
- `StreamLifetimeBenchmarks` 现在按 direct handler、`GFramework.Cqrs` reflection、`GFramework.Cqrs` generated、`MediatR` 四层口径组织,并额外区分 `FirstItem` 与 `DrainAll` 两种观测方式,用于把 stream 建流/首个元素成本与完整枚举成本拆开观察
|
||||
- 当前短跑结果显示,`StreamLifetimeBenchmarks` 在 `Singleton` 下无论 `FirstItem` 还是 `DrainAll` 都表现为 generated 略优于 reflection;在 `Transient` 下,`FirstItem` 仍是 reflection 略优于 generated,但 `DrainAll` 已转为 generated 优于 reflection。这说明当前差值主要集中在建流到首个元素之间的瞬时成本,而不是完整枚举阶段整体退化
|
||||
- 只要变更影响 `GFramework.Cqrs` request dispatch、DI 解析热路径、invoker/provider、pipeline 或 benchmark 宿主,就应至少复跑能覆盖该路径的过滤场景;request 热路径通常先看:
|
||||
- `RequestBenchmarks.SendRequest_*`
|
||||
- `RequestLifetimeBenchmarks.SendRequest_*`
|
||||
- 只要变更影响 stream dispatch、建流绑定或相关宿主接线,就应补跑:
|
||||
- `StreamingBenchmarks.Stream_*`
|
||||
- `StreamLifetimeBenchmarks.Stream_*`
|
||||
- 当前性能目标不是超过 source-generated `Mediator`,而是让默认 request steady-state 路径尽量接近它,并至少稳定快于基于反射 / 扫描的 `MediatR`
|
||||
|
||||
## 后续扩展方向
|
||||
|
||||
- 若继续优化 stream lifetime,可优先复核 `Transient + FirstItem` 下 generated 与 reflection 的小幅差值是否稳定,再决定继续压 generated 宿主的建流瞬时成本,还是把后续对照切回 `StreamInvokerBenchmarks` / `Mediator` concrete runtime 批次
|
||||
- request / stream 的真实 source-generator 产物与 handwritten generated provider 对照
|
||||
- `Mediator` 的 transient / scoped compile-time lifetime 矩阵对照
|
||||
- 带真实显式作用域边界的 scoped host 对照
|
||||
|
||||
@ -195,6 +195,55 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 stream 的“是否存在 pipeline behavior”判定会按 dispatcher 实例缓存,
|
||||
/// 并与当前容器的实际服务可见性保持一致,同时不同 dispatcher 不共享该实例级状态。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Dispatcher_Should_Cache_Stream_Behavior_Presence_Per_Dispatcher_Instance()
|
||||
{
|
||||
var firstContext = new ArchitectureContext(_container!);
|
||||
var secondContext = new ArchitectureContext(_container!);
|
||||
var firstDispatcher = GetDispatcherFromContext(firstContext);
|
||||
var secondDispatcher = GetDispatcherFromContext(secondContext);
|
||||
using var isolatedContainer = CreateFrozenContainer();
|
||||
var isolatedContext = new ArchitectureContext(isolatedContainer);
|
||||
var isolatedDispatcher = GetDispatcherFromContext(isolatedContext);
|
||||
var zeroPipelineBehaviorType = typeof(IStreamPipelineBehavior<DispatcherZeroPipelineStreamRequest, int>);
|
||||
var twoPipelineBehaviorType = typeof(IStreamPipelineBehavior<DispatcherStreamPipelineOrderRequest, int>);
|
||||
var expectedZeroPipelinePresence = _container!.HasRegistration(zeroPipelineBehaviorType);
|
||||
var expectedTwoPipelinePresence = _container.HasRegistration(twoPipelineBehaviorType);
|
||||
|
||||
AssertStreamBehaviorPresenceIsUnset(firstDispatcher, zeroPipelineBehaviorType);
|
||||
AssertStreamBehaviorPresenceIsUnset(secondDispatcher, zeroPipelineBehaviorType);
|
||||
AssertStreamBehaviorPresenceIsUnset(isolatedDispatcher, zeroPipelineBehaviorType);
|
||||
AssertStreamBehaviorPresenceIsUnset(firstDispatcher, twoPipelineBehaviorType);
|
||||
|
||||
await DrainAsync(firstContext.CreateStream(new DispatcherZeroPipelineStreamRequest()));
|
||||
await DrainAsync(firstContext.CreateStream(new DispatcherStreamPipelineOrderRequest()));
|
||||
|
||||
var zeroPipelinePresence = GetStreamBehaviorPresenceCacheValue(
|
||||
firstDispatcher,
|
||||
zeroPipelineBehaviorType);
|
||||
var twoPipelinePresence = GetStreamBehaviorPresenceCacheValue(
|
||||
firstDispatcher,
|
||||
twoPipelineBehaviorType);
|
||||
|
||||
AssertSharedStreamDispatcherCacheState(
|
||||
firstDispatcher,
|
||||
secondDispatcher,
|
||||
isolatedDispatcher,
|
||||
zeroPipelinePresence,
|
||||
twoPipelinePresence,
|
||||
zeroPipelineBehaviorType,
|
||||
expectedZeroPipelinePresence,
|
||||
expectedTwoPipelinePresence);
|
||||
|
||||
await DrainAsync(isolatedContext.CreateStream(new DispatcherZeroPipelineStreamRequest()));
|
||||
|
||||
AssertStreamBehaviorPresenceEquals(isolatedDispatcher, zeroPipelineBehaviorType, expectedZeroPipelinePresence);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 request pipeline executor 会按行为数量在 binding 内首次创建并在后续分发中复用。
|
||||
/// </summary>
|
||||
@ -713,6 +762,33 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
return found ? arguments[1] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定 dispatcher 实例中当前保存的 stream behavior presence 缓存项。
|
||||
/// </summary>
|
||||
private static object? GetStreamBehaviorPresenceCacheValue(object dispatcher, Type behaviorType)
|
||||
{
|
||||
var field = dispatcher.GetType().GetField(
|
||||
"_streamBehaviorPresenceCache",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
Assert.That(field, Is.Not.Null, "Missing dispatcher stream behavior presence cache field.");
|
||||
|
||||
var cache = field!.GetValue(dispatcher)
|
||||
?? throw new InvalidOperationException(
|
||||
"Dispatcher stream behavior presence cache returned null.");
|
||||
var tryGetValueMethod = cache.GetType().GetMethod(
|
||||
"TryGetValue",
|
||||
BindingFlags.Instance | BindingFlags.Public);
|
||||
|
||||
Assert.That(tryGetValueMethod, Is.Not.Null, "Missing ConcurrentDictionary.TryGetValue accessor.");
|
||||
|
||||
object?[] arguments = [behaviorType, null];
|
||||
var found = (bool)(tryGetValueMethod!.Invoke(cache, arguments)
|
||||
?? throw new InvalidOperationException(
|
||||
"ConcurrentDictionary.TryGetValue returned null."));
|
||||
return found ? arguments[1] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断言指定 dispatcher 上某个 request behavior presence 缓存项尚未建立。
|
||||
/// </summary>
|
||||
@ -729,6 +805,22 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
Assert.That(GetRequestBehaviorPresenceCacheValue(dispatcher, behaviorType), Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断言指定 dispatcher 上某个 stream behavior presence 缓存项尚未建立。
|
||||
/// </summary>
|
||||
private static void AssertStreamBehaviorPresenceIsUnset(object dispatcher, Type behaviorType)
|
||||
{
|
||||
Assert.That(GetStreamBehaviorPresenceCacheValue(dispatcher, behaviorType), Is.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断言指定 dispatcher 上某个 stream behavior presence 缓存项等于预期值。
|
||||
/// </summary>
|
||||
private static void AssertStreamBehaviorPresenceEquals(object dispatcher, Type behaviorType, bool expected)
|
||||
{
|
||||
Assert.That(GetStreamBehaviorPresenceCacheValue(dispatcher, behaviorType), Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断言同一容器解析出的 dispatcher 会共享实例级缓存,而另一独立容器的 dispatcher 不会提前命中。
|
||||
/// </summary>
|
||||
@ -754,6 +846,29 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断言同一容器解析出的 dispatcher 会共享 stream 的实例级缓存,而另一独立容器的 dispatcher 不会提前命中。
|
||||
/// </summary>
|
||||
private static void AssertSharedStreamDispatcherCacheState(
|
||||
object firstDispatcher,
|
||||
object secondDispatcher,
|
||||
object isolatedDispatcher,
|
||||
object? zeroPipelinePresence,
|
||||
object? twoPipelinePresence,
|
||||
Type zeroPipelineBehaviorType,
|
||||
bool expectedZeroPipelinePresence,
|
||||
bool expectedTwoPipelinePresence)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(secondDispatcher, Is.SameAs(firstDispatcher));
|
||||
Assert.That(zeroPipelinePresence, Is.EqualTo(expectedZeroPipelinePresence));
|
||||
Assert.That(twoPipelinePresence, Is.EqualTo(expectedTwoPipelinePresence));
|
||||
AssertStreamBehaviorPresenceEquals(secondDispatcher, zeroPipelineBehaviorType, expectedZeroPipelinePresence);
|
||||
AssertStreamBehaviorPresenceIsUnset(isolatedDispatcher, zeroPipelineBehaviorType);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 request dispatch binding 中指定行为数量的 pipeline executor 缓存项。
|
||||
/// </summary>
|
||||
@ -788,7 +903,7 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
Type responseType,
|
||||
int behaviorCount)
|
||||
{
|
||||
var binding = GetPairCacheValue(streamBindings, requestType, responseType);
|
||||
var binding = GetStreamDispatchBindingValue(streamBindings, requestType, responseType);
|
||||
return binding is null
|
||||
? null
|
||||
: InvokeInstanceMethod(binding, "GetPipelineExecutorForTesting", behaviorCount);
|
||||
@ -842,6 +957,32 @@ internal sealed class CqrsDispatcherCacheTests
|
||||
.Invoke(bindingBox, Array.Empty<object>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定流式请求/响应类型对对应的强类型 stream dispatch binding。
|
||||
/// </summary>
|
||||
/// <param name="streamBindings">dispatcher 内部的 stream binding 缓存对象。</param>
|
||||
/// <param name="requestType">要读取的流式请求运行时类型。</param>
|
||||
/// <param name="responseType">要读取的响应元素类型。</param>
|
||||
/// <returns>强类型 binding;若缓存尚未建立则返回 <see langword="null" />。</returns>
|
||||
private static object? GetStreamDispatchBindingValue(object streamBindings, Type requestType, Type responseType)
|
||||
{
|
||||
var bindingBox = GetPairCacheValue(streamBindings, requestType, responseType);
|
||||
if (bindingBox is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var method = bindingBox.GetType().GetMethod(
|
||||
"Get",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
Assert.That(method, Is.Not.Null, $"Missing stream binding accessor on {bindingBox.GetType().FullName}.");
|
||||
|
||||
return method!
|
||||
.MakeGenericMethod(responseType)
|
||||
.Invoke(bindingBox, Array.Empty<object>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 CQRS dispatcher 运行时类型。
|
||||
/// </summary>
|
||||
|
||||
@ -222,6 +222,49 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 generated stream binding 与对应的 pipeline executor 在首次建流后会被缓存并复用,
|
||||
/// 同时保持 generated invoker 的结果与行为执行语义不变。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task CreateStream_Should_Reuse_Cached_Generated_Stream_Binding_And_Pipeline_Executor()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedStreamInvokerAssembly();
|
||||
var container = new MicrosoftDiContainer();
|
||||
container.RegisterCqrsStreamPipelineBehavior<GeneratedStreamPipelineTrackingBehavior>();
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
container.Freeze();
|
||||
|
||||
var streamBindings = GetDispatcherCacheField("StreamDispatchBindings");
|
||||
var requestType = typeof(GeneratedStreamInvokerRequest);
|
||||
var responseType = typeof(int);
|
||||
|
||||
Assert.That(
|
||||
GetStreamPipelineExecutorValue(streamBindings, requestType, responseType, 1),
|
||||
Is.Null);
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
var firstResults = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
|
||||
var bindingAfterFirstDispatch = GetPairCacheValue(streamBindings, requestType, responseType);
|
||||
var executorAfterFirstDispatch = GetStreamPipelineExecutorValue(streamBindings, requestType, responseType, 1);
|
||||
|
||||
var secondResults = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
|
||||
var bindingAfterSecondDispatch = GetPairCacheValue(streamBindings, requestType, responseType);
|
||||
var executorAfterSecondDispatch = GetStreamPipelineExecutorValue(streamBindings, requestType, responseType, 1);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(firstResults, Is.EqualTo([30, 31]));
|
||||
Assert.That(secondResults, Is.EqualTo([30, 31]));
|
||||
Assert.That(bindingAfterFirstDispatch, Is.Not.Null);
|
||||
Assert.That(bindingAfterSecondDispatch, Is.SameAs(bindingAfterFirstDispatch));
|
||||
Assert.That(executorAfterFirstDispatch, Is.Not.Null);
|
||||
Assert.That(executorAfterSecondDispatch, Is.SameAs(executorAfterFirstDispatch));
|
||||
Assert.That(GeneratedStreamPipelineTrackingBehavior.InvocationCount, Is.EqualTo(2));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当实现类型隐藏、但 stream handler interface 仍可直接表达时,
|
||||
/// dispatcher 仍会消费 generated stream invoker descriptor。
|
||||
@ -959,6 +1002,65 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
.Invoke(cache, Array.Empty<object>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取双键缓存中当前保存的对象。
|
||||
/// </summary>
|
||||
private static object? GetPairCacheValue(object cache, Type primaryType, Type secondaryType)
|
||||
{
|
||||
return InvokeInstanceMethod(cache, "GetValueOrDefaultForTesting", primaryType, secondaryType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定 stream dispatch binding 中当前缓存的 pipeline executor。
|
||||
/// </summary>
|
||||
private static object? GetStreamPipelineExecutorValue(
|
||||
object streamBindings,
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
int behaviorCount)
|
||||
{
|
||||
var binding = GetStreamDispatchBindingValue(streamBindings, requestType, responseType);
|
||||
return binding is null
|
||||
? null
|
||||
: InvokeInstanceMethod(binding, "GetPipelineExecutorForTesting", behaviorCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定流式请求/响应类型对对应的强类型 stream dispatch binding。
|
||||
/// </summary>
|
||||
private static object? GetStreamDispatchBindingValue(object streamBindings, Type requestType, Type responseType)
|
||||
{
|
||||
var bindingBox = GetPairCacheValue(streamBindings, requestType, responseType);
|
||||
if (bindingBox is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var method = bindingBox.GetType().GetMethod(
|
||||
"Get",
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
Assert.That(method, Is.Not.Null, $"Missing stream binding accessor on {bindingBox.GetType().FullName}.");
|
||||
|
||||
return method!
|
||||
.MakeGenericMethod(responseType)
|
||||
.Invoke(bindingBox, Array.Empty<object>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调用目标对象上的实例方法。
|
||||
/// </summary>
|
||||
private static object? InvokeInstanceMethod(object target, string methodName, params object[] arguments)
|
||||
{
|
||||
var method = target.GetType().GetMethod(
|
||||
methodName,
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
Assert.That(method, Is.Not.Null, $"Missing method {target.GetType().FullName}.{methodName}.");
|
||||
|
||||
return method!.Invoke(target, arguments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 枚举并收集当前异步流中的全部元素,便于断言 generated stream invoker 的输出。
|
||||
/// </summary>
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 处理 <see cref="DispatcherZeroPipelineStreamRequest" />,用于验证零管道 stream 的 dispatcher 缓存路径。
|
||||
/// </summary>
|
||||
internal sealed class DispatcherZeroPipelineStreamHandler : IStreamRequestHandler<DispatcherZeroPipelineStreamRequest, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回一个单元素异步流,便于在缓存测试中最小化处理噪音。
|
||||
/// </summary>
|
||||
/// <param name="request">当前零管道 stream 请求。</param>
|
||||
/// <param name="cancellationToken">用于终止异步枚举的取消令牌。</param>
|
||||
/// <returns>只包含一个元素的异步响应流。</returns>
|
||||
public async IAsyncEnumerable<int> Handle(
|
||||
DispatcherZeroPipelineStreamRequest request,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
yield return 1;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 表示未注册任何 stream pipeline behavior 的最小缓存验证请求。
|
||||
/// </summary>
|
||||
internal sealed record DispatcherZeroPipelineStreamRequest : IStreamRequest<int>;
|
||||
@ -26,6 +26,10 @@ internal sealed class CqrsDispatcher(
|
||||
// 每次 SendAsync 都重复询问容器。缓存值只反映当前 dispatcher 持有容器的注册可见性,不跨 runtime 共享。
|
||||
private readonly ConcurrentDictionary<Type, bool> _requestBehaviorPresenceCache = new();
|
||||
|
||||
// 与 request 路径相同,stream 的 behavior 注册可见性在当前 dispatcher 生命周期内保持稳定。
|
||||
// 这里缓存 “CreateStream(...) 对应 behaviorType 是否存在注册”,避免零管道 stream 每次建流都重复询问容器。
|
||||
private readonly ConcurrentDictionary<Type, bool> _streamBehaviorPresenceCache = new();
|
||||
|
||||
// 卸载安全的进程级缓存:当 generated registry 提供 request invoker 元数据时,
|
||||
// registrar 会按请求/响应类型对把它们写入这里;若类型被卸载,条目会自然失效。
|
||||
private static readonly WeakTypePairCache<GeneratedRequestInvokerMetadata>
|
||||
@ -41,8 +45,9 @@ internal sealed class CqrsDispatcher(
|
||||
private static readonly WeakKeyCache<Type, NotificationDispatchBinding>
|
||||
NotificationDispatchBindings = new();
|
||||
|
||||
// 卸载安全的进程级缓存:请求/响应类型对采用弱键缓存,避免流式消息类型被静态字典永久保留。
|
||||
private static readonly WeakTypePairCache<StreamDispatchBinding>
|
||||
// 卸载安全的进程级缓存:流式请求/响应类型对命中后复用强类型 dispatch binding 盒子,
|
||||
// 避免 stream 响应元素在热路径上退化为 object 桥接,同时仍保持弱键卸载安全语义。
|
||||
private static readonly WeakTypePairCache<StreamDispatchBindingBox>
|
||||
StreamDispatchBindings = new();
|
||||
|
||||
// 卸载安全的进程级缓存:请求/响应类型对命中后复用强类型 dispatch binding;
|
||||
@ -186,18 +191,15 @@ internal sealed class CqrsDispatcher(
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var requestType = request.GetType();
|
||||
var dispatchBinding = StreamDispatchBindings.GetOrAdd(
|
||||
requestType,
|
||||
typeof(TResponse),
|
||||
static (requestType, responseType) => CreateStreamDispatchBinding(requestType, responseType));
|
||||
var dispatchBinding = GetStreamDispatchBinding<TResponse>(requestType);
|
||||
var handler = container.Get(dispatchBinding.HandlerType)
|
||||
?? throw new InvalidOperationException(
|
||||
$"No CQRS stream handler registered for {requestType.FullName}.");
|
||||
|
||||
PrepareHandler(handler, context);
|
||||
if (!container.HasRegistration(dispatchBinding.BehaviorType))
|
||||
if (!HasStreamBehaviorRegistration(dispatchBinding.BehaviorType))
|
||||
{
|
||||
return (IAsyncEnumerable<TResponse>)dispatchBinding.StreamInvoker(handler, request, cancellationToken);
|
||||
return dispatchBinding.StreamInvoker(handler, request, cancellationToken);
|
||||
}
|
||||
|
||||
var behaviors = container.GetAll(dispatchBinding.BehaviorType);
|
||||
@ -207,10 +209,25 @@ internal sealed class CqrsDispatcher(
|
||||
PrepareHandler(behavior, context);
|
||||
}
|
||||
|
||||
return (IAsyncEnumerable<TResponse>)dispatchBinding.GetPipelineExecutor(behaviors.Count)
|
||||
return dispatchBinding.GetPipelineExecutor(behaviors.Count)
|
||||
.Invoke(handler, behaviors, dispatchBinding.StreamInvoker, request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取当前 dispatcher 容器里是否存在指定 stream pipeline 行为注册,并在首次命中后缓存结果。
|
||||
/// </summary>
|
||||
/// <param name="behaviorType">目标 stream pipeline 行为服务类型。</param>
|
||||
/// <returns>存在注册时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
private bool HasStreamBehaviorRegistration(Type behaviorType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(behaviorType);
|
||||
|
||||
return _streamBehaviorPresenceCache.GetOrAdd(
|
||||
behaviorType,
|
||||
static (cachedBehaviorType, currentContainer) => currentContainer.HasRegistration(cachedBehaviorType),
|
||||
container);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为上下文感知处理器注入当前 CQRS 分发上下文。
|
||||
/// </summary>
|
||||
@ -380,75 +397,114 @@ internal sealed class CqrsDispatcher(
|
||||
/// <summary>
|
||||
/// 为指定流式请求类型构造完整分发绑定,把服务类型与调用委托聚合到同一缓存项。
|
||||
/// </summary>
|
||||
private static StreamDispatchBinding CreateStreamDispatchBinding(Type requestType, Type responseType)
|
||||
private static StreamDispatchBinding<TResponse> CreateStreamDispatchBinding<TResponse>(Type requestType)
|
||||
{
|
||||
var generatedDescriptor = TryGetGeneratedStreamInvokerDescriptor(requestType, responseType);
|
||||
var generatedDescriptor = TryGetGeneratedStreamInvokerDescriptor<TResponse>(requestType);
|
||||
if (generatedDescriptor is not null)
|
||||
{
|
||||
var resolvedGeneratedDescriptor = generatedDescriptor.Value;
|
||||
return new StreamDispatchBinding(
|
||||
return new StreamDispatchBinding<TResponse>(
|
||||
resolvedGeneratedDescriptor.HandlerType,
|
||||
typeof(IStreamPipelineBehavior<,>).MakeGenericType(requestType, responseType),
|
||||
typeof(IStreamPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)),
|
||||
requestType,
|
||||
responseType,
|
||||
resolvedGeneratedDescriptor.Invoker);
|
||||
}
|
||||
|
||||
return new StreamDispatchBinding(
|
||||
typeof(IStreamRequestHandler<,>).MakeGenericType(requestType, responseType),
|
||||
typeof(IStreamPipelineBehavior<,>).MakeGenericType(requestType, responseType),
|
||||
return new StreamDispatchBinding<TResponse>(
|
||||
typeof(IStreamRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse)),
|
||||
typeof(IStreamPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)),
|
||||
requestType,
|
||||
responseType,
|
||||
CreateStreamInvoker(requestType, responseType));
|
||||
CreateStreamInvoker<TResponse>(requestType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定流式请求/响应类型对的 dispatch binding;若缓存未命中则按当前加载状态创建。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
|
||||
/// <param name="requestType">流式请求运行时类型。</param>
|
||||
/// <returns>当前请求/响应类型对对应的强类型 stream dispatch binding。</returns>
|
||||
private static StreamDispatchBinding<TResponse> GetStreamDispatchBinding<TResponse>(Type requestType)
|
||||
{
|
||||
var bindingBox = StreamDispatchBindings.GetOrAdd(
|
||||
requestType,
|
||||
typeof(TResponse),
|
||||
static (cachedRequestType, cachedResponseType) =>
|
||||
CreateStreamDispatchBindingBox<TResponse>(cachedRequestType, cachedResponseType));
|
||||
return bindingBox.Get<TResponse>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为弱键流式请求缓存创建强类型 binding 盒子,避免响应元素走 object 结果桥接。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
|
||||
/// <param name="requestType">流式请求运行时类型。</param>
|
||||
/// <param name="responseType">缓存命中的响应运行时类型。</param>
|
||||
/// <returns>可放入弱键缓存的强类型 binding 盒子。</returns>
|
||||
private static StreamDispatchBindingBox CreateStreamDispatchBindingBox<TResponse>(
|
||||
Type requestType,
|
||||
Type responseType)
|
||||
{
|
||||
if (responseType != typeof(TResponse))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Stream dispatch binding cache expected response type {typeof(TResponse).FullName}, but received {responseType.FullName}.");
|
||||
}
|
||||
|
||||
return StreamDispatchBindingBox.Create(CreateStreamDispatchBinding<TResponse>(requestType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试从容器已注册的 generated stream invoker provider 中获取指定流式请求/响应类型对的元数据。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
|
||||
/// <param name="requestType">流式请求运行时类型。</param>
|
||||
/// <param name="responseType">流式响应元素类型。</param>
|
||||
/// <returns>命中时返回强类型化后的描述符;否则返回 <see langword="null" />。</returns>
|
||||
private static StreamInvokerDescriptor? TryGetGeneratedStreamInvokerDescriptor(Type requestType, Type responseType)
|
||||
private static StreamInvokerDescriptor<TResponse>? TryGetGeneratedStreamInvokerDescriptor<TResponse>(Type requestType)
|
||||
{
|
||||
return GeneratedStreamInvokers.TryGetValue(requestType, responseType, out var metadata) &&
|
||||
return GeneratedStreamInvokers.TryGetValue(requestType, typeof(TResponse), out var metadata) &&
|
||||
metadata is not null
|
||||
? CreateStreamInvokerDescriptor(requestType, responseType, metadata)
|
||||
? CreateStreamInvokerDescriptor<TResponse>(requestType, metadata)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把 provider 返回的弱类型描述符转换为 dispatcher 内部使用的 stream invoker 描述符。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
|
||||
/// <param name="requestType">流式请求运行时类型。</param>
|
||||
/// <param name="responseType">流式响应元素类型。</param>
|
||||
/// <param name="descriptor">provider 返回的弱类型描述符。</param>
|
||||
/// <returns>可直接用于创建 stream dispatch binding 的描述符。</returns>
|
||||
/// <exception cref="InvalidOperationException">当 provider 返回的委托签名与当前流式请求/响应类型对不匹配时抛出。</exception>
|
||||
private static StreamInvokerDescriptor CreateStreamInvokerDescriptor(
|
||||
private static StreamInvokerDescriptor<TResponse> CreateStreamInvokerDescriptor<TResponse>(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
GeneratedStreamInvokerMetadata descriptor)
|
||||
{
|
||||
if (!descriptor.InvokerMethod.IsStatic)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Generated CQRS stream invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {responseType.FullName}.");
|
||||
$"Generated CQRS stream invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Delegate.CreateDelegate(typeof(StreamInvoker), descriptor.InvokerMethod) is not StreamInvoker invoker)
|
||||
if (Delegate.CreateDelegate(typeof(WeakStreamInvoker), descriptor.InvokerMethod) is not
|
||||
WeakStreamInvoker weakInvoker)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {responseType.FullName}.");
|
||||
$"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.");
|
||||
}
|
||||
|
||||
return new StreamInvokerDescriptor(descriptor.HandlerType, invoker);
|
||||
// generated stream descriptor 的公开契约仍以 object 返回值暴露异步流;
|
||||
// 这里在 binding 创建时只做一次适配,把后续 CreateStream 热路径保持为强类型调用。
|
||||
var adapter = new GeneratedStreamInvokerAdapter<TResponse>(weakInvoker);
|
||||
StreamInvoker<TResponse> invoker = (handler, request, cancellationToken) =>
|
||||
adapter.Invoke(handler, request, cancellationToken);
|
||||
return new StreamInvokerDescriptor<TResponse>(descriptor.HandlerType, invoker);
|
||||
}
|
||||
catch (ArgumentException exception)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {responseType.FullName}.",
|
||||
$"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.",
|
||||
exception);
|
||||
}
|
||||
}
|
||||
@ -520,11 +576,11 @@ internal sealed class CqrsDispatcher(
|
||||
/// <summary>
|
||||
/// 生成流式处理器调用委托,避免每次创建流都重复反射。
|
||||
/// </summary>
|
||||
private static StreamInvoker CreateStreamInvoker(Type requestType, Type responseType)
|
||||
private static StreamInvoker<TResponse> CreateStreamInvoker<TResponse>(Type requestType)
|
||||
{
|
||||
var method = StreamHandlerInvokerMethodDefinition
|
||||
.MakeGenericMethod(requestType, responseType);
|
||||
return (StreamInvoker)Delegate.CreateDelegate(typeof(StreamInvoker), method);
|
||||
.MakeGenericMethod(requestType, typeof(TResponse));
|
||||
return (StreamInvoker<TResponse>)Delegate.CreateDelegate(typeof(StreamInvoker<TResponse>), method);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -575,7 +631,7 @@ internal sealed class CqrsDispatcher(
|
||||
/// <summary>
|
||||
/// 执行已强类型化的流式处理器调用。
|
||||
/// </summary>
|
||||
private static object InvokeStreamHandler<TRequest, TResponse>(
|
||||
private static IAsyncEnumerable<TResponse> InvokeStreamHandler<TRequest, TResponse>(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
@ -590,10 +646,10 @@ internal sealed class CqrsDispatcher(
|
||||
/// 执行指定行为数量的强类型 stream pipeline executor。
|
||||
/// 该入口本身是缓存的固定 executor 形状;每次建流只绑定当前 handler 与 behaviors 实例。
|
||||
/// </summary>
|
||||
private static object InvokeStreamPipelineExecutor<TRequest, TResponse>(
|
||||
private static IAsyncEnumerable<TResponse> InvokeStreamPipelineExecutor<TRequest, TResponse>(
|
||||
object handler,
|
||||
IReadOnlyList<object> behaviors,
|
||||
StreamInvoker streamInvoker,
|
||||
StreamInvoker<TResponse> streamInvoker,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
where TRequest : IStreamRequest<TResponse>
|
||||
@ -619,12 +675,17 @@ internal sealed class CqrsDispatcher(
|
||||
private delegate ValueTask NotificationInvoker(object handler, object notification,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken);
|
||||
private delegate IAsyncEnumerable<TResponse> StreamInvoker<TResponse>(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
private delegate object StreamPipelineInvoker(
|
||||
private delegate object WeakStreamInvoker(object handler, object request, CancellationToken cancellationToken);
|
||||
|
||||
private delegate IAsyncEnumerable<TResponse> StreamPipelineInvoker<TResponse>(
|
||||
object handler,
|
||||
IReadOnlyList<object> behaviors,
|
||||
StreamInvoker streamInvoker,
|
||||
StreamInvoker<TResponse> streamInvoker,
|
||||
object request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
@ -673,6 +734,72 @@ internal sealed class CqrsDispatcher(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将不同响应类型的 stream dispatch binding 包装到统一弱缓存值中,
|
||||
/// 同时保留强类型流式委托,避免响应元素退化为 object 桥接。
|
||||
/// </summary>
|
||||
private abstract class StreamDispatchBindingBox
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建一个新的强类型 stream dispatch binding 盒子。
|
||||
/// </summary>
|
||||
public static StreamDispatchBindingBox Create<TResponse>(StreamDispatchBinding<TResponse> binding)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(binding);
|
||||
return new StreamDispatchBindingBox<TResponse>(binding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定响应类型的 stream dispatch binding。
|
||||
/// </summary>
|
||||
public abstract StreamDispatchBinding<TResponse> Get<TResponse>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存特定响应类型的 stream dispatch binding。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
|
||||
private sealed class StreamDispatchBindingBox<TResponse>(StreamDispatchBinding<TResponse> binding)
|
||||
: StreamDispatchBindingBox
|
||||
{
|
||||
private readonly StreamDispatchBinding<TResponse> _binding = binding;
|
||||
|
||||
/// <summary>
|
||||
/// 以原始强类型返回当前 binding;若请求的响应类型不匹配则抛出异常。
|
||||
/// </summary>
|
||||
public override StreamDispatchBinding<TRequestedResponse> Get<TRequestedResponse>()
|
||||
{
|
||||
if (typeof(TRequestedResponse) != typeof(TResponse))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cached stream dispatch binding for {typeof(TResponse).FullName} cannot be used as {typeof(TRequestedResponse).FullName}.");
|
||||
}
|
||||
|
||||
return (StreamDispatchBinding<TRequestedResponse>)(object)_binding;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 generated stream provider 的弱类型开放静态入口适配为 dispatcher 内部的强类型流式委托。
|
||||
/// 适配对象与 binding 同生命周期缓存,避免在每次建流时重复创建桥接闭包。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">流式响应元素类型。</typeparam>
|
||||
private sealed class GeneratedStreamInvokerAdapter<TResponse>(WeakStreamInvoker invoker)
|
||||
{
|
||||
private readonly WeakStreamInvoker _invoker = invoker;
|
||||
|
||||
/// <summary>
|
||||
/// 调用 generated provider 暴露的弱类型入口,并把返回结果物化为当前响应类型的异步流。
|
||||
/// </summary>
|
||||
public IAsyncEnumerable<TResponse> Invoke(
|
||||
object handler,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return (IAsyncEnumerable<TResponse>)_invoker(handler, request, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存通知分发路径所需的服务类型与强类型调用委托。
|
||||
/// 该绑定把“容器解析哪个服务类型”与“如何调用处理器”聚合到同一缓存项中。
|
||||
@ -703,17 +830,16 @@ internal sealed class CqrsDispatcher(
|
||||
/// 保存流式请求分发路径所需的服务类型与调用委托。
|
||||
/// 该绑定让建流热路径只需一次缓存命中即可获得解析与调用所需元数据。
|
||||
/// </summary>
|
||||
private sealed class StreamDispatchBinding(
|
||||
private sealed class StreamDispatchBinding<TResponse>(
|
||||
Type handlerType,
|
||||
Type behaviorType,
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
StreamInvoker streamInvoker)
|
||||
StreamInvoker<TResponse> streamInvoker)
|
||||
{
|
||||
// 线程安全:该缓存按 behaviorCount 复用 stream pipeline executor 形状,缓存项只保存委托与数量信息,
|
||||
// 不会跨建流缓存 handler 或 behavior 实例。若不同请求持续出现新的行为数量组合,字典会随之增长。
|
||||
private readonly ConcurrentDictionary<int, StreamPipelineExecutor> _pipelineExecutors = new();
|
||||
private readonly StreamPipelineInvoker _pipelineInvoker = CreateStreamPipelineInvoker(requestType, responseType);
|
||||
private readonly ConcurrentDictionary<int, StreamPipelineExecutor<TResponse>> _pipelineExecutors = new();
|
||||
private readonly StreamPipelineInvoker<TResponse> _pipelineInvoker = CreateStreamPipelineInvoker<TResponse>(requestType);
|
||||
|
||||
/// <summary>
|
||||
/// 获取流式请求处理器在容器中的服务类型。
|
||||
@ -728,19 +854,19 @@ internal sealed class CqrsDispatcher(
|
||||
/// <summary>
|
||||
/// 获取执行流式请求处理器的调用委托。
|
||||
/// </summary>
|
||||
public StreamInvoker StreamInvoker { get; } = streamInvoker;
|
||||
public StreamInvoker<TResponse> StreamInvoker { get; } = streamInvoker;
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定行为数量对应的 stream pipeline executor。
|
||||
/// executor 形状会按行为数量缓存,但不会缓存 handler 或 behavior 实例。
|
||||
/// </summary>
|
||||
public StreamPipelineExecutor GetPipelineExecutor(int behaviorCount)
|
||||
public StreamPipelineExecutor<TResponse> GetPipelineExecutor(int behaviorCount)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount);
|
||||
return _pipelineExecutors.GetOrAdd(
|
||||
return _pipelineExecutors.GetOrAdd<StreamPipelineExecutorFactoryState<TResponse>>(
|
||||
behaviorCount,
|
||||
static (count, state) => new StreamPipelineExecutor(count, state.PipelineInvoker),
|
||||
new StreamPipelineExecutorFactoryState(_pipelineInvoker));
|
||||
static (count, state) => CreateStreamPipelineExecutor(count, state.PipelineInvoker),
|
||||
new StreamPipelineExecutorFactoryState<TResponse>(_pipelineInvoker));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -903,27 +1029,41 @@ internal sealed class CqrsDispatcher(
|
||||
/// </summary>
|
||||
/// <param name="HandlerType">流式请求处理器在容器中的服务类型。</param>
|
||||
/// <param name="Invoker">执行流式请求处理器的调用委托。</param>
|
||||
private readonly record struct StreamInvokerDescriptor(
|
||||
private readonly record struct StreamInvokerDescriptor<TResponse>(
|
||||
Type HandlerType,
|
||||
StreamInvoker Invoker);
|
||||
StreamInvoker<TResponse> Invoker);
|
||||
|
||||
/// <summary>
|
||||
/// 为指定流式请求类型创建可跨多个 behaviorCount 复用的 typed pipeline invoker。
|
||||
/// </summary>
|
||||
private static StreamPipelineInvoker CreateStreamPipelineInvoker(Type requestType, Type responseType)
|
||||
private static StreamPipelineInvoker<TResponse> CreateStreamPipelineInvoker<TResponse>(Type requestType)
|
||||
{
|
||||
var method = StreamPipelineInvokerMethodDefinition
|
||||
.MakeGenericMethod(requestType, responseType);
|
||||
return (StreamPipelineInvoker)Delegate.CreateDelegate(typeof(StreamPipelineInvoker), method);
|
||||
.MakeGenericMethod(requestType, typeof(TResponse));
|
||||
return (StreamPipelineInvoker<TResponse>)Delegate.CreateDelegate(
|
||||
typeof(StreamPipelineInvoker<TResponse>),
|
||||
method);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为指定流式请求/响应类型与固定行为数量创建 pipeline executor。
|
||||
/// 行为数量用于表达缓存形状,实际建流仍会消费本次容器解析出的 handler 与 behaviors 实例。
|
||||
/// </summary>
|
||||
private static StreamPipelineExecutor<TResponse> CreateStreamPipelineExecutor<TResponse>(
|
||||
int behaviorCount,
|
||||
StreamPipelineInvoker<TResponse> invoker)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount);
|
||||
return new StreamPipelineExecutor<TResponse>(behaviorCount, invoker);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存固定行为数量下的 typed stream pipeline executor 形状。
|
||||
/// 该对象自身可跨建流复用,但每次调用都只绑定当前 handler 与 behavior 实例。
|
||||
/// </summary>
|
||||
private sealed class StreamPipelineExecutor(
|
||||
private sealed class StreamPipelineExecutor<TResponse>(
|
||||
int behaviorCount,
|
||||
StreamPipelineInvoker invoker)
|
||||
StreamPipelineInvoker<TResponse> invoker)
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取此 executor 预期处理的行为数量。
|
||||
@ -933,10 +1073,10 @@ internal sealed class CqrsDispatcher(
|
||||
/// <summary>
|
||||
/// 使用当前 handler / behaviors / request 执行缓存的 pipeline 形状。
|
||||
/// </summary>
|
||||
public object Invoke(
|
||||
public IAsyncEnumerable<TResponse> Invoke(
|
||||
object handler,
|
||||
IReadOnlyList<object> behaviors,
|
||||
StreamInvoker streamInvoker,
|
||||
StreamInvoker<TResponse> streamInvoker,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@ -953,8 +1093,8 @@ internal sealed class CqrsDispatcher(
|
||||
/// <summary>
|
||||
/// 为 stream pipeline executor 缓存携带 typed pipeline invoker,避免按行为数量建缓存时创建闭包。
|
||||
/// </summary>
|
||||
private readonly record struct StreamPipelineExecutorFactoryState(
|
||||
StreamPipelineInvoker PipelineInvoker);
|
||||
private readonly record struct StreamPipelineExecutorFactoryState<TResponse>(
|
||||
StreamPipelineInvoker<TResponse> PipelineInvoker);
|
||||
|
||||
/// <summary>
|
||||
/// 供 registrar 在 generated registry 激活后登记 request invoker 元数据。
|
||||
@ -1092,12 +1232,12 @@ internal sealed class CqrsDispatcher(
|
||||
/// </summary>
|
||||
private sealed class StreamPipelineInvocation<TRequest, TResponse>(
|
||||
IStreamRequestHandler<TRequest, TResponse> handler,
|
||||
StreamInvoker streamInvoker,
|
||||
StreamInvoker<TResponse> streamInvoker,
|
||||
IReadOnlyList<object> behaviors)
|
||||
where TRequest : IStreamRequest<TResponse>
|
||||
{
|
||||
private readonly IStreamRequestHandler<TRequest, TResponse> _handler = handler;
|
||||
private readonly StreamInvoker _streamInvoker = streamInvoker;
|
||||
private readonly StreamInvoker<TResponse> _streamInvoker = streamInvoker;
|
||||
private readonly IReadOnlyList<object> _behaviors = behaviors;
|
||||
private readonly StreamMessageHandlerDelegate<TRequest, TResponse>?[] _continuations =
|
||||
new StreamMessageHandlerDelegate<TRequest, TResponse>?[behaviors.Count + 1];
|
||||
@ -1148,7 +1288,7 @@ internal sealed class CqrsDispatcher(
|
||||
/// </summary>
|
||||
private IAsyncEnumerable<TResponse> InvokeHandler(TRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return (IAsyncEnumerable<TResponse>)_streamInvoker(_handler, request, cancellationToken);
|
||||
return _streamInvoker(_handler, request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -21,6 +21,10 @@ help the current worktree land on the right recovery documents without scanning
|
||||
API-reference alignment baseline.
|
||||
- Tracking: `ai-plan/public/documentation-full-coverage-governance/todos/documentation-full-coverage-governance-tracking.md`
|
||||
- Trace: `ai-plan/public/documentation-full-coverage-governance/traces/documentation-full-coverage-governance-trace.md`
|
||||
- `ai-plan-governance`
|
||||
- Purpose: govern repository AI workflow rules, recovery entrypoints, and multi-agent coordination conventions.
|
||||
- Tracking: `ai-plan/public/ai-plan-governance/todos/ai-plan-governance-tracking.md`
|
||||
- Trace: `ai-plan/public/ai-plan-governance/traces/ai-plan-governance-trace.md`
|
||||
- `coroutine-optimization`
|
||||
- Purpose: continue the coroutine semantics, host integration, observability, regression coverage, and migration-doc
|
||||
follow-up work.
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
# AI-Plan Governance 跟踪
|
||||
|
||||
## 目标
|
||||
|
||||
保持仓库级 AI 工作流规则、公开恢复入口与多 Agent 协作约定在 `AGENTS.md`、`.agents/skills/**` 与
|
||||
`ai-plan/public/**` 之间一致,并让 `boot` 能稳定恢复这类治理主题。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`AI-PLAN-GOVERNANCE-RP-002`
|
||||
- 当前阶段:`Phase 2`
|
||||
- 当前焦点:
|
||||
- 已补齐 `ai-plan/public/README.md` 的 active-topic 暴露,使 `feat/cqrs-optimization` 映射到
|
||||
`ai-plan-governance` 时,`boot` 能落到真实的 public tracking / trace 入口。
|
||||
- 已在 `AGENTS.md` 增补主 Agent 协调多 worker wave 时的强约束,明确 critical path、`ai-plan`、
|
||||
review、validation 与 final integration 归主 Agent 所有。
|
||||
- 已新增 `.agents/skills/gframework-multi-agent-batch/`,把“主 Agent 负责派发、核对、更新 `ai-plan`、
|
||||
验收并决定是否继续下一波”的流程沉淀为可复用 skill。
|
||||
- 已轻量更新 `gframework-boot`、`gframework-batch-boot` 与 `.agents/skills/README.md`,把该模式接入既有
|
||||
boot / batch 入口而不重复定义规则。
|
||||
- 当前变更面仍限定在 `AGENTS.md`、`.agents/skills/**` 与 `ai-plan/public/**`,没有扩散到运行时代码或产品
|
||||
模块。
|
||||
|
||||
## 当前状态摘要
|
||||
|
||||
- 已落地“`AGENTS.md` 负责强治理约束,skill 负责可执行流程”的混合方案。
|
||||
- 已修复本 worktree 的一个恢复入口缺口:topic 映射存在,但 active-topic 列表和 public recovery 文件此前缺失。
|
||||
- 当前主题已经具备后续继续演进 AI workflow 规则所需的 public tracking / trace 基线。
|
||||
|
||||
## 当前活跃事实
|
||||
|
||||
- 当前分支:`feat/cqrs-optimization`
|
||||
- `AGENTS.md` 的 `Repository Boot Skill` 现已与仓库实际目录对齐,使用 `.agents/skills/gframework-boot/`
|
||||
而不是失效的 `.codex/skills/gframework-boot/` 路径。
|
||||
- `AGENTS.md` 新增 `Multi-Agent Coordination Rules`,用于约束主 Agent 的职责边界、worker ownership、
|
||||
acceptance gate 与 stop condition。
|
||||
- `.agents/skills/gframework-multi-agent-batch/` 现已包含:
|
||||
- `SKILL.md`
|
||||
- `agents/openai.yaml`
|
||||
- `.agents/skills/gframework-boot/SKILL.md` 与 `.agents/skills/gframework-batch-boot/SKILL.md` 现已明确在
|
||||
orchestration-heavy 场景下切换或让位给 `gframework-multi-agent-batch`。
|
||||
- `.agents/skills/README.md` 已把 `gframework-multi-agent-batch` 纳入公开入口说明。
|
||||
- `ai-plan/public/ai-plan-governance/` 已建立 public tracking / trace 入口,可供后续治理任务继续复用。
|
||||
|
||||
## 当前风险
|
||||
|
||||
- 漂移风险:若后续只改 `AGENTS.md` 或只改 skill,主 Agent 职责定义可能再次分叉。
|
||||
- 入口重叠风险:若 `gframework-batch-boot` 与 `gframework-multi-agent-batch` 的边界被继续模糊,公开入口会变得难以选择。
|
||||
- 恢复噪音风险:若该 topic 后续把每一轮治理细节都堆进 active 文件,`boot` 的默认恢复效率会下降。
|
||||
|
||||
## 验证说明
|
||||
|
||||
- `python3 scripts/license-header.py --check --paths AGENTS.md ai-plan/public/README.md ai-plan/public/ai-plan-governance/todos/ai-plan-governance-tracking.md ai-plan/public/ai-plan-governance/traces/ai-plan-governance-trace.md`
|
||||
- 结果:通过
|
||||
- 备注:本轮受 license-header 规则约束的 public 文档文件均已具备 Apache-2.0 头
|
||||
- `git diff --check`
|
||||
- 结果:通过
|
||||
- 备注:本轮治理变更未引入 trailing whitespace 或 patch 格式问题
|
||||
- `dotnet build GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj -c Release`
|
||||
- 结果:通过
|
||||
- 备注:`0 warning / 0 error`;满足仓库“完成任务前至少通过一条 build validation”的要求
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 后续如再次出现“主 Agent 持续派发并验收多个 worker”的任务,优先直接用真实任务验证 `gframework-multi-agent-batch`
|
||||
的可恢复性与边界清晰度
|
||||
2. 若该 skill 的 stop condition、ownership 或 `ai-plan` 记录格式在实战中出现歧义,再回到本 topic 做下一轮治理收口
|
||||
@ -0,0 +1,43 @@
|
||||
# AI-Plan Governance 追踪
|
||||
|
||||
## 2026-05-09
|
||||
|
||||
### 阶段:多 Agent 协作治理入口落地(AI-PLAN-GOVERNANCE-RP-002)
|
||||
|
||||
- 确认本轮目标采用混合方案,而不是只写 skill 或只写 `AGENTS.md`:
|
||||
- `AGENTS.md` 承载仓库级强约束
|
||||
- `.agents/skills/gframework-multi-agent-batch/` 承载主 Agent 持续派发 / review / `ai-plan` / validation 的执行流程
|
||||
- 修复 repository boot 入口的一处一致性问题:
|
||||
- `AGENTS.md` 先前仍引用 `.codex/skills/gframework-boot/`
|
||||
- 仓库实际技能目录是 `.agents/skills/`
|
||||
- 本轮已对齐为 `.agents/skills/gframework-boot/`,并补入新的多 Agent skill 路径
|
||||
- 本轮已落地的治理变更:
|
||||
- 在 `AGENTS.md` 增加 `Multi-Agent Coordination Rules`
|
||||
- 在 `.agents/skills/README.md` 新增 `gframework-multi-agent-batch` 公开入口说明
|
||||
- 在 `.agents/skills/gframework-boot/SKILL.md` 增加 orchestration-heavy 场景切换说明
|
||||
- 在 `.agents/skills/gframework-batch-boot/SKILL.md` 增加与新 skill 的边界说明
|
||||
- 新建 `.agents/skills/gframework-multi-agent-batch/`
|
||||
- 在 `ai-plan/public/README.md` 补入 `ai-plan-governance` active topic
|
||||
- 新建 `ai-plan/public/ai-plan-governance/` 的 tracking / trace 入口
|
||||
- 关键约束决策:
|
||||
- 主 Agent 负责 critical path、stop condition、`ai-plan`、validation、review、final integration
|
||||
- worker 只能拿到显式且互不冲突的 ownership slice
|
||||
- 继续派发下一波前,主 Agent 必须重算 reviewability、branch diff 与 context budget
|
||||
- 当前收尾目标:
|
||||
- 运行校验
|
||||
- 把验证结果回填到 active tracking / trace
|
||||
- 以 Conventional Commit 提交本轮治理变更
|
||||
|
||||
### 验证里程碑
|
||||
|
||||
- `python3 scripts/license-header.py --check --paths AGENTS.md ai-plan/public/README.md ai-plan/public/ai-plan-governance/todos/ai-plan-governance-tracking.md ai-plan/public/ai-plan-governance/traces/ai-plan-governance-trace.md`
|
||||
- 结果:通过
|
||||
- `git diff --check`
|
||||
- 结果:通过
|
||||
- `dotnet build GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj -c Release`
|
||||
- 结果:通过(`0 warning / 0 error`)
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 提交本轮多 Agent 协作治理变更
|
||||
2. 未来若有真实的 orchestration-heavy 任务进入该 worktree,优先直接用新 skill 驱动并据实补充下一恢复点
|
||||
@ -7,44 +7,24 @@ CQRS 迁移与收敛。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-123`
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-128`
|
||||
- 当前阶段:`Phase 8`
|
||||
- 当前 PR 锚点:`PR #344`
|
||||
- 当前 PR 锚点:`PR #345`
|
||||
- 当前结论:
|
||||
- 当前 `RP-123` 通过 `$gframework-pr-review` 重新复核 `feat/cqrs-optimization` 的 latest-head review,确认 `PR #344` 仍成立且值得在本轮一起收口的问题共有四类:`CqrsDispatcher.ResolveNotificationPublisher()` 默认路径每次 publish 都重复查容器并在零注册分支分配新的 `SequentialNotificationPublisher`;`CqrsDispatcherContextValidationTests` 与 `CqrsNotificationPublisherTests` 的 strict `IIocContainer` helper 缺少 `GetAll(typeof(INotificationPublisher))` 默认装配,导致 CI 在真正断言前就被 mock 异常短路;`NotificationPublisherRegistrationExtensionsTests` 缺少“唯一注册”断言;`CqrsDispatcherCacheTests` 的隔离容器构建复制了 `SetUp()` 的注册形状,存在后续漂移风险
|
||||
- 本轮保持改动面只落在 `GFramework.Cqrs`、`GFramework.Cqrs.Tests` 与 `ai-plan/public/cqrs-rewrite`,不扩散到新的 benchmark 宿主或额外 notification API;其中 `CqrsDispatcher` 新增 dispatcher 实例级 `_resolvedNotificationPublisher` 缓存,并在首次解析后通过线程安全比较交换固定最终策略实例,继续保持“显式实例优先、容器内唯一注册次之、默认顺序发布器兜底”的既有契约
|
||||
- 两个 strict mock runtime helper 现统一预设 `IIocContainer.GetAll(typeof(INotificationPublisher)) => Array.Empty<object>()`,把“未注册自定义 publisher 时回退到默认顺序发布器”这条默认路径显式纳入测试装配,避免后续相同语义再次被环境性 mock 配置遗漏掩盖
|
||||
- `NotificationPublisherRegistrationExtensionsTests` 现在对泛型组合根重载补上 `container.GetAll(typeof(INotificationPublisher))` 的唯一注册断言,防止实现未来意外追加重复 descriptor 却仍因 `GetRequired<INotificationPublisher>()` 返回单个实例而误通过
|
||||
- `CqrsDispatcherCacheTests` 新增 `ConfigureDispatcherCacheFixture(MicrosoftDiContainer)` 共享装配 helper,让 `SetUp()` 与 `CreateFrozenContainer()` 复用同一份 CQRS 注册形状,消除 latest-head nitpick 指出的夹具/隔离容器漂移风险
|
||||
- 本轮本地权威验证已通过:许可证头检查通过,`GFramework.Cqrs` 与 `GFramework.Cqrs.Tests` 的 Release build 通过;目标回归 `CqrsDispatcherContextValidationTests`、`CqrsNotificationPublisherTests`、`NotificationPublisherRegistrationExtensionsTests` 与 `CqrsDispatcherCacheTests` 合计 `30/30` passed
|
||||
- `GFramework.Cqrs` 首轮与测试项目并行构建时曾出现 `MSB3026` 单次复制重试;串行重跑同一 `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release` 后稳定为 `0 warning / 0 error`,因此该信号判定为并行输出目录竞争噪音而非代码问题
|
||||
- 当前 `RP-122` 继续沿用 `$gframework-batch-boot 50`,并在 `RP-121` 收口 notification 线阶段性闭环后切回 request steady-state 热点;本轮不再继续压 `HasRegistration(Type)` 内部实现,而是把“是否存在 request pipeline behavior”从每次 `SendAsync(...)` 都查询容器,收口为 `CqrsDispatcher` 实例级的首次判定缓存
|
||||
- `GFramework.Cqrs/Internal/CqrsDispatcher.cs` 现新增 `_requestBehaviorPresenceCache`,按 `IPipelineBehavior<,>` 的闭合服务类型记住当前 dispatcher 持有容器里该行为是否存在注册;零管道 request 在首次命中后会直接走缓存分支,不再重复询问容器
|
||||
- `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs` 现新增 `Dispatcher_Should_Cache_Zero_Pipeline_Request_Presence_Per_Dispatcher_Instance()`:该回归同时锁住两件事,一是同一容器解析出的多个 `ArchitectureContext` 共享同一个 runtime/dispatcher,因此会复用同一实例级缓存;二是另一套独立容器创建的 dispatcher 不会提前共享该缓存
|
||||
- 本轮 short-job benchmark 表明这刀继续有效:默认 request steady-state 当前约为 baseline `5.876 ns / 32 B`、`Mediator` `5.275 ns / 32 B`、`GFramework.Cqrs` `51.717 ns / 32 B`、`MediatR` `56.108 ns / 232 B`;request lifetime 下 `Singleton` 约 `52.490 ns / 32 B` vs `MediatR` `56.890 ns / 232 B`,`Transient` 约 `57.746 ns / 56 B` vs `MediatR` `55.545 ns / 232 B`
|
||||
- 当前已提交分支相对 `origin/main`(`d389eb36`, `2026-05-08 20:08:33 +0800`)的累计 branch diff 为 `10 files / 377 changed lines`;本批待提交工作树只新增 `2 files / 187 changed lines`,即使提交后也仍明显低于 `$gframework-batch-boot 50` 的文件阈值
|
||||
- request 线经过这批后已经从“direct-return ValueTask”“generated provider 宿主吸收”“零管道 presence cache”三层继续下探,但 `Transient` 仍未稳定快于 `MediatR`;因此下一轮若继续压 request 热点,应继续选择真正减少 steady-state 常量路径的切片,而不是回头重试已被否决的 `IContextAware` 类型判定缓存
|
||||
- 当前 `RP-119` 继续沿用 `$gframework-batch-boot 50`,并在分支已与 `origin/main` 对齐(`d389eb36`, `2026-05-08 20:08:33 +0800`)后,重新选择 notification publisher 线上一个更小的采用面切片:补齐 `UseNotificationPublisher<TPublisher>()` 的组合根采用说明与回归,而不是提前切回 request dispatch 热路径
|
||||
- 本轮不修改 `GFramework.Cqrs` runtime 语义,只收口“泛型组合根入口是否真的可用、以及读者是否知道该在什么情况下选它”这两个采用缺口
|
||||
- `NotificationPublisherRegistrationExtensionsTests` 现额外覆盖两条行为:泛型重载会把指定 publisher 类型注册为容器内唯一的单例策略;当容器里已存在 `INotificationPublisher` 注册时,泛型重载也会像实例重载一样在组合根阶段拒绝重复声明
|
||||
- `GFramework.Cqrs/README.md` 与 `docs/zh-CN/core/cqrs.md` 现在把自定义策略入口统一写成 `UseNotificationPublisher(...)` / `UseNotificationPublisher<TPublisher>()`,并明确前者复用现成实例、后者让容器负责单例生命周期,避免用户误以为只能手写实例注册
|
||||
- 当前批次提交前的工作树 diff 为 `5 files / 77 lines`,仍远低于 `$gframework-batch-boot 50` 的文件阈值;但这一轮的主停止依据仍是上下文预算与自然评审边界,因此本批完成后应直接收口,而不是顺手再开启新的 runtime 热点实验
|
||||
- 当前 `RP-118` 已使用 `$gframework-pr-review` 复核 `PR #342` latest-head review:CodeRabbit 当前仍成立的是 `NotificationFanOutBenchmarks` 中 MediatR 分支绕过共享 `HandleCore(...)`、`GFramework.Cqrs/README.md` 的 MD058 表格空行、以及恢复文档的 PR 锚点与 fan-out 历史值表述;Greptile 额外指出的 `UseTaskWhenAllNotificationPublisher()` 示例多余 `using GFramework.Cqrs.Notification;` 也在本轮一并收口
|
||||
- 本轮不改 `GFramework.Cqrs` runtime 语义,只让 benchmark 的 MediatR handler 与其余对照分支共用同一组空值 / 取消检查,并把 README、中文文档与 `cqrs-rewrite` 恢复文档同步到当前 PR #342 上下文
|
||||
- 本轮按 `NotificationFanOutBenchmarks` short-job 复跑确认,对称化 MediatR handler 后当前 fixed `4 handler` fan-out 结果约为 `Mediator` `3.598 ns / 0 B`、baseline `7.033 ns / 0 B`、`MediatR` `257.533 ns / 1256 B`、`GFramework.Cqrs` 顺序 `409.557 ns / 408 B`、`TaskWhenAll` `484.531 ns / 496 B`
|
||||
- `RP-117` 留在“最近权威验证”中的固定 `4 handler` fan-out 数值属于早期 benchmark 记录,因此本轮选择显式补上 `历史基线(RP-112)` 标注,而不是把历史验证段落改写成新的 benchmark 结果,避免混淆恢复轨迹
|
||||
- 当前 `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 配置面内的低风险、可评审切片
|
||||
- 当前 `RP-115` 已继续沿用 `$gframework-batch-boot 50`,并把 notification publisher 线从“已具备 seam + benchmark 事实”继续收口到组合根配置面:新增 `GFramework.Cqrs.Extensions.NotificationPublisherRegistrationExtensions`,提供 `UseNotificationPublisher(...)` / `UseNotificationPublisher<TPublisher>()` / `UseTaskWhenAllNotificationPublisher()` 三个显式入口,避免用户再手写 `Register<INotificationPublisher>(new ...)`
|
||||
- 这一批同时把重复策略注册前移到组合根阶段显式阻止,并在回归里确认 `UseTaskWhenAllNotificationPublisher()` 经过默认 runtime 基础设施后仍会命中“失败不阻断其余 handler”的并行语义;这让 notification publisher 的采用路径从“知道内部 seam 如何接线”收口为“知道该在容器里选哪条策略”
|
||||
- 用户文档现同步写明 `TaskWhenAllNotificationPublisher` 更适合“并行完成 + 统一观察失败”的语义诉求,而不是 fixed fan-out steady-state publish 优化;这与 `RP-114` 的 benchmark 结论保持一致,减少使用者把它误解成默认的性能升级开关
|
||||
- 当前 `RP-114` 已继续沿用 `$gframework-batch-boot 50`,并沿着 `RP-113` 刚落地的 notification publisher 能力切片继续补 benchmark:`NotificationFanOutBenchmarks` 现同时纳入 `GFramework.Cqrs` 默认顺序发布器与新内置 `TaskWhenAllNotificationPublisher`,用于量化“能力差距收口后,固定 4 handler fan-out 的成本变化”
|
||||
- `RP-114` 的 short-job 结果显示:fixed `4 handler` fan-out 下,默认顺序发布器约 `427.453 ns / 408 B`,内置 `TaskWhenAllNotificationPublisher` 约 `472.574 ns / 496 B`,`MediatR` 约 `225.940 ns / 1256 B`,NuGet `Mediator` concrete runtime 约 `3.854 ns / 0 B`;这说明当前内置并行 publisher 的主要价值是语义补齐,而不是 steady-state fan-out 性能收益
|
||||
- 这一批保持 runtime 公开 API 与 notification 语义不变,只扩 benchmark 对照口径与恢复文档;原因是 `RP-113` 已经把并行 publisher 能力落到 production path,当前更高价值的是先证明它相对默认顺序发布器、`Mediator` 与 `MediatR` 的成本位置,而不是立即继续扩第二个 publisher strategy
|
||||
- 当前 `RP-128` 以 `$gframework-pr-review` 复核 `PR #345` latest-head review body,确认没有新的 unresolved thread,但仍有 `4` 条 CodeRabbit actionable comments 需要回到本地代码逐条验真
|
||||
- 本轮已收口仍成立的 review 输入:为 `AGENTS.md` 补充 multi-agent budget 术语释义;为 `StreamLifetimeBenchmarks` 补齐 `CancellationToken` 传播、`[EnumeratorCancellation]` 短名引用与类级 `<remarks>` 说明;为 benchmark `README` 去掉治理型 `RP-127` 表述;并把 `ai-plan/public/cqrs-rewrite/**` 的 PR 锚点与 branch diff 数字更新到当前 head
|
||||
- 最新权威 diff 基线已统一为当前分支相对 `origin/main`(`d85828c5`, `2026-05-09 12:25:41 +0800`)的 `21 files / 1344 insertions / 194 deletions`;active tracking、active trace 与后续 review triage 均以该组数字为准
|
||||
- 当前 `RP-127` 延续 `$gframework-batch-boot 50`,在 `RP-126` 已补齐 stream lifetime 四方口径后,再用 `b7fa3eee` 把 `CqrsDispatcher.CreateStream(...)` 的 stream dispatch binding 改为按 `TResponse` 强类型缓存,同时为 `StreamLifetimeBenchmarks` 增加 `FirstItem / DrainAll` 观测维度,并把新的结果回填到公开可恢复文档
|
||||
- 本轮写面落在 `GFramework.Cqrs/Internal/CqrsDispatcher.cs`、`GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs`、`GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs`、`GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs`,以及 `GFramework.Cqrs.Benchmarks/README.md` / `ai-plan/public/cqrs-rewrite/**` 恢复文档;没有扩散到 request runtime、notification runtime 或额外文档模块
|
||||
- `f9c9561f` 为 request lifetime 补入 handwritten generated registry,并在 setup/cleanup 清理 dispatcher cache;这样 `Singleton / Transient` 生命周期矩阵继续只比较 handler 生命周期与 dispatch 常量路径,不再混入旧宿主差异
|
||||
- `9107e232` 将 stream lifetime 的 reflection、generated 与 `MediatR` 请求/响应/handler 彻底拆开,并限制 generated registry 只绑定 generated lane,避免静态 dispatcher cache 把不同 stream 对照口径污染到一起
|
||||
- 当前 request lifetime benchmark 已用新宿主重新验证:`Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR` 约为 `5.012 ns / 32 B`、`49.612 ns / 32 B`、`51.796 ns / 232 B`;`Transient` 下约为 `3.962 ns / 32 B`、`50.480 ns / 56 B`、`50.284 ns / 232 B`
|
||||
- 当前 stream lifetime benchmark 已更新为 `Observation=FirstItem / DrainAll` 双口径:`Singleton + FirstItem` 下 baseline / generated / reflection / `MediatR` 约为 `48.704 ns / 216 B`、`94.629 ns / 216 B`、`95.417 ns / 216 B`、`152.886 ns / 608 B`;`Singleton + DrainAll` 下约为 `73.335 ns / 280 B`、`118.860 ns / 280 B`、`119.632 ns / 280 B`、`205.629 ns / 672 B`
|
||||
- `Transient + FirstItem` 下 baseline / reflection / generated / `MediatR` 约为 `48.293 ns / 216 B`、`97.628 ns / 240 B`、`100.011 ns / 240 B`、`154.149 ns / 608 B`;`Transient + DrainAll` 下约为 `78.466 ns / 280 B`、`124.174 ns / 304 B`、`116.780 ns / 304 B`、`220.040 ns / 672 B`
|
||||
- 现阶段可恢复结论收口为三点:一是 stream lifetime 已具备四方口径加 `FirstItem / DrainAll` 双观测维度;二是 `b7fa3eee` 已让 generated lane 在 `DrainAll` 口径下重新领先 reflection;三是 `Transient + FirstItem` 仍保留约 `2.4 ns` 的小幅反向差值,更像建流到首个元素之间的瞬时成本,而不是完整枚举阶段退化
|
||||
- 当前已提交分支相对 `origin/main`(`d85828c5`, `2026-05-09 12:25:41 +0800`)的累计 branch diff 已到 `21 files`(`1344 insertions / 194 deletions`),仍明显低于 `$gframework-batch-boot 50` 的 `50 files` stop condition
|
||||
- 下一推荐步骤:若继续 benchmark 线,优先从 `StreamLifetimeBenchmarks` 的 `Transient + FirstItem` 小幅差值继续恢复,并用 `StreamInvokerBenchmarks` 复核 generated lane 的常量成本收益是否能在更窄口径下复现;若差值不再稳定,再决定是否转去 `Mediator` concrete runtime 的 stream lifetime 对照批次
|
||||
- 更早的 `RP-123` 及之前阶段细节以下方 trace 与归档为准,active 入口不再重复展开旧阶段流水。
|
||||
- 当前分支相对 `origin/main` 的累计 branch diff 启动时为 `9 files`,仍明显低于 `$gframework-batch-boot 50` 的停止阈值;这一批继续保持单模块、低风险、可直接评审的 benchmark 边界
|
||||
- 当前 `RP-113` 已继续沿用 `$gframework-batch-boot 50`,并把 notification 线从 benchmark 对照推进到实际 runtime 能力:新增公开内置 `TaskWhenAllNotificationPublisher`,让 `GFramework.Cqrs` 在保留默认顺序发布器的同时,提供与 `Mediator` `TaskWhenAllPublisher` 对齐的并行 notification publish 策略
|
||||
- `TaskWhenAllNotificationPublisher` 当前语义明确为:零处理器静默完成,单处理器直接透传,多处理器并行启动并等待全部结束;它不保留默认顺序发布器的“首个异常立即停止”语义,而是把全部处理器的失败/取消结果收敛到同一个返回任务
|
||||
@ -97,18 +77,20 @@ CQRS 迁移与收敛。
|
||||
## 当前活跃事实
|
||||
|
||||
- 当前分支为 `feat/cqrs-optimization`
|
||||
- 本轮 `$gframework-batch-boot 50` 以 `origin/main` (`d389eb36`, 2026-05-08 20:08:33 +0800) 为基线;本地 `main` 仍落后,不作为 branch diff 基线
|
||||
- 当前已提交分支相对 `origin/main` 的累计 branch diff 为 `10 files / 377 changed lines`
|
||||
- 本批待提交工作树集中在 `GFramework.Cqrs/Internal/CqrsDispatcher.cs` 与 `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs`
|
||||
- 本轮 `$gframework-batch-boot 50` 以 `origin/main` (`d85828c5`, `2026-05-09 12:25:41 +0800`) 为基线;本地 `main` (`c2d22285`, `2026-05-06 21:34:59 +0800`) 已落后,不作为 branch diff 基线
|
||||
- 当前已提交分支相对 `origin/main` 的累计 branch diff 为 `21 files`(`1344 insertions / 194 deletions`),仍明显低于 `$gframework-batch-boot 50` 的 `50 files` 停止阈值
|
||||
- 当前用于收口 `PR #345` review 的写面额外覆盖 `AGENTS.md`、`GFramework.Cqrs.Benchmarks/README.md` 与 `ai-plan/public/cqrs-rewrite/**`;本轮代码层面的唯一触点保持在 `StreamLifetimeBenchmarks.cs`
|
||||
- 当前 `PR #345` latest-head review body 已本地复核完毕;仍成立并已吸收的反馈集中在 `AGENTS.md` 术语说明、`StreamLifetimeBenchmarks` 的取消/注释细节,以及 benchmark README / `ai-plan` 恢复入口漂移
|
||||
- 当前批次后的默认停止依据已改为 AI 上下文预算:若下一轮预计会让活动对话、已加载 recovery 文档、验证输出与当前 diff 接近约 `80%` 安全上下文占用,应在当前自然批次边界停止,即使 branch diff 仍有余量
|
||||
- `GFramework.Cqrs.Benchmarks` 作为 benchmark 基础设施项目,必须持续排除在 NuGet / GitHub Packages 发布集合之外
|
||||
- `GFramework.Cqrs.Benchmarks` 现已覆盖 request steady-state、pipeline 数量矩阵、startup、request/stream generated invoker,以及 request handler `Singleton / Transient` 生命周期矩阵
|
||||
- `GFramework.Cqrs.Benchmarks` 当前以 NuGet 方式引用 `Mediator.Abstractions` / `Mediator.SourceGenerator` `3.0.2`;`ai-libs/Mediator` 只保留为本地源码/README 对照资料,不再参与 benchmark 项目编译
|
||||
- 当前 request steady-state benchmark 已形成 baseline / `Mediator` / `MediatR` / `GFramework.Cqrs` 四方对照:最新约 `5.608 ns / 32 B`、`5.445 ns / 32 B`、`57.071 ns / 232 B`、`64.825 ns / 32 B`
|
||||
- 当前 request lifetime benchmark 已继续收敛:`Singleton` 下 `GFramework.Cqrs` 最新约 `69.275 ns / 32 B`,`Transient` 下约 `74.301 ns / 56 B`;相较 `RP-104` 前的 `73.005 ns / 32 B` 与 `74.757 ns / 56 B` 仍维持同一收敛区间
|
||||
- 当前 request lifetime benchmark 已对齐 generated-provider 宿主路径:`Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR` 约为 `5.012 ns / 32 B`、`49.612 ns / 32 B`、`51.796 ns / 232 B`;`Transient` 下约为 `3.962 ns / 32 B`、`50.480 ns / 56 B`、`50.284 ns / 232 B`
|
||||
- 当前 request pipeline benchmark 已改为与默认 request steady-state 相同的 generated-provider 宿主接线路径:`0 pipeline` 约 `64.755 ns / 32 B`,`1 pipeline` 约 `353.141 ns / 536 B`,`4 pipeline` 约 `555.083 ns / 896 B`
|
||||
- 当前 stream steady-state benchmark 也已切到 generated-provider 宿主接线路径:baseline 约 `5.535 ns / 32 B`、`MediatR` 约 `59.499 ns / 232 B`、`GFramework.Cqrs` 约 `66.778 ns / 32 B`
|
||||
- 当前 stream lifetime benchmark 已补齐 `Singleton / Transient` 两档矩阵,并沿用 generated-provider 宿主接线:`Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR` 约 `80.144 ns / 137.515 ns / 229.242 ns`,`Transient` 下约 `77.198 ns / 144.998 ns / 228.185 ns`
|
||||
- 当前 stream lifetime benchmark 已更新为 `Observation=FirstItem / DrainAll` 双口径:`Singleton + FirstItem` 下 baseline / generated / reflection / `MediatR` 约为 `48.704 ns / 216 B`、`94.629 ns / 216 B`、`95.417 ns / 216 B`、`152.886 ns / 608 B`;`Singleton + DrainAll` 下约为 `73.335 ns / 280 B`、`118.860 ns / 280 B`、`119.632 ns / 280 B`、`205.629 ns / 672 B`
|
||||
- `Transient + FirstItem` 下 baseline / reflection / generated / `MediatR` 约为 `48.293 ns / 216 B`、`97.628 ns / 240 B`、`100.011 ns / 240 B`、`154.149 ns / 608 B`;`Transient + DrainAll` 下约为 `78.466 ns / 280 B`、`124.174 ns / 304 B`、`116.780 ns / 304 B`、`220.040 ns / 672 B`
|
||||
- 本轮已验证旧 benchmark 劣化的两个主热点:`0 pipeline` 场景下仍解析空行为列表,以及容器查询热路径在 debug 禁用时仍构造日志字符串;两者收口后,`GFramework.Cqrs` request 路径不再出现额外数百字节分配
|
||||
- `HasRegistration(Type)` 现在只把“同一服务键已注册”或“开放泛型服务键可闭合到目标类型”视为命中,不再把“仅以具体实现类型自注册”的行为误判为接口服务已注册;该语义与 `Get(Type)` / `GetAll(Type)` 已重新对齐
|
||||
- `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 已同步适配 `HasRegistration(Type)` fast-path,避免 strict mock 因缺少新调用配置而在上下文失败语义断言前提前抛出 `Moq.MockException`
|
||||
@ -156,6 +138,7 @@ CQRS 迁移与收敛。
|
||||
## 当前风险
|
||||
|
||||
- 当前 `_requestBehaviorPresenceCache` 依赖“同一 dispatcher 生命周期内,request pipeline 行为注册在容器冻结后保持稳定”这一约束;若未来引入运行时动态增删 request behavior 的模型,需要重新评估这类实例级 presence cache 的失效策略
|
||||
- 当前 `_streamBehaviorPresenceCache` 也依赖“同一 dispatcher 生命周期内,stream pipeline 行为注册在容器冻结后保持稳定”这一约束;若后续引入运行期动态增删 stream behavior 或按 scope 改写可见性的模型,需要同步设计失效策略,而不能继续假设实例级缓存永久有效
|
||||
- 标准架构启动路径现在已经有“自定义 notification publisher 不被默认顺序策略短路”的集成回归;但若后续再引入第三种仓库内置策略或新的启动快捷入口,仍需要同步补这条生产路径验证,不能只看 `CqrsTestRuntime` 测试宿主
|
||||
- 顶层 `GFramework.sln` / `GFramework.csproj` 在 WSL 下仍可能受 Windows NuGet fallback 配置影响,完整 solution 级验证成本高于模块级验证
|
||||
- 若后续新增 benchmark / example / tooling 项目但未同步校验发布面,solution 级 `dotnet pack` 仍可能在 tag 发布前才暴露异常包
|
||||
@ -172,10 +155,30 @@ CQRS 迁移与收敛。
|
||||
- stream pipeline 当前只在“单次建流”层面包裹 handler 调用;若后续需要 per-item 拦截、元素级重试或流内 metrics 聚合,仍需额外设计更细粒度 contract,而不是把本轮 seam 直接等同于元素级 middleware
|
||||
- `PR #339` 在 GitHub 上仍有 1 个已本地失效但未 resolve 的 stale test-thread;若后续 head 再次变化,需要重新抓取 latest-head review 确认未解决线程是否收敛
|
||||
- 若后续继续依赖 `HasRegistration(Type)` 做热路径短路,新增测试替身或 strict mock 时必须同步配置该调用,否则容易在真正业务断言之前被 mock 框架短路成环境性失败
|
||||
- `PR #344` 当前 latest-head review 仍需等待新 commit 推送后的 GitHub 重新索引;在远端 thread 状态刷新前,不应仅凭现有 open-thread 计数判断本轮修复未生效
|
||||
- `PR #345` 当前 latest-head review 仍以 CodeRabbit review body 的 `4` 条 actionable comments 为主;最新本地复核后,仍成立的问题集中在 `AGENTS.md` 术语说明、`StreamLifetimeBenchmarks` 的取消/文档细节,以及 benchmark README / `ai-plan` 恢复入口漂移
|
||||
|
||||
## 最近权威验证
|
||||
|
||||
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- 备注:覆盖 `StreamLifetimeBenchmarks` 的代码收口与 benchmark `README` 同步后的最小 Release build
|
||||
- `python3 scripts/license-header.py --check --paths AGENTS.md GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`
|
||||
- 结果:通过
|
||||
- `git --git-dir=<repo>/.git/worktrees/GFramework-cqrs --work-tree=. diff --shortstat d85828c5...HEAD`
|
||||
- 结果:通过
|
||||
- 备注:当前分支相对 `origin/main` 的累计 diff 为 `21 files changed, 1344 insertions(+), 194 deletions(-)`
|
||||
|
||||
- `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 "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过
|
||||
- 备注:宿主已对齐 generated-provider 路径;`Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR` 约为 `5.012 ns / 32 B`、`49.612 ns / 32 B`、`51.796 ns / 232 B`;`Transient` 下约为 `3.962 ns / 32 B`、`50.480 ns / 56 B`、`50.284 ns / 232 B`
|
||||
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*StreamLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过
|
||||
- 备注:已产出 `baseline / GFramework reflection / GFramework generated / MediatR` 四方矩阵;`Singleton` 下约为 `79.602 / 120.553 / 111.547 / 208.381 ns`,`Transient` 下约为 `76.351 / 119.632 / 129.166 / 213.420 ns`
|
||||
- `python3 scripts/license-header.py --check --paths ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`
|
||||
- 结果:通过
|
||||
|
||||
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs/Internal/CqrsDispatcher.cs GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs GFramework.Cqrs.Tests/Cqrs/CqrsNotificationPublisherTests.cs GFramework.Cqrs.Tests/Cqrs/NotificationPublisherRegistrationExtensionsTests.cs GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`
|
||||
- 结果:通过
|
||||
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
|
||||
|
||||
@ -2,6 +2,121 @@
|
||||
|
||||
## 2026-05-09
|
||||
|
||||
### 阶段:PR #345 latest-head review 收口(CQRS-REWRITE-RP-128)
|
||||
|
||||
- 使用 `$gframework-pr-review` 抓取当前分支对应的 `PR #345`,确认 latest-head 没有新的 unresolved review thread,但 CodeRabbit 最新 review body 仍保留 `4` 条 actionable comments
|
||||
- 本轮本地复核后接受并修复的反馈收敛到四类:
|
||||
- `AGENTS.md` 的 multi-agent budget 术语缺少明确释义,影响新贡献者理解 stop condition
|
||||
- `StreamLifetimeBenchmarks` 的 `ConsumeFirstItemAsync(...)` 未显式向枚举器传播 `CancellationToken`,且 `[EnumeratorCancellation]` 仍使用全限定名
|
||||
- `StreamLifetimeBenchmarks` 类级 `<remarks>` 尚未解释 `FirstItem / DrainAll` 观测维度的取舍
|
||||
- `GFramework.Cqrs.Benchmarks/README.md` 与 `ai-plan/public/cqrs-rewrite/todos/**` 仍保留治理型 `RP-127` 表述、过期 `PR #344` 锚点与旧 branch diff 数字
|
||||
- 本轮验证计划保持最小化:
|
||||
- 代码路径用 `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` 证明 benchmark 工程仍可编译
|
||||
- 文档与 tracking 更新后补跑 `python3 scripts/license-header.py --check --paths AGENTS.md GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`
|
||||
- 本轮验证结果:
|
||||
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `python3 scripts/license-header.py --check --paths AGENTS.md GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`
|
||||
- 结果:通过
|
||||
- 当前分支相对 `origin/main` (`d85828c5`) 的累计 branch diff 已复核为 `21 files / 1344 insertions / 194 deletions`,后续 active tracking 与 trace 均以这组数字为准
|
||||
- 下一恢复点:
|
||||
- 推送本轮 commit 后,再次运行 `$gframework-pr-review` 复核 `PR #345` latest-head review body 是否已收敛
|
||||
- 若 stream lifetime 后续仍要继续压热路径,优先恢复 `Transient + FirstItem` 的小幅差值复核,而不是重新展开已收口的 `README` / `ai-plan` 漂移问题
|
||||
|
||||
### 阶段:stream lifetime 观测维度补齐与 generated binding 强类型缓存(CQRS-REWRITE-RP-127)
|
||||
|
||||
- 延续 `$gframework-batch-boot 50`,在 `RP-126` 已建立四方 stream lifetime 口径后,本轮改用多 worker wave 同时推进三个互不冲突切片:
|
||||
- `benchmark-only`:为 `StreamLifetimeBenchmarks` 增加 `FirstItem / DrainAll` 观测维度,并收口 `MA0004`
|
||||
- `runtime-only`:把 `CqrsDispatcher.CreateStream(...)` 的 stream dispatch binding 改成按 `TResponse` 强类型缓存,避免 generated lane 在热路径上继续通过 `object -> IAsyncEnumerable<TResponse>` 桥接
|
||||
- `docs / ai-plan`:把 `RP-126` 的旧恢复结论更新为本轮实测结果,并给出下一恢复入口
|
||||
- 本轮主线程验收与修正:
|
||||
- 首次并行验证时,`dotnet test` 与 `dotnet build` 同跑触发 `MSB3030` 输出争用;已按仓库规则改为串行重跑同一命令,并以串行结果为权威
|
||||
- runtime worker 初版在 `CqrsDispatcher.cs` 留下一个 `StreamInvoker<TResponse>` 方法组绑定编译错误;主线程已局部修正为显式 lambda 适配,未改变预期语义
|
||||
- 经修正后,generated lane 不再出现 “incompatible invoker signature” 运行时异常,`StreamLifetimeBenchmarks` 16 个 case 全部通过
|
||||
- 本轮验证:
|
||||
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `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~CqrsGeneratedRequestInvokerProviderTests"`
|
||||
- 结果:通过,`31/31` passed
|
||||
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*StreamLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过
|
||||
- 备注:`Transient + FirstItem` 下 generated 约 `100.011 ns / 240 B`、reflection 约 `97.628 ns / 240 B`;`Transient + DrainAll` 下 generated 约 `116.780 ns / 304 B`、reflection 约 `124.174 ns / 304 B`
|
||||
- 本轮结论:
|
||||
- `FirstItem / DrainAll` 双观测维度把“建流到首个元素的瞬时成本”和“完整枚举总成本”拆开后,`Transient` 场景下的 generated lane 已不再呈现统一的反向退化
|
||||
- 当前仍保留的差值集中在 `Transient + FirstItem`,规模约 `2.4 ns`,明显小于 `RP-126` 的旧结论;而 `Transient + DrainAll` 已转为 generated 领先 reflection
|
||||
- 当前分支相对 `origin/main` 的累计 branch diff 已到 `21 files`(`1231 insertions / 181 deletions`),仍低于 `$gframework-batch-boot 50` 阈值;但主线程已接近当前回合的安全上下文预算,因此在本轮自然边界停止,不继续开下一波 worker
|
||||
- 下一恢复点:
|
||||
- 若继续 benchmark 线,先从 `Transient + FirstItem` 的小幅差值恢复,并用 `StreamInvokerBenchmarks` 复核 generated lane 的常量成本收益是否仍成立;若差值不稳定,再考虑把下一批切到 `Mediator` concrete runtime 的 stream lifetime 对照
|
||||
- 若切回 runtime 线,则以 `b7fa3eee` 与本轮 `StreamLifetimeBenchmarks` 双口径结果作为后续回归基线
|
||||
|
||||
### 阶段:stream lifetime 对照口径补齐(CQRS-REWRITE-RP-126)
|
||||
|
||||
- 延续 `$gframework-batch-boot 50`,在 `RP-125` 先把 request lifetime benchmark 宿主对齐到 generated-provider 路径后,本轮继续补齐 stream 生命周期矩阵的当前对照口径,不回到 runtime 或测试代码
|
||||
- 本轮主线程决策:
|
||||
- 只修改 `GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs` 与 `StreamLifetimeBenchmarks.cs`,不扩散到 `GFramework.Cqrs` runtime、README 或 docs
|
||||
- 将 stream lifetime 的 GFramework reflection、GFramework generated 与 `MediatR` 请求/响应/handler 类型拆开,避免不同宿主继续共用同一 stream 合同而污染对照语义
|
||||
- 将 generated registry 收口为只绑定 `GeneratedBenchmarkStreamRequest/Response` 这一条 generated lane,避免静态 dispatcher cache 把 reflection 与 generated 结果混在一起
|
||||
- 本轮验证:
|
||||
- `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 "*StreamLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过
|
||||
- 备注:`Singleton` 下 baseline / generated / reflection / `MediatR` 约为 `79.602 ns / 280 B`、`111.547 ns / 280 B`、`120.553 ns / 280 B`、`208.381 ns / 672 B`;`Transient` 下 baseline / reflection / generated / `MediatR` 约为 `76.351 ns / 280 B`、`119.632 ns / 304 B`、`129.166 ns / 304 B`、`213.420 ns / 672 B`
|
||||
- 本轮结论:
|
||||
- 当前 stream 生命周期矩阵已经具备可直接比较 `baseline / reflection / generated / MediatR` 的四方口径,后续恢复时无需再回头拼接不同批次的 stream 生命周期数据
|
||||
- 当前短跑下,generated lane 在 `Singleton` 档优于 reflection,但在 `Transient` 档仍慢于 reflection,差值约为 `9.5 ns` 与 `24 B`;这里先只把它记录为 benchmark 观察,不把它放大成更宽泛的 runtime 优劣结论
|
||||
- 当前已提交分支相对 `origin/main`(`d85828c5`, `2026-05-09 12:25:41 +0800`)的累计 branch diff 已到 `10 files`(`556 insertions / 75 deletions`),仍远低于 `$gframework-batch-boot 50` 的 `50 files` stop condition
|
||||
- 下一恢复点:
|
||||
- 若继续 benchmark 线,优先从 `Stream_GFrameworkGenerated` 与 `Stream_GFrameworkReflection` 的 `Transient` 差值恢复,先确认该差值是否稳定,再决定继续压 generated 宿主的瞬时解析成本,或单开 `Mediator` concrete runtime 的 stream lifetime 对照批次;若切回 runtime 线,则以 `RP-126` 的四方矩阵作为后续性能回归基线
|
||||
|
||||
### 阶段:request lifetime generated-provider 宿主对齐(CQRS-REWRITE-RP-125)
|
||||
|
||||
- 延续 `$gframework-batch-boot 50`,在 `RP-124` 收口 stream behavior presence cache 后,本轮先不继续改 dispatcher,而是回头补齐 request lifetime benchmark 宿主路径,让生命周期矩阵与当前默认 request steady-state 的 generated-provider 口径重新对齐
|
||||
- 本轮主线程决策:
|
||||
- 只修改 `GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestLifetimeBenchmarkRegistry.cs` 与 `RequestLifetimeBenchmarks.cs`
|
||||
- 为 request lifetime 补入 handwritten generated registry,只暴露最小 generated request descriptor,再由 benchmark 主体显式控制 `Singleton / Transient` handler 生命周期
|
||||
- 在 setup / cleanup 统一清理 dispatcher cache,避免不同生命周期矩阵之间共享静态缓存而污染结果
|
||||
- 本轮验证:
|
||||
- `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 "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过
|
||||
- 备注:`Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR` 约为 `5.012 ns / 32 B`、`49.612 ns / 32 B`、`51.796 ns / 232 B`;`Transient` 下约为 `3.962 ns / 32 B`、`50.480 ns / 56 B`、`50.284 ns / 232 B`
|
||||
- 本轮结论:
|
||||
- request lifetime 宿主现已与当前 generated-provider request 宿主保持一致,后续不再需要把旧生命周期矩阵结果与 generated steady-state 数据做“跨宿主”比较
|
||||
- 当前短跑下,`Singleton` 仍稳定快于 `MediatR`,`Transient` 已缩到基本持平但仍略慢;这给下一步 stream 线 benchmark 同步提供了更干净的 request 基线
|
||||
- 下一恢复点:
|
||||
- 继续补齐 `StreamLifetimeBenchmarks` 的当前对照口径,把 `baseline / reflection / generated / MediatR` 四方生命周期矩阵补完整
|
||||
|
||||
### 阶段:stream behavior presence cache(CQRS-REWRITE-RP-124)
|
||||
|
||||
- 延续 `$gframework-batch-boot 50`,在 `RP-123` 收口 notification publisher review 线程后,继续把 `CqrsDispatcher` 的 stream 热路径与 `RP-122` 的 request hot path 对齐,选择“缓存 `CreateStream(...)` 的 behavior presence 判定”这一条最小且可验证的 runtime 切片
|
||||
- 本轮主线程决策:
|
||||
- 仅修改 `GFramework.Cqrs/Internal/CqrsDispatcher.cs`、`GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs`,并新增 `DispatcherZeroPipelineStreamRequest/Handler` 测试桩,不扩散到新的公开 API、中文文档或额外 benchmark 宿主实现
|
||||
- 为 `CqrsDispatcher` 新增实例级 `_streamBehaviorPresenceCache`,按闭合 `IStreamPipelineBehavior<,>` 服务类型缓存当前 dispatcher 容器中的服务可见性,让 `CreateStream(...)` 在 steady-state 下不再重复调用 `HasRegistration(Type)`
|
||||
- 在 `CqrsDispatcherCacheTests` 新增 `Dispatcher_Should_Cache_Stream_Behavior_Presence_Per_Dispatcher_Instance()`,显式锁住“同容器共享同一 dispatcher/cache、独立容器不共享”的实例级边界,并把缓存值与容器实际 `HasRegistration(...)` 语义对齐,避免测试再依赖夹具对某个具体 stream 类型的错误可见性假设
|
||||
- 本轮验证:
|
||||
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs/Internal/CqrsDispatcher.cs GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs GFramework.Cqrs.Tests/Cqrs/DispatcherZeroPipelineStreamRequest.cs GFramework.Cqrs.Tests/Cqrs/DispatcherZeroPipelineStreamHandler.cs`
|
||||
- 结果:通过
|
||||
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CqrsDispatcherCacheTests"`
|
||||
- 结果:通过,`12/12` passed
|
||||
- `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 "*StreamLifetimeBenchmarks.Stream_GFrameworkCqrs*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过
|
||||
- 备注:`Singleton` 约 `107.241 ns / 240 B`,`Transient` 约 `119.434 ns / 264 B`
|
||||
- `git diff --check`
|
||||
- 结果:通过
|
||||
- 备注:仅剩 `GFramework.sln` 的历史 CRLF 提示,无本轮新增 diff 格式问题
|
||||
- 下一恢复点:
|
||||
- 推送本轮 commit 后,再次运行 `$gframework-pr-review` 复核 `PR #344` latest-head thread 是否已收敛;若 review 已清空,则下一批优先补完整 `StreamLifetimeBenchmarks` 三方对照,再决定继续压 stream 零管道常量路径还是切回 request `Transient` 热点
|
||||
|
||||
### 阶段:PR #344 latest-head review 收尾(CQRS-REWRITE-RP-123)
|
||||
|
||||
- 使用 `$gframework-pr-review` 重新抓取当前分支 PR,确认当前 worktree 对应 `PR #344`,latest-head 仍有 `CodeRabbit 2` / `Greptile 1` open thread
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user