Copilot commented on code in PR #1180:
URL: 
https://github.com/apache/incubator-seata-go/pull/1180#discussion_r4028716273


##########
pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler.go:
##########
@@ -72,6 +80,11 @@ var (
        fenceHandler       *tccFenceWrapperHandler
        fenceOnce          sync.Once
        cleanIntervalNanos atomic.Int64
+
+       fenceLogCleanRetryExhaustedTotal = 
promauto.NewCounter(prometheus.CounterOpts{
+               Name: "tcc_fence_log_clean_retry_exhausted_total",
+               Help: "Number of TCC fence log identities removed from the 
in-memory retry cache after cleanup retries were exhausted; database rows 
remain eligible for a later scan.",
+       })

Review Comment:
   Using promauto.NewCounter registers the metric on the global Prometheus 
default registry as a package side-effect at init time. In library code this 
can cause unexpected registration/panic on duplicate metric names in host 
applications (especially if multiple versions or multiple registries are used). 
Consider using prometheus.NewCounter and registering via an explicit 
init/registration function (or allowing a registry to be injected), or at 
minimum document the side-effect and naming expectations.



##########
pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler.go:
##########
@@ -287,65 +336,130 @@ func (handler *tccFenceWrapperHandler) 
DestroyLogCleanChannel() {
 func (handler *tccFenceWrapperHandler) deleteBatchFence(tx *sql.Tx, batch 
[]model.FenceLogIdentity) error {
        err := handler.tccFenceDao.DeleteMultipleTCCFenceLogIdentity(tx, batch)
        if err != nil {
-               return fmt.Errorf("delete batch fence log failed, batch: %v, 
err: %v", batch, err)
+               return fmt.Errorf("delete batch fence log failed, batch: %v: 
%w", batch, err)

Review Comment:
   Logging/returning errors with the full `batch: %v` can produce very large 
log lines and potentially leak sensitive identifiers (Xid), especially when 
drainCacheOnce batches are large. Prefer reporting batch_size plus a small, 
bounded sample (e.g., first identity) or a correlation id, and keep the full 
batch out of logs/errors.



##########
pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler.go:
##########
@@ -204,18 +223,18 @@ func (handler *tccFenceWrapperHandler) 
InitLogCleanChannel(dsn string) {
        }
 
        handler.logQueueOnce.Do(func() {
-               handler.backgroundWg.Add(1)
+               handler.cleanerWg.Add(1)
                go func() {
-                       defer handler.backgroundWg.Done()
+                       defer handler.cleanerWg.Done()
                        handler.traversalCleanChannel(db)
                }()
        })
 
        handler.logTaskOnce.Do(func() {
                handler.stopLogCleanTask = make(chan struct{})
-               handler.backgroundWg.Add(1)
+               handler.logTaskWg.Add(1)
                go func() {
-                       defer handler.backgroundWg.Done()
+                       defer handler.logTaskWg.Done()
                        handler.initLogCleanTask(db)
                }()
        })

Review Comment:
   Critical race between InitLogCleanChannel() and DestroyLogCleanChannel(): 
Init releases lifecycleMutex at line 215 via defer, then later executes 
logQueueOnce.Do/logTaskOnce.Do (WaitGroup.Add + goroutine start) without 
holding the mutex. If DestroyLogCleanChannel() concurrently calls 
logTaskWg.Wait()/cleanerWg.Wait(), this can trigger 'sync: WaitGroup misuse: 
Add called concurrently with Wait' and/or leak goroutines started after 
destruction begins. Fix by ensuring Init’s lifecycleMutex critical section 
covers all state publication and WaitGroup.Add (i.e., hold lifecycleMutex 
through logQueueOnce.Do/logTaskOnce.Do and any channel/db assignments), or 
introduce an init/destroy state machine that prevents WaitGroup.Add once 
teardown starts.



##########
pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler_test.go:
##########
@@ -36,6 +39,71 @@ import (
        "seata.apache.org/seata-go/v2/pkg/util/log"
 )
 
+type captureTestLogger struct {
+       mu       sync.Mutex
+       warnings []string
+       errors   []string
+}
+
+func (logger *captureTestLogger) Debug(v ...interface{})                 {}
+func (logger *captureTestLogger) Debugf(format string, v ...interface{}) {}
+func (logger *captureTestLogger) Info(v ...interface{})                  {}
+func (logger *captureTestLogger) Infof(format string, v ...interface{})  {}
+func (logger *captureTestLogger) Warn(v ...interface{}) {
+       logger.mu.Lock()
+       defer logger.mu.Unlock()
+       logger.warnings = append(logger.warnings, fmt.Sprint(v...))
+}
+func (logger *captureTestLogger) Warnf(format string, v ...interface{}) {
+       logger.mu.Lock()
+       defer logger.mu.Unlock()
+       logger.warnings = append(logger.warnings, fmt.Sprintf(format, v...))
+}
+func (logger *captureTestLogger) Error(v ...interface{}) {
+       logger.mu.Lock()
+       defer logger.mu.Unlock()
+       logger.errors = append(logger.errors, fmt.Sprint(v...))
+}
+func (logger *captureTestLogger) Errorf(format string, v ...interface{}) {
+       logger.mu.Lock()
+       defer logger.mu.Unlock()
+       logger.errors = append(logger.errors, fmt.Sprintf(format, v...))
+}
+func (logger *captureTestLogger) Panic(v ...interface{})                 {}
+func (logger *captureTestLogger) Panicf(format string, v ...interface{}) {}
+func (logger *captureTestLogger) Fatal(v ...interface{})                 {}
+func (logger *captureTestLogger) Fatalf(format string, v ...interface{}) {}
+
+func (logger *captureTestLogger) warningText() string {
+       logger.mu.Lock()
+       defer logger.mu.Unlock()
+       return strings.Join(logger.warnings, "\n")
+}
+
+func (logger *captureTestLogger) errorText() string {
+       logger.mu.Lock()
+       defer logger.mu.Unlock()
+       return strings.Join(logger.errors, "\n")
+}
+
+func installCaptureTestLogger(t *testing.T) *captureTestLogger {
+       t.Helper()
+       logger := &captureTestLogger{}
+       previous := log.GetLogger()
+       log.SetLogger(logger)
+       t.Cleanup(func() {
+               log.SetLogger(previous)
+       })
+       return logger
+}
+
+func stopTestDrainTask(handler *tccFenceWrapperHandler) {
+       if handler.stopDrainCache != nil {
+               close(handler.stopDrainCache)
+               handler.drainWg.Wait()
+       }

Review Comment:
   In tests, stopTestDrainTask can panic if stopDrainCache is closed elsewhere 
(e.g., if a test later calls DestroyLogCleanChannel() after stopTestDrainTask, 
or if multiple cleanup paths invoke it). To make the helper robust and reduce 
flakiness, guard the close with a sync.Once-like mechanism, or set 
handler.stopDrainCache to nil under the same lock used in production before 
closing it.



##########
pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler.go:
##########
@@ -355,79 +469,58 @@ func (handler *tccFenceWrapperHandler) drainCacheTask() {
        for {
                select {
                case <-ticker.C:
-                       handler.cacheMutex.Lock()
-
-                       if handler.logCache.Len() == 0 {
-                               handler.cacheMutex.Unlock()
-                               continue
-                       }
-
-                       handler.dbMutex.RLock()
-                       db := handler.db
-                       handler.dbMutex.RUnlock()
-                       if db == nil {
-                               handler.cacheMutex.Unlock()
-                               continue
-                       }
+                       handler.drainCacheOnce()
+               case <-handler.stopDrainCache:
+                       return
+               }
+       }
 
-                       var drained []fenceLogCacheEntry
-                       for e := handler.logCache.Front(); e != nil; {
-                               next := e.Next()
+}
 
-                               ent := e.Value.(*fenceLogCacheEntry)
-                               drained = append(drained, *ent)
-                               handler.logCache.Remove(e)
+func (handler *tccFenceWrapperHandler) drainCacheOnce() {
+       handler.cacheMutex.Lock()
+       if handler.logCache.Len() == 0 {
+               handler.cacheMutex.Unlock()
+               return
+       }
 
-                               e = next
-                       }
+       handler.dbMutex.RLock()
+       db := handler.db
+       handler.dbMutex.RUnlock()
+       if db == nil {
+               handler.cacheMutex.Unlock()
+               return
+       }

Review Comment:
   drainCacheOnce() holds cacheMutex while acquiring dbMutex (line 487). This 
introduces a lock-order dependency (cacheMutex -> dbMutex) that can deadlock if 
any other path ever acquires dbMutex before cacheMutex (even outside this 
diff). Safer pattern: snapshot db under dbMutex first, then lock cacheMutex to 
drain, or release cacheMutex before taking dbMutex (and re-check conditions). 
This reduces deadlock risk and makes lock ordering easier to reason about.



-- 
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