[Proposal] Establish a consistent context model across dubbo-go
- Dominant language
- Go
- Stars
- 5k
- Forks
- 1k
- Avg merge
- 2d 8h
- Merged PRs (30d)
- 31
Description
## Problem
dubbo-go exposes `context.Context` throughout the client, invoker, cluster, filter, protocol, and server layers, but the meaning and propagation rules are not consistent across protocols.
Today, the implementation mixes several concerns:
- Go request lifecycle: cancellation and deadlines.
- RPC metadata: attachments, headers, and gRPC metadata.
- Internal invocation attributes.
- Protocol-specific timeout and metadata mechanisms.
- Context values used as an implicit source of RPC attachments.
This makes it difficult to answer basic questions consistently:
- Does caller cancellation reach the remote operation?
- Which timeout wins when `ctx.Deadline()` and an attachment timeout are both present?
- Which context values are local-only and which are serialized onto the wire?
- Should inbound metadata automatically be propagated to downstream RPCs?
- Do Dubbo, Triple, Dubbo3/gRPC, JSON-RPC, and REST preserve the same context semantics?
## Examples
- The client path passes `ctx` to `Invoker.Invoke`, while also reading `ctx.Value(constant.AttachmentKey)` into `Invocation.attachments`.
- Traditional Dubbo primarily uses invocation attachments for timeout and tracing, and its server-side request handling rebuilds a context from `context.Background()`.
- Triple and Dubbo3 convert invocation attachments into outgoing HTTP/gRPC metadata.
- Server paths may represent the same inbound metadata both in the Context and in `Invocation.attachments`.
- Context-to-attachment conversion helpers create a second abstraction for moving data between Context and Invocation.
Related issue: #3557 tracks several concrete cases where request context is dropped via `context.Background()`.
`dubbo-go` 目前不是“所有地方都把 `context` 当作 Go Context 使用”,而是把几种不同概念揉在了一起:
1. Go 的生命周期 Context
2. RPC 跨网络传递的 metadata/attachments
3. Dubbo 内部的 Invocation 属性
4. 协议自己的 timeout/header 机制
所以它的 API 看起来统一:
```go
invoker.Invoke(ctx, invocation)
```
但不同协议对 `ctx` 的实际处理并不一致。
**一次客户端调用的大致流程**
在 [client/client.go:75](/C:/Users/m1346/Desktop/repos/dubbo-go/client/client.go:75) 中:
```go
conn.refOpts.invoker.Invoke(ctx, inv)
```
同时,`generateInvocation` 又把 `ctx.Value(AttachmentKey)` 中的值复制到了 Invocation attachments:
[client/client.go:246](/C:/Users/m1346/Desktop/repos/dubbo-go/client/client.go:246)
```go
attachments := map[string]any{
constant.TimeoutKey: opts.RequestTimeout,
constant.RetriesKey: opts.Retries,
}
```
这意味着:
- `ctx` 本身负责传递取消、deadline、trace 等信息;
- attachment 负责传递 RPC metadata;
- 但用户又可以通过 `ctx.Value` 间接设置 attachment。
这已经和 Go Context 的设计哲学有些冲突,因为 `context.Value` 变成了一个隐式的 RPC 参数入口。
**不同协议的差异**
传统 Dubbo 协议:
[protocol/dubbo/dubbo_invoker.go:209](/C:/Users/m1346/Desktop/repos/dubbo-go/protocol/dubbo/dubbo_invoker.go:209) 中,`ctx` 基本只用于提取 OpenTracing:
```go
currentSpan := opentracing.SpanFromContext(ctx)
```
它不会把普通 context metadata 传到远端。超时主要来自 Invocation attachment:
[protocol/dubbo/dubbo_invoker.go:121](/C:/Users/m1346/Desktop/repos/dubbo-go/protocol/dubbo/dubbo_invoker.go:121)
而且服务端重建 Context 时直接从 `context.Background()` 开始:
[protocol/dubbo/dubbo_protocol.go:147](/C:/Users/m1346/Desktop/repos/dubbo-go/protocol/dubbo/dubbo_protocol.go:147)
这意味着传统 Dubbo 服务端拿到的 Context 不是原始请求生命周期的延续,客户端取消通常无法自然传播到服务端。
Triple 协议:
[protocol/triple/triple_invoker.go:71](/C:/Users/m1346/Desktop/repos/dubbo-go/protocol/triple/triple_invoker.go:71)
它会把 Invocation attachments 合并进 outgoing Context:
[protocol/triple/triple_invoker.go:185](/C:/Users/m1346/Desktop/repos/dubbo-go/protocol/triple/triple_invoker.go:185)
同时,原始 `ctx` 会继续传给 HTTP 请求,因此 deadline/cancel 更接近真正生效的网络请求。
但这里也有明显的过渡痕迹:
```go
// Todo: Temporarily solve the problem that the timeout time is not valid
```
Dubbo3/gRPC:
[protocol/dubbo3/dubbo3_invoker.go:234](/C:/Users/m1346/Desktop/repos/dubbo-go/protocol/dubbo3/dubbo3_invoker.go:234)
会把 attachments 转成 gRPC metadata,再重新包装 Context:
```go
ctx = metadata.NewOutgoingContext(ctx, gRPCMD)
```
这种模型是比较清晰的:Context 管生命周期,metadata 管网络头。
服务端也存在双重表示。以 Triple 为例,同一份请求 metadata 同时被放进:
- `ctx.Value(constant.AttachmentKey)`
- `RPCInvocation.attachments`
见 [protocol/triple/server.go:549](/C:/Users/m1346/Desktop/repos/dubbo-go/protocol/triple/server.go:549)。
**我认为真正混乱的地方**
主要有四个:
1. **Context 和 attachments 的边界不清晰**
```go
ctx.Value(constant.AttachmentKey)
```
让 Context 变成了 attachment 容器。
2. **timeout 有两个来源**
- `ctx.Deadline()`
- `constant.TimeoutKey` attachment
而且传统 Dubbo、Triple、Dubbo3 的优先级和转换方式不完全相同。
3. **Invocation 也承担了一部分 Context 的职责**
`GetAttachmentAsContext` 和 `MergeAttachmentFromContext`:
[protocol/invocation/rpcinvocation.go:246](/C:/Users/m1346/Desktop/repos/dubbo-go/protocol/invocation/rpcinvocation.go:246)
实际上是在 Context 和 Invocation 之间做双向转换,但它只支持 header-like 的字符串数据,不是真正意义上的 Context 转换。
4. **传统 Dubbo 服务端丢失请求取消链**
服务端从 `context.Background()` 重建 Context,而不是从底层连接的请求 Context 派生。这是语义上的不一致,不只是代码风格问题。
**比较合理的统一模型**
可以把它明确拆成三层:
```text
context.Context
├── cancel / deadline
├── tracing span
└── 本地请求生命周期
RPC metadata / attachments
├── 认证信息
├── trace headers
├── group / version
└── 跨网络传输的数据
Invocation attributes
├── CallType
├── response header target
└── 仅本地使用的内部控制信息
```
建议遵循:
- `ctx` 只负责取消、deadline、trace 和请求级本地状态;
- attachments 只负责跨网络传输;
- 不再通过 `ctx.Value(AttachmentKey)` 作为普通 attachment 输入;
- 所有协议都提供明确的 `Context -> metadata` 和 `metadata -> Context` 转换;
- deadline 应该是超时的首要来源;
- 传统 Dubbo 的 timeout attachment 只作为兼容协议字段;
- 服务端 Context 必须从底层请求 Context 派生,而不是从 `context.Background()` 重建;
- Invocation 不应该拥有 `GetAttachmentAsContext` 这类模糊的双向转换职责。
Contributor guide
Assessment
This issue has not been assessed yet.