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

tew pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-seata-go.git


The following commit(s) were added to refs/heads/master by this push:
     new 987b9c4b fix: background goroutines cannot exit gracefully (#996)
987b9c4b is described below

commit 987b9c4bb8cc6c3c5d31c06938d849d313e4b651
Author: WyRainBow <[email protected]>
AuthorDate: Tue Mar 10 17:35:53 2026 +0800

    fix: background goroutines cannot exit gracefully (#996)
    
    * fix: Background Goroutines Cannot Exit Gracefully
    
    * trigger ci: rerun integration tests
    
    * trigger ci: rerun integration tests
---
 pkg/datasource/sql/async_worker.go                 | 10 ++-
 pkg/datasource/sql/async_worker_test.go            | 50 +++++++++++++
 pkg/datasource/sql/at_resource_manager.go          |  2 +-
 pkg/datasource/sql/datasource/base/meta_cache.go   | 87 ++++++++++++----------
 .../sql/datasource/base/meta_cache_test.go         | 47 +++++++++---
 pkg/datasource/sql/datasource/mysql/meta_cache.go  |  2 +-
 6 files changed, 140 insertions(+), 58 deletions(-)

diff --git a/pkg/datasource/sql/async_worker.go 
b/pkg/datasource/sql/async_worker.go
index bfdf736d..9f7f0afc 100644
--- a/pkg/datasource/sql/async_worker.go
+++ b/pkg/datasource/sql/async_worker.go
@@ -70,7 +70,7 @@ type AsyncWorker struct {
        rePutBackToQueue           prometheus.Counter
 }
 
-func NewAsyncWorker(prom prometheus.Registerer, conf AsyncWorkerConfig, 
sourceManager datasource.DataSourceManager) *AsyncWorker {
+func NewAsyncWorker(ctx context.Context, prom prometheus.Registerer, conf 
AsyncWorkerConfig, sourceManager datasource.DataSourceManager) *AsyncWorker {
        var asyncWorker AsyncWorker
        asyncWorker.conf = conf
        asyncWorker.commitQueue = make(chan phaseTwoContext, 
asyncWorker.conf.ReceiveChanSize)
@@ -97,7 +97,7 @@ func NewAsyncWorker(prom prometheus.Registerer, conf 
AsyncWorkerConfig, sourceMa
                Help: "the counter of commit failure retry counter",
        })
 
-       go asyncWorker.run()
+       go asyncWorker.run(ctx)
 
        return &asyncWorker
 }
@@ -122,8 +122,9 @@ func (aw *AsyncWorker) BranchCommit(ctx context.Context, 
req rm.BranchResource)
        return branch.BranchStatusPhasetwoCommitted, nil
 }
 
-func (aw *AsyncWorker) run() {
+func (aw *AsyncWorker) run(ctx context.Context) {
        ticker := time.NewTicker(aw.conf.BufferCleanInterval)
+       defer ticker.Stop()
        phaseCtxs := make([]phaseTwoContext, 0, aw.conf.BufferLimit)
        for {
                select {
@@ -134,6 +135,9 @@ func (aw *AsyncWorker) run() {
                        }
                case <-ticker.C:
                        aw.doBranchCommit(&phaseCtxs)
+               case <-ctx.Done():
+                       ticker.Stop()
+                       return
                }
        }
 }
diff --git a/pkg/datasource/sql/async_worker_test.go 
b/pkg/datasource/sql/async_worker_test.go
new file mode 100644
index 00000000..e21006ef
--- /dev/null
+++ b/pkg/datasource/sql/async_worker_test.go
@@ -0,0 +1,50 @@
+/*
+ * 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 sql
+
+import (
+       "context"
+       "testing"
+       "time"
+
+       "github.com/prometheus/client_golang/prometheus"
+)
+
+func TestAsyncWorker_Lifecycle(t *testing.T) {
+       cfg := AsyncWorkerConfig{
+               BufferLimit:            10,
+               BufferCleanInterval:    time.Second,
+               ReceiveChanSize:        10,
+               CommitWorkerCount:      1,
+               CommitWorkerBufferSize: 10,
+       }
+
+       ctx, cancel := context.WithCancel(context.Background())
+
+       // Pass nil as sourceManager since we don't process tasks in this test
+       _ = NewAsyncWorker(ctx, prometheus.NewRegistry(), cfg, nil)
+
+       // Allow it to run for a bit
+       time.Sleep(100 * time.Millisecond)
+
+       // Cancel context to stop worker
+       cancel()
+
+       // Give it some time to exit (though we can't verify it easily without 
exposing internal state)
+       time.Sleep(50 * time.Millisecond)
+}
diff --git a/pkg/datasource/sql/at_resource_manager.go 
b/pkg/datasource/sql/at_resource_manager.go
index 49ce2f51..e6b59f39 100644
--- a/pkg/datasource/sql/at_resource_manager.go
+++ b/pkg/datasource/sql/at_resource_manager.go
@@ -41,7 +41,7 @@ func InitAT(cfg undo.Config, asyncCfg AsyncWorkerConfig) {
        }
 
        undo.InitUndoConfig(cfg)
-       atSourceManager.worker = NewAsyncWorker(prometheus.DefaultRegisterer, 
asyncCfg, atSourceManager)
+       atSourceManager.worker = NewAsyncWorker(context.Background(), 
prometheus.DefaultRegisterer, asyncCfg, atSourceManager)
        rm.GetRmCacheInstance().RegisterResourceManager(atSourceManager)
 }
 
diff --git a/pkg/datasource/sql/datasource/base/meta_cache.go 
b/pkg/datasource/sql/datasource/base/meta_cache.go
index 521ef4a9..7f2766aa 100644
--- a/pkg/datasource/sql/datasource/base/meta_cache.go
+++ b/pkg/datasource/sql/datasource/base/meta_cache.go
@@ -45,31 +45,29 @@ type (
 
 // BaseTableMetaCache
 type BaseTableMetaCache struct {
-       lock           sync.RWMutex
-       expireDuration time.Duration
-       capity         int32
-       size           int32
-       cache          map[string]*entry
-       cancel         context.CancelFunc
-       trigger        trigger
-       db             *sql.DB
-       cfg            *mysql.Config
+       lock            sync.RWMutex
+       expireDuration  time.Duration
+       refreshInterval time.Duration
+       capity          int32
+       size            int32
+       cache           map[string]*entry
+       trigger         trigger
+       db              *sql.DB
+       cfg             *mysql.Config
 }
 
 // NewBaseCache
-func NewBaseCache(capity int32, expireDuration time.Duration, trigger trigger, 
db *sql.DB, cfg *mysql.Config) *BaseTableMetaCache {
-       ctx, cancel := context.WithCancel(context.Background())
-
+func NewBaseCache(ctx context.Context, capity int32, expireDuration 
time.Duration, trigger trigger, db *sql.DB, cfg *mysql.Config) 
*BaseTableMetaCache {
        c := &BaseTableMetaCache{
-               lock:           sync.RWMutex{},
-               capity:         capity,
-               size:           0,
-               expireDuration: expireDuration,
-               cache:          map[string]*entry{},
-               cancel:         cancel,
-               trigger:        trigger,
-               cfg:            cfg,
-               db:             db,
+               lock:            sync.RWMutex{},
+               capity:          capity,
+               size:            0,
+               expireDuration:  expireDuration,
+               refreshInterval: time.Minute,
+               cache:           map[string]*entry{},
+               trigger:         trigger,
+               cfg:             cfg,
+               db:              db,
        }
 
        c.Init(ctx)
@@ -77,11 +75,10 @@ func NewBaseCache(capity int32, expireDuration 
time.Duration, trigger trigger, d
        return c
 }
 
-// init
+// Init
 func (c *BaseTableMetaCache) Init(ctx context.Context) error {
        go c.refresh(ctx)
        go c.scanExpire(ctx)
-
        return nil
 }
 
@@ -128,10 +125,15 @@ func (c *BaseTableMetaCache) refresh(ctx context.Context) 
{
 
        f()
 
-       ticker := time.NewTicker(time.Duration(1 * time.Minute))
+       ticker := time.NewTicker(c.refreshInterval)
        defer ticker.Stop()
-       for range ticker.C {
-               f()
+       for {
+               select {
+               case <-ticker.C:
+                       f()
+               case <-ctx.Done():
+                       return
+               }
        }
 }
 
@@ -139,23 +141,27 @@ func (c *BaseTableMetaCache) refresh(ctx context.Context) 
{
 func (c *BaseTableMetaCache) scanExpire(ctx context.Context) {
        ticker := time.NewTicker(c.expireDuration)
        defer ticker.Stop()
-       for range ticker.C {
-
-               f := func() {
-                       c.lock.Lock()
-                       defer c.lock.Unlock()
-
-                       cur := time.Now()
-                       for k := range c.cache {
-                               entry := c.cache[k]
-
-                               if cur.Sub(entry.lastAccess) > c.expireDuration 
{
-                                       delete(c.cache, k)
+       for {
+               select {
+               case <-ticker.C:
+                       f := func() {
+                               c.lock.Lock()
+                               defer c.lock.Unlock()
+
+                               cur := time.Now()
+                               for k := range c.cache {
+                                       entry := c.cache[k]
+
+                                       if cur.Sub(entry.lastAccess) > 
c.expireDuration {
+                                               delete(c.cache, k)
+                                       }
                                }
                        }
-               }
 
-               f()
+                       f()
+               case <-ctx.Done():
+                       return
+               }
        }
 }
 
@@ -191,6 +197,5 @@ func (c *BaseTableMetaCache) GetTableMeta(ctx 
context.Context, dbName, tableName
 }
 
 func (c *BaseTableMetaCache) Destroy() error {
-       c.cancel()
        return nil
 }
diff --git a/pkg/datasource/sql/datasource/base/meta_cache_test.go 
b/pkg/datasource/sql/datasource/base/meta_cache_test.go
index 0a5746cd..6f10303b 100644
--- a/pkg/datasource/sql/datasource/base/meta_cache_test.go
+++ b/pkg/datasource/sql/datasource/base/meta_cache_test.go
@@ -78,7 +78,6 @@ func TestBaseTableMetaCache_refresh(t *testing.T) {
                capity         int32
                size           int32
                cache          map[string]*entry
-               cancel         context.CancelFunc
                trigger        trigger
                db             *sql.DB
                cfg            *mysql.Config
@@ -87,6 +86,7 @@ func TestBaseTableMetaCache_refresh(t *testing.T) {
                ctx context.Context
        }
        ctx, cancel := context.WithCancel(context.Background())
+       defer cancel()
        tests := []struct {
                name   string
                fields fields
@@ -105,7 +105,6 @@ func TestBaseTableMetaCache_refresh(t *testing.T) {
                                                lastAccess: time.Now(),
                                        },
                                },
-                               cancel:  cancel,
                                trigger: &mockTrigger{},
                                cfg:     &mysql.Config{},
                        },
@@ -124,7 +123,6 @@ func TestBaseTableMetaCache_refresh(t *testing.T) {
                                                lastAccess: time.Now(),
                                        },
                                },
-                               cancel:  cancel,
                                trigger: &mockTrigger{},
                                cfg:     &mysql.Config{},
                        },
@@ -149,14 +147,14 @@ func TestBaseTableMetaCache_refresh(t *testing.T) {
                        defer loadAllStub.Reset()
 
                        c := &BaseTableMetaCache{
-                               expireDuration: tt.fields.expireDuration,
-                               capity:         tt.fields.capity,
-                               size:           tt.fields.size,
-                               cache:          tt.fields.cache,
-                               cancel:         tt.fields.cancel,
-                               trigger:        tt.fields.trigger,
-                               db:             db,
-                               cfg:            tt.fields.cfg,
+                               expireDuration:  tt.fields.expireDuration,
+                               refreshInterval: time.Minute,
+                               capity:          tt.fields.capity,
+                               size:            tt.fields.size,
+                               cache:           tt.fields.cache,
+                               trigger:         tt.fields.trigger,
+                               db:              db,
+                               cfg:             tt.fields.cfg,
                        }
                        go c.refresh(tt.args.ctx)
                        time.Sleep(time.Second * 3)
@@ -220,7 +218,6 @@ func TestBaseTableMetaCache_refresh_EarlyReturn(t 
*testing.T) {
                                capity:         capacity,
                                size:           0,
                                cache:          tt.cache,
-                               cancel:         cancel,
                                trigger:        &mockTrigger{},
                                db:             tt.db,
                                cfg:            tt.cfg,
@@ -364,3 +361,29 @@ func TestBaseTableMetaCache_GetTableMeta(t *testing.T) {
                })
        }
 }
+
+func TestBaseTableMetaCache_GracefulShutdown(t *testing.T) {
+       // Create context manually as we are bypassing NewBaseCache
+       ctx, cancel := context.WithCancel(context.Background())
+
+       c := &BaseTableMetaCache{
+               expireDuration:  1 * time.Millisecond,
+               refreshInterval: 1 * time.Millisecond,
+               cache:           make(map[string]*entry),
+               // db and cfg are nil, so refresh() logic will return early, 
which is fine for coverage
+       }
+
+       // Init starts the goroutines
+       err := c.Init(ctx)
+       assert.Nil(t, err)
+
+       // Give enough time for tickers to trigger multiple times
+       time.Sleep(20 * time.Millisecond)
+
+       // Cancel context to stop goroutines
+       cancel()
+
+       // Destroy (now a no-op)
+       err = c.Destroy()
+       assert.Nil(t, err)
+}
diff --git a/pkg/datasource/sql/datasource/mysql/meta_cache.go 
b/pkg/datasource/sql/datasource/mysql/meta_cache.go
index 4d7879f7..4f3440ea 100644
--- a/pkg/datasource/sql/datasource/mysql/meta_cache.go
+++ b/pkg/datasource/sql/datasource/mysql/meta_cache.go
@@ -43,7 +43,7 @@ type TableMetaCache struct {
 
 func NewTableMetaInstance(db *sql.DB, cfg *mysql.Config) *TableMetaCache {
        tableMetaInstance := &TableMetaCache{
-               tableMetaCache: base.NewBaseCache(capacity, EexpireTime, 
NewMysqlTrigger(), db, cfg),
+               tableMetaCache: base.NewBaseCache(context.Background(), 
capacity, EexpireTime, NewMysqlTrigger(), db, cfg),
                db:             db,
        }
        return tableMetaInstance


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

Reply via email to