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 9fba1ad6e fix(config): guard the global dynamicConfiguration with a 
RWMutex (#3669)
9fba1ad6e is described below

commit 9fba1ad6e4ee47a696b085ff63f88048bdc5317b
Author: Li Zining <[email protected]>
AuthorDate: Mon Aug 17 15:53:38 2026 +0800

    fix(config): guard the global dynamicConfiguration with a RWMutex (#3669)
    
    * fix(config): guard the global dynamicConfiguration with a RWMutex
    
    The Environment singleton keeps dynamicConfiguration as a lock-free field.
    TestNewRegistryDirectoryConsumerListenerUsesResolvedApplicationName writes
    it via SetDynamicConfiguration while subscription goroutines leaked from
    earlier normalRegistryDir tests keep reading it through
    GetDynamicConfiguration (tag.PriorityRouter.Notify) — a data race that the
    CI -race job caught. Guard the field with a sync.RWMutex: Set takes the
    write lock, Get takes the read lock. The API and nil semantics are
    unchanged, so existing callers and tests are unaffected.
    
    Signed-off-by: lizining <[email protected]>
    
    * test(config): add a concurrent Set/Get race regression test
    
    TestDynamicConfigurationRace drives 100k concurrent reads and writes on
    the global Environment dynamicConfiguration, mirroring the CI flaky race
    where a registry subscription goroutine kept reading the field while a
    test goroutine wrote it. It fails on the previous lock-free field under
    -race and passes with the RWMutex guard.
    
    Signed-off-by: lizining <[email protected]>
    
    * test(config): gate the writer until the race test reader is ready
    
    TestDynamicConfigurationRace starts the reader goroutine and immediately 
begins the 100k writes, so on some schedulers the writer can finish before the 
reader is scheduled, making the pre-fix race reproduction probabilistic rather 
than guaranteed. Gate the writer behind a ready/start barrier: the reader 
reports ready and blocks until the writer closes the start channel, so the 
concurrent access always overlaps. The test now fails deterministically on the 
previous lock-free field under -race.
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    
    * style(config): release the dynamicConfiguration mutex with defer
    
    ---------
    
    Signed-off-by: lizining <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 common/config/environment.go           |  5 ++++
 common/config/environment_race_test.go | 55 ++++++++++++++++++++++++++++++++++
 2 files changed, 60 insertions(+)

diff --git a/common/config/environment.go b/common/config/environment.go
index c2a391b3b..527301e77 100644
--- a/common/config/environment.go
+++ b/common/config/environment.go
@@ -38,6 +38,7 @@ type Environment struct {
        // externalConfigs      sync.Map
        externalConfigMap    sync.Map
        appExternalConfigMap sync.Map
+       dynamicMu            sync.RWMutex
        dynamicConfiguration config_center.DynamicConfiguration
 }
 
@@ -97,11 +98,15 @@ func (env *Environment) Configuration() *list.List {
 
 // SetDynamicConfiguration sets value for dynamicConfiguration
 func (env *Environment) SetDynamicConfiguration(dc 
config_center.DynamicConfiguration) {
+       env.dynamicMu.Lock()
+       defer env.dynamicMu.Unlock()
        env.dynamicConfiguration = dc
 }
 
 // GetDynamicConfiguration gets dynamicConfiguration
 func (env *Environment) GetDynamicConfiguration() 
config_center.DynamicConfiguration {
+       env.dynamicMu.RLock()
+       defer env.dynamicMu.RUnlock()
        return env.dynamicConfiguration
 }
 
diff --git a/common/config/environment_race_test.go 
b/common/config/environment_race_test.go
new file mode 100644
index 000000000..6da6ec82a
--- /dev/null
+++ b/common/config/environment_race_test.go
@@ -0,0 +1,55 @@
+/*
+ * 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 config
+
+import (
+       "sync"
+       "testing"
+)
+
+// TestDynamicConfigurationRace verifies that concurrent 
SetDynamicConfiguration
+// and GetDynamicConfiguration calls on the global Environment instance are
+// race-free. It mirrors the CI flaky race where a registry subscription
+// goroutine kept reading the field while a test goroutine wrote it.
+func TestDynamicConfigurationRace(t *testing.T) {
+       env := GetEnvInstance()
+       previous := env.GetDynamicConfiguration()
+       t.Cleanup(func() {
+               env.SetDynamicConfiguration(previous)
+       })
+
+       const iterations = 100000
+       start := make(chan struct{})
+       ready := make(chan struct{})
+
+       var wg sync.WaitGroup
+       wg.Go(func() { // reader goroutine mimics a lingering registry 
subscription goroutine
+               close(ready)
+               <-start
+               for range iterations {
+                       _ = env.GetDynamicConfiguration()
+               }
+       })
+
+       <-ready
+       close(start)
+       for range iterations { // the test goroutine keeps writing
+               env.SetDynamicConfiguration(nil)
+       }
+       wg.Wait()
+}

Reply via email to