apache / apache/dubbo-go

[Feature] Define unified extension runtime contract primitives

Open
#3,686 4 comments 0 reactions 1 assignee Claimed by @XnLemon View on GitHub
✏️ Feature 3.3.3
Dominant language
Go
Stars
5k
Forks
1k
Avg merge
2d 8h
Merged PRs (30d)
31

Description

Sub-issue of #3672

## 背景

dubbo-go 与 dubbo-go-extensions 需要统一扩展配置和 typed option 的接入方式。

当前 `common/extension.Config` 只提供:

```go
type Config interface {
Prefix() string
}
```

它只能标识扩展配置,无法:

- 为每个 Instance、client 或 server 创建独立配置对象;
- 接收统一的 typed options;
- 根据调用入口初始化扩展;
- 声明需要自动加入现有 filter chain 的 filter。

现有 dubbo-go 已经具备完成这些需求的大部分能力:

| 现有能力 | 职责 |
| --- | --- |
| Config registry | 注册和查找扩展配置 |
| `extension.SetFilter` / `GetFilter` | 注册和创建 filter |
| InstanceOptions + koanf | 加载 YAML |
| `Invoker.GetURL()` | 获取 interface、group、version |
| `Invocation.MethodName()` | 获取 method |

因此,本 issue 不再建立独立的 extension runtime 系统,而是在现有机制上补充最小协议。

本次允许对现有 `extension.Config` 接口进行 breaking change。

## 设计原则

1. 复用现有 Config 和 filter registry;
2. scope 由 `dubbo`、`client`、`server` 调用入口决定;
3. 每次激活扩展时创建独立配置对象;
4. filter 仍通过现有注册名称加入 filter chain;
5. 资源信息由 filter 执行时从 URL 和 Invocation 获取;
6. 核心不依赖 Hystrix、Sentinel 等具体扩展;
7. 不为尚未出现的扩展场景提前建立完整 runtime 抽象。

## 基础类型

### Scope

```go
type Scope uint8

const (
InstanceScope Scope = iota + 1
ClientScope
ServerScope
)
```

scope 与调用入口一一对应:

- `dubbo.WithExtension(...)` → `InstanceScope`
- `client.WithExtension(...)` → `ClientScope`
- `server.WithExtension(...)` → `ServerScope`

`Scope` 表示一次具体初始化所处的生命周期,是单值枚举,不作为 bitmask 使用。

consumer/provider 不再通过额外的 Role 字段表达:

- `ClientScope` 即 consumer;
- `ServerScope` 即 provider;
- `InstanceScope` 没有 consumer/provider 角色。

零值和未知 scope 均为非法值。扩展不支持某个 scope 时,应从 `Init` 返回明确错误。

### Config

扩展现有的 `Config` 接口:

```go
type Config interface {
// Prefix 返回扩展注册名,同时对应:
// dubbo.extensions.
Prefix() string

// New 创建一个带默认值的独立配置对象。
New() Config

// Init 在 YAML 和 typed options 应用完成后初始化扩展。
//
// 不支持当前 scope 时返回错误。
Init(scope Scope) error

// FilterNames 返回当前 scope 需要加入现有 filter chain
// 的已注册 filter name。
//
// 非 filter 型扩展可以返回 nil。
FilterNames(scope Scope) []string
}
```

注册到 registry 中的 Config 作为不可变原型使用。核心不得直接向原型加载 YAML 或应用 options。

`New()` 必须:

- 返回非 nil 的新配置对象;
- 初始化扩展默认值;
- 不与其他调用共享可变配置;
- 返回与注册原型具有相同 Prefix 的 Config。

`New()` 只保证配置对象独立。扩展依赖的外部运行时是否支持实例隔离,由具体扩展负责。

### Option

```go
type Option interface {
// Prefix 标识 option 所属的扩展。
Prefix() string

// Apply 将 typed option 应用到 Config.New 创建的配置对象。
Apply(config Config) error
}
```

核心根据 `Prefix()` 查找 Config,并按照用户声明顺序应用 options。

具体扩展负责定义自己的 typed options,例如:

```go
hystrix.WithConfig(
hystrix.WithCommandName("greet.GreetService:::Greet"),
hystrix.WithTimeout(1000),
)
```

核心中不增加 `WithHystrix` 等具体扩展 API。

## Config registry

继续复用现有泛型 registry,但注册时拒绝重复 Prefix:

```go
func RegisterConfig(config Config) error
func MustRegisterConfig(config Config)
func LookupConfig(prefix string) (Config, bool)
func UnregisterConfig(prefix string)
```

约定:

- `RegisterConfig` 对 nil、空 Prefix 和重复 Prefix 返回错误;
- `MustRegisterConfig` 供扩展的 `init()` 使用,注册失败时 panic;
- `LookupConfig` 用于 loader 和 typed option 查找扩展;
- `UnregisterConfig` 主要用于隔离测试。

扩展通过 side-effect import 注册 Config 和 filter:

```go
func init() {
extension.MustRegisterConfig(&Config{})

extension.SetFilter(
HystrixConsumerFilterKey,
newConsumerFilter,
)
extension.SetFilter(
HystrixProviderFilterKey,
newProviderFilter,
)
}
```

不再增加独立的 Definition registry。

## 配置和初始化顺序

后续 #3672 的入口与 loader 按照以下顺序处理扩展:

```text
Config.New 中的默认值
< YAML
< typed options
< Init
```

具体流程:

1. 根据 YAML 或 option 的 Prefix 查找已注册 Config;
2. 调用 `Config.New()` 创建独立配置对象;
3. 将对应 YAML 解码到该对象;
4. 按声明顺序调用 `Option.Apply`;
5. 调用 `FilterNames(scope)` 获取 filter name;
6. 校验所有 filter name 已通过 `SetFilter` 注册;
7. 调用 `Init(scope)`;
8. 初始化成功后,将 filter name 合并到现有 filter 配置。

`FilterNames` 返回的是现有 filter registry 中的名称。核心只负责把名称按稳定顺序合并到 client/server filter 配置,实际 filter 实例仍由现有 filter chain 在构建 Invoker 时通过 `GetFilter` 创建。

filter 合并需要:

- 保持稳定顺序;
- 对重复名称去重;
- 保留用户显式 filter 配置的覆盖能力;
- 不允许未注册的 filter name 静默进入 chain。

如果 `Init` 返回错误:

- 当前 Instance、client 或 server 构造失败;
- filter 配置不得提交;
- 扩展需要自行回滚本次 `Init` 已产生的外部副作用。

本 issue 不增加 `Close`。当前 Instance 和 Client 没有统一的关闭生命周期,暂时不存在能够可靠拥有扩展清理职责的公共 owner。

## 不再引入的类型

删除原设计中的以下类型:

```text
Resource
RawNode
RawConfig
FilterSpec
RoleNone
Context
Definition
```

原因如下:

- `Resource`:filter 已经可以从 `Invoker.GetURL()` 和
`Invocation.MethodName()` 获取资源信息;
- `RawNode` / `RawConfig`:当前需求使用现有 koanf 和
`map[string]any` 即可完成,不提前建立 parser-independent 配置树;
- `FilterSpec`:继续使用现有 filter name 和 filter registry;
- `RoleNone` / `Context`:scope 已由调用入口唯一确定,不重复表达 role;
- `Definition`:创建、初始化和 filter 声明能力直接由 Config 提供,
不建立第二套注册系统。

如果以后出现当前 Config 无法表达的真实扩展场景,再基于具体消费者扩展协议。

## 本 issue 范围

本 issue 只负责:

- 定义 `Scope`;
- 扩展 `Config` 接口;
- 定义统一的 `Option` 接口;
- 完善 Config 注册和查找能力;
- 定义 Config 创建、初始化和 filter name 声明的契约;
- 增加对应的契约测试。

以下内容由 #3672 的后续子任务完成:

- `dubbo.WithExtension`;
- `client.WithExtension`;
- `server.WithExtension`;
- `dubbo.Load()` 的 extension YAML 加载;
- 配置优先级处理;
- filter 合并、去重和显式覆盖;
- Hystrix typed options 和 YAML 迁移;
- Hystrix README 和示例更新。

## 验收标准

- [ ] 定义 `InstanceScope`、`ClientScope`、`ServerScope`
- [ ] 零值和未知 Scope 被拒绝
- [ ] `Config.New()` 每次返回独立、非 nil 的配置对象
- [ ] `Config.New()` 返回对象的 Prefix 与注册 Prefix 一致
- [ ] `Config.Init(scope)` 接收入口确定的具体 scope
- [ ] 不支持的 scope 返回明确错误
- [ ] `Config.FilterNames(scope)` 只返回现有 registry 中的 filter name
- [ ] `Option` 可以通过 Prefix 定位并修改对应 Config
- [ ] 多个 options 按声明顺序应用
- [ ] nil option、空 Prefix 和未注册 Prefix 返回错误
- [ ] Config registry 拒绝重复 Prefix
- [ ] filter 合并保持稳定顺序并去重
- [ ] 未注册 filter name 返回错误
- [ ] Init 失败时不提交 filter 配置
- [ ] 单元测试覆盖默认值、实例隔离、非法输入、错误和 option 顺序
- [ ] 不新增 Resource、RawConfig、FilterSpec、Context 或 Definition
- [ ] dubbo-go 核心不依赖任何具体扩展

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.