mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +02:00
fix(client): keep the run's exit signal readable after supervisor Stop
Stop cleared the done channel, so a waiter that raced a teardown got nil from Done() and selected on a nil channel: an Up racing a Down hung in waitForUp for its full 50s timeout where it previously failed fast with "client gave up to connect". The same clearing made Alive() report a timed-out run dead while its goroutine was still tearing down, letting Up start an overlapping second run. Keep the channel in place instead: the exiting run closes it, so late waiters observe the real exit and Alive() stays true until the run is actually gone — the lifecycle clientGiveUpChan used to have. Stop still drops the current client and invalidates the generation. The stale-running-chan test begins a fresh run before waiting, as Up does, so the kept exit signal does not race the stale clientRunningChan.
This commit is contained in:
@@ -72,8 +72,10 @@ func (s *RunSupervisor) Current() *ConnectClient {
|
||||
return s.current
|
||||
}
|
||||
|
||||
// Done returns the channel the current run closes when it exits, or nil when no
|
||||
// run has been started. Callers that only need a yes/no answer should use Alive.
|
||||
// Done returns the channel the most recent run closes when it exits, or nil
|
||||
// when no run has been started. It stays readable after Stop so a waiter that
|
||||
// raced a teardown still observes the run's exit instead of a nil channel.
|
||||
// Callers that only need a yes/no answer should use Alive.
|
||||
func (s *RunSupervisor) Done() <-chan struct{} {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -110,7 +112,6 @@ func (s *RunSupervisor) Stop(ctx context.Context) error {
|
||||
cc := s.current
|
||||
done := s.done
|
||||
s.current = nil
|
||||
s.done = nil
|
||||
s.mu.Unlock()
|
||||
|
||||
if cc != nil {
|
||||
|
||||
@@ -96,6 +96,23 @@ func TestRunSupervisorStopGivesUpWaitOnContext(t *testing.T) {
|
||||
assert.ErrorIs(t, s.Stop(ctx), context.DeadlineExceeded)
|
||||
}
|
||||
|
||||
func TestRunSupervisorDoneSurvivesStop(t *testing.T) {
|
||||
var s RunSupervisor
|
||||
|
||||
_, done := s.Begin()
|
||||
close(done)
|
||||
|
||||
require.NoError(t, s.Stop(context.Background()))
|
||||
|
||||
runDone := s.Done()
|
||||
require.NotNil(t, runDone, "a waiter that raced Stop must still observe the run's exit")
|
||||
select {
|
||||
case <-runDone:
|
||||
default:
|
||||
t.Fatal("the last run's exit signal should remain readable after Stop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSupervisorAliveTracksRun(t *testing.T) {
|
||||
var s RunSupervisor
|
||||
|
||||
|
||||
@@ -190,6 +190,11 @@ func TestDownThenUp_StaleRunningChan(t *testing.T) {
|
||||
assert.False(t, s.clientRunning, "clientRunning should be cleared by cleanupConnection (intent = down)")
|
||||
s.mutex.Unlock()
|
||||
|
||||
// A fresh Up begins a new run before waiting; without it the previous
|
||||
// run's kept exit signal would race the stale clientRunningChan below.
|
||||
_, nextDone := s.runs.Begin()
|
||||
defer close(nextDone)
|
||||
|
||||
// waitForUp() returns immediately due to stale closed clientRunningChan
|
||||
waitCtx, ctxCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer ctxCancel()
|
||||
|
||||
Reference in New Issue
Block a user