Copilot commented on code in PR #3439:
URL: https://github.com/apache/dubbo-go/pull/3439#discussion_r3426258721


##########
cluster/cluster/base/cluster_invoker.go:
##########
@@ -39,7 +41,7 @@ type BaseClusterInvoker struct {
        Directory      directory.Directory
        AvailableCheck bool
        Destroyed      *atomic.Bool
-       StickyInvoker  base.Invoker
+       stickyInvoker  stdatomic.Value // stores base.Invoker

Review Comment:
   `sync/atomic.Value` cannot store `nil` (it panics), so 
`invoker.setStickyInvoker(nil)` will crash at runtime. Also, `atomic.Value` 
requires all stored values have the exact same concrete dynamic type; storing 
different `base.Invoker` implementations over time can panic as soon as a 
different concrete type is stored. A robust fix is to store a single, 
consistent wrapper type (e.g., a struct containing `base.Invoker`) so the 
stored dynamic type never changes and clearing becomes storing a non-nil 
wrapper whose inner invoker is `nil`.



##########
cluster/cluster/base/cluster_invoker.go:
##########
@@ -50,6 +52,20 @@ func NewBaseClusterInvoker(directory directory.Directory) 
BaseClusterInvoker {
        }
 }
 
+// getStickyInvoker returns the current sticky invoker in a race-free manner.
+func (invoker *BaseClusterInvoker) getStickyInvoker() base.Invoker {
+       v := invoker.stickyInvoker.Load()
+       if v == nil {
+               return nil
+       }
+       return v.(base.Invoker)
+}
+
+// setStickyInvoker sets the sticky invoker in a race-free manner.
+func (invoker *BaseClusterInvoker) setStickyInvoker(invokerVal base.Invoker) {
+       invoker.stickyInvoker.Store(invokerVal)
+}

Review Comment:
   `sync/atomic.Value` cannot store `nil` (it panics), so 
`invoker.setStickyInvoker(nil)` will crash at runtime. Also, `atomic.Value` 
requires all stored values have the exact same concrete dynamic type; storing 
different `base.Invoker` implementations over time can panic as soon as a 
different concrete type is stored. A robust fix is to store a single, 
consistent wrapper type (e.g., a struct containing `base.Invoker`) so the 
stored dynamic type never changes and clearing becomes storing a non-nil 
wrapper whose inner invoker is `nil`.



##########
cluster/cluster/base/cluster_invoker.go:
##########
@@ -100,19 +116,21 @@ func (invoker *BaseClusterInvoker) DoSelect(lb 
loadbalance.LoadBalance, invocati
        // Get the service method sticky config if have
        sticky = url.GetMethodParamBool(invocation.MethodName(), 
constant.StickyKey, sticky)
 
-       if invoker.StickyInvoker != nil && !isInvoked(invoker.StickyInvoker, 
invokers) {
-               invoker.StickyInvoker = nil
+       stickyInvoker := invoker.getStickyInvoker()
+       if stickyInvoker != nil && !isInvoked(stickyInvoker, invokers) {
+               invoker.setStickyInvoker(nil)
+               stickyInvoker = nil
        }
 
        if sticky && invoker.AvailableCheck &&
-               invoker.StickyInvoker != nil && 
invoker.StickyInvoker.IsAvailable() &&
-               (invoked == nil || !isInvoked(invoker.StickyInvoker, invoked)) {
-               return invoker.StickyInvoker
+               stickyInvoker != nil && stickyInvoker.IsAvailable() &&
+               (invoked == nil || !isInvoked(stickyInvoker, invoked)) {
+               return stickyInvoker
        }
 
        selectedInvoker = invoker.doSelectInvoker(lb, invocation, invokers, 
invoked)
        if sticky {
-               invoker.StickyInvoker = selectedInvoker
+               invoker.setStickyInvoker(selectedInvoker)
        }

Review Comment:
   `sync/atomic.Value` cannot store `nil` (it panics), so 
`invoker.setStickyInvoker(nil)` will crash at runtime. Also, `atomic.Value` 
requires all stored values have the exact same concrete dynamic type; storing 
different `base.Invoker` implementations over time can panic as soon as a 
different concrete type is stored. A robust fix is to store a single, 
consistent wrapper type (e.g., a struct containing `base.Invoker`) so the 
stored dynamic type never changes and clearing becomes storing a non-nil 
wrapper whose inner invoker is `nil`.



##########
cluster/cluster/base/cluster_invoker.go:
##########
@@ -39,7 +41,7 @@ type BaseClusterInvoker struct {
        Directory      directory.Directory
        AvailableCheck bool
        Destroyed      *atomic.Bool
-       StickyInvoker  base.Invoker
+       stickyInvoker  stdatomic.Value // stores base.Invoker

Review Comment:
   This change removes/renames the previously exported `StickyInvoker` field to 
an unexported `stickyInvoker`, which is a breaking API change for any external 
package that referenced `BaseClusterInvoker.StickyInvoker`. If 
`BaseClusterInvoker` is part of a public API surface, consider preserving 
backward compatibility (e.g., keep `StickyInvoker` as a deprecated field or 
provide exported accessor methods and avoid removing exported fields in a 
non-major change).



##########
cluster/cluster/base/cluster_invoker_test.go:
##########
@@ -73,3 +74,73 @@ func TestStickyNormalWhenError(t *testing.T) {
        result1 := base.DoSelect(random.NewRandomLoadBalance(), 
invocation.NewRPCInvocation(baseClusterInvokerMethodName, nil, nil), invokers, 
invoked)
        assert.NotEqual(t, result, result1)
 }
+
+// TestStickyConcurrentDoSelect verifies that concurrent calls to DoSelect
+// with sticky enabled do not cause a data race on StickyInvoker.
+func TestStickyConcurrentDoSelect(t *testing.T) {
+       var invokers []protocolbase.Invoker
+       for i := 0; i < 10; i++ {
+               url, _ := common.NewURL(fmt.Sprintf(baseClusterInvokerFormat, 
i))
+               url.SetParam("sticky", "true")
+               invokers = append(invokers, clusterpkg.NewMockInvoker(url, 1))
+       }
+       base := &BaseClusterInvoker{}
+       base.AvailableCheck = true
+
+       lb := random.NewRandomLoadBalance()
+       invocation1 := 
invocation.NewRPCInvocation(baseClusterInvokerMethodName, nil, nil)
+
+       const concurrency = 100
+       var wg sync.WaitGroup
+       wg.Add(concurrency)
+       for i := 0; i < concurrency; i++ {
+               go func() {
+                       defer wg.Done()
+                       invoked := make([]protocolbase.Invoker, 0)
+                       result := base.DoSelect(lb, invocation1, invokers, 
invoked)
+                       assert.NotNil(t, result)
+                       // Second call should return the same sticky invoker
+                       result2 := base.DoSelect(lb, invocation1, invokers, 
invoked)
+                       assert.NotNil(t, result2)
+               }()
+       }
+       wg.Wait()
+}
+
+// TestStickyConcurrentIsAvailableAndDoSelect verifies that concurrent
+// IsAvailable and DoSelect calls do not cause a data race on StickyInvoker.

Review Comment:
   The test name/comments claim it verifies concurrent `IsAvailable` and 
`DoSelect`, but the goroutine calls `base.getStickyInvoker()` directly rather 
than `base.IsAvailable()`. Either update the comment/name to match what’s 
exercised, or adjust the test to actually call `IsAvailable()` (e.g., by 
providing a non-nil `Directory` stub) so it matches the stated intent.



##########
cluster/cluster/base/cluster_invoker_test.go:
##########
@@ -73,3 +74,73 @@ func TestStickyNormalWhenError(t *testing.T) {
        result1 := base.DoSelect(random.NewRandomLoadBalance(), 
invocation.NewRPCInvocation(baseClusterInvokerMethodName, nil, nil), invokers, 
invoked)
        assert.NotEqual(t, result, result1)
 }
+
+// TestStickyConcurrentDoSelect verifies that concurrent calls to DoSelect
+// with sticky enabled do not cause a data race on StickyInvoker.
+func TestStickyConcurrentDoSelect(t *testing.T) {
+       var invokers []protocolbase.Invoker
+       for i := 0; i < 10; i++ {
+               url, _ := common.NewURL(fmt.Sprintf(baseClusterInvokerFormat, 
i))
+               url.SetParam("sticky", "true")
+               invokers = append(invokers, clusterpkg.NewMockInvoker(url, 1))
+       }
+       base := &BaseClusterInvoker{}
+       base.AvailableCheck = true
+
+       lb := random.NewRandomLoadBalance()
+       invocation1 := 
invocation.NewRPCInvocation(baseClusterInvokerMethodName, nil, nil)
+
+       const concurrency = 100
+       var wg sync.WaitGroup
+       wg.Add(concurrency)
+       for i := 0; i < concurrency; i++ {
+               go func() {
+                       defer wg.Done()
+                       invoked := make([]protocolbase.Invoker, 0)
+                       result := base.DoSelect(lb, invocation1, invokers, 
invoked)
+                       assert.NotNil(t, result)
+                       // Second call should return the same sticky invoker
+                       result2 := base.DoSelect(lb, invocation1, invokers, 
invoked)
+                       assert.NotNil(t, result2)

Review Comment:
   The test comment states the second call should return the same sticky 
invoker, but the test doesn’t assert that invariant. Consider asserting `result 
== result2` (or equivalent) so the test verifies sticky semantics in addition 
to relying on `-race`.



##########
cluster/cluster/base/cluster_invoker.go:
##########
@@ -100,19 +116,21 @@ func (invoker *BaseClusterInvoker) DoSelect(lb 
loadbalance.LoadBalance, invocati
        // Get the service method sticky config if have
        sticky = url.GetMethodParamBool(invocation.MethodName(), 
constant.StickyKey, sticky)
 
-       if invoker.StickyInvoker != nil && !isInvoked(invoker.StickyInvoker, 
invokers) {
-               invoker.StickyInvoker = nil
+       stickyInvoker := invoker.getStickyInvoker()
+       if stickyInvoker != nil && !isInvoked(stickyInvoker, invokers) {
+               invoker.setStickyInvoker(nil)
+               stickyInvoker = nil
        }

Review Comment:
   `sync/atomic.Value` cannot store `nil` (it panics), so 
`invoker.setStickyInvoker(nil)` will crash at runtime. Also, `atomic.Value` 
requires all stored values have the exact same concrete dynamic type; storing 
different `base.Invoker` implementations over time can panic as soon as a 
different concrete type is stored. A robust fix is to store a single, 
consistent wrapper type (e.g., a struct containing `base.Invoker`) so the 
stored dynamic type never changes and clearing becomes storing a non-nil 
wrapper whose inner invoker is `nil`.



##########
cluster/cluster/base/cluster_invoker_test.go:
##########
@@ -73,3 +74,73 @@ func TestStickyNormalWhenError(t *testing.T) {
        result1 := base.DoSelect(random.NewRandomLoadBalance(), 
invocation.NewRPCInvocation(baseClusterInvokerMethodName, nil, nil), invokers, 
invoked)
        assert.NotEqual(t, result, result1)
 }
+
+// TestStickyConcurrentDoSelect verifies that concurrent calls to DoSelect
+// with sticky enabled do not cause a data race on StickyInvoker.
+func TestStickyConcurrentDoSelect(t *testing.T) {
+       var invokers []protocolbase.Invoker
+       for i := 0; i < 10; i++ {
+               url, _ := common.NewURL(fmt.Sprintf(baseClusterInvokerFormat, 
i))
+               url.SetParam("sticky", "true")
+               invokers = append(invokers, clusterpkg.NewMockInvoker(url, 1))
+       }
+       base := &BaseClusterInvoker{}
+       base.AvailableCheck = true
+
+       lb := random.NewRandomLoadBalance()
+       invocation1 := 
invocation.NewRPCInvocation(baseClusterInvokerMethodName, nil, nil)
+
+       const concurrency = 100
+       var wg sync.WaitGroup
+       wg.Add(concurrency)
+       for i := 0; i < concurrency; i++ {
+               go func() {
+                       defer wg.Done()
+                       invoked := make([]protocolbase.Invoker, 0)
+                       result := base.DoSelect(lb, invocation1, invokers, 
invoked)
+                       assert.NotNil(t, result)
+                       // Second call should return the same sticky invoker
+                       result2 := base.DoSelect(lb, invocation1, invokers, 
invoked)
+                       assert.NotNil(t, result2)
+               }()
+       }
+       wg.Wait()
+}
+
+// TestStickyConcurrentIsAvailableAndDoSelect verifies that concurrent
+// IsAvailable and DoSelect calls do not cause a data race on StickyInvoker.
+// Only the sticky invoker path is exercised (not the Directory fallback),
+// since Directory is nil in this test setup.
+func TestStickyConcurrentIsAvailableAndDoSelect(t *testing.T) {
+       var invokers []protocolbase.Invoker
+       for i := 0; i < 10; i++ {
+               url, _ := common.NewURL(fmt.Sprintf(baseClusterInvokerFormat, 
i))
+               url.SetParam("sticky", "true")
+               invokers = append(invokers, clusterpkg.NewMockInvoker(url, 1))
+       }
+       base := &BaseClusterInvoker{}
+       base.AvailableCheck = true
+
+       lb := random.NewRandomLoadBalance()
+       invocation1 := 
invocation.NewRPCInvocation(baseClusterInvokerMethodName, nil, nil)
+
+       // First DoSelect to set the sticky invoker so IsAvailable uses the 
sticky path
+       invoked := make([]protocolbase.Invoker, 0)
+       base.DoSelect(lb, invocation1, invokers, invoked)
+
+       const concurrency = 100
+       var wg sync.WaitGroup
+       wg.Add(concurrency * 2)
+       for i := 0; i < concurrency; i++ {
+               go func() {
+                       defer wg.Done()
+                       // IsAvailable reads StickyInvoker; this would race 
without atomic protection
+                       _ = base.getStickyInvoker()

Review Comment:
   The test name/comments claim it verifies concurrent `IsAvailable` and 
`DoSelect`, but the goroutine calls `base.getStickyInvoker()` directly rather 
than `base.IsAvailable()`. Either update the comment/name to match what’s 
exercised, or adjust the test to actually call `IsAvailable()` (e.g., by 
providing a non-nil `Directory` stub) so it matches the stated intent.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to