This is an automated email from the ASF dual-hosted git repository.
xuetaoli pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git
The following commit(s) were added to refs/heads/develop by this push:
new 707124f71 feat(router): add static router configuration injection
(#3252)
707124f71 is described below
commit 707124f719536a926a3171a5932785fb86028321
Author: Aether <[email protected]>
AuthorDate: Sun Apr 12 20:34:53 2026 +0800
feat(router): add static router configuration injection (#3252)
* feat(router): add static router configuration injection
- Add StaticConfigSetter interface for static config injection
- Add global static router registry
- Add router chain injection mechanism
- Implement static config setters for condition and tag routers
* add unit tests
* fix(client): make setRouters use override semantics
* docs(client): clarify router option append/replace semantics
* fix(router): clone static router config before injecting setters
* fix(tag-router): guard nil router flags in tag routing
* docs(router): clarify static injection and dynamic override semantics
* docs(client): document WithRouter append behavior
* docs(router): clarify dynamic Process overrides static router config
* remove unnecessary outer clone in injectRouterConfig
* fix: ignore service-scope static tag configs
* refactor: remove redundant nil check when appending routers
* fix(router): remove chain-level static router scope matching
* fix(router): log static router config type assertion failures as errors
---------
Co-authored-by: Aetherance <[email protected]>
---
client/action.go | 1 +
client/client.go | 1 +
client/options.go | 44 ++++++
client/options_test.go | 70 ++++++++++
cluster/router/chain/chain.go | 53 ++++++++
cluster/router/chain/chain_test.go | 206 +++++++++++++++++++++++++++++
cluster/router/condition/dynamic_router.go | 58 ++++++++
cluster/router/condition/router_test.go | 88 ++++++++++++
cluster/router/router.go | 18 +++
cluster/router/tag/match.go | 2 +-
cluster/router/tag/router.go | 28 +++-
cluster/router/tag/router_test.go | 127 ++++++++++++++++++
common/constant/key.go | 3 +
dubbo.go | 4 +
14 files changed, 701 insertions(+), 2 deletions(-)
diff --git a/client/action.go b/client/action.go
index 2b9aca347..643126208 100644
--- a/client/action.go
+++ b/client/action.go
@@ -155,6 +155,7 @@ func (refOpts *ReferenceOptions) refer(srv
common.RPCService, info *ClientInfo)
common.WithAttribute(constant.ConsumerConfigKey,
refOpts.Consumer),
common.WithAttribute(constant.ProtocolConfigKey, ref.Protocol),
common.WithAttribute(constant.RegistriesConfigKey,
refOpts.Registries),
+ common.WithAttribute(constant.RoutersConfigKey,
refOpts.Routers),
)
// for new triple IDL mode
diff --git a/client/client.go b/client/client.go
index e443e5da6..794da976d 100644
--- a/client/client.go
+++ b/client/client.go
@@ -199,6 +199,7 @@ func (cli *Client) dial(interfaceName string, info
*ClientInfo, srv any, opts ..
setOtel(cli.cliOpts.Otel),
setTLS(cli.cliOpts.TLS),
setProtocols(cli.cliOpts.Protocols),
+ setRouters(cli.cliOpts.Routers),
// this config must be set after Reference initialized
setInterfaceName(interfaceName),
}
diff --git a/client/options.go b/client/options.go
index 273deeb69..2be018441 100644
--- a/client/options.go
+++ b/client/options.go
@@ -50,6 +50,7 @@ type ReferenceOptions struct {
TLS *global.TLSConfig
Protocols map[string]*global.ProtocolConfig
Registries map[string]*global.RegistryConfig
+ Routers []*global.RouterConfig
pxy *proxy.Proxy
id string
@@ -445,6 +446,17 @@ func WithParam(k, v string) ReferenceOption {
}
}
+// WithRouter appends router configurations to the reference options.
+// This is a user-facing option for incrementally adding routers.
+// It appends to the current router config slice instead of replacing it.
+func WithRouter(routers ...*global.RouterConfig) ReferenceOption {
+ return func(opts *ReferenceOptions) {
+ if len(routers) > 0 {
+ opts.Routers = append(opts.Routers, routers...)
+ }
+ }
+}
+
// ---------- For framework ----------
// These functions should not be invoked by users
@@ -512,6 +524,15 @@ func setRegistries(regs map[string]*global.RegistryConfig)
ReferenceOption {
}
}
+// setRouters sets the routers configuration for the service reference.
+// This is an internal framework function for applying router settings to
+// reference options. It replaces the current router slice.
+func setRouters(routers []*global.RouterConfig) ReferenceOption {
+ return func(opts *ReferenceOptions) {
+ opts.Routers = routers
+ }
+}
+
type ClientOptions struct {
Consumer *global.ConsumerConfig
Application *global.ApplicationConfig
@@ -521,6 +542,7 @@ type ClientOptions struct {
Otel *global.OtelConfig
TLS *global.TLSConfig
Protocols map[string]*global.ProtocolConfig
+ Routers []*global.RouterConfig
overallReference *global.ReferenceConfig
}
@@ -835,6 +857,17 @@ func WithClientParam(k, v string) ClientOption {
}
}
+// WithClientRouter appends router configurations to the client options.
+// This is a user-facing option for incrementally adding routers.
+// It appends to the current router slice instead of replacing it.
+func WithClientRouter(routers ...*global.RouterConfig) ClientOption {
+ return func(opts *ClientOptions) {
+ if len(routers) > 0 {
+ opts.Routers = append(opts.Routers, routers...)
+ }
+ }
+}
+
// todo(DMwangnima): implement this functionality
// func WithClientGeneric(generic bool) ClientOption {
// return func(opts *ClientOptions) {
@@ -953,6 +986,17 @@ func SetClientProtocols(protocols
map[string]*global.ProtocolConfig) ClientOptio
}
}
+// SetClientRouters sets the routers configuration for the client.
+// This is an internal framework function for applying router settings to
+// client options.
+// End users should not use this function for configuration.
+// It replaces the current router slice instead of appending to it.
+func SetClientRouters(routers []*global.RouterConfig) ClientOption {
+ return func(opts *ClientOptions) {
+ opts.Routers = routers
+ }
+}
+
// todo: need to be consistent with MethodConfig
type CallOptions struct {
RequestTimeout string
diff --git a/client/options_test.go b/client/options_test.go
index 7d7141023..9b8bf4505 100644
--- a/client/options_test.go
+++ b/client/options_test.go
@@ -1063,6 +1063,76 @@ func TestWithParams(t *testing.T) {
processReferenceOptionsInitCases(t, cases)
}
+func TestWithRouter(t *testing.T) {
+ router1 := &global.RouterConfig{Key: "svc-a"}
+ router2 := &global.RouterConfig{Key: "svc-b"}
+
+ cases := []referenceOptionsInitCase{
+ {
+ desc: "append router configs",
+ opts: []ReferenceOption{
+ WithRouter(router1, router2),
+ },
+ verify: func(t *testing.T, refOpts *ReferenceOptions,
err error) {
+ require.NoError(t, err)
+ assert.Equal(t, []*global.RouterConfig{router1,
router2}, refOpts.Routers)
+ },
+ },
+ {
+ desc: "ignore empty router configs",
+ opts: []ReferenceOption{
+ WithRouter(),
+ },
+ verify: func(t *testing.T, refOpts *ReferenceOptions,
err error) {
+ require.NoError(t, err)
+ assert.Nil(t, refOpts.Routers)
+ },
+ },
+ {
+ desc: "framework setter replaces router configs",
+ opts: []ReferenceOption{
+ setRouters([]*global.RouterConfig{router1}),
+ setRouters([]*global.RouterConfig{router2}),
+ },
+ verify: func(t *testing.T, refOpts *ReferenceOptions,
err error) {
+ require.NoError(t, err)
+ assert.Equal(t,
[]*global.RouterConfig{router2}, refOpts.Routers)
+ },
+ },
+ }
+ processReferenceOptionsInitCases(t, cases)
+}
+
+func TestClientRouterOptions(t *testing.T) {
+ router1 := &global.RouterConfig{Key: "app-a"}
+ router2 := &global.RouterConfig{Key: "app-b"}
+
+ cases := []newClientCase{
+ {
+ desc: "append client router configs",
+ opts: []ClientOption{
+ WithClientRouter(router1),
+ WithClientRouter(router2),
+ },
+ verify: func(t *testing.T, cli *Client, err error) {
+ require.NoError(t, err)
+ assert.Equal(t, []*global.RouterConfig{router1,
router2}, cli.cliOpts.Routers)
+ },
+ },
+ {
+ desc: "framework setter replaces client router configs",
+ opts: []ClientOption{
+
SetClientRouters([]*global.RouterConfig{router1, router2}),
+ },
+ verify: func(t *testing.T, cli *Client, err error) {
+ require.NoError(t, err)
+ assert.Equal(t, []*global.RouterConfig{router1,
router2}, cli.cliOpts.Routers)
+ },
+ },
+ }
+ processNewClientCases(t, cases)
+}
+
//func TestWithClientParam(t *testing.T) {
// cases := []newClientCase{
// {
diff --git a/cluster/router/chain/chain.go b/cluster/router/chain/chain.go
index d849c5804..c64968be7 100644
--- a/cluster/router/chain/chain.go
+++ b/cluster/router/chain/chain.go
@@ -35,6 +35,7 @@ import (
"dubbo.apache.org/dubbo-go/v3/common"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"dubbo.apache.org/dubbo-go/v3/common/extension"
+ "dubbo.apache.org/dubbo-go/v3/global"
"dubbo.apache.org/dubbo-go/v3/protocol/base"
)
@@ -110,6 +111,53 @@ func (c *RouterChain) copyRouters()
[]router.PriorityRouter {
return ret
}
+// injectStaticRouters injects static router configurations into the router
chain.
+// Called after all routers are created to ensure they exist.
+// The injected static configs act as bootstrap state only during
initialization. For the shared
+// static and dynamic lifecycle semantics, see router.StaticConfigSetter.
+func (c *RouterChain) injectStaticRouters(url *common.URL) {
+ staticRoutersAttrAny, ok := url.GetAttribute(constant.RoutersConfigKey)
+ if !ok && url.SubURL != nil {
+ staticRoutersAttrAny, ok =
url.SubURL.GetAttribute(constant.RoutersConfigKey)
+ }
+ if !ok {
+ return
+ }
+ staticRoutersAttr, ok := staticRoutersAttrAny.([]*global.RouterConfig)
+ if !ok {
+ logger.Errorf("failed to type assert routers config: expected
[]*global.RouterConfig, got %T", staticRoutersAttrAny)
+ return
+ }
+ if len(staticRoutersAttr) == 0 {
+ return
+ }
+
+ for _, routerCfg := range staticRoutersAttr {
+ if routerCfg == nil {
+ continue
+ }
+ if routerCfg.Enabled != nil && !*routerCfg.Enabled {
+ continue
+ }
+ if routerCfg.Valid != nil && !*routerCfg.Valid {
+ continue
+ }
+ c.injectRouterConfig(routerCfg)
+ }
+}
+
+// injectRouterConfig injects router configuration into routers that implement
StaticConfigSetter.
+// Each router decides whether the config applies to it and no-ops if not.
+func (c *RouterChain) injectRouterConfig(routerCfg *global.RouterConfig) {
+ c.mutex.RLock()
+ defer c.mutex.RUnlock()
+ for _, r := range c.routers {
+ if setter, ok := r.(router.StaticConfigSetter); ok {
+ setter.SetStaticConfig(routerCfg.Clone())
+ }
+ }
+}
+
// NewRouterChain init router chain
// Loop routerFactories and call NewRouter method
func NewRouterChain(url *common.URL) (*RouterChain, error) {
@@ -150,6 +198,11 @@ func NewRouterChain(url *common.URL) (*RouterChain, error)
{
builtinRouters: routers,
}
+ // Inject static router configurations after all routers are created.
+ // This happens before the first registry notification triggers dynamic
Notify/Process
+ // updates on builtin routers.
+ chain.injectStaticRouters(url)
+
return chain, nil
}
diff --git a/cluster/router/chain/chain_test.go
b/cluster/router/chain/chain_test.go
index afdc39420..96557c608 100644
--- a/cluster/router/chain/chain_test.go
+++ b/cluster/router/chain/chain_test.go
@@ -29,10 +29,216 @@ import (
import (
"dubbo.apache.org/dubbo-go/v3/cluster/router"
"dubbo.apache.org/dubbo-go/v3/common"
+ "dubbo.apache.org/dubbo-go/v3/common/constant"
+ "dubbo.apache.org/dubbo-go/v3/global"
"dubbo.apache.org/dubbo-go/v3/protocol/base"
"dubbo.apache.org/dubbo-go/v3/protocol/invocation"
)
+type mockStaticRouter struct {
+ configs []*global.RouterConfig
+}
+
+func (m *mockStaticRouter) Route(invokers []base.Invoker, _ *common.URL, _
base.Invocation) []base.Invoker {
+ return invokers
+}
+
+func (m *mockStaticRouter) URL() *common.URL {
+ return nil
+}
+
+func (m *mockStaticRouter) Priority() int64 {
+ return 0
+}
+
+func (m *mockStaticRouter) Notify(_ []base.Invoker) {
+}
+
+func (m *mockStaticRouter) SetStaticConfig(cfg *global.RouterConfig) {
+ m.configs = append(m.configs, cfg)
+}
+
+type mutatingStaticRouter struct {
+ configs []*global.RouterConfig
+}
+
+func (m *mutatingStaticRouter) Route(invokers []base.Invoker, _ *common.URL, _
base.Invocation) []base.Invoker {
+ return invokers
+}
+
+func (m *mutatingStaticRouter) URL() *common.URL {
+ return nil
+}
+
+func (m *mutatingStaticRouter) Priority() int64 {
+ return 0
+}
+
+func (m *mutatingStaticRouter) Notify(_ []base.Invoker) {
+}
+
+func (m *mutatingStaticRouter) SetStaticConfig(cfg *global.RouterConfig) {
+ if cfg != nil {
+ if cfg.Force != nil {
+ *cfg.Force = !*cfg.Force // NOSONAR
+ }
+ if len(cfg.Conditions) > 0 {
+ cfg.Conditions[0] = "mutated" // NOSONAR
+ }
+ }
+ m.configs = append(m.configs, cfg)
+}
+
+type mockRouter struct{}
+
+func (m *mockRouter) Route(invokers []base.Invoker, _ *common.URL, _
base.Invocation) []base.Invoker {
+ return invokers
+}
+
+func (m *mockRouter) URL() *common.URL {
+ return nil
+}
+
+func (m *mockRouter) Priority() int64 {
+ return 0
+}
+
+func (m *mockRouter) Notify(_ []base.Invoker) {
+}
+
+func TestInjectStaticRouters(t *testing.T) {
+ trueValue := true
+ falseValue := false
+
+ staticRouter := &mockStaticRouter{}
+ chain := &RouterChain{
+ routers: []router.PriorityRouter{
+ staticRouter,
+ &mockRouter{},
+ },
+ }
+
+ url := common.NewURLWithOptions(
+ common.WithProtocol("consumer"),
+ common.WithPath("consumer.path"),
+ )
+ url.SubURL = common.NewURLWithOptions(
+ common.WithProtocol("dubbo"),
+ common.WithPath("svc.test"),
+ )
+ url.SubURL.SetParam(constant.ApplicationKey, "app.test")
+ url.SubURL.SetAttribute(constant.RoutersConfigKey,
[]*global.RouterConfig{
+ nil,
+ {
+ Scope: constant.RouterScopeService,
+ Key: "svc.test",
+ Enabled: &trueValue,
+ Valid: &trueValue,
+ },
+ {
+ Scope: constant.RouterScopeApplication,
+ Key: "app.test",
+ },
+ {
+ Scope: constant.RouterScopeApplication,
+ Key: "disabled",
+ Enabled: &falseValue,
+ },
+ {
+ Scope: constant.RouterScopeApplication,
+ Key: "invalid",
+ Valid: &falseValue,
+ },
+ {
+ Scope: constant.RouterScopeApplication,
+ Key: "other-app",
+ },
+ })
+
+ chain.injectStaticRouters(url)
+
+ if assert.Len(t, staticRouter.configs, 3) {
+ assert.Equal(t, "svc.test", staticRouter.configs[0].Key)
+ assert.Equal(t, "app.test", staticRouter.configs[1].Key)
+ assert.Equal(t, "other-app", staticRouter.configs[2].Key)
+ }
+}
+
+func TestInjectStaticRouters_InvalidAttributeType(t *testing.T) {
+ staticRouter := &mockStaticRouter{}
+ chain := &RouterChain{
+ routers: []router.PriorityRouter{staticRouter},
+ }
+
+ url := common.NewURLWithOptions(
+ common.WithProtocol("consumer"),
+ common.WithPath("svc.test"),
+ )
+ url.SetAttribute(constant.RoutersConfigKey, "invalid")
+
+ chain.injectStaticRouters(url)
+
+ assert.Empty(t, staticRouter.configs)
+}
+
+func TestInjectStaticRouters_RegistryURLUsesSubURLConfig(t *testing.T) {
+ staticRouter := &mockStaticRouter{}
+ chain := &RouterChain{
+ routers: []router.PriorityRouter{staticRouter},
+ }
+
+ registryURL := common.NewURLWithOptions(
+ common.WithProtocol(constant.RegistryProtocol),
+ common.WithPath("registry"),
+ )
+ registryURL.SubURL = common.NewURLWithOptions(
+ common.WithProtocol("consumer"),
+ common.WithPath("consumer.path"),
+ )
+ registryURL.SubURL.SetAttribute(constant.RoutersConfigKey,
[]*global.RouterConfig{{
+ Scope: constant.RouterScopeApplication,
+ Key: "provider-app",
+ }})
+
+ chain.injectStaticRouters(registryURL)
+
+ if assert.Len(t, staticRouter.configs, 1) {
+ assert.Equal(t, "provider-app", staticRouter.configs[0].Key)
+ }
+}
+
+func TestInjectRouterConfig_ClonePerSetter(t *testing.T) {
+ trueValue := true
+ mutatingRouter := &mutatingStaticRouter{}
+ observingRouter := &mockStaticRouter{}
+ chain := &RouterChain{
+ routers: []router.PriorityRouter{
+ mutatingRouter,
+ observingRouter,
+ },
+ }
+
+ routerCfg := &global.RouterConfig{
+ Force: &trueValue,
+ Conditions: []string{"original"}, // NOSONAR
+ }
+
+ chain.injectRouterConfig(routerCfg)
+
+ if assert.Len(t, mutatingRouter.configs, 1) && assert.Len(t,
observingRouter.configs, 1) {
+ assert.NotSame(t, routerCfg, mutatingRouter.configs[0])
+ assert.NotSame(t, routerCfg, observingRouter.configs[0])
+ assert.NotSame(t, mutatingRouter.configs[0],
observingRouter.configs[0])
+ assert.False(t, *mutatingRouter.configs[0].Force)
+ assert.Equal(t, "mutated",
mutatingRouter.configs[0].Conditions[0]) // NOSONAR
+ assert.True(t, *observingRouter.configs[0].Force)
+ assert.Equal(t, "original",
observingRouter.configs[0].Conditions[0]) // NOSONAR
+ }
+
+ assert.True(t, *routerCfg.Force)
+ assert.Equal(t, "original", routerCfg.Conditions[0]) // NOSONAR
+}
+
const testConsumerServiceURL = "consumer://127.0.0.1/com.demo.Service"
type testPriorityRouter struct {
diff --git a/cluster/router/condition/dynamic_router.go
b/cluster/router/condition/dynamic_router.go
index 4bdc34c3e..7b61a686b 100644
--- a/cluster/router/condition/dynamic_router.go
+++ b/cluster/router/condition/dynamic_router.go
@@ -134,6 +134,48 @@ func (d *DynamicRouter) URL() *common.URL {
return nil
}
+// SetStaticConfig applies a RouterConfig with string conditions directly,
bypassing YAML parsing.
+// This is the correct entry point for static (code-configured) rules;
+// Process is designed for dynamic config-center updates that arrive as YAML
text.
+// Static and dynamic rules are not merged: later Process updates replace the
current state built here.
+func (d *DynamicRouter) SetStaticConfig(cfg *global.RouterConfig) {
+ if cfg == nil || len(cfg.Conditions) == 0 {
+ return
+ }
+
+ d.mu.Lock()
+ defer d.mu.Unlock()
+
+ force := cfg.Force != nil && *cfg.Force
+ enable := cfg.Enabled == nil || *cfg.Enabled
+
+ if !enable {
+ d.force, d.enable, d.conditionRouter = force, false, nil
+ return
+ }
+
+ conditionRouters := make([]*StateRouter, 0, len(cfg.Conditions))
+ for _, conditionRule := range cfg.Conditions {
+ url, err := common.NewURL("condition://")
+ if err != nil {
+ logger.Warnf("[condition router] failed to create
condition URL: %v", err)
+ continue
+ }
+ url.AddParam(constant.RuleKey, conditionRule)
+ url.AddParam(constant.ForceKey, strconv.FormatBool(force))
+ conditionRoute, err := NewConditionStateRouter(url)
+ if err != nil {
+ logger.Warnf("[condition router] failed to parse
condition rule %q: %v", conditionRule, err)
+ continue
+ }
+ conditionRouters = append(conditionRouters, conditionRoute)
+ }
+ d.force, d.enable, d.conditionRouter = force, enable,
stateRouters(conditionRouters)
+}
+
+// Process applies config-center updates as the authoritative rule source at
runtime.
+// It does not merge with static rules bootstrapped via SetStaticConfig; any
later
+// dynamic update replaces the current static-derived state.
func (d *DynamicRouter) Process(event *config_center.ConfigChangeEvent) {
d.mu.Lock()
defer d.mu.Unlock()
@@ -282,6 +324,14 @@ func NewServiceRouter() *ServiceRouter {
return &ServiceRouter{}
}
+// SetStaticConfig applies config only when scope is service and Conditions
are present.
+func (s *ServiceRouter) SetStaticConfig(cfg *global.RouterConfig) {
+ if cfg == nil || cfg.Scope != constant.RouterScopeService ||
len(cfg.Conditions) == 0 {
+ return
+ }
+ s.DynamicRouter.SetStaticConfig(cfg)
+}
+
func (s *ServiceRouter) Priority() int64 {
return 140
}
@@ -340,6 +390,14 @@ func NewApplicationRouter(url *common.URL)
*ApplicationRouter {
return a
}
+// SetStaticConfig applies config only when scope is application and
Conditions are present.
+func (a *ApplicationRouter) SetStaticConfig(cfg *global.RouterConfig) {
+ if cfg == nil || cfg.Scope != constant.RouterScopeApplication ||
len(cfg.Conditions) == 0 {
+ return
+ }
+ a.DynamicRouter.SetStaticConfig(cfg)
+}
+
func (a *ApplicationRouter) Priority() int64 {
return 145
}
diff --git a/cluster/router/condition/router_test.go
b/cluster/router/condition/router_test.go
index 5c6da3b2d..c9b852168 100644
--- a/cluster/router/condition/router_test.go
+++ b/cluster/router/condition/router_test.go
@@ -137,6 +137,94 @@ func TestRouteMatchWhen(t *testing.T) {
}
}
+func TestDynamicRouterSetStaticConfig(t *testing.T) {
+ t.Run("enabled config builds condition routers", func(t *testing.T) {
+ d := &DynamicRouter{}
+ force := true
+ enabled := true
+
+ d.SetStaticConfig(&global.RouterConfig{
+ Force: &force,
+ Enabled: &enabled,
+ Conditions: []string{"host = 127.0.0.1 => host =
dubbo.apache.org"},
+ })
+
+ assert.True(t, d.force)
+ assert.True(t, d.enable)
+ routers, ok := d.conditionRouter.(stateRouters)
+ require.True(t, ok)
+ assert.Len(t, routers, 1)
+ })
+
+ t.Run("disabled config clears router", func(t *testing.T) {
+ d := &DynamicRouter{}
+ force := true
+ enabled := false
+
+ d.SetStaticConfig(&global.RouterConfig{
+ Force: &force,
+ Enabled: &enabled,
+ Conditions: []string{"host = 127.0.0.1 => host =
dubbo.apache.org"},
+ })
+
+ assert.True(t, d.force)
+ assert.False(t, d.enable)
+ assert.Nil(t, d.conditionRouter)
+ })
+
+ t.Run("empty condition is ignored without corrupting state", func(t
*testing.T) {
+ d := &DynamicRouter{}
+
+ d.SetStaticConfig(&global.RouterConfig{
+ Conditions: []string{""},
+ })
+
+ assert.False(t, d.force)
+ assert.True(t, d.enable)
+ routers, ok := d.conditionRouter.(stateRouters)
+ require.True(t, ok)
+ assert.Empty(t, routers)
+ })
+}
+
+func TestScopedStaticConfigSetters(t *testing.T) {
+ t.Run("service router only accepts service scope", func(t *testing.T) {
+ router := NewServiceRouter()
+
+ router.SetStaticConfig(&global.RouterConfig{
+ Scope: constant.RouterScopeApplication,
+ Conditions: []string{"host = 127.0.0.1 => host =
dubbo.apache.org"},
+ })
+ assert.Nil(t, router.conditionRouter)
+
+ router.SetStaticConfig(&global.RouterConfig{
+ Scope: constant.RouterScopeService,
+ Conditions: []string{"host = 127.0.0.1 => host =
dubbo.apache.org"},
+ })
+ assert.NotNil(t, router.conditionRouter)
+ })
+
+ t.Run("application router only accepts application scope", func(t
*testing.T) {
+ url := common.NewURLWithOptions(
+ common.WithProtocol("consumer"),
+ common.WithPath("com.foo.BarService"),
+ )
+ router := NewApplicationRouter(url)
+
+ router.SetStaticConfig(&global.RouterConfig{
+ Scope: constant.RouterScopeService,
+ Conditions: []string{"host = 127.0.0.1 => host =
dubbo.apache.org"},
+ })
+ assert.Nil(t, router.conditionRouter)
+
+ router.SetStaticConfig(&global.RouterConfig{
+ Scope: constant.RouterScopeApplication,
+ Conditions: []string{"host = 127.0.0.1 => host =
dubbo.apache.org"},
+ })
+ assert.NotNil(t, router.conditionRouter)
+ })
+}
+
// TestRouteMatchFilter also tests pattern_value.WildcardValuePattern's Match
method
func TestRouteMatchFilter(t *testing.T) {
diff --git a/cluster/router/router.go b/cluster/router/router.go
index 93df2c0ee..8c5f1b226 100644
--- a/cluster/router/router.go
+++ b/cluster/router/router.go
@@ -23,6 +23,7 @@ import (
import (
"dubbo.apache.org/dubbo-go/v3/common"
+ "dubbo.apache.org/dubbo-go/v3/global"
"dubbo.apache.org/dubbo-go/v3/protocol/base"
)
@@ -48,6 +49,23 @@ type PriorityRouter interface {
Notify(invokers []base.Invoker)
}
+// StaticConfigSetter is implemented by routers that accept static router
config injection.
+//
+// The chain passes every static config to every router that implements this
interface, so that
+// the chain does not need to know which config type
(condition/tag/script/...) belongs to which
+// router. Each router must approve only configs that apply to it: at the
start of SetStaticConfig,
+// if cfg is nil or does not match this router's type (e.g. no Conditions for
condition router,
+// no Tags for tag router), return immediately without modifying state. Only
apply when the config
+// is intended for this router.
+//
+// Static config injection happens once during RouterChain initialization and
serves as bootstrap
+// state for builtin routers. The chain does not merge static and dynamic
rules. Later dynamic
+// updates still go through each router's own Notify/Process path and override
the router's
+// current rule.
+type StaticConfigSetter interface {
+ SetStaticConfig(cfg *global.RouterConfig)
+}
+
// Poolable caches address pool and address metadata for a router instance
which will be used later in Router's Route.
type Poolable interface {
// Pool created address pool and address metadata from the invokers.
diff --git a/cluster/router/tag/match.go b/cluster/router/tag/match.go
index e8208848c..21254584c 100644
--- a/cluster/router/tag/match.go
+++ b/cluster/router/tag/match.go
@@ -133,7 +133,7 @@ func requestTag(invokers []base.Invoker, url *common.URL,
invocation base.Invoca
}
}
// returns the result directly
- if *cfg.Force || requestIsForce(url, invocation) {
+ if (cfg.Force != nil && *cfg.Force) || requestIsForce(url, invocation) {
return result
}
if len(result) != 0 {
diff --git a/cluster/router/tag/router.go b/cluster/router/tag/router.go
index 8fde2d38a..df8d36299 100644
--- a/cluster/router/tag/router.go
+++ b/cluster/router/tag/router.go
@@ -60,7 +60,9 @@ func (p *PriorityRouter) Route(invokers []base.Invoker, url
*common.URL, invocat
return staticTag(invokers, url, invocation)
}
routerCfg := value.(global.RouterConfig)
- if !*routerCfg.Enabled || !*routerCfg.Valid {
+ enabled := routerCfg.Enabled == nil || *routerCfg.Enabled
+ valid := (routerCfg.Valid != nil && *routerCfg.Valid) ||
(routerCfg.Valid == nil && len(routerCfg.Tags) > 0)
+ if !enabled || !valid {
return staticTag(invokers, url, invocation)
}
return dynamicTag(invokers, url, invocation, routerCfg)
@@ -102,6 +104,30 @@ func (p *PriorityRouter) Notify(invokers []base.Invoker) {
p.Process(&config_center.ConfigChangeEvent{Key: key, Value: value,
ConfigType: remoting.EventTypeAdd})
}
+// SetStaticConfig applies a RouterConfig directly, bypassing YAML parsing.
+// This is the correct entry point for static (code-configured) rules;
+// Process is designed for dynamic config-center updates that arrive as YAML
text.
+// Static and dynamic rules are not merged: later Process updates replace the
current state built here.
+func (p *PriorityRouter) SetStaticConfig(cfg *global.RouterConfig) {
+ if cfg == nil || cfg.Scope != constant.RouterScopeApplication ||
len(cfg.Tags) == 0 {
+ return
+ }
+ cfgCopy := cfg.Clone()
+ cfgCopy.Valid = new(bool)
+ *cfgCopy.Valid = len(cfgCopy.Tags) > 0
+ if cfgCopy.Enabled == nil {
+ cfgCopy.Enabled = new(bool)
+ *cfgCopy.Enabled = true
+ }
+ // Derive storage key the same way Notify() does: application + suffix
+ key := strings.Join([]string{cfg.Key, constant.TagRouterRuleSuffix}, "")
+ p.routerConfigs.Store(key, *cfgCopy)
+ logger.Infof("[tag router] Applied static tag router config: key=%s",
key)
+}
+
+// Process applies config-center updates as the authoritative rule source at
runtime.
+// It does not merge with static rules bootstrapped via SetStaticConfig; any
later
+// dynamic update replaces the current static-derived state.
func (p *PriorityRouter) Process(event *config_center.ConfigChangeEvent) {
if event.ConfigType == remoting.EventTypeDel {
p.routerConfigs.Delete(event.Key)
diff --git a/cluster/router/tag/router_test.go
b/cluster/router/tag/router_test.go
index 7b51aec91..630d573a9 100644
--- a/cluster/router/tag/router_test.go
+++ b/cluster/router/tag/router_test.go
@@ -419,3 +419,130 @@ tags:
assert.Nil(t, value)
})
}
+
+func TestSetStaticConfig(t *testing.T) {
+ t.Run("empty tag config is ignored", func(t *testing.T) {
+ p, err := NewTagPriorityRouter()
+ require.NoError(t, err)
+
+ p.SetStaticConfig(&global.RouterConfig{
+ Scope: constant.RouterScopeApplication,
+ Key: "test-app",
+ })
+
+ value, ok := p.routerConfigs.Load("test-app" +
constant.TagRouterRuleSuffix)
+ assert.False(t, ok)
+ assert.Nil(t, value)
+ })
+
+ t.Run("service-scope tag config is ignored", func(t *testing.T) {
+ p, err := NewTagPriorityRouter()
+ require.NoError(t, err)
+
+ p.SetStaticConfig(&global.RouterConfig{
+ Scope: constant.RouterScopeService,
+ Key: "svc.test",
+ Tags: []global.Tag{{
+ Name: "gray",
+ }},
+ })
+
+ value, ok := p.routerConfigs.Load("svc.test" +
constant.TagRouterRuleSuffix)
+ assert.False(t, ok)
+ assert.Nil(t, value)
+ })
+
+ t.Run("static tag config is cloned and stored with default enabled",
func(t *testing.T) { // NOSONAR
+ p, err := NewTagPriorityRouter()
+ require.NoError(t, err)
+
+ cfg := &global.RouterConfig{
+ Scope: constant.RouterScopeApplication,
+ Key: "test-app",
+ Tags: []global.Tag{{
+ Name: "gray",
+ Addresses: []string{"192.168.0.1:20000"}, //
NOSONAR
+ }},
+ }
+
+ p.SetStaticConfig(cfg)
+ cfg.Tags[0].Addresses[0] = "192.168.0.9:20000" // NOSONAR
+
+ value, ok := p.routerConfigs.Load("test-app" +
constant.TagRouterRuleSuffix)
+ require.True(t, ok)
+ routerCfg := value.(global.RouterConfig)
+ assert.True(t, *routerCfg.Enabled)
+ assert.True(t, *routerCfg.Valid)
+ assert.Equal(t, "192.168.0.1:20000",
routerCfg.Tags[0].Addresses[0]) // NOSONAR
+ })
+}
+
+func TestParseRoute(t *testing.T) {
+ t.Run("route with tags is valid", func(t *testing.T) {
+ cfg, err := parseRoute(`
+key: test-app
+tags:
+ - name: gray
+ addresses: [192.168.0.1:20000]
+`)
+ require.NoError(t, err)
+ require.NotNil(t, cfg.Valid)
+ assert.True(t, *cfg.Valid)
+ })
+
+ t.Run("route without tags is invalid", func(t *testing.T) {
+ cfg, err := parseRoute("key: test-app")
+ require.NoError(t, err)
+ require.NotNil(t, cfg.Valid)
+ assert.False(t, *cfg.Valid)
+ })
+}
+
+func TestRequestTagNilForceFallback(t *testing.T) {
+ initUrl()
+
+ ivk := base.NewBaseInvoker(url1)
+ ivk1 := base.NewBaseInvoker(url2)
+ ivk2 := base.NewBaseInvoker(url3)
+ invokerList := []base.Invoker{ivk, ivk1, ivk2}
+
+ result := requestTag(
+ invokerList,
+ consumerUrl,
+ invocation.NewRPCInvocation("GetUser", nil,
map[string]any{constant.Tagkey: "gray"}),
+ global.RouterConfig{
+ Tags: []global.Tag{{
+ Name: "gray",
+ }},
+ },
+ "gray",
+ )
+
+ assert.Len(t, result, 3)
+}
+
+func TestRouteNilDefaults(t *testing.T) {
+ initUrl()
+
+ p, err := NewTagPriorityRouter()
+ require.NoError(t, err)
+
+ ivk := base.NewBaseInvoker(url1)
+ ivk1 := base.NewBaseInvoker(url2)
+ ivk2 := base.NewBaseInvoker(url3)
+ invokerList := []base.Invoker{ivk, ivk1, ivk2}
+
+ p.routerConfigs.Store(constant.TagRouterRuleSuffix, global.RouterConfig{
+ Tags: []global.Tag{{
+ Name: "gray",
+ }},
+ })
+
+ result := p.Route(
+ invokerList,
+ consumerUrl,
+ invocation.NewRPCInvocation("GetUser", nil,
map[string]any{constant.Tagkey: "gray"}),
+ )
+
+ assert.Len(t, result, 3)
+}
diff --git a/common/constant/key.go b/common/constant/key.go
index 98aab2c65..2be7ffc88 100644
--- a/common/constant/key.go
+++ b/common/constant/key.go
@@ -73,6 +73,7 @@ const (
TripleConfigKey = "triple-config"
ConsumerConfigKey = "consumer-config"
RegistriesConfigKey = "registries-config"
+ RoutersConfigKey = "routers-config"
)
// TODO: remove this after old triple removed
@@ -349,6 +350,8 @@ const (
ConditionAppRouterFactoryKey = "provider.condition"
ConditionServiceRouterFactoryKey = "service.condition"
ScriptRouterFactoryKey = "consumer.script"
+ RouterScopeService = "service"
+ RouterScopeApplication = "application"
ForceKey = "force"
TrafficDisableKey = "trafficDisable"
Arguments = "arguments"
diff --git a/dubbo.go b/dubbo.go
index 125af0385..4fedea24b 100644
--- a/dubbo.go
+++ b/dubbo.go
@@ -77,6 +77,7 @@ func (ins *Instance) NewClient(opts ...client.ClientOption)
(*client.Client, err
otelCfg := ins.insOpts.CloneOtel()
tlsCfg := ins.insOpts.CloneTLSConfig()
protocolsCfg := ins.insOpts.CloneProtocols()
+ routersCfg := ins.insOpts.CloneRouter()
if conCfg != nil {
if !conCfg.Check {
@@ -113,6 +114,9 @@ func (ins *Instance) NewClient(opts ...client.ClientOption)
(*client.Client, err
if protocolsCfg != nil {
cliOpts = append(cliOpts,
client.SetClientProtocols(protocolsCfg))
}
+ if routersCfg != nil {
+ cliOpts = append(cliOpts, client.SetClientRouters(routersCfg))
+ }
// options passed by users has higher priority
cliOpts = append(cliOpts, opts...)