[Feature] Define unified extension runtime contract primitives
- 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
Assessment
This issue has not been assessed yet.