Aias00 opened a new issue, #3526:
URL: https://github.com/apache/dubbo-go/issues/3526

   ### Problem
   
   `failbackClusterInvoker` has three defects in its retry lifecycle, all 
surfaced by `go test -race` (the `cluster/cluster/failback` package fails 
`TestFailbackRetryFailed` and `TestFailbackOutOfLimit` under `-race` on 
`develop`):
   
   1. **Goroutine leak.** `process` drives the retry loop with `for range 
invoker.ticker.C`. `Destroy` calls `ticker.Stop()`, but `time.Ticker.Stop()` 
does **not** close `ticker.C`, so the goroutine blocks on `range .C` forever 
waiting for a tick that never comes. The loop body's `Peek → ErrDisposed → 
return` exit is unreachable because the body only runs on a tick. Every 
failback invoker that ever failed a call leaks one goroutine + ticker for the 
life of the process (compounds across registry refresh / provider reconnect 
cycles).
   
   2. **`ticker` field race.** `invoker.ticker` is written from the `process` 
goroutine and read in `Destroy` with no synchronization.
   
   3. **`taskList` nil-pointer on Destroy.** `taskList` is initialized lazily 
(only when an `Invoke` fails). If `Destroy` is called on an invoker whose every 
`Invoke` succeeded (or was never called), `taskList` is nil and 
`invoker.taskList.Dispose()` dereferences a nil `*queue.Queue`.
   
   4. **`retryTimerTask.lastT` field race.** `checkRetry` writes `t.lastT = 
time.Now()` **after** `taskList.Put(t)`, while `process` reads 
`retryTask.lastT` after `Peek`. The queue's Put/Peek synchronization only makes 
**pre-Put** writes visible to Peek, so the post-Put `lastT` write races 
process's read.
   
   ### Current behavior
   
   ```go
   // process — leaks, and reads ticker/taskList fields
   func (invoker *failbackClusterInvoker) process(ctx context.Context) {
       invoker.ticker = time.NewTicker(time.Second * 1)   // unsync write
       for range invoker.ticker.C {                       // blocks forever 
after Stop
           for { ... invoker.taskList.Peek() ... retryTask.lastT ... }  // 
reads lastT
       }
   }
   
   // Destroy — reads ticker (race), nil-derefs taskList, can't stop process
   func (invoker *failbackClusterInvoker) Destroy() {
       invoker.BaseClusterInvoker.Destroy()
       if invoker.ticker != nil { invoker.ticker.Stop() }   // unsync read; 
Stop doesn't close .C
       _ = invoker.taskList.Dispose()                        // nil-deref if 
never failed
   }
   
   // checkRetry — writes lastT after Put (race)
   func (t *retryTimerTask) checkRetry() {
       ...
       t.clusterInvoker.taskList.Put(t)
       t.lastT = time.Now()   // post-Put write races process's Peek-read of 
lastT
   }
   ```
   
   ### Expected behavior
   
   1. `Destroy` must deterministically stop the `process` goroutine (no leak).
   2. `ticker`/`taskList`/lifecycle fields must be accessed under a lock so 
`Destroy` observes a consistent state.
   3. `Destroy` on a never-failed invoker must not panic.
   4. `retryTimerTask` field writes must be visible to `process`'s Peek-read 
(happen-before via the queue).
   
   ### Suggested approach
   
   - Replace the `once sync.Once` lazy init with a `mu sync.Mutex` + 
`initialized bool` `ensureInit`, so `Destroy` can take the same lock and 
observe the state.
   - Add a `done chan struct{}`; `process` selects on `ticker.C` **and** 
`done`, with a `defer ticker.Stop()`; `Destroy` closes `done`.
   - `Destroy` nil-checks `taskList` (only Dispose if initialized).
   - In `checkRetry`, set `t.lastT` **before** `taskList.Put(t)` so the write 
is published to the queue and visible to Peek.
   
   ### Acceptance criteria
   
   - [ ] `Destroy` stops the `process` goroutine (no goroutine leak across 
Subscribe/Destroy cycles).
   - [ ] `Destroy` on a never-failed failback invoker does not panic.
   - [ ] `ticker`/`taskList`/`done` access is synchronized; no data race.
   - [ ] `checkRetry` writes `lastT` before `Put`.
   - [ ] `cluster/cluster/failback` passes under `-race`, including 
`TestFailbackRetryFailed` and `TestFailbackOutOfLimit` (both fail on `develop`).
   


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