# Service group 手册

`rs-zero` 的 service group 用于在一个进程内同时运行多个长期服务，例如 REST API、RPC 服务和后台 worker。它参考 go-zero `ServiceGroup` 的使用体验，但按 Rust/Tokio 做成 async lifecycle。

## 核心类型

- `Service`：异步服务生命周期 trait。
- `ServiceGroup`：并发启动一组服务，并在停止信号或服务错误时统一收敛。
- `ServiceGroupConfig`：控制 shutdown timeout 和服务错误处理策略。
- `ServiceGroupHandle`：运行时请求停止 group。
- `ShutdownToken`：传给服务的可克隆停止信号。
- `FnService`：用 async 函数快速包装后台任务。

## 最小示例

```rust
use rs_zero::core::{CoreResult, ServiceGroup};

let mut group = ServiceGroup::new();
group.add_fn("worker", |shutdown| async move {
    loop {
        tokio::select! {
            _ = shutdown.cancelled() => break,
            _ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {
                // do periodic work
            }
        }
    }
    Ok::<(), rs_zero::core::CoreError>(())
});

group.start().await?;
# Ok::<(), rs_zero::core::CoreError>(())
```

## REST 服务接入

`RestService` 可把 `RestServer` 放进 service group：

```rust
use axum::{Router, routing::get};
use rs_zero::{
    core::ServiceGroup,
    rest::{ApiResponse, RestConfig, RestServer, RestService},
};

let router = Router::new().route("/ready", get(|| async {
    ApiResponse::success("ok")
}));
let addr = "127.0.0.1:8080".parse()?;
let rest = RestService::new("api", addr, RestServer::new(RestConfig::default(), router));

let mut group = ServiceGroup::new();
group.add(rest);
group.start().await?;
# Ok::<(), Box<dyn std::error::Error>>(())
```

## RPC 服务接入

`TonicHealthService` 可直接运行 health server。普通 tonic generated service 可用 `TonicService::new` 包装。

```rust
use rs_zero::{
    core::ServiceGroup,
    rpc::TonicHealthService,
};

let rpc = TonicHealthService::new("health-rpc", "127.0.0.1:50051".parse()?);

let mut group = ServiceGroup::new();
group.add(rpc);
group.start().await?;
# Ok::<(), Box<dyn std::error::Error>>(())
```

普通 tonic service：

```rust
use rs_zero::rpc::TonicService;

let generated_service = my_proto::greeter_server::GreeterServer::new(greeter_impl);
let rpc = TonicService::new("greeter-rpc", "127.0.0.1:50051".parse()?, generated_service);
```

## 行为语义

- 所有服务并发启动；不要依赖启动顺序。
- `ServiceGroup::start()` 默认等待 Ctrl-C。
- `start_with_shutdown(future)` 可用于测试或自定义关闭信号。
- 任一服务返回错误时，默认会请求全组 shutdown。
- shutdown 时先广播 `ShutdownToken`，再并发执行每个服务的 `stop()` hook。
- `stop()` 与服务任务退出受 `ServiceGroupConfig::shutdown_timeout` 保护。
- 错误会汇总为 `CoreError::Service`，不会静默吞掉。

## 自定义服务

```rust
use rs_zero::core::{CoreResult, Service, ServiceFuture, ShutdownToken};

struct Worker;

impl Service for Worker {
    fn name(&self) -> &str {
        "worker"
    }

    fn start(&self, shutdown: ShutdownToken) -> ServiceFuture<'_> {
        Box::pin(async move {
            shutdown.cancelled().await;
            Ok(())
        })
    }
}
```

## 生产建议

- 用 service group 管理同进程内的 REST、RPC 和 worker 生命周期。
- worker 必须监听 `ShutdownToken`，不要无限阻塞。
- 长耗时清理放进 `stop()`，并设置合理 `shutdown_timeout`。
- 多服务依赖启动顺序时，先在服务内部做 readiness/health，而不是依赖 group 启动顺序。
- 不要把数据库事务、消息确认等关键一致性只寄托在进程退出 hook 上。

## 相关测试

- `cargo test -p rs-zero --test service_group`
- `cargo test -p rs-zero --test rest_integration rest_service_runs_inside_service_group`
- `cargo test -p rs-zero --test rpc_integration tonic_health_service_runs_inside_service_group`
