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 c2d08ba8f feat(ospp): task 10 (#3662)
c2d08ba8f is described below

commit c2d08ba8fe7c265d8147e01774df26c76243fa4e
Author: SouthwestAsiaFloat <[email protected]>
AuthorDate: Sun Aug 16 08:13:57 2026 +0800

    feat(ospp): task 10 (#3662)
---
 cluster/router/chain/cache.go      |  3 ++
 cluster/router/chain/cache_test.go | 95 ++++++++++++++++++++++++++++++++++++++
 cluster/router/chain/chain.go      |  4 +-
 cluster/router/chain/chain_test.go | 50 ++++++++++++++++++++
 cluster/router/options.go          | 15 ++++++
 cluster/router/router.go           |  2 +
 6 files changed, 168 insertions(+), 1 deletion(-)

diff --git a/cluster/router/chain/cache.go b/cluster/router/chain/cache.go
index 24ddfb99a..93e295b9d 100644
--- a/cluster/router/chain/cache.go
+++ b/cluster/router/chain/cache.go
@@ -50,6 +50,7 @@ func newRouterCache() *routerCache {
        }
 }
 
+// GetInvokers returns a copy of the invoker snapshot used to build the cache.
 func (c *routerCache) GetInvokers() []base.Invoker {
        c.mu.RLock()
        defer c.mu.RUnlock()
@@ -62,6 +63,8 @@ func (c *routerCache) GetInvokers() []base.Invoker {
 // snapshot for the given Poolable. The returned invokers slice is shared and 
must not be
 // modified by the caller. The generation is always returned (even on a miss) 
so callers can
 // detect a snapshot rebuilt by a concurrent SetInvokers.
+// The lookup key is Poolable.Name. A hit returns the pool with its invoker 
snapshot and
+// generation; a miss returns a nil pool, nil invokers, and the current 
generation.
 func (c *routerCache) FindAddrPool(p router.Poolable) (router.AddrPool, 
[]base.Invoker, uint64) {
        c.mu.RLock()
        defer c.mu.RUnlock()
diff --git a/cluster/router/chain/cache_test.go 
b/cluster/router/chain/cache_test.go
new file mode 100644
index 000000000..39577509a
--- /dev/null
+++ b/cluster/router/chain/cache_test.go
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package chain
+
+import (
+       "testing"
+)
+
+import (
+       "github.com/RoaringBitmap/roaring"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+import (
+       "dubbo.apache.org/dubbo-go/v3/cluster/router"
+       "dubbo.apache.org/dubbo-go/v3/common"
+       "dubbo.apache.org/dubbo-go/v3/protocol/base"
+)
+
+type cacheTestRouter struct {
+       name       string
+       shouldPool bool
+       poolCalls  int
+}
+
+func (r *cacheTestRouter) Name() string          { return r.name }
+func (r *cacheTestRouter) ShouldPool() bool      { return r.shouldPool }
+func (r *cacheTestRouter) URL() *common.URL      { return nil }
+func (r *cacheTestRouter) Priority() int64       { return 0 }
+func (r *cacheTestRouter) Notify([]base.Invoker) {}
+
+func (r *cacheTestRouter) Route(invokers []base.Invoker, _ *common.URL, _ 
base.Invocation) []base.Invoker {
+       return invokers
+}
+
+func (r *cacheTestRouter) Pool(invokers []base.Invoker) (router.AddrPool, 
router.AddrMetadata) {
+       r.poolCalls++
+       all := roaring.New()
+       for i := range invokers {
+               all.Add(uint32(i))
+       }
+       return router.AddrPool{"all": all}, nil
+}
+
+// To test if the router loads its invokers into cache so that next time we 
could get invokers from cache
+func TestRouterCacheHit(t *testing.T) {
+       invoker := buildInvoker(t, "dubbo://127.0.0.1:20000/com.demo.Service")
+       poolable := &cacheTestRouter{name: "test", shouldPool: true}
+       cache := newRouterCache()
+
+       // A poolable router should populate an address pool during cache 
rebuild.
+       cache.rebuild(1, []base.Invoker{invoker}, 
[]router.PriorityRouter{poolable})
+       pool, cachedInvokers, generation := 
cache.FindAddrPool(&cacheTestRouter{name: "test"})
+       require.NotNil(t, pool)
+       assert.True(t, pool["all"].Contains(0))
+       assert.Equal(t, []base.Invoker{invoker}, cachedInvokers)
+       assert.Equal(t, uint64(1), generation)
+       assert.Equal(t, 1, poolable.poolCalls)
+}
+
+// To test if the cache is invalid after the cache is terminated and rebuilt.
+func TestRouterCacheInvalidation(t *testing.T) {
+       invoker := buildInvoker(t, "dubbo://127.0.0.1:20000/com.demo.Service")
+       poolable := &cacheTestRouter{name: "test", shouldPool: true}
+       cache := newRouterCache()
+       cache.rebuild(1, []base.Invoker{invoker}, 
[]router.PriorityRouter{poolable})
+       pool, _, _ := cache.FindAddrPool(poolable)
+       require.NotNil(t, pool)
+
+       // Rebuilding with pooling disabled should remove the previous cache 
entry.
+       poolable.shouldPool = false
+       cache.rebuild(2, []base.Invoker{invoker}, 
[]router.PriorityRouter{poolable})
+       pool, cachedInvokers, generation := cache.FindAddrPool(poolable)
+       assert.Nil(t, pool)
+       assert.Nil(t, cachedInvokers)
+       assert.Equal(t, uint64(2), generation)
+       assert.Equal(t, 1, poolable.poolCalls)
+}
diff --git a/cluster/router/chain/chain.go b/cluster/router/chain/chain.go
index c2ad25b40..422289472 100644
--- a/cluster/router/chain/chain.go
+++ b/cluster/router/chain/chain.go
@@ -39,7 +39,9 @@ import (
        "dubbo.apache.org/dubbo-go/v3/protocol/base"
 )
 
-// RouterChain Router chain
+// RouterChain first selects invokers for the requested service, then applies
+// priority routers in ascending priority order. Each router receives the
+// invokers returned by the previous router.
 type RouterChain struct {
        // Full list of addresses from registry, classified by method name.
        invokers []base.Invoker
diff --git a/cluster/router/chain/chain_test.go 
b/cluster/router/chain/chain_test.go
index 8adfd56f4..358d8f7d3 100644
--- a/cluster/router/chain/chain_test.go
+++ b/cluster/router/chain/chain_test.go
@@ -348,6 +348,56 @@ func TestRouteAppliesRoutersOnSnapshot(t *testing.T) {
        assert.Equal(t, 1, r2.lastSize)
 }
 
+// To test the situation where router is empty, and we expect that if there is 
no router,
+// the provider should be returned without doing anything
+func TestRouteWithoutRoutersReturnsInvokers(t *testing.T) {
+       consumerURL, err := common.NewURL(testConsumerServiceURL)
+       require.NoError(t, err)
+       invoker := buildInvoker(t, "dubbo://127.0.0.1:20000/com.demo.Service")
+       chain := &RouterChain{invokers: []base.Invoker{invoker}}
+
+       result := chain.Route(consumerURL, invocation.NewRPCInvocation("Say", 
nil, nil))
+
+       assert.Equal(t, []base.Invoker{invoker}, result)
+}
+
+// To test if the router chain works within ascending priority order, we add 
Router with priority 30
+// -> Router 10 -> Router 20, the result should be expected to be Router 10 -> 
Router 20 -> Router 30
+func TestAddRoutersAppliesAscendingPriorityOrder(t *testing.T) {
+       consumerURL, err := common.NewURL(testConsumerServiceURL)
+       require.NoError(t, err)
+       invoker := buildInvoker(t, "dubbo://127.0.0.1:20000/com.demo.Service")
+       order := make([]int64, 0, 3)
+       newRouter := func(priority int64) *testPriorityRouter {
+               return &testPriorityRouter{
+                       priority: priority,
+                       routeFn: func(invokers []base.Invoker, _ *common.URL, _ 
base.Invocation) []base.Invoker {
+                               order = append(order, priority)
+                               return invokers
+                       },
+               }
+       }
+       chain := &RouterChain{invokers: []base.Invoker{invoker}}
+       chain.AddRouters([]router.PriorityRouter{newRouter(30), newRouter(10), 
newRouter(20)})
+
+       chain.Route(consumerURL, invocation.NewRPCInvocation("Say", nil, nil))
+
+       assert.Equal(t, []int64{10, 20, 30}, order)
+}
+
+// TestRouteWithNilInvokersReturnsEmpty verifies that a nil invoker list 
produces an empty route result.
+func TestRouteWithNilInvokersReturnsEmpty(t *testing.T) {
+       consumerURL, err := common.NewURL(testConsumerServiceURL)
+       require.NoError(t, err)
+       chain := &RouterChain{}
+
+       chain.SetInvokers(nil)
+       result := chain.Route(consumerURL, invocation.NewRPCInvocation("Say", 
nil, nil))
+
+       assert.Empty(t, result)
+       assert.Empty(t, chain.cache.GetInvokers())
+}
+
 // TestSetInvokersIncrementsAndPublishesGeneration verifies that each 
SetInvokers bumps the
 // chain generation and that Route publishes the current generation into the 
invocation so
 // Poolable routers can validate their cache against it.
diff --git a/cluster/router/options.go b/cluster/router/options.go
index 3b3c1a5c4..6d91559ff 100644
--- a/cluster/router/options.go
+++ b/cluster/router/options.go
@@ -21,7 +21,9 @@ import (
        "dubbo.apache.org/dubbo-go/v3/global"
 )
 
+// Options contains the router configuration built by router options.
 type Options struct {
+       // Router holds the configuration modified by Option values.
        Router *global.RouterConfig
 }
 
@@ -31,6 +33,7 @@ func defaultOptions() *Options {
        }
 }
 
+// NewOptions returns router options initialized with the default router 
configuration.
 func NewOptions(opts ...Option) *Options {
        defOpts := defaultOptions()
        for _, opt := range opts {
@@ -39,68 +42,80 @@ func NewOptions(opts ...Option) *Options {
        return defOpts
 }
 
+// Option modifies router options.
 type Option func(*Options)
 
+// WithScope sets the rule scope, such as service or application.
 func WithScope(scope string) Option {
        return func(opts *Options) {
                opts.Router.Scope = scope
        }
 }
 
+// WithKey sets the service or application key to which the rule applies.
 func WithKey(key string) Option {
        return func(opts *Options) {
                opts.Router.Key = key
        }
 }
 
+// WithForce sets whether the rule should be enforced when it produces no 
matching provider.
 func WithForce(force bool) Option {
        return func(opts *Options) {
                opts.Router.Force = &force
        }
 }
 
+// WithRuntime sets whether the rule is evaluated at runtime.
 func WithRuntime(runtime bool) Option {
        return func(opts *Options) {
                opts.Router.Runtime = &runtime
        }
 }
 
+// WithEnabled sets whether the router rule is enabled.
 func WithEnabled(enabled bool) Option {
        return func(opts *Options) {
                opts.Router.Enabled = &enabled
        }
 }
 
+// WithValid records whether the router rule passed validation.
 func WithValid(valid bool) Option {
        return func(opts *Options) {
                opts.Router.Valid = &valid
        }
 }
 
+// WithPriority sets the rule priority. Lower values run before higher values.
 func WithPriority(priority int) Option {
        return func(opts *Options) {
                opts.Router.Priority = priority
        }
 }
 
+// WithConditions sets the condition expressions used by a condition router.
 func WithConditions(conditions []string) Option {
        return func(opts *Options) {
                opts.Router.Conditions = conditions
        }
 }
 
+// WithTags sets the tag definitions used by a tag router.
 func WithTags(tags []global.Tag) Option {
        return func(opts *Options) {
                opts.Router.Tags = tags
        }
 }
 
+// WithScript sets the script body used by a script router.
 func WithScript(script string) Option {
        return func(opts *Options) {
                opts.Router.Script = script
        }
 }
 
+// WithScriptType sets the script language used to evaluate the script body.
 func WithScriptType(scriptType string) Option {
        return func(opts *Options) {
                opts.Router.ScriptType = scriptType
diff --git a/cluster/router/router.go b/cluster/router/router.go
index 80c4765fc..69dd94929 100644
--- a/cluster/router/router.go
+++ b/cluster/router/router.go
@@ -63,6 +63,7 @@ type PriorityRouter interface {
 // updates still go through each router's own Notify/Process path and override 
the router's
 // current rule.
 type StaticConfigSetter interface {
+       // SetStaticConfig applies static configuration accepted by the router.
        SetStaticConfig(cfg *global.RouterConfig)
 }
 
@@ -110,5 +111,6 @@ type Cache interface {
 // Implemented by Poolable routers so that RouterChain can pass the cache
 // reference after it is built in SetInvokers.
 type CacheAccessor interface {
+       // SetCache supplies the cache owned by the router chain.
        SetCache(Cache)
 }

Reply via email to