apache / apache/dubbo-go

[Proposal] Establish a consistent context model across dubbo-go

Open
#3,595 2 comments 0 reactions 1 assignee Claimed by @AsperforMias View on GitHub
✏️ Feature 3.3.3
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.