Files
netbird/client/internal/run_supervisor_test.go
Zoltán Papp 3615c01fe3 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.
2026-08-27 11:27:25 +02:00

134 lines
3.0 KiB
Go

package internal
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newSupervisedClient() *ConnectClient {
return NewConnectClient(context.Background(), nil, nil)
}
func TestRunSupervisorPublishRejectsSupersededRun(t *testing.T) {
var s RunSupervisor
ctx := context.Background()
staleGen, staleDone := s.Begin()
defer close(staleDone)
freshGen, freshDone := s.Begin()
defer close(freshDone)
fresh := newSupervisedClient()
require.True(t, s.Publish(ctx, freshGen, fresh))
assert.False(t, s.Publish(ctx, staleGen, newSupervisedClient()))
assert.Same(t, fresh, s.Current())
}
func TestRunSupervisorPublishStopsDisplacedClient(t *testing.T) {
var s RunSupervisor
ctx := context.Background()
generation, done := s.Begin()
defer close(done)
displaced := newSupervisedClient()
require.True(t, s.Publish(ctx, generation, displaced))
replacement := newSupervisedClient()
require.True(t, s.Publish(ctx, generation, replacement))
assert.Same(t, replacement, s.Current())
assert.Error(t, displaced.ctx.Err(), "the displaced client's run context should be cancelled")
}
func TestRunSupervisorStopInvalidatesRunInFlight(t *testing.T) {
var s RunSupervisor
ctx := context.Background()
generation, done := s.Begin()
close(done)
require.NoError(t, s.Stop(ctx))
assert.False(t, s.Publish(ctx, generation, newSupervisedClient()))
assert.Nil(t, s.Current())
}
func TestRunSupervisorStopWaitsForRunExit(t *testing.T) {
var s RunSupervisor
_, done := s.Begin()
stopped := make(chan error, 1)
go func() { stopped <- s.Stop(context.Background()) }()
select {
case <-stopped:
t.Fatal("Stop returned before the run signalled its exit")
case <-time.After(50 * time.Millisecond):
}
close(done)
select {
case err := <-stopped:
require.NoError(t, err)
case <-time.After(time.Second):
t.Fatal("Stop did not return after the run exited")
}
}
func TestRunSupervisorStopGivesUpWaitOnContext(t *testing.T) {
var s RunSupervisor
_, done := s.Begin()
defer close(done)
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
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
assert.False(t, s.Alive(), "no run has started")
_, done := s.Begin()
assert.True(t, s.Alive(), "a run is in flight")
close(done)
assert.False(t, s.Alive(), "the run signalled its exit")
}
func TestRunSupervisorStopWithoutRunIsNoop(t *testing.T) {
var s RunSupervisor
require.NoError(t, s.Stop(context.Background()))
assert.Nil(t, s.Current())
}