Skip to content

Commit 130d5c3

Browse files
author
developerworks
committed
Fix yaml service example event handling and document ChildSpec paths
- Fix yaml service example: skip first interval tick to delay first output by one second - Switch from batched polling to immediate event receive in yaml service example - Add ChildSpec construction paths comparison table to the manual (EN/ZH) - Fix brainstorming session frontmatter formatting and table alignment
1 parent fe9d221 commit 130d5c3

4 files changed

Lines changed: 91 additions & 27 deletions

File tree

_bmad-output/brainstorming/brainstorming-session-2026-06-02-112930.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
---
22
stepsCompleted: [1, 2]
33
inputDocuments: []
4-
session_topic: 'Supervisor(监督器), Service(服务), Job(一次性任务) 等角色接入契约'
5-
session_goals: '方向清单, API(应用程序接口) 草案, specification(规格) 文档, 实现任务拆分'
6-
selected_approach: 'Progressive Technique Flow(渐进技巧流程)'
4+
session_topic: "Supervisor(监督器), Service(服务), Job(一次性任务) 等角色接入契约"
5+
session_goals: "方向清单, API(应用程序接口) 草案, specification(规格) 文档, 实现任务拆分"
6+
selected_approach: "Progressive Technique Flow(渐进技巧流程)"
77
techniques_used:
88
- What If Scenarios(如果场景)
99
- Morphological Analysis(形态分析)
@@ -16,7 +16,7 @@ ideas_generated:
1616
- macro entry(宏入口) 默认使用形态 2, 即在 impl block(实现块) 上标记角色, 并通过生命周期方法表达角色流程.
1717
- macro entry(宏入口) 的形态 1 和形态 3 记录为 optional implementation(可选实现), 但不作为默认推荐路径.
1818
- 过程宏需要独立 proc-macro crate(过程宏包), 因此项目需要评估从单 crate(包) 结构升级为 workspace(工作区) 结构.
19-
context_file: ''
19+
context_file: ""
2020
---
2121

2222
# 头脑风暴会话结果
@@ -65,13 +65,13 @@ context_file: ''
6565

6666
当前项目有 5 个 `TaskRole`(任务角色) variants(变体), 这些角色都必须纳入 role contract(角色契约) 工作范围.
6767

68-
| Role(角色) | Current meaning(当前语义) | Contract focus(契约重点) | Existing anchors(现有锚点) |
69-
| --- | --- | --- | --- |
70-
| `Service` | Long-running service(长期运行服务), 需要保持在线. | 必须显式表达 initialization(初始化), readiness(就绪), heartbeat(心跳), long-running loop(长期运行循环), cancellation(取消), cooperative shutdown(协作关闭). | `TaskRole::Service`, `ChildSpecBuilder::service`, `examples/service/service_task.rs`, `examples/step_02_supervisor_with_service/main.rs`. |
71-
| `Worker` | Bounded background worker(有界后台任务), 完成有限工作后停止. | 必须显式表达 batch work(批量工作) 或 bounded work(有界工作), success completion(成功完成), retry on failure(失败重试), completion event(完成事件). | `TaskRole::Worker`, `ChildSpecBuilder::worker`, `examples/worker/worker_task.rs`. |
72-
| `Job` | One-shot job(一次性任务), 成功运行一次后保持停止. | 必须显式表达 one-shot execution(一次性执行), success stop(成功后停止), failure retry budget(失败重试预算), timeout escalation(超时升级), no permanent restart(禁止永久重启语义). | `TaskRole::Job`, `ChildSpecBuilder::job`, `examples/job/job_task.rs`, `semantic_conflicts_for_child`. |
73-
| `Sidecar` | Auxiliary sidecar(辅助边车), 绑定到 primary service(主服务). | 必须显式表达 primary binding(主任务绑定), linked lifecycle(绑定生命周期), dependency(依赖), own restart scope(自身重启范围), no sidecar chain(禁止边车链). | `TaskRole::Sidecar`, `SidecarConfig`, `ChildSpecBuilder::sidecar`, `examples/sidecar/sidecar_task.rs`, `validate_sidecar_local`. |
74-
| `Supervisor` | Nested supervisor unit(嵌套监督器单元), 外层把它当成一个受监督单元. | 必须把 role contract(角色契约) 与 `TaskKind::Supervisor`(监督器执行种类) 分开, 并显式表达 nested tree ownership(嵌套树归属), readiness(就绪), restart budget(重启预算), cancellation(取消), shutdown propagation(关闭传播). | `TaskRole::Supervisor`, `ChildSpecBuilder::supervisor`, `examples/supervisor/supervisor_task.rs`, `examples/task_role_demo.rs`. |
68+
| Role(角色) | Current meaning(当前语义) | Contract focus(契约重点) | Existing anchors(现有锚点) |
69+
| ------------ | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
70+
| `Service` | Long-running service(长期运行服务), 需要保持在线. | 必须显式表达 initialization(初始化), readiness(就绪), heartbeat(心跳), long-running loop(长期运行循环), cancellation(取消), cooperative shutdown(协作关闭). | `TaskRole::Service`, `ChildSpecBuilder::service`, `examples/service/service_task.rs`, `examples/step_02_supervisor_with_service/main.rs`. |
71+
| `Worker` | Bounded background worker(有界后台任务), 完成有限工作后停止. | 必须显式表达 batch work(批量工作) 或 bounded work(有界工作), success completion(成功完成), retry on failure(失败重试), completion event(完成事件). | `TaskRole::Worker`, `ChildSpecBuilder::worker`, `examples/worker/worker_task.rs`. |
72+
| `Job` | One-shot job(一次性任务), 成功运行一次后保持停止. | 必须显式表达 one-shot execution(一次性执行), success stop(成功后停止), failure retry budget(失败重试预算), timeout escalation(超时升级), no permanent restart(禁止永久重启语义). | `TaskRole::Job`, `ChildSpecBuilder::job`, `examples/job/job_task.rs`, `semantic_conflicts_for_child`. |
73+
| `Sidecar` | Auxiliary sidecar(辅助边车), 绑定到 primary service(主服务). | 必须显式表达 primary binding(主任务绑定), linked lifecycle(绑定生命周期), dependency(依赖), own restart scope(自身重启范围), no sidecar chain(禁止边车链). | `TaskRole::Sidecar`, `SidecarConfig`, `ChildSpecBuilder::sidecar`, `examples/sidecar/sidecar_task.rs`, `validate_sidecar_local`. |
74+
| `Supervisor` | Nested supervisor unit(嵌套监督器单元), 外层把它当成一个受监督单元. | 必须把 role contract(角色契约) 与 `TaskKind::Supervisor`(监督器执行种类) 分开, 并显式表达 nested tree ownership(嵌套树归属), readiness(就绪), restart budget(重启预算), cancellation(取消), shutdown propagation(关闭传播). | `TaskRole::Supervisor`, `ChildSpecBuilder::supervisor`, `examples/supervisor/supervisor_task.rs`, `examples/task_role_demo.rs`. |
7575

7676
### 角色与执行种类边界
7777

@@ -219,7 +219,7 @@ rust-supervisor/
219219

220220
#### 形态 1: 函数标记
221221

222-
形态 1 在 free function(自由函数) 上使用 role macro(角色宏). 这个形态最短, 适合极小示例或快速迁移, 但它会把 lifecycle(生命周期) 压缩到一个函数里, 因此不适合作为主要教学路径.
222+
形态 1 在 free function(自由函数) 上使用 role macro(角色宏). 这个形态最短, 适合极小示例或快速迁移, 但它会把 lifecycle(生命周期) 压缩到一个函数里, 因此不适合作为首要路径.
223223

224224
```rust
225225
#[service(id = "quote-service", name = "Quote Service")]

rust-supervisor/examples/yaml_create_supervisor_with_service/main.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ async fn run_service(ctx: TaskContext, events: mpsc::UnboundedSender<String>) ->
7878
let mut interval = tokio::time::interval(Duration::from_secs(1));
7979
let cancellation_token = ctx.cancellation_token();
8080
let mut tick = 0_u64;
81+
// Skip the immediate first interval tick so running output starts after one second.
82+
interval.tick().await;
8183
loop {
8284
tokio::select! {
8385
_ = cancellation_token.cancelled() => {
@@ -168,11 +170,9 @@ async fn main() -> ExampleResult {
168170
config_path.display()
169171
);
170172

171-
// Build a periodic observation interval for service facts.
172-
let mut observation_interval = tokio::time::interval(Duration::from_secs(1));
173173
// Keep the example alive until the operator sends Ctrl+C.
174174
loop {
175-
// Wait for either an operator signal or an observation tick.
175+
// Wait for either an operator signal or the next service fact.
176176
tokio::select! {
177177
signal = tokio::signal::ctrl_c() => {
178178
// Convert Ctrl+C errors into supervisor errors.
@@ -186,9 +186,13 @@ async fn main() -> ExampleResult {
186186
// Leave the long-running example loop.
187187
break;
188188
}
189-
_ = observation_interval.tick() => {
190-
// Print any service facts that are already available.
191-
drain_service_events(&mut service_events);
189+
event = service_events.recv() => {
190+
// Convert an unexpected event channel close into a supervisor error.
191+
let event = event.ok_or_else(|| {
192+
SupervisorError::fatal_config("service event channel closed while running")
193+
})?;
194+
// Print the service fact immediately instead of batching it by observation tick.
195+
println!("yaml_service {event}");
192196
}
193197
}
194198
}

rust-supervisor/manual/en/child-spec.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,36 @@ When a child is added dynamically, `PendingChild` keeps **both** the `declaratio
5656

5757
Shared enums and config structs such as `RestartPolicy`, `TaskKind`, and `HealthCheckConfig` are defined in `child.rs`. `ChildDeclaration` **reuses** them to avoid parallel type trees. The **top-level containers** remain separate: declaration container vs specification container.
5858

59+
## ChildSpec construction paths
60+
61+
The repository has 6 paths that construct `ChildSpec`. They serve different use cases and should not be collapsed into one entry point.
62+
63+
| Path | Typical entry | Use case | Validation behavior |
64+
| --- | --- | --- | --- |
65+
| Builder | `ChildSpecBuilder::worker`, `service`, `job`, `sidecar`, `supervisor`, `new` | Direct runtime spec construction in Rust code | `build()` calls `ChildSpec::validate()` |
66+
| Worker convenience function | `ChildSpec::worker(...)` | Worker default bundle only | Delegates to `ChildSpecBuilder::worker(...).build()` |
67+
| Declaration conversion | `TryFrom<ChildDeclaration> for ChildSpec` | YAML config, RPC payloads, dynamic child adds | `validate_child_declaration` runs before conversion, and supervisor-level validation catches final issues |
68+
| Role template | `ServiceTemplate::child_spec`, `JobTemplate::child_spec`, and related role templates | Caller already implemented role traits but does not want to hand-build adapters and specs | Calls the matching `ChildSpecBuilder` internally |
69+
| Macro-generated helper | `child_spec()` generated by `#[service]`, `#[worker]`, `#[job]`, `#[sidecar]`, and `#[supervisor_role]` | Default role contract entry path | Generated code calls the matching `ChildSpecBuilder` |
70+
| Serde | `serde_json::from_value::<ChildSpec>(...)` | Mainly tests for deserialization defaults and invalid enum handling | Does not pass through the builder, so callers must validate before runtime use or rely on later spec validation |
71+
72+
Important boundaries:
73+
74+
- `ChildSpecBuilder::build()` is the main exit for Rust code construction paths.
75+
- Configuration and RPC should not accept `ChildSpec` directly. They should accept `ChildDeclaration` first, then convert it into `ChildSpec`.
76+
- Role templates and macros are not new runtime models. They turn role lifecycle objects into adapters, then call `ChildSpecBuilder` to produce specs.
77+
- Serde can construct `ChildSpec` because `ChildSpec` derives `Deserialize`. That path does not automatically call `ChildSpecBuilder::build()`.
78+
79+
Adjacent paths that do not construct a `ChildSpec`:
80+
81+
| Entry | Why it is not a `ChildSpec` construction path |
82+
| --- | --- |
83+
| `SupervisorSpec::root(Vec<ChildSpec>)` | It accepts already constructed child specs and builds a supervisor spec |
84+
| `SupervisorSpecBuilder::root(Vec<ChildSpec>)` | It wraps supervisor spec construction and does not create an individual child spec |
85+
| `ConfigState::to_supervisor_spec()` | It assembles a supervisor spec from the `Vec<ChildSpec>` already stored in `ConfigState` |
86+
| `bind_child_factory(...)` | It binds a task factory to an existing `ChildSpec` and does not create a new one |
87+
| `clone()` | It copies an existing `ChildSpec` instead of generating one from an input model |
88+
5989
## How to remember them
6090

6191
- Writing config, handling API input, validating declarations -> think **`ChildDeclaration`**

rust-supervisor/manual/zh/child-spec.md

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,36 @@ YAML / add_child RPC
5454

5555
`RestartPolicy``TaskKind``HealthCheckConfig`**公共枚举/配置结构**定义在 `child.rs`, `ChildDeclaration` **复用**它们, 避免两套平行类型. 但**顶层容器**仍是两个: 声明容器 vs 规格容器.
5656

57+
## `ChildSpec` 构造路径总览
58+
59+
仓库中构造 `ChildSpec`(子任务规格) 的路径可以分成 6 类. 这些路径面向不同使用场景, 不应该混成一个入口.
60+
61+
| 路径 | 典型入口 | 适用场景 | 校验方式 |
62+
| ----------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
63+
| Builder(构建器) | `ChildSpecBuilder::worker`, `service`, `job`, `sidecar`, `supervisor`, `new` | Rust(编程语言) 代码里直接拼运行时规格 | `build()` 调用 `ChildSpec::validate()` |
64+
| Worker 便捷函数 | `ChildSpec::worker(...)` | 只需要 worker(后台任务) 默认值包 | 内部委托 `ChildSpecBuilder::worker(...).build()` |
65+
| 声明转换 | `TryFrom<ChildDeclaration> for ChildSpec` | YAML(配置文件), RPC(远程过程调用), dynamic add child(动态添加子任务) | 转换前走 `validate_child_declaration`, 转换后由 supervisor(监督器) 规格校验兜底 |
66+
| Role Template(角色模板) | `ServiceTemplate::child_spec`, `JobTemplate::child_spec`| 已经手写 `ServiceRole`(服务角色特征) 等 trait(特征), 但不想手写 adapter(适配器) 和规格 | 内部调用对应 `ChildSpecBuilder` |
67+
| Macro(宏) 生成 | `#[service]`, `#[worker]`, `#[job]`, `#[sidecar]`, `#[supervisor_role]` 生成的 `child_spec()` | 默认角色接入路径, 使用者只写生命周期方法 | 宏生成代码调用对应 `ChildSpecBuilder` |
68+
| Serde(序列化和反序列化) | `serde_json::from_value::<ChildSpec>(...)` | 主要用于测试反序列化默认值和非法枚举 | 不经过 builder(构建器), 使用前必须显式校验或进入后续规格校验 |
69+
70+
几条关键边界:
71+
72+
- `ChildSpecBuilder::build()` 是 Rust(编程语言) 代码构造路径的主要出口.
73+
- 配置和 RPC(远程过程调用) 不应该直接接收 `ChildSpec`, 应该先接收 `ChildDeclaration`(子任务声明), 再转换为 `ChildSpec`(子任务规格).
74+
- `Role Template`(角色模板) 和 `Macro`(宏) 都不是新的运行时模型. 它们只是把角色生命周期对象装配成 adapter(适配器), 再调用 `ChildSpecBuilder` 生成规格.
75+
- `Serde`(序列化和反序列化) 可以构造 `ChildSpec`, 因为 `ChildSpec` 派生了 `Deserialize`(反序列化特征). 这条路径不会自动调用 `ChildSpecBuilder::build()`.
76+
77+
相邻但不算构造 `ChildSpec` 的路径:
78+
79+
| 入口 | 为什么不算 |
80+
| --------------------------------------------- | ------------------------------------------------------------------------------- |
81+
| `SupervisorSpec::root(Vec<ChildSpec>)` | 它接收已经构造好的子任务规格列表, 只构造 supervisor(监督器) 规格 |
82+
| `SupervisorSpecBuilder::root(Vec<ChildSpec>)` | 它包装 supervisor(监督器) 规格构造, 不创建单个子任务规格 |
83+
| `ConfigState::to_supervisor_spec()` | 它把 `ConfigState` 中已经保存的 `Vec<ChildSpec>` 组装成 supervisor(监督器) 规格 |
84+
| `bind_child_factory(...)` | 它给已有 `ChildSpec` 绑定 factory(任务工厂), 不创建新的 `ChildSpec` |
85+
| `clone()` | 它复制已有 `ChildSpec`, 不从输入模型生成新规格 |
86+
5787
## 怎么记
5888

5989
- 写配置、接 API、做声明校验 -> 想 **`ChildDeclaration`**
@@ -86,19 +116,19 @@ let spec = ChildSpecBuilder::worker(
86116

87117
入口方法:
88118

89-
| 方法 | 用途 |
90-
| ----------------------------------- | ---------------------------------------------------- |
91-
| `ChildSpecBuilder::worker(...)` | 异步或阻塞 worker, 默认值与 `ChildSpec::worker` 一致 |
92-
| `ChildSpecBuilder::service(...)` | 常驻 service(服务), 自动设置 `TaskRole::Service` |
119+
| 方法 | 用途 |
120+
| ----------------------------------- | ------------------------------------------------------ |
121+
| `ChildSpecBuilder::worker(...)` | 异步或阻塞 worker, 默认值与 `ChildSpec::worker` 一致 |
122+
| `ChildSpecBuilder::service(...)` | 常驻 service(服务), 自动设置 `TaskRole::Service` |
93123
| `ChildSpecBuilder::job(...)` | 有限生命周期 job(一次性任务), 自动设置 `TaskRole::Job` |
94-
| `ChildSpecBuilder::sidecar(...)` | sidecar(边车), 自动设置绑定和主子任务依赖 |
95-
| `ChildSpecBuilder::supervisor(...)` | 嵌套 supervisor, 无 factory |
96-
| `ChildSpecBuilder::new(...)` | 最小骨架, 需自行补 `kind``factory` |
124+
| `ChildSpecBuilder::sidecar(...)` | sidecar(边车), 自动设置绑定和主子任务依赖 |
125+
| `ChildSpecBuilder::supervisor(...)` | 嵌套 supervisor, 无 factory |
126+
| `ChildSpecBuilder::new(...)` | 最小骨架, 需自行补 `kind``factory` |
97127

98128
构建出口:
99129

100-
| 方法 | 行为 |
101-
|---|---|
130+
| 方法 | 行为 |
131+
| --------- | ---------------------------------------------------------------- |
102132
| `build()` | 构造后调用 `ChildSpec::validate()`, 失败时返回 `SupervisorError` |
103133

104134
`ChildSpec::worker(...)` 仍可使用, 内部委托 `ChildSpecBuilder::worker(...).build()`, 同样返回 `Result<ChildSpec, SupervisorError>`.

0 commit comments

Comments
 (0)