From e9bf31e734c2cfa47b349f71807249d81bb6703d Mon Sep 17 00:00:00 2001 From: liuhy Date: Wed, 17 Jun 2026 15:10:33 +0800 Subject: [PATCH 1/4] fix(cluster): eliminate data race on StickyInvoker with atomic.Value StickyInvoker in BaseClusterInvoker was a plain field read/written by multiple goroutines (e.g. failback retry goroutine + Invoke goroutine) without any synchronization, causing a data race on the RPC hot path. Replace the unexported StickyInvoker field with an atomic.Value, accessed via getStickyInvoker()/setStickyInvoker() helpers. This eliminates the race while keeping the lock-free read path fast. Key changes: - StickyInvoker base.Invoker -> stickyInvoker atomic.Value (unexported) - Add getStickyInvoker() / setStickyInvoker() helpers - Update IsAvailable() and DoSelect() to use atomic accessors - DoSelect reads sticky invoker into a local variable to avoid redundant atomic loads and ensure consistent logic within one call - Add concurrent tests (TestStickyConcurrentDoSelect, TestStickyConcurrentIsAvailableAndDoSelect) that pass with -race Co-Authored-By: Claude --- cluster/cluster/base/cluster_invoker.go | 36 +++++++--- cluster/cluster/base/cluster_invoker_test.go | 71 ++++++++++++++++++++ 2 files changed, 98 insertions(+), 9 deletions(-) diff --git a/cluster/cluster/base/cluster_invoker.go b/cluster/cluster/base/cluster_invoker.go index 80f0ca89e4..8899f9b1d3 100644 --- a/cluster/cluster/base/cluster_invoker.go +++ b/cluster/cluster/base/cluster_invoker.go @@ -19,6 +19,8 @@ package base import ( + stdatomic "sync/atomic" + "github.com/dubbogo/gost/log/logger" perrors "github.com/pkg/errors" @@ -39,7 +41,7 @@ type BaseClusterInvoker struct { Directory directory.Directory AvailableCheck bool Destroyed *atomic.Bool - StickyInvoker base.Invoker + stickyInvoker stdatomic.Value // stores base.Invoker } func NewBaseClusterInvoker(directory directory.Directory) BaseClusterInvoker { @@ -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) +} + func (invoker *BaseClusterInvoker) GetURL() *common.URL { return invoker.Directory.GetURL() } @@ -62,8 +78,8 @@ func (invoker *BaseClusterInvoker) Destroy() { } func (invoker *BaseClusterInvoker) IsAvailable() bool { - if invoker.StickyInvoker != nil { - return invoker.StickyInvoker.IsAvailable() + if sticky := invoker.getStickyInvoker(); sticky != nil { + return sticky.IsAvailable() } return invoker.Directory.IsAvailable() } @@ -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) } return selectedInvoker } diff --git a/cluster/cluster/base/cluster_invoker_test.go b/cluster/cluster/base/cluster_invoker_test.go index 454ae7df7a..e7a5ae516e 100644 --- a/cluster/cluster/base/cluster_invoker_test.go +++ b/cluster/cluster/base/cluster_invoker_test.go @@ -19,6 +19,7 @@ package base import ( "fmt" + "sync" "testing" ) @@ -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() + }() + go func() { + defer wg.Done() + base.DoSelect(lb, invocation1, invokers, invoked) + }() + } + wg.Wait() +} From 9f5cdbd071a8cd29444adc0103658ed0100d501d Mon Sep 17 00:00:00 2001 From: liuhy Date: Wed, 17 Jun 2026 16:16:06 +0800 Subject: [PATCH 2/4] fix(cluster): use wrapper type for atomic.Value to prevent nil Store and type mismatch panics Address Copilot review feedback: 1. atomic.Value.Store(nil) panics - wrap base.Invoker in stickyInvokerWrapper struct so nil invoker is stored as {invoker: nil} (non-nil wrapper) 2. atomic.Value requires consistent concrete type - the wrapper struct ensures the same type is always stored regardless of Invoker impl 3. Fix test TestStickyConcurrentIsAvailableAndDoSelect to actually call IsAvailable() with a proper mock Directory (instead of getStickyInvoker) 4. Fix imports formatting per dubbo-go convention (separate import groups) Co-Authored-By: Claude --- cluster/cluster/base/cluster_invoker.go | 15 +++++++-- cluster/cluster/base/cluster_invoker_test.go | 32 +++++++++++++++----- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/cluster/cluster/base/cluster_invoker.go b/cluster/cluster/base/cluster_invoker.go index 8899f9b1d3..0ad07e1c67 100644 --- a/cluster/cluster/base/cluster_invoker.go +++ b/cluster/cluster/base/cluster_invoker.go @@ -20,7 +20,9 @@ package base import ( stdatomic "sync/atomic" +) +import ( "github.com/dubbogo/gost/log/logger" perrors "github.com/pkg/errors" @@ -37,11 +39,18 @@ import ( "dubbo.apache.org/dubbo-go/v3/protocol/base" ) +// stickyInvokerWrapper wraps a base.Invoker so that atomic.Value always +// stores the same concrete type (avoiding panic on inconsistent types) +// and can represent a nil invoker (avoiding panic on Store(nil)). +type stickyInvokerWrapper struct { + invoker base.Invoker +} + type BaseClusterInvoker struct { Directory directory.Directory AvailableCheck bool Destroyed *atomic.Bool - stickyInvoker stdatomic.Value // stores base.Invoker + stickyInvoker stdatomic.Value // stores stickyInvokerWrapper } func NewBaseClusterInvoker(directory directory.Directory) BaseClusterInvoker { @@ -58,12 +67,12 @@ func (invoker *BaseClusterInvoker) getStickyInvoker() base.Invoker { if v == nil { return nil } - return v.(base.Invoker) + return v.(stickyInvokerWrapper).invoker } // setStickyInvoker sets the sticky invoker in a race-free manner. func (invoker *BaseClusterInvoker) setStickyInvoker(invokerVal base.Invoker) { - invoker.stickyInvoker.Store(invokerVal) + invoker.stickyInvoker.Store(stickyInvokerWrapper{invoker: invokerVal}) } func (invoker *BaseClusterInvoker) GetURL() *common.URL { diff --git a/cluster/cluster/base/cluster_invoker_test.go b/cluster/cluster/base/cluster_invoker_test.go index e7a5ae516e..a9f8d3a464 100644 --- a/cluster/cluster/base/cluster_invoker_test.go +++ b/cluster/cluster/base/cluster_invoker_test.go @@ -99,9 +99,6 @@ func TestStickyConcurrentDoSelect(t *testing.T) { 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() @@ -109,8 +106,6 @@ func TestStickyConcurrentDoSelect(t *testing.T) { // 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++ { @@ -118,7 +113,11 @@ func TestStickyConcurrentIsAvailableAndDoSelect(t *testing.T) { url.SetParam("sticky", "true") invokers = append(invokers, clusterpkg.NewMockInvoker(url, 1)) } - base := &BaseClusterInvoker{} + + // Use NewBaseClusterInvoker so that Directory is initialized, + // allowing IsAvailable() to work without panicking. + dir := newMockDirectory(invokers) + base := NewBaseClusterInvoker(dir) base.AvailableCheck = true lb := random.NewRandomLoadBalance() @@ -134,8 +133,7 @@ func TestStickyConcurrentIsAvailableAndDoSelect(t *testing.T) { for i := 0; i < concurrency; i++ { go func() { defer wg.Done() - // IsAvailable reads StickyInvoker; this would race without atomic protection - _ = base.getStickyInvoker() + base.IsAvailable() }() go func() { defer wg.Done() @@ -144,3 +142,21 @@ func TestStickyConcurrentIsAvailableAndDoSelect(t *testing.T) { } wg.Wait() } + +// mockDirectory is a minimal directory.Directory implementation for testing. +type mockDirectory struct { + invokers []protocolbase.Invoker + url *common.URL +} + +func newMockDirectory(invokers []protocolbase.Invoker) *mockDirectory { + url, _ := common.NewURL(baseClusterInvokerFormat) + url.SetParam("sticky", "true") + return &mockDirectory{invokers: invokers, url: url} +} + +func (d *mockDirectory) GetURL() *common.URL { return d.url } +func (d *mockDirectory) IsAvailable() bool { return true } +func (d *mockDirectory) Destroy() {} +func (d *mockDirectory) List(protocolbase.Invocation) []protocolbase.Invoker { return d.invokers } +func (d *mockDirectory) Subscribe(*common.URL) error { return nil } From d8eb2cc89b7bae591a06201170509e567bc48a11 Mon Sep 17 00:00:00 2001 From: liuhy Date: Wed, 17 Jun 2026 16:45:55 +0800 Subject: [PATCH 3/4] style: fix import formatting in test file per imports-formatter Co-Authored-By: Claude --- cluster/cluster/base/cluster_invoker_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cluster/cluster/base/cluster_invoker_test.go b/cluster/cluster/base/cluster_invoker_test.go index a9f8d3a464..a4399a3480 100644 --- a/cluster/cluster/base/cluster_invoker_test.go +++ b/cluster/cluster/base/cluster_invoker_test.go @@ -155,8 +155,8 @@ func newMockDirectory(invokers []protocolbase.Invoker) *mockDirectory { return &mockDirectory{invokers: invokers, url: url} } -func (d *mockDirectory) GetURL() *common.URL { return d.url } -func (d *mockDirectory) IsAvailable() bool { return true } -func (d *mockDirectory) Destroy() {} +func (d *mockDirectory) GetURL() *common.URL { return d.url } +func (d *mockDirectory) IsAvailable() bool { return true } +func (d *mockDirectory) Destroy() {} func (d *mockDirectory) List(protocolbase.Invocation) []protocolbase.Invoker { return d.invokers } -func (d *mockDirectory) Subscribe(*common.URL) error { return nil } +func (d *mockDirectory) Subscribe(*common.URL) error { return nil } From ea68f4ded0f7dd66eeb51b287b23dd0844226966 Mon Sep 17 00:00:00 2001 From: liuhy Date: Thu, 18 Jun 2026 13:28:20 +0800 Subject: [PATCH 4/4] refactor(cluster): use atomic.Pointer for StickyInvoker, return *BaseClusterInvoker from constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace atomic.Value + stickyInvokerWrapper + getter/setter with atomic.Pointer[base.Invoker] — idiomatic Go, nil-safe, type-safe, no wrapper needed - NewBaseClusterInvoker now returns *BaseClusterInvoker (pointer), preventing accidental value copies of the struct containing atomic fields - All 9 cluster implementations updated to use pointer embedding (*base.BaseClusterInvoker) to match the new constructor return type - StickyInvoker field remains exported (atomic.Pointer is safe for concurrent access by design) Co-Authored-By: Claude --- .../cluster/adaptivesvc/cluster_invoker.go | 2 +- cluster/cluster/available/cluster_invoker.go | 2 +- cluster/cluster/base/cluster_invoker.go | 53 ++++++------------- cluster/cluster/broadcast/cluster_invoker.go | 2 +- cluster/cluster/failback/cluster_invoker.go | 2 +- cluster/cluster/failfast/cluster_invoker.go | 2 +- cluster/cluster/failover/cluster_invoker.go | 2 +- cluster/cluster/failsafe/cluster_invoker.go | 2 +- cluster/cluster/forking/cluster_invoker.go | 2 +- cluster/cluster/zoneaware/cluster_invoker.go | 2 +- 10 files changed, 25 insertions(+), 46 deletions(-) diff --git a/cluster/cluster/adaptivesvc/cluster_invoker.go b/cluster/cluster/adaptivesvc/cluster_invoker.go index ef960e0291..4ea8e0e163 100644 --- a/cluster/cluster/adaptivesvc/cluster_invoker.go +++ b/cluster/cluster/adaptivesvc/cluster_invoker.go @@ -40,7 +40,7 @@ import ( ) type adaptiveServiceClusterInvoker struct { - base.BaseClusterInvoker + *base.BaseClusterInvoker } func newAdaptiveServiceClusterInvoker(directory directory.Directory) protocolbase.Invoker { diff --git a/cluster/cluster/available/cluster_invoker.go b/cluster/cluster/available/cluster_invoker.go index d645977ec3..d8840a8e45 100644 --- a/cluster/cluster/available/cluster_invoker.go +++ b/cluster/cluster/available/cluster_invoker.go @@ -34,7 +34,7 @@ import ( ) type availableClusterInvoker struct { - base.BaseClusterInvoker + *base.BaseClusterInvoker } // NewClusterInvoker returns a availableCluster invoker instance diff --git a/cluster/cluster/base/cluster_invoker.go b/cluster/cluster/base/cluster_invoker.go index 0ad07e1c67..1492100af0 100644 --- a/cluster/cluster/base/cluster_invoker.go +++ b/cluster/cluster/base/cluster_invoker.go @@ -19,7 +19,7 @@ package base import ( - stdatomic "sync/atomic" + "sync/atomic" ) import ( @@ -27,7 +27,7 @@ import ( perrors "github.com/pkg/errors" - "go.uber.org/atomic" + uberatomic "go.uber.org/atomic" ) import ( @@ -39,40 +39,19 @@ import ( "dubbo.apache.org/dubbo-go/v3/protocol/base" ) -// stickyInvokerWrapper wraps a base.Invoker so that atomic.Value always -// stores the same concrete type (avoiding panic on inconsistent types) -// and can represent a nil invoker (avoiding panic on Store(nil)). -type stickyInvokerWrapper struct { - invoker base.Invoker -} - type BaseClusterInvoker struct { Directory directory.Directory AvailableCheck bool - Destroyed *atomic.Bool - stickyInvoker stdatomic.Value // stores stickyInvokerWrapper + Destroyed *uberatomic.Bool + StickyInvoker atomic.Pointer[base.Invoker] } -func NewBaseClusterInvoker(directory directory.Directory) BaseClusterInvoker { - return BaseClusterInvoker{ +func NewBaseClusterInvoker(directory directory.Directory) *BaseClusterInvoker { + return &BaseClusterInvoker{ Directory: directory, AvailableCheck: true, - Destroyed: atomic.NewBool(false), - } -} - -// 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 + Destroyed: uberatomic.NewBool(false), } - return v.(stickyInvokerWrapper).invoker -} - -// setStickyInvoker sets the sticky invoker in a race-free manner. -func (invoker *BaseClusterInvoker) setStickyInvoker(invokerVal base.Invoker) { - invoker.stickyInvoker.Store(stickyInvokerWrapper{invoker: invokerVal}) } func (invoker *BaseClusterInvoker) GetURL() *common.URL { @@ -87,8 +66,8 @@ func (invoker *BaseClusterInvoker) Destroy() { } func (invoker *BaseClusterInvoker) IsAvailable() bool { - if sticky := invoker.getStickyInvoker(); sticky != nil { - return sticky.IsAvailable() + if sticky := invoker.StickyInvoker.Load(); sticky != nil { + return (*sticky).IsAvailable() } return invoker.Directory.IsAvailable() } @@ -125,21 +104,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) - stickyInvoker := invoker.getStickyInvoker() - if stickyInvoker != nil && !isInvoked(stickyInvoker, invokers) { - invoker.setStickyInvoker(nil) + stickyInvoker := invoker.StickyInvoker.Load() + if stickyInvoker != nil && !isInvoked(*stickyInvoker, invokers) { + invoker.StickyInvoker.Store(nil) stickyInvoker = nil } if sticky && invoker.AvailableCheck && - stickyInvoker != nil && stickyInvoker.IsAvailable() && - (invoked == nil || !isInvoked(stickyInvoker, invoked)) { - return stickyInvoker + stickyInvoker != nil && (*stickyInvoker).IsAvailable() && + (invoked == nil || !isInvoked(*stickyInvoker, invoked)) { + return *stickyInvoker } selectedInvoker = invoker.doSelectInvoker(lb, invocation, invokers, invoked) if sticky { - invoker.setStickyInvoker(selectedInvoker) + invoker.StickyInvoker.Store(&selectedInvoker) } return selectedInvoker } diff --git a/cluster/cluster/broadcast/cluster_invoker.go b/cluster/cluster/broadcast/cluster_invoker.go index bace1e63e3..d938cad198 100644 --- a/cluster/cluster/broadcast/cluster_invoker.go +++ b/cluster/cluster/broadcast/cluster_invoker.go @@ -33,7 +33,7 @@ import ( ) type broadcastClusterInvoker struct { - base.BaseClusterInvoker + *base.BaseClusterInvoker } func newBroadcastClusterInvoker(directory directory.Directory) protocolbase.Invoker { diff --git a/cluster/cluster/failback/cluster_invoker.go b/cluster/cluster/failback/cluster_invoker.go index 3509aad838..2b89ccc525 100644 --- a/cluster/cluster/failback/cluster_invoker.go +++ b/cluster/cluster/failback/cluster_invoker.go @@ -49,7 +49,7 @@ import ( * Failback */ type failbackClusterInvoker struct { - base.BaseClusterInvoker + *base.BaseClusterInvoker once sync.Once ticker *time.Ticker diff --git a/cluster/cluster/failfast/cluster_invoker.go b/cluster/cluster/failfast/cluster_invoker.go index 4d04695ecf..89f4b4c083 100644 --- a/cluster/cluster/failfast/cluster_invoker.go +++ b/cluster/cluster/failfast/cluster_invoker.go @@ -29,7 +29,7 @@ import ( ) type failfastClusterInvoker struct { - base.BaseClusterInvoker + *base.BaseClusterInvoker } func newFailfastClusterInvoker(directory directory.Directory) protocolbase.Invoker { diff --git a/cluster/cluster/failover/cluster_invoker.go b/cluster/cluster/failover/cluster_invoker.go index 946c4e8f59..6799176049 100644 --- a/cluster/cluster/failover/cluster_invoker.go +++ b/cluster/cluster/failover/cluster_invoker.go @@ -39,7 +39,7 @@ import ( ) type failoverClusterInvoker struct { - base.BaseClusterInvoker + *base.BaseClusterInvoker } func newFailoverClusterInvoker(directory directory.Directory) protocolbase.Invoker { diff --git a/cluster/cluster/failsafe/cluster_invoker.go b/cluster/cluster/failsafe/cluster_invoker.go index 537690726f..71463afb64 100644 --- a/cluster/cluster/failsafe/cluster_invoker.go +++ b/cluster/cluster/failsafe/cluster_invoker.go @@ -42,7 +42,7 @@ import ( * */ type failsafeClusterInvoker struct { - base.BaseClusterInvoker + *base.BaseClusterInvoker } func newFailsafeClusterInvoker(directory directory.Directory) protocolbase.Invoker { diff --git a/cluster/cluster/forking/cluster_invoker.go b/cluster/cluster/forking/cluster_invoker.go index 326a89f682..4c5f98e9a2 100644 --- a/cluster/cluster/forking/cluster_invoker.go +++ b/cluster/cluster/forking/cluster_invoker.go @@ -38,7 +38,7 @@ import ( ) type forkingClusterInvoker struct { - base.BaseClusterInvoker + *base.BaseClusterInvoker } func newForkingClusterInvoker(directory directory.Directory) protocolbase.Invoker { diff --git a/cluster/cluster/zoneaware/cluster_invoker.go b/cluster/cluster/zoneaware/cluster_invoker.go index 3086c39383..3da8bb57b9 100644 --- a/cluster/cluster/zoneaware/cluster_invoker.go +++ b/cluster/cluster/zoneaware/cluster_invoker.go @@ -38,7 +38,7 @@ import ( // 3. Evenly balance traffic between all registries based on each registry's weight. // 4. Pick anyone that's available. type zoneawareClusterInvoker struct { - base.BaseClusterInvoker + *base.BaseClusterInvoker } func newZoneawareClusterInvoker(directory directory.Directory) protocolbase.Invoker {