# RPC 手册

`rs-zero` 的 `rpc` feature 提供 tonic 相关 helper，用于减少项目重复配置。

## 核心能力

- client endpoint 配置。
- request-id interceptor。
- `grpc-timeout` deadline metadata helper。
- 客户端 retry/backoff helper，默认只重试 `Unavailable`、`DeadlineExceeded`、`ResourceExhausted`。
- memory/static discovery 到 RPC endpoint 的解析适配。
- weighted round-robin endpoint selector。
- streaming wrapper，记录 send/recv、完成状态、timeout 和 tonic code。
- health server helper。
- graceful shutdown 配合 `rs_zero::core::shutdown_signal`。

## 示例

```rust
use rs_zero::core::shutdown_signal;
use rs_zero::rpc::serve_health_with_shutdown;

let addr = "127.0.0.1:50051".parse().unwrap();
serve_health_with_shutdown(addr, shutdown_signal()).await.unwrap();
```

完整示例见 `examples/rpc-hello/src/main.rs`。

## 客户端生产配置

默认 `RpcClientConfig::new` 保持保守行为：不启用 retry、deadline 传播、发现负载均衡或 streaming 观测。需要 go-zero 风格生产默认值时使用 `go_zero_defaults`：

```rust
use rs_zero::rpc::{LoadBalancePolicy, RpcClientConfig};

let config = RpcClientConfig::go_zero_defaults("http://127.0.0.1:50051");

assert!(config.retry.enabled);
assert!(config.deadline.propagate);
assert!(config.streaming.observe);
assert_eq!(config.load_balance.policy, LoadBalancePolicy::WeightedRoundRobin);
```

retry helper 会限制最大尝试次数，并按指数退避执行。需要把重试裁剪到调用总预算时，使用 `DeadlineBudget`：

```rust
use std::time::Duration;
use rs_zero::rpc::{
    RpcRetryConfig,
    deadline::DeadlineBudget,
    retry::run_with_retry_budget,
};

let retry = RpcRetryConfig::go_zero_defaults();
let budget = DeadlineBudget::new(Duration::from_secs(2));

let result = run_with_retry_budget(&retry, &budget, |remaining| async move {
    // 将 remaining 传给下游 request timeout 或 grpc-timeout metadata。
    Ok::<_, tonic::Status>(())
}).await;
```

## deadline 与 metadata

`deadline_interceptor` 可给 tonic request 注入 `grpc-timeout`，已存在 metadata 时不会覆盖：

```rust
use std::time::Duration;
use rs_zero::rpc::deadline_interceptor;

let interceptor = deadline_interceptor(Duration::from_millis(500));
```

手写 adapter 可直接使用 `insert_grpc_timeout` 和 `remaining_timeout_from_metadata`。

## discovery 与负载均衡

启用 `discovery` feature 后，可把 `Discovery` 实例解析为 tonic endpoint：

```rust
use rs_zero::{
    discovery::{InstanceEndpoint, MemoryRegistry, Registry, ServiceInstance},
    rpc::{
        RpcClientConfig,
        balancer::WeightedRoundRobinBalancer,
        discovery::resolve_service_endpoint,
        endpoint_from_rpc_endpoint,
    },
};

let registry = MemoryRegistry::new();
registry.register(
    ServiceInstance::new(
        "greeter",
        "a",
        InstanceEndpoint::new("127.0.0.1", 50051)?,
    )
    .with_weight(2),
).await?;

let selector = WeightedRoundRobinBalancer::new();
let endpoint = resolve_service_endpoint(&registry, &selector, "greeter").await?;
let config = RpcClientConfig::go_zero_defaults(&endpoint.uri);
let tonic_endpoint = endpoint_from_rpc_endpoint(&endpoint, &config)?;
```

当前 selector 保持低基数观测边界：指标和日志只记录 service/method/code，不记录实例 id、IP 或动态 key。

## RPC unary 自动韧性层

`RpcUnaryResilienceLayer` 可挂到 tonic/Tower service，在业务方法未显式调用 `run_unary` 时也能应用 timeout、concurrency、breaker、shedder 和 Redis limiter：

```rust
use rs_zero::rpc::{RpcResilienceLayer, RpcServerConfig, RpcUnaryResilienceLayer};

let config = RpcServerConfig::go_zero_defaults("hello", "127.0.0.1:50051".parse()?);
let resilience = RpcResilienceLayer::new(config.name.clone(), config.resilience.clone());
let layer = RpcUnaryResilienceLayer::new(resilience);
```

生成代码仍保留 `run_unary` helper，便于 skeleton 内直接包裹业务占位。手写 tonic service 推荐使用自动层降低漏接风险。

## streaming 观测边界

`RpcStreamingObserver` 与 `ObservedRecvStream` 是给生成代码或手写 streaming adapter 使用的组合式 wrapper：

```rust
use rs_zero::rpc::{RpcStreamingConfig, streaming::RpcStreamingObserver};

let observer = RpcStreamingObserver::new(
    "chat.Chat",
    "Talk",
    RpcStreamingConfig::go_zero_defaults(),
);

observer.record_send().await;
observer.record_recv().await;
observer.finish::<()>(Ok(())).await;
let snapshot = observer.snapshot().await;
```

`ObservedRecvStream` 可包装 `Stream<Item = Result<T, tonic::Status>>`，`record_stream_send` 可包裹发送动作，`run_observed_stream` 可应用 per-stream timeout。它们不会替代 tonic 生成的 stream 类型；业务代码仍负责处理消息流、取消和 backpressure。

## Service group

需要和 REST 或 worker 同进程运行时，可用 `TonicHealthService` 或 `TonicService` 接入 [`ServiceGroup`](service-group.md)：

```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>>(())
```


## RPC INFO 日志与调用链字段

启用 `observability` feature 后，`RpcResilienceLayer::run_unary` 会在 unary 调用完成时输出 INFO 事件 `rpc unary observed`。生成的 `rzcli rpc gen` 项目默认启用 `rpc`、`resil`、`observability`，因此生成代码中的 unary 方法默认有 RPC 观测日志。

关键字段：

- `rpc.service` / `service`：逻辑 RPC 服务名。
- `rpc.method` / `method`：RPC 方法名，例如 `SayHello` 或 `say_hello`。
- `route`：同 `method`，用于和 HTTP route 统一检索；不再写固定值 `rpc`。
- `request_id`：`x-request-id` metadata。
- `traceparent`：入站或显式传入的 W3C TraceContext。
- `trace_id` / `span_id`：来自合法 `traceparent` 或 OTLP 当前 span。
- `code`：gRPC 状态码，例如 `Ok`、`Unavailable`。

如果只看到 `h2::*` DEBUG 日志，说明当前日志过滤器打开了底层 HTTP/2 debug，但 RPC 业务层没有启用 `observability` 或没有走 `RpcResilienceLayer` / `observe_rpc_unary`。生产建议用类似过滤器减少底层噪音：

```bash
RUST_LOG=info,h2=warn,hyper=warn,tower=warn
```

## API 调 RPC 的调用链追踪

仅靠默认文本日志，最稳定的串联字段是 `request_id`。如果要跨 HTTP -> RPC 关联完整 trace，需要：

1. API 入口启用 REST metrics middleware。
2. API 调 RPC 的 client 使用 `request_id_interceptor()` 传播 request id。REST metrics middleware 会把当前 request id 写入请求 extensions；同一 handler 内创建的 tonic `Request` 可直接由 `request_id_interceptor()` 读取并写入 metadata。若是异步任务、队列消费或手写边界，也可用 `with_rpc_request_id(request_id, async { ... })` 包住 RPC 调用，或向 `tonic::Request` extensions 插入 `RpcRequestId`。
3. 需要 trace id / span id 时，启用 `otlp`，并在 RPC client 加 `trace_context_interceptor()`。
4. RPC server 侧如果有 `tonic::Request<T>`，先保留 metadata，再走 `RpcResilienceLayer::run_unary_with_metadata` 或 `observe_rpc_unary_with_metadata`。`rzcli rpc gen` 会生成 `RpcRequestParts::from_request(request)` 和 `*_with_parts`，避免在观测前 `request.into_inner()` 丢掉 `x-request-id` 与 `traceparent`。

没有启用 OTLP 或没有把 `traceparent` 注入 RPC metadata 时，日志中不会自动出现可串联的跨服务 `trace_id`。这种情况下只能按 `request_id` 或外部网关日志关联。

## RPC codegen

`rzcli rpc gen` 提供 proto 子集解析和 tonic skeleton 生成。使用方式见 [RPC 服务教程](../tutorials/rpc-service.md)。

## 限制

- 不替代 `protoc`。
- 生成 skeleton 默认不包含业务实现。
- 复杂 proto 特性需在应用层接入 prost/tonic build。
- 当前负载均衡先提供 weighted round-robin；P2C、channel pool 和连接预热仍需应用层或后续版本扩展。
- streaming wrapper 提供组合式 send/recv/finish/timeout 观察边界，不强行替代所有 tonic stream 类型。

## 相关测试

- `cargo test --test rpc_integration`
- `cargo test --test rpc_production_integration`
- `cargo test -p rs-zero-cli --test goctl_rpc_compat`
