Files
netbird/shared/relay/client/manager_dial_test.go
2026-07-07 18:14:38 +02:00

73 lines
2.7 KiB
Go

package client
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// TestManager_InProgressDialIsSafeForReadersAndCleanup covers the new state that
// option 1 introduces: openConnVia now publishes a relay track in the map and
// releases the map lock BEFORE dialing, so a track can legitimately exist with
// relayClient == nil and no track lock held while its Connect() is in flight.
//
// The status path (RelayStates) and the cleanup loop both touch every track, so
// both must tolerate that mid-dial state — neither deref the nil relayClient nor
// evict a track whose dial is still running.
func TestManager_InProgressDialIsSafeForReadersAndCleanup(t *testing.T) {
m := &Manager{
relayClients: make(map[string]*RelayTrack),
// 0 so the created-time grace does not mask the mid-dial (nil relayClient)
// guard in cleanUpUnusedRelays.
keepUnusedServerTime: 0,
}
const addr = "relay.example.com:443"
rt := NewRelayTrack() // ready open, relayClient nil, unlocked == dial in progress
m.relayClients[addr] = rt
// A status call must not block or panic on a relay still being dialed; it has
// no state to report yet.
require.Empty(t, m.RelayStates(), "a relay still being dialed has no state to report")
// Cleanup must not deref the nil relayClient, and must not evict an in-flight dial.
m.cleanUpUnusedRelays()
m.relayClientsMutex.RLock()
_, stillTracked := m.relayClients[addr]
m.relayClientsMutex.RUnlock()
require.True(t, stillTracked, "an in-progress dial must not be cleaned up")
}
// TestOpenConnOnTrack_ReleasesOnContextCancelDuringDial verifies the core option-1
// property: a caller that finds a track whose dial is in progress waits on
// rt.ready, not on the track lock, and can be released by its own context. This
// is what keeps a slow relay dial from serializing behind the track lock.
func TestOpenConnOnTrack_ReleasesOnContextCancelDuringDial(t *testing.T) {
m := &Manager{relayClients: make(map[string]*RelayTrack)}
rt := NewRelayTrack() // ready deliberately left open == dial in progress
ctx, cancel := context.WithCancel(context.Background())
errCh := make(chan error, 1)
go func() {
_, err := m.openConnOnTrack(ctx, rt, "peerKey")
errCh <- err
}()
// While waiting for the dial the caller holds no track lock, so a concurrent
// reader (e.g. RelayStates) can still take it.
require.True(t, rt.TryRLock(), "waiter must not hold the track lock while the dial is in progress")
rt.RUnlock()
// Caller gives up: openConnOnTrack must return via ctx rather than hang on the dial.
cancel()
select {
case err := <-errCh:
require.ErrorIs(t, err, context.Canceled)
case <-time.After(2 * time.Second):
t.Fatal("openConnOnTrack did not return on ctx cancellation while a dial was in progress")
}
}