From 20b60b906691b0f78a1bf5c7872bac1c1d2cf3d2 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 15 Jun 2026 11:47:27 +0800 Subject: [PATCH 1/5] feat(router): support static script router config --- cluster/router/script/router.go | 40 +++++++++++++++++++ cluster/router/script/router_test.go | 57 ++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/cluster/router/script/router.go b/cluster/router/script/router.go index 84073e15f6..1172e7505e 100644 --- a/cluster/router/script/router.go +++ b/cluster/router/script/router.go @@ -66,6 +66,46 @@ func parseRoute(routeContent string) (*global.RouterConfig, error) { return routerConfig, nil } +// SetStaticConfig applies a RouterConfig directly, bypassing YAML parsing. +// Static and dynamic rules are not merged: later Process updates replace the +// current state built here. +func (s *ScriptRouter) SetStaticConfig(cfg *global.RouterConfig) { + if cfg == nil || cfg.Scope != constant.RouterScopeApplication || cfg.ScriptType == "" || cfg.Script == "" { + return + } + + s.mu.Lock() + defer s.mu.Unlock() + + if s.enabled && s.scriptType != "" { + in, err := ins.GetInstances(s.scriptType) + if err != nil { + logger.Errorf("[Router][Script] GetInstances failed to destroy static config, error=%v", err) + } else { + in.Destroy(s.rawScript) + } + } + + s.scriptType = cfg.ScriptType + s.rawScript = cfg.Script + s.enabled = cfg.Enabled == nil || *cfg.Enabled + if !s.enabled { + logger.Infof("[Router][Script] static config is disabled, script=%s", cfg.Script) + return + } + + in, err := ins.GetInstances(s.scriptType) + if err != nil { + logger.Errorf("[Router][Script] GetInstances failed for static config, error=%v", err) + s.enabled = false + return + } + if err = in.Compile(s.rawScript); err != nil { + s.enabled = false + logger.Errorf("[Router][Script] compile static script failed, error=%v", err) + } +} + func (s *ScriptRouter) Process(event *config_center.ConfigChangeEvent) { s.mu.Lock() defer s.mu.Unlock() diff --git a/cluster/router/script/router_test.go b/cluster/router/script/router_test.go index e9b04c13ef..41589ae261 100644 --- a/cluster/router/script/router_test.go +++ b/cluster/router/script/router_test.go @@ -28,7 +28,9 @@ import ( import ( "dubbo.apache.org/dubbo-go/v3/common" + "dubbo.apache.org/dubbo-go/v3/common/constant" "dubbo.apache.org/dubbo-go/v3/config_center" + "dubbo.apache.org/dubbo-go/v3/global" "dubbo.apache.org/dubbo-go/v3/protocol/base" "dubbo.apache.org/dubbo-go/v3/protocol/invocation" "dubbo.apache.org/dubbo-go/v3/remoting" @@ -257,6 +259,61 @@ func TestScriptRouterProcessDelSkipsConfigBody(t *testing.T) { assert.Empty(t, s.rawScript) } +func TestScriptRouterSetStaticConfig(t *testing.T) { + staticScript := `(function route(invokers, invocation, context) { + var result = []; + for (var i = 0; i < invokers.length; i++) { + if (invokers[i].GetURL().Port === "20001") { + result.push(invokers[i]); + } + } + return result; + }(invokers, invocation, context));` + + t.Run("apply static script config", func(t *testing.T) { + invokers, inv, _ := getRouteCheckArgs() + s := NewScriptRouter() + s.SetStaticConfig(&global.RouterConfig{ + Scope: constant.RouterScopeApplication, + Key: "dubbo.io", + ScriptType: "javascript", + Script: staticScript, + }) + + got := s.Route(invokers, nil, inv) + assert.Len(t, got, 1) + assert.Equal(t, "20001", got[0].GetURL().Port) + }) + + t.Run("ignore disabled static script config", func(t *testing.T) { + invokers, inv, _ := getRouteCheckArgs() + enabled := false + s := NewScriptRouter() + s.SetStaticConfig(&global.RouterConfig{ + Scope: constant.RouterScopeApplication, + Key: "dubbo.io", + Enabled: &enabled, + ScriptType: "javascript", + Script: staticScript, + }) + + got := s.Route(invokers, nil, inv) + assert.True(t, checkInvokersSame(got, invokers)) + }) + + t.Run("ignore config without script", func(t *testing.T) { + invokers, inv, _ := getRouteCheckArgs() + s := NewScriptRouter() + s.SetStaticConfig(&global.RouterConfig{ + Scope: constant.RouterScopeApplication, + Key: "dubbo.io", + }) + + got := s.Route(invokers, nil, inv) + assert.True(t, checkInvokersSame(got, invokers)) + }) +} + func checkInvokersSame(invokers []base.Invoker, otherInvokers []base.Invoker) bool { k := map[string]struct{}{} for _, invoker := range otherInvokers { From 0f852bc639c02a464ea4e9753fee20ec8349e486 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 15 Jun 2026 11:55:38 +0800 Subject: [PATCH 2/5] test(router): cover static script config branches --- cluster/router/script/router_test.go | 59 ++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/cluster/router/script/router_test.go b/cluster/router/script/router_test.go index 41589ae261..07bdd76a15 100644 --- a/cluster/router/script/router_test.go +++ b/cluster/router/script/router_test.go @@ -260,15 +260,17 @@ func TestScriptRouterProcessDelSkipsConfigBody(t *testing.T) { } func TestScriptRouterSetStaticConfig(t *testing.T) { - staticScript := `(function route(invokers, invocation, context) { + staticScriptForPort := func(port string) string { + return `(function route(invokers, invocation, context) { var result = []; for (var i = 0; i < invokers.length; i++) { - if (invokers[i].GetURL().Port === "20001") { + if (invokers[i].GetURL().Port === "` + port + `") { result.push(invokers[i]); } } return result; }(invokers, invocation, context));` + } t.Run("apply static script config", func(t *testing.T) { invokers, inv, _ := getRouteCheckArgs() @@ -277,7 +279,7 @@ func TestScriptRouterSetStaticConfig(t *testing.T) { Scope: constant.RouterScopeApplication, Key: "dubbo.io", ScriptType: "javascript", - Script: staticScript, + Script: staticScriptForPort("20001"), }) got := s.Route(invokers, nil, inv) @@ -294,7 +296,7 @@ func TestScriptRouterSetStaticConfig(t *testing.T) { Key: "dubbo.io", Enabled: &enabled, ScriptType: "javascript", - Script: staticScript, + Script: staticScriptForPort("20001"), }) got := s.Route(invokers, nil, inv) @@ -312,6 +314,55 @@ func TestScriptRouterSetStaticConfig(t *testing.T) { got := s.Route(invokers, nil, inv) assert.True(t, checkInvokersSame(got, invokers)) }) + + t.Run("replace previous static script config", func(t *testing.T) { + invokers, inv, _ := getRouteCheckArgs() + s := NewScriptRouter() + s.SetStaticConfig(&global.RouterConfig{ + Scope: constant.RouterScopeApplication, + Key: "dubbo.io", + ScriptType: "javascript", + Script: staticScriptForPort("20001"), + }) + s.SetStaticConfig(&global.RouterConfig{ + Scope: constant.RouterScopeApplication, + Key: "dubbo.io", + ScriptType: "javascript", + Script: staticScriptForPort("20002"), + }) + + got := s.Route(invokers, nil, inv) + assert.Len(t, got, 1) + assert.Equal(t, "20002", got[0].GetURL().Port) + }) + + t.Run("disable unsupported script type", func(t *testing.T) { + invokers, inv, _ := getRouteCheckArgs() + s := NewScriptRouter() + s.SetStaticConfig(&global.RouterConfig{ + Scope: constant.RouterScopeApplication, + Key: "dubbo.io", + ScriptType: "unsupported", + Script: staticScriptForPort("20001"), + }) + + got := s.Route(invokers, nil, inv) + assert.True(t, checkInvokersSame(got, invokers)) + }) + + t.Run("disable invalid static script", func(t *testing.T) { + invokers, inv, _ := getRouteCheckArgs() + s := NewScriptRouter() + s.SetStaticConfig(&global.RouterConfig{ + Scope: constant.RouterScopeApplication, + Key: "dubbo.io", + ScriptType: "javascript", + Script: "bad input", + }) + + got := s.Route(invokers, nil, inv) + assert.True(t, checkInvokersSame(got, invokers)) + }) } func checkInvokersSame(invokers []base.Invoker, otherInvokers []base.Invoker) bool { From fcb7aeada19e2af5c07feb353ebbd14649928a2d Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 15 Jun 2026 12:04:15 +0800 Subject: [PATCH 3/5] test(router): cover stale static script config --- cluster/router/script/router_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/cluster/router/script/router_test.go b/cluster/router/script/router_test.go index 07bdd76a15..2752ca51bd 100644 --- a/cluster/router/script/router_test.go +++ b/cluster/router/script/router_test.go @@ -336,6 +336,25 @@ func TestScriptRouterSetStaticConfig(t *testing.T) { assert.Equal(t, "20002", got[0].GetURL().Port) }) + t.Run("replace stale config with unsupported script type", func(t *testing.T) { + invokers, inv, _ := getRouteCheckArgs() + s := &ScriptRouter{ + enabled: true, + scriptType: "unsupported", + rawScript: "old script", + } + s.SetStaticConfig(&global.RouterConfig{ + Scope: constant.RouterScopeApplication, + Key: "dubbo.io", + ScriptType: "javascript", + Script: staticScriptForPort("20001"), + }) + + got := s.Route(invokers, nil, inv) + assert.Len(t, got, 1) + assert.Equal(t, "20001", got[0].GetURL().Port) + }) + t.Run("disable unsupported script type", func(t *testing.T) { invokers, inv, _ := getRouteCheckArgs() s := NewScriptRouter() From 3a3445066f01f5854a4179442051449bbc42da4e Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Tue, 16 Jun 2026 09:43:11 +0800 Subject: [PATCH 4/5] fix(router): validate static script config key --- cluster/router/script/router.go | 4 ++-- cluster/router/script/router_test.go | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/cluster/router/script/router.go b/cluster/router/script/router.go index 1172e7505e..055d61f55f 100644 --- a/cluster/router/script/router.go +++ b/cluster/router/script/router.go @@ -70,7 +70,7 @@ func parseRoute(routeContent string) (*global.RouterConfig, error) { // Static and dynamic rules are not merged: later Process updates replace the // current state built here. func (s *ScriptRouter) SetStaticConfig(cfg *global.RouterConfig) { - if cfg == nil || cfg.Scope != constant.RouterScopeApplication || cfg.ScriptType == "" || cfg.Script == "" { + if cfg == nil || cfg.Scope != constant.RouterScopeApplication || cfg.Key == "" || cfg.ScriptType == "" || cfg.Script == "" { return } @@ -90,7 +90,7 @@ func (s *ScriptRouter) SetStaticConfig(cfg *global.RouterConfig) { s.rawScript = cfg.Script s.enabled = cfg.Enabled == nil || *cfg.Enabled if !s.enabled { - logger.Infof("[Router][Script] static config is disabled, script=%s", cfg.Script) + logger.Infof("[Router][Script] static config is disabled, key=%s, type=%s", cfg.Key, cfg.ScriptType) return } diff --git a/cluster/router/script/router_test.go b/cluster/router/script/router_test.go index 2752ca51bd..ae2a074366 100644 --- a/cluster/router/script/router_test.go +++ b/cluster/router/script/router_test.go @@ -315,6 +315,19 @@ func TestScriptRouterSetStaticConfig(t *testing.T) { assert.True(t, checkInvokersSame(got, invokers)) }) + t.Run("ignore config without key", func(t *testing.T) { + invokers, inv, _ := getRouteCheckArgs() + s := NewScriptRouter() + s.SetStaticConfig(&global.RouterConfig{ + Scope: constant.RouterScopeApplication, + ScriptType: "javascript", + Script: staticScriptForPort("20001"), + }) + + got := s.Route(invokers, nil, inv) + assert.True(t, checkInvokersSame(got, invokers)) + }) + t.Run("replace previous static script config", func(t *testing.T) { invokers, inv, _ := getRouteCheckArgs() s := NewScriptRouter() From 5b25f500ab8ea47d293185fbb807096d98f7b696 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Thu, 18 Jun 2026 16:06:15 +0800 Subject: [PATCH 5/5] fix(router): isolate static script rules by application --- cluster/router/script/router.go | 56 ++++++++++++++++++++-------- cluster/router/script/router_test.go | 48 ++++++++++++++++++------ 2 files changed, 78 insertions(+), 26 deletions(-) diff --git a/cluster/router/script/router.go b/cluster/router/script/router.go index 055d61f55f..3d1e0685b1 100644 --- a/cluster/router/script/router.go +++ b/cluster/router/script/router.go @@ -43,7 +43,14 @@ import ( type ScriptRouter struct { applicationName string - mu sync.RWMutex + mu sync.RWMutex + enabled bool + scriptType string + rawScript string + staticRules map[string]scriptRule +} + +type scriptRule struct { enabled bool scriptType string rawScript string @@ -67,8 +74,6 @@ func parseRoute(routeContent string) (*global.RouterConfig, error) { } // SetStaticConfig applies a RouterConfig directly, bypassing YAML parsing. -// Static and dynamic rules are not merged: later Process updates replace the -// current state built here. func (s *ScriptRouter) SetStaticConfig(cfg *global.RouterConfig) { if cfg == nil || cfg.Scope != constant.RouterScopeApplication || cfg.Key == "" || cfg.ScriptType == "" || cfg.Script == "" { return @@ -77,33 +82,36 @@ func (s *ScriptRouter) SetStaticConfig(cfg *global.RouterConfig) { s.mu.Lock() defer s.mu.Unlock() - if s.enabled && s.scriptType != "" { - in, err := ins.GetInstances(s.scriptType) + storageKey := strings.Join([]string{cfg.Key, constant.ScriptRouterRuleSuffix}, "") + if oldRule, ok := s.staticRules[storageKey]; ok && oldRule.enabled && oldRule.scriptType != "" { + in, err := ins.GetInstances(oldRule.scriptType) if err != nil { logger.Errorf("[Router][Script] GetInstances failed to destroy static config, error=%v", err) } else { - in.Destroy(s.rawScript) + in.Destroy(oldRule.rawScript) } } + delete(s.staticRules, storageKey) - s.scriptType = cfg.ScriptType - s.rawScript = cfg.Script - s.enabled = cfg.Enabled == nil || *cfg.Enabled - if !s.enabled { + enabled := cfg.Enabled == nil || *cfg.Enabled + if !enabled { logger.Infof("[Router][Script] static config is disabled, key=%s, type=%s", cfg.Key, cfg.ScriptType) return } - in, err := ins.GetInstances(s.scriptType) + in, err := ins.GetInstances(cfg.ScriptType) if err != nil { logger.Errorf("[Router][Script] GetInstances failed for static config, error=%v", err) - s.enabled = false return } - if err = in.Compile(s.rawScript); err != nil { - s.enabled = false + if err = in.Compile(cfg.Script); err != nil { logger.Errorf("[Router][Script] compile static script failed, error=%v", err) + return + } + if s.staticRules == nil { + s.staticRules = make(map[string]scriptRule) } + s.staticRules[storageKey] = scriptRule{enabled: true, scriptType: cfg.ScriptType, rawScript: cfg.Script} } func (s *ScriptRouter) Process(event *config_center.ConfigChangeEvent) { @@ -127,6 +135,10 @@ func (s *ScriptRouter) Process(event *config_center.ConfigChangeEvent) { logger.Errorf("[Router][Script] route config value must be string, actualType=%T", event.Value) return } + if strings.TrimSpace(rawConf) == "" { + logger.Infof("[Router][Script] script rule is empty, key=%s", event.Key) + return + } cfg, err := parseRoute(rawConf) if err != nil { logger.Errorf("[Router][Script] parse route config failed, error=%v", err) @@ -199,9 +211,18 @@ func (s *ScriptRouter) Route(invokers []base.Invoker, _ *common.URL, invocation s.mu.RLock() enabled, scriptType, rawScript := s.enabled, s.scriptType, s.rawScript + if !enabled || scriptType == "" || rawScript == "" { + if url := invokers[0].GetURL(); url != nil { + providerApplication := url.GetParam("application", "") + staticKey := strings.Join([]string{providerApplication, constant.ScriptRouterRuleSuffix}, "") + if rule, ok := s.staticRules[staticKey]; ok { + enabled, scriptType, rawScript = rule.enabled, rule.scriptType, rule.rawScript + } + } + } s.mu.RUnlock() - if !enabled || s.scriptType == "" || s.rawScript == "" { + if !enabled || scriptType == "" || rawScript == "" { return invokers } @@ -258,6 +279,11 @@ func (s *ScriptRouter) Notify(invokers []base.Invoker) { value, err = dynamicConfiguration.GetRule(listenTarget) if err != nil { logger.Errorf("[Router][Script] query script rule failed, applicationName=%s listenTarget=%s error=%v", s.applicationName, listenTarget, err) + return + } + if value == "" { + logger.Infof("[Router][Script] script rule is empty, listenTarget=%s", listenTarget) + return } s.Process(&config_center.ConfigChangeEvent{Key: listenTarget, Value: value, ConfigType: remoting.EventTypeUpdate}) } diff --git a/cluster/router/script/router_test.go b/cluster/router/script/router_test.go index ae2a074366..6e90f29c5a 100644 --- a/cluster/router/script/router_test.go +++ b/cluster/router/script/router_test.go @@ -277,7 +277,7 @@ func TestScriptRouterSetStaticConfig(t *testing.T) { s := NewScriptRouter() s.SetStaticConfig(&global.RouterConfig{ Scope: constant.RouterScopeApplication, - Key: "dubbo.io", + Key: "BDTService", ScriptType: "javascript", Script: staticScriptForPort("20001"), }) @@ -293,7 +293,7 @@ func TestScriptRouterSetStaticConfig(t *testing.T) { s := NewScriptRouter() s.SetStaticConfig(&global.RouterConfig{ Scope: constant.RouterScopeApplication, - Key: "dubbo.io", + Key: "BDTService", Enabled: &enabled, ScriptType: "javascript", Script: staticScriptForPort("20001"), @@ -308,7 +308,7 @@ func TestScriptRouterSetStaticConfig(t *testing.T) { s := NewScriptRouter() s.SetStaticConfig(&global.RouterConfig{ Scope: constant.RouterScopeApplication, - Key: "dubbo.io", + Key: "BDTService", }) got := s.Route(invokers, nil, inv) @@ -333,13 +333,13 @@ func TestScriptRouterSetStaticConfig(t *testing.T) { s := NewScriptRouter() s.SetStaticConfig(&global.RouterConfig{ Scope: constant.RouterScopeApplication, - Key: "dubbo.io", + Key: "BDTService", ScriptType: "javascript", Script: staticScriptForPort("20001"), }) s.SetStaticConfig(&global.RouterConfig{ Scope: constant.RouterScopeApplication, - Key: "dubbo.io", + Key: "BDTService", ScriptType: "javascript", Script: staticScriptForPort("20002"), }) @@ -349,16 +349,42 @@ func TestScriptRouterSetStaticConfig(t *testing.T) { assert.Equal(t, "20002", got[0].GetURL().Port) }) + t.Run("select static script config by provider application", func(t *testing.T) { + invokers, inv, _ := getRouteCheckArgs() + s := NewScriptRouter() + s.SetStaticConfig(&global.RouterConfig{ + Scope: constant.RouterScopeApplication, + Key: "BDTService", + ScriptType: "javascript", + Script: staticScriptForPort("20002"), + }) + s.SetStaticConfig(&global.RouterConfig{ + Scope: constant.RouterScopeApplication, + Key: "OtherService", + ScriptType: "javascript", + Script: staticScriptForPort("20001"), + }) + + got := s.Route(invokers, nil, inv) + assert.Len(t, got, 1) + assert.Equal(t, "20002", got[0].GetURL().Port) + assert.Len(t, s.staticRules, 2) + }) + t.Run("replace stale config with unsupported script type", func(t *testing.T) { invokers, inv, _ := getRouteCheckArgs() s := &ScriptRouter{ - enabled: true, - scriptType: "unsupported", - rawScript: "old script", + staticRules: map[string]scriptRule{ + "BDTService" + constant.ScriptRouterRuleSuffix: { + enabled: true, + scriptType: "unsupported", + rawScript: "old script", + }, + }, } s.SetStaticConfig(&global.RouterConfig{ Scope: constant.RouterScopeApplication, - Key: "dubbo.io", + Key: "BDTService", ScriptType: "javascript", Script: staticScriptForPort("20001"), }) @@ -373,7 +399,7 @@ func TestScriptRouterSetStaticConfig(t *testing.T) { s := NewScriptRouter() s.SetStaticConfig(&global.RouterConfig{ Scope: constant.RouterScopeApplication, - Key: "dubbo.io", + Key: "BDTService", ScriptType: "unsupported", Script: staticScriptForPort("20001"), }) @@ -387,7 +413,7 @@ func TestScriptRouterSetStaticConfig(t *testing.T) { s := NewScriptRouter() s.SetStaticConfig(&global.RouterConfig{ Scope: constant.RouterScopeApplication, - Key: "dubbo.io", + Key: "BDTService", ScriptType: "javascript", Script: "bad input", })