This is an automated email from the ASF dual-hosted git repository.

Alanxtl 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 9757e7735 docs(router): complete script router comments and unit test 
coverage (#3682)
9757e7735 is described below

commit 9757e7735d720e4f3bba348f8c4f94803b5fb61d
Author: MaoMeng <[email protected]>
AuthorDate: Wed Aug 19 14:25:54 2026 +0800

    docs(router): complete script router comments and unit test coverage (#3682)
    
    * docs(router): complete script router comments and unit test coverage
    
    * test(router): fix CI failures in script router tests
---
 cluster/router/script/instance/instances_pool.go   | 10 +++
 cluster/router/script/instance/js_instance.go      | 16 ++++
 cluster/router/script/instance/js_instance_test.go | 93 ++++++++++++++++++++++
 cluster/router/script/router.go                    | 16 +++-
 cluster/router/script/router_test.go               | 35 ++++++++
 5 files changed, 168 insertions(+), 2 deletions(-)

diff --git a/cluster/router/script/instance/instances_pool.go 
b/cluster/router/script/instance/instances_pool.go
index 891668dc8..bf6216e3d 100644
--- a/cluster/router/script/instance/instances_pool.go
+++ b/cluster/router/script/instance/instances_pool.go
@@ -34,6 +34,14 @@ func init() {
        setInstances(`javascript`, newJsInstances())
 }
 
+// ScriptInstances is the engine bound to one script type. It owns the whole
+// lifecycle of scripts of that type: compile, run and destroy.
+// Compile compiles the script and increments its reference count; compiling
+// the same script again only increments the count.
+// Run executes a compiled script; Run returns the original invokers for a
+// script that was never compiled or has been destroyed.
+// Destroy decrements the reference count of the compiled script; the entry
+// is removed from the cache once the count drops to zero.
 type ScriptInstances interface {
        Run(rawScript string, invokers []base.Invoker, invocation 
base.Invocation) ([]base.Invoker, error)
        Compile(rawScript string) error
@@ -42,6 +50,7 @@ type ScriptInstances interface {
 
 var factory map[string]ScriptInstances
 
+// GetInstances returns the engine instance for the given script type.
 func GetInstances(scriptType string) (ScriptInstances, error) {
        ins, ok := factory[strings.ToLower(scriptType)]
        if !ok {
@@ -58,6 +67,7 @@ func RangeInstances(f func(instance ScriptInstances) bool) {
        }
 }
 
+// setInstances sets the engine instance for the given script type.
 func setInstances(tpName string, instance ScriptInstances) {
        factory[tpName] = instance
 }
diff --git a/cluster/router/script/instance/js_instance.go 
b/cluster/router/script/instance/js_instance.go
index 58e51693b..db319c32f 100644
--- a/cluster/router/script/instance/js_instance.go
+++ b/cluster/router/script/instance/js_instance.go
@@ -38,21 +38,25 @@ const (
        jsScriptPrefix     = "\n" + jsScriptResultName + ` = `
 )
 
+// jsInstances is the JavaScript engine.
 type jsInstances struct {
        insPool *sync.Pool // store *goja.runtime
        pgLock  sync.RWMutex
        program map[string]*program // rawScript to compiledProgram
 }
 
+// jsInstance is one goja runtime.
 type jsInstance struct {
        rt *goja.Runtime
 }
 
+// program is the compiled JavaScript script with its reference count.
 type program struct {
        pg    *goja.Program
        count int32
 }
 
+// newProgram creates a new program.
 func newProgram(pg *goja.Program) *program {
        return &program{
                pg:    pg,
@@ -64,6 +68,7 @@ func (p *program) addCount(i int) int {
        return int(atomic.AddInt32(&p.count, int32(i)))
 }
 
+// newJsInstances creates a new jsInstances.
 func newJsInstances() *jsInstances {
        return &jsInstances{
                program: map[string]*program{},
@@ -73,6 +78,9 @@ func newJsInstances() *jsInstances {
        }
 }
 
+// Run runs the JavaScript script. When the script is not compiled
+// or invokers is empty, it returns the original invokers directly.
+// When an error occurs, it returns the original invokers and the error.
 func (i *jsInstances) Run(rawScript string, invokers []base.Invoker, 
invocation base.Invocation) ([]base.Invoker, error) {
        i.pgLock.RLock()
        pg, ok := i.program[rawScript]
@@ -114,6 +122,10 @@ func (i *jsInstances) Run(rawScript string, invokers 
[]base.Invoker, invocation
        return result, nil
 }
 
+// Compile compiles the JavaScript script. When the script is not
+// compiled, it compiles the script and increments the reference count.
+// When the script is already compiled, it only increments the reference
+// count, reusing the compiled script.
 func (i *jsInstances) Compile(rawScript string) error {
        var (
                ok bool
@@ -145,6 +157,9 @@ func (i *jsInstances) Compile(rawScript string) error {
        }
 }
 
+// Destroy destroys the JavaScript script. When the script reference
+// count is greater than 0, it only decrements the reference count.
+// When the script reference count reaches 0, it deletes the script.
 func (i *jsInstances) Destroy(rawScript string) {
        i.pgLock.Lock()
        if pg, ok := i.program[rawScript]; ok {
@@ -179,6 +194,7 @@ func (j jsInstance) initReplyVar() {
        }
 }
 
+// newJsInstance creates a new jsInstance.
 func newJsInstance() *jsInstance {
        return &jsInstance{
                rt: goja.New(),
diff --git a/cluster/router/script/instance/js_instance_test.go 
b/cluster/router/script/instance/js_instance_test.go
index 2ea812fa8..eccc80f5e 100644
--- a/cluster/router/script/instance/js_instance_test.go
+++ b/cluster/router/script/instance/js_instance_test.go
@@ -513,6 +513,38 @@ func TestFuncWithCompileConcurrent(t *testing.T) {
        wg.Wait()
 }
 
+func TestCompileConcurrentSingleProgram(t *testing.T) {
+       ins := newJsInstances()
+       const script = `(function route(i,v,c){ return [invokers[1], 
invokers[2]]; }(invokers,invocation,context));`
+       const n = 20
+       var wg sync.WaitGroup
+       for range n {
+               wg.Go(func() { ; assert.NoError(t, ins.Compile(script)) })
+       }
+       wg.Wait()
+       assert.Len(t, ins.program, 1)
+       assert.EqualValues(t, n, ins.program[script].count)
+}
+
+func TestRunConcurrentWithRace(t *testing.T) {
+       globalIns, err := GetInstances("javascript")
+       assert.NoError(t, err)
+       const script = `(function route(i,v,c){ return [invokers[1], 
invokers[2]]; }(invokers,invocation,context));`
+       assert.NoError(t, globalIns.Compile(script))
+       var wg sync.WaitGroup
+       for range 30 {
+               wg.Go(func() {
+                       ins, err := GetInstances("javascript")
+                       testify_require.NoError(t, err)
+                       invokers, inv, _ := getRouteArgs()
+                       got, err := ins.Run(script, invokers, inv)
+                       testify_require.NoError(t, err)
+                       assert.Len(t, got, 2)
+               })
+       }
+       wg.Wait()
+}
+
 func TestFuncWithCompileAndRunRepeatedly(t *testing.T) {
        pg, err := goja.Compile("routeJs", jsScriptPrefix+`(
 function route(invokers,invocation,context) {
@@ -549,6 +581,44 @@ function route(invokers,invocation,context) {
        }
 }
 
+func TestJsInstancesRunFailurePaths(t *testing.T) {
+       const throwScript = `(function route(i,v,c){ throw new Error("boom"); 
}(invokers,invocation,context));`
+       const nonArrayScript = `(function route(i,v,c){ return {}; 
}(invokers,invocation,context));`
+       const badElemScript = `(function route(i,v,c){ var r=[]; r.push(1); 
return r; }(invokers,invocation,context));`
+
+       t.Run("run without compile", func(t *testing.T) {
+               ins := newJsInstances()
+               invokers, inv, _ := getRouteArgs()
+               got, err := ins.Run(throwScript, invokers, inv)
+               testify_require.NoError(t, err)
+               assert.Equal(t, invokers, got)
+       })
+       t.Run("script throws", func(t *testing.T) {
+               ins := newJsInstances()
+               testify_require.NoError(t, ins.Compile(throwScript))
+               invokers, inv, _ := getRouteArgs()
+               got, err := ins.Run(throwScript, invokers, inv)
+               testify_require.Error(t, err)
+               assert.Equal(t, invokers, got)
+       })
+       t.Run("script returns non-array", func(t *testing.T) {
+               ins := newJsInstances()
+               testify_require.NoError(t, ins.Compile(nonArrayScript))
+               invokers, inv, _ := getRouteArgs()
+               got, err := ins.Run(nonArrayScript, invokers, inv)
+               testify_require.Error(t, err)
+               assert.Equal(t, invokers, got)
+       })
+       t.Run("script returns array with invalid element", func(t *testing.T) {
+               ins := newJsInstances()
+               testify_require.NoError(t, ins.Compile(badElemScript))
+               invokers, inv, _ := getRouteArgs()
+               got, err := ins.Run(badElemScript, invokers, inv)
+               testify_require.Error(t, err)
+               assert.Equal(t, invokers, got)
+       })
+}
+
 func setRunScriptEnv() *goja.Runtime {
        runtime := goja.New()
        rt_link_external_libraries(runtime)
@@ -579,6 +649,29 @@ func setRunScriptEnv() *goja.Runtime {
        return runtime
 }
 
+func TestProgramRefcountLifecycle(t *testing.T) {
+       ins := newJsInstances()
+       const script = `(function route(i,v,c){ return [invokers[1], 
invokers[2]]; }(invokers,invocation,context));`
+
+       assert.NoError(t, ins.Compile(script))
+       assert.NoError(t, ins.Compile(script))
+       assert.Len(t, ins.program, 1)
+       assert.EqualValues(t, 2, ins.program[script].count)
+
+       invokers, inv, _ := getRouteArgs()
+       ins.Destroy(script)
+       assert.Len(t, ins.program, 1)
+       got, err := ins.Run(script, invokers, inv)
+       testify_require.NoError(t, err)
+       assert.Len(t, got, 2)
+
+       ins.Destroy(script)
+       assert.Empty(t, ins.program)
+       got, err = ins.Run(script, invokers, inv)
+       testify_require.NoError(t, err)
+       assert.Len(t, got, 3)
+}
+
 func TestRunScriptInPanic(t *testing.T) {
        willPanic := func(errScript string) {
                rt := setRunScriptEnv()
diff --git a/cluster/router/script/router.go b/cluster/router/script/router.go
index 44ef8531d..9e73de792 100644
--- a/cluster/router/script/router.go
+++ b/cluster/router/script/router.go
@@ -49,6 +49,7 @@ type ScriptRouter struct {
        rawScript  string
 }
 
+// NewScriptRouter creates a new ScriptRouter.
 func NewScriptRouter() *ScriptRouter {
        return &ScriptRouter{
                applicationName: "",
@@ -56,6 +57,7 @@ func NewScriptRouter() *ScriptRouter {
        }
 }
 
+// parseRoute decodes a YAML script rule into a RouterConfig.
 func parseRoute(routeContent string) (*global.RouterConfig, error) {
        routeDecoder := yaml.NewDecoder(strings.NewReader(routeContent))
        routerConfig := &global.RouterConfig{}
@@ -66,6 +68,10 @@ func parseRoute(routeContent string) (*global.RouterConfig, 
error) {
        return routerConfig, nil
 }
 
+// Process receives a script rule change event and mutates the router state
+// accordingly. On a Del event it resets the router to the disabled, empty
+// state. On an Add or Update event it destroys the old instance first, then
+// compiles the new instance.
 func (s *ScriptRouter) Process(event *config_center.ConfigChangeEvent) {
        s.mu.Lock()
        defer s.mu.Unlock()
@@ -117,11 +123,11 @@ func (s *ScriptRouter) Process(event 
*config_center.ConfigChangeEvent) {
                        logger.Error("[Router][Script] applicationName field 
must be set in config")
                        return
                }
-               if !*cfg.Enabled {
+               if cfg.Enabled != nil && !*cfg.Enabled {
                        logger.Infof("[Router][Script] enabled field equals 
false, this rule will be ignored, script=%s", cfg.Script)
                }
                // rewrite to ScriptRouter
-               s.enabled = *cfg.Enabled
+               s.enabled = cfg.Enabled == nil || *cfg.Enabled
                s.rawScript = cfg.Script
                s.scriptType = cfg.ScriptType
 
@@ -144,6 +150,7 @@ func (s *ScriptRouter) Process(event 
*config_center.ConfigChangeEvent) {
        }
 }
 
+// runScript executes rawScript through the engine registered for scriptType.
 func (s *ScriptRouter) runScript(scriptType, rawScript string, invokers 
[]base.Invoker, invocation base.Invocation) ([]base.Invoker, error) {
        in, err := ins.GetInstances(scriptType)
        if err != nil {
@@ -152,6 +159,7 @@ func (s *ScriptRouter) runScript(scriptType, rawScript 
string, invokers []base.I
        return in.Run(rawScript, invokers, invocation)
 }
 
+// Route determines the target invokers by executing the enabled script.
 func (s *ScriptRouter) Route(invokers []base.Invoker, _ *common.URL, 
invocation base.Invocation) []base.Invoker {
        if len(invokers) == 0 {
                return []base.Invoker{}
@@ -173,14 +181,18 @@ func (s *ScriptRouter) Route(invokers []base.Invoker, _ 
*common.URL, invocation
        return res
 }
 
+// URL always returns nil.
 func (s *ScriptRouter) URL() *common.URL {
        return nil
 }
 
+// Priority always returns 0.
 func (s *ScriptRouter) Priority() int64 {
        return 0
 }
 
+// Notify subscribes this router to the script rule of the provider
+// application in the invoker list.
 func (s *ScriptRouter) Notify(invokers []base.Invoker) {
        if len(invokers) == 0 {
                return
diff --git a/cluster/router/script/router_test.go 
b/cluster/router/script/router_test.go
index e9b04c13e..75afb1471 100644
--- a/cluster/router/script/router_test.go
+++ b/cluster/router/script/router_test.go
@@ -242,6 +242,41 @@ func TestScriptRouterProcessSkipsNonStringConfig(t 
*testing.T) {
        assert.Equal(t, "old script", s.rawScript)
 }
 
+func TestProcessInvalidRuleDisablesRouter(t *testing.T) {
+       cases := []struct{ name, cfg string }{
+               {"invalid yaml", ":\n\tnot yaml : ["},
+               {"missing key", "configVersion: v3.0\ntype: 
javascript\nenabled: true\nscript: |-\n  1"},
+               {"missing type", "configVersion: v3.0\nkey: dubbo.io\nenabled: 
true\nscript: |-\n  1"},
+               {"missing script", "configVersion: v3.0\nkey: dubbo.io\ntype: 
javascript\nenabled: true"},
+       }
+       for _, c := range cases {
+               t.Run(c.name, func(t *testing.T) {
+                       s := NewScriptRouter()
+                       s.Process(&config_center.ConfigChangeEvent{Value: 
c.cfg, ConfigType: remoting.EventTypeUpdate})
+                       assert.False(t, s.enabled)
+                       invokers, inv, _ := getRouteCheckArgs()
+                       assert.True(t, checkInvokersSame(s.Route(invokers, nil, 
inv), invokers))
+               })
+       }
+}
+
+func TestProcessMissingEnabledDefaultsTrue(t *testing.T) {
+       s := NewScriptRouter()
+       assert.NotPanics(t, func() {
+               s.Process(&config_center.ConfigChangeEvent{
+                       Value: `configVersion: v3.0
+key: dubbo.io
+type: javascript
+script: |-
+  (function(){return invokers;}(invokers,invocation,context));`,
+                       ConfigType: remoting.EventTypeUpdate,
+               })
+       })
+       assert.True(t, s.enabled)
+       invokers, inv, _ := getRouteCheckArgs()
+       assert.Len(t, s.Route(invokers, nil, inv), 3)
+}
+
 func TestScriptRouterProcessDelSkipsConfigBody(t *testing.T) {
        s := &ScriptRouter{
                enabled:    true,

Reply via email to