mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-03 20:19:07 +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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user