DadaVinqi opened a new pull request, #3641:
URL: https://github.com/apache/dubbo-go/pull/3641
## 关联 Issue
Related #3445
## 一、问题背景
Dubbo-go 当前已经支持通过 Triple unary 调用传递和读取 response headers/trailers。
对于生成式 Triple client,可以通过 call options 获取 provider 返回的 response metadata:
```go
var trailers http.Header
resp, err := service.Greet(
ctx,
request,
client.WithResponseTrailer(&trailers),
)
```
但是,GenericService 的调用路径没有提供等价能力。
GenericService 主要用于 non-IDL 泛化调用,当前典型调用方式如下:
```go
result, err := genericService.Invoke(
ctx,
"echo",
[]string{"java.lang.String"},
[]hessian.Object{"hello"},
)
```
当 provider 通过 result attachments 将数据写入 Triple response trailers 后,generic
consumer 无法向底层 Triple unary client 提供 response trailer 的目标 `http.Header`,因此无法读取
provider 返回的 response attachments。
期望的数据流是:
```text
provider result attachments
-> Triple response trailers
-> Triple client response metadata
-> GenericService 调用方提供的 http.Header
```
## 二、根因分析
### 1. GenericService.Invoke 没有 per-call options
`GenericService.Invoke` 原本是一个公开的函数类型字段:
```go
type GenericService struct {
Invoke func(
ctx context.Context,
methodName string,
types []string,
args []hessian.Object,
) (any, error) `dubbo:"$invoke"`
}
```
该字段没有 `opts ...CallOption` 参数,因此调用方无法传入:
```go
client.WithResponseHeader(&headers)
client.WithResponseTrailer(&trailers)
```
### 2. Invoke 是公开函数类型字段,不能直接改变签名
如果直接把 `Invoke` 改成带 `opts ...client.CallOption` 的函数类型,已有用户对 `Invoke`
字段的直接赋值会发生源码不兼容。
同时,Go 不支持同名重载,因此无法同时保留旧的四参数函数类型字段,并让同一个字段接受带 options 的调用。
### 3. CallOption 定义在 client 包会造成依赖方向问题
proxy 和 generic filter 都需要识别并传递 call options。如果它们直接依赖
`client.CallOption`,会引入不必要的包依赖,甚至可能形成 import cycle。call option 类型需要放在更底层、与
client 无关的共享包中。
### 4. Proxy 反射调用必须区分 options 和普通业务参数
GenericService 的调用通过 proxy 反射生成。proxy 需要从反射参数中识别并移除 `opts
...base.CallOption`,但不能通过“最后一个参数是 variadic”这种宽泛规则判断,否则普通业务方法的 variadic
参数也可能被错误剥离。
### 5. Generic filter 会重建 Invocation
generic filter 在 `$invoke` 调用过程中会重建 Invocation,将原始业务参数转换为 generic
调用需要的形式。如果重建时不复制 response metadata attributes,之前由 proxy 写入的 response
header/trailer 目标就会丢失,底层 Triple client 最终无法回写用户提供的 `http.Header`。
## 三、解决方案
本次修改采用兼容优先的最小方案:
1. 将 `CallOptions` / `CallOption` 下沉到 `protocol/base`。
2. 在 `client` 包保留类型别名,兼容已有 `client.CallOption` 使用方式。
3. 保留原有 `GenericService.Invoke` 字段和调用方式。
4. 新增 `InvokeWithOptions` 作为带 options 的 generic 调用入口。
5. 扩展 `InvokeWithType`,支持可选的 `...base.CallOption`。
6. 保留显式命名的 `InvokeWithTypeOptions` API。
7. proxy 仅精确识别 `...base.CallOption`,避免影响普通业务 variadic 参数。
8. 通过 Invocation attachments/attributes 复用现有 Triple client 的 metadata 传递路径。
9. generic filter 重建 Invocation 时,只复制 response header/trailer 两类必要
attributes。
## 四、具体实现
### 1. 增加共享 CallOption 类型
新增:
```text
protocol/base/call_options.go
```
定义:
```go
type CallOptions struct {
RequestTimeout string
Retries string
ResponseHeader *http.Header
ResponseTrailer *http.Header
}
type CallOption func(*CallOptions)
```
`CallOptions` 同时承载 request timeout、retries、response header 目标和 response
trailer 目标。
### 2. 保留 client 包的兼容 API
`client/options.go` 中保留:
```go
type CallOptions = base.CallOptions
type CallOption = base.CallOption
```
因此以下已有写法继续有效:
```go
client.WithResponseHeader(&headers)
client.WithResponseTrailer(&trailers)
client.WithCallRequestTimeout(time.Second)
```
proxy 和 generic filter 只依赖 `protocol/base`,不反向依赖 `client`。
### 3. 新增 GenericService options 调用入口
`GenericService` 保留旧字段:
```go
Invoke func(
ctx context.Context,
methodName string,
types []string,
args []hessian.Object,
) (any, error)
```
同时新增:
```go
InvokeWithOptions func(
ctx context.Context,
methodName string,
types []string,
args []hessian.Object,
opts ...base.CallOption,
) (any, error)
```
带 options 的泛化调用示例:
```go
var headers http.Header
var trailers http.Header
result, err := genericService.InvokeWithOptions(
ctx,
"echo",
[]string{"java.lang.String"},
[]hessian.Object{"hello"},
client.WithResponseHeader(&headers),
client.WithResponseTrailer(&trailers),
)
```
### 4. 扩展 InvokeWithType
`InvokeWithType` 现在支持可选 options:
```go
func (s *GenericService) InvokeWithType(
ctx context.Context,
methodName string,
types []string,
args []hessian.Object,
reply any,
opts ...base.CallOption,
) error
```
示例:
```go
var trailers http.Header
var user User
err := genericService.InvokeWithType(
ctx,
"getUser",
[]string{"java.lang.String"},
[]hessian.Object{"123"},
&user,
client.WithResponseTrailer(&trailers),
)
```
同时保留显式 options 命名形式:
```go
err := genericService.InvokeWithTypeOptions(
ctx,
"getUser",
[]string{"java.lang.String"},
[]hessian.Object{"123"},
&user,
client.WithResponseTrailer(&trailers),
)
```
### 5. Proxy 传递 Invocation metadata
修改:
```text
proxy/proxy.go
```
proxy 只在方法满足以下条件时处理 call options:
```go
method.IsVariadic()
method.LastParameterElementType == reflect.TypeFor[base.CallOption]()
```
处理过程:
1. 从最后一个 variadic 参数中取出 `[]base.CallOption`;
2. 执行每个 option,生成 `CallOptions`;
3. 不将 options 放入业务参数;
4. 将 request timeout/retries 写入 Invocation attachments;
5. 将 response header/trailer 目标写入 Invocation attributes。
对应 attributes:
```go
constant.ResponseHeaderKey
constant.ResponseTrailerKey
```
普通业务 variadic 方法不会进入这条逻辑,原有参数语义保持不变。
### 6. Generic filter 保留 response metadata attributes
修改:
```text
filter/generic/filter.go
```
generic filter 在重建 Invocation 时,复制:
```go
constant.ResponseHeaderKey
constant.ResponseTrailerKey
```
只复制这两类 response metadata attributes,不复制全部内部 attributes,避免将不必要的 Invocation
状态泄漏到新的调用对象中。
该逻辑覆盖 generic filter 中两条会重建 Invocation 的路径。
### 复用现有 Triple response metadata 路径
本次没有修改 Triple invoker 底层 response metadata 读取逻辑。
调用链如下:
```text
client.WithResponseTrailer(&trailers)
-> proxy 解析 CallOption
-> Invocation.ResponseTrailerKey
-> generic filter 重建 Invocation 并保留 attribute
-> Triple unary call
-> 读取 response trailers
-> 写回 trailers
```
response header 的流程相同。
## 五、API 兼容性说明
原有 `Invoke` 调用保持不变:
```go
result, err := genericService.Invoke(
ctx,
"echo",
types,
args,
)
```
由于 `Invoke` 是公开的非 variadic 函数类型字段,无法在不破坏已有字段赋值代码的情况下支持以下形式:
```go
genericService.Invoke(ctx, method, types, args, opts...)
```
因此本次使用:
```go
genericService.InvokeWithOptions(...)
```
作为兼容入口。
`InvokeWithType` 是公开方法,普通的五参数直接调用仍然有效:
```go
err := genericService.InvokeWithType(
ctx,
method,
types,
args,
&reply,
)
```
新增调用可以直接追加 options:
```go
err := genericService.InvokeWithType(
ctx,
method,
types,
args,
&reply,
client.WithResponseTrailer(&trailers),
)
```
如果 GenericService 仅手动初始化了旧的 `Invoke` 字段,没有通过 `proxy.Implement` 等方式初始化
options-aware 调用字段,options API 会返回明确的初始化错误。
## 六、测试与验证
已通过:
```text
go test -count=1 ./protocol/base ./client ./proxy ./filter/generic
Go test: 278 passed in 4 packages
```
```text
go test -race -count=1 ./proxy ./filter/generic
Go test: 73 passed in 2 packages
```
```text
go test -count=1 ./protocol/triple -run 'Response|Trailer'
Go test: 3 passed in 1 packages
```
```text
cd tools/dubbogo-cli && go test -count=1 ./...
Go test: 12 passed in 15 packages
```
其他验证:
```text
make fmt
通过
```
```text
make lint
0 issues.
```
```text
git diff --check
通过
```
`make test` 的 root module 测试除既有 Triple 外部 timeout
测试外均通过,但以下测试仍受当前环境/外部服务行为影响:
```text
protocol/triple/TestClientInvokeWithTimeout/without_TLS
unexpected EOF
protocol/triple/TestClientInvokeWithTimeout/with_TLS
no Grpc-Status trailer
```
这两个失败发生在现有外部 Triple timeout 测试路径,与本次 GenericService response metadata 修改路径无关。
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]