From f763bd127d8cd91b29242a2616d9f82e7a7e8857 Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Mon, 23 Mar 2026 10:04:57 -0600 Subject: [PATCH 1/2] Add OutboundChecker interface and CheckOutbounds to MutableGroupManager Allows callers to trigger an immediate URL test cycle on a group, used by radiance to fire bandit callbacks as soon as a new config with URL overrides arrives rather than waiting for the next scheduled 3-minute interval. Co-Authored-By: Claude Opus 4.6 (1M context) --- adapter/group.go | 5 +++++ adapter/groups/manager.go | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/adapter/group.go b/adapter/group.go index 01bb159..a2a7816 100644 --- a/adapter/group.go +++ b/adapter/group.go @@ -17,6 +17,11 @@ type URLOverrideSetter interface { SetURLOverrides(overrides map[string]string) } +// OutboundChecker is implemented by outbound groups that support on-demand URL testing. +type OutboundChecker interface { + CheckOutbounds() +} + // TaggedConn is a net.Conn tagged with the outbound tag used to create it. type TaggedConn struct { net.Conn diff --git a/adapter/groups/manager.go b/adapter/groups/manager.go index c145cb9..a4fdb7c 100644 --- a/adapter/groups/manager.go +++ b/adapter/groups/manager.go @@ -179,6 +179,26 @@ func (m *MutableGroupManager) SetURLOverrides(group string, overrides map[string return nil } +// CheckOutbounds triggers an immediate URL test cycle on the specified group. +// The group must implement [adapter.OutboundChecker]. +func (m *MutableGroupManager) CheckOutbounds(group string) error { + m.mu.Lock() + defer m.mu.Unlock() + if m.closed.Load() { + return ErrIsClosed + } + outGroup, ok := m.groups[group] + if !ok { + return fmt.Errorf("group %q not found", group) + } + checker, ok := outGroup.(adapter.OutboundChecker) + if !ok { + return fmt.Errorf("group %q does not support outbound checking", group) + } + checker.CheckOutbounds() + return nil +} + // RemoveFromGroup removes an outbound/endpoint from the specified group. func (m *MutableGroupManager) RemoveFromGroup(group, tag string) error { m.mu.Lock() From 3916107f50d161c0a77137d1070f7622b5604bfd Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Mon, 23 Mar 2026 10:54:48 -0600 Subject: [PATCH 2/2] Address PR review: release lock before CheckOutbounds, add tests - Release mutex before calling checker.CheckOutbounds() to avoid holding the lock during potentially long network operations - Add unit tests for CheckOutbounds: delegates to OutboundChecker, errors on missing group, errors on non-checker group, errors when closed Co-Authored-By: Claude Opus 4.6 (1M context) --- adapter/groups/manager.go | 5 ++- adapter/groups/manager_test.go | 72 +++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/adapter/groups/manager.go b/adapter/groups/manager.go index a4fdb7c..1c8a117 100644 --- a/adapter/groups/manager.go +++ b/adapter/groups/manager.go @@ -183,18 +183,21 @@ func (m *MutableGroupManager) SetURLOverrides(group string, overrides map[string // The group must implement [adapter.OutboundChecker]. func (m *MutableGroupManager) CheckOutbounds(group string) error { m.mu.Lock() - defer m.mu.Unlock() if m.closed.Load() { + m.mu.Unlock() return ErrIsClosed } outGroup, ok := m.groups[group] if !ok { + m.mu.Unlock() return fmt.Errorf("group %q not found", group) } checker, ok := outGroup.(adapter.OutboundChecker) if !ok { + m.mu.Unlock() return fmt.Errorf("group %q does not support outbound checking", group) } + m.mu.Unlock() checker.CheckOutbounds() return nil } diff --git a/adapter/groups/manager_test.go b/adapter/groups/manager_test.go index b8bcccf..7285f37 100644 --- a/adapter/groups/manager_test.go +++ b/adapter/groups/manager_test.go @@ -167,6 +167,76 @@ func TestSetURLOverrides(t *testing.T) { }) } +func TestCheckOutbounds(t *testing.T) { + logger := log.NewNOPFactory().Logger() + + t.Run("delegates to OutboundChecker group", func(t *testing.T) { + mock := &mockCheckerGroup{} + mgr := &MutableGroupManager{ + groups: map[string]lbAdapter.MutableOutboundGroup{"auto-test": mock}, + removalQueue: newRemovalQueue( + logger, &mockOutboundManager{}, &mockEndpointManager{}, &mockConnectionManager{}, + pollInterval, forceAfter, + ), + } + err := mgr.CheckOutbounds("auto-test") + require.NoError(t, err) + assert.True(t, mock.checked) + }) + + t.Run("error when group not found", func(t *testing.T) { + mgr := &MutableGroupManager{ + groups: map[string]lbAdapter.MutableOutboundGroup{}, + removalQueue: newRemovalQueue( + logger, &mockOutboundManager{}, &mockEndpointManager{}, &mockConnectionManager{}, + pollInterval, forceAfter, + ), + } + err := mgr.CheckOutbounds("nonexistent") + assert.Error(t, err) + assert.Contains(t, err.Error(), "not found") + }) + + t.Run("error when group does not implement OutboundChecker", func(t *testing.T) { + mock := &mockPlainGroup{} + mgr := &MutableGroupManager{ + groups: map[string]lbAdapter.MutableOutboundGroup{"plain": mock}, + removalQueue: newRemovalQueue( + logger, &mockOutboundManager{}, &mockEndpointManager{}, &mockConnectionManager{}, + pollInterval, forceAfter, + ), + } + err := mgr.CheckOutbounds("plain") + assert.Error(t, err) + assert.Contains(t, err.Error(), "does not support outbound checking") + }) + + t.Run("error when manager is closed", func(t *testing.T) { + mock := &mockCheckerGroup{} + mgr := &MutableGroupManager{ + groups: map[string]lbAdapter.MutableOutboundGroup{"auto-test": mock}, + removalQueue: newRemovalQueue( + logger, &mockOutboundManager{}, &mockEndpointManager{}, &mockConnectionManager{}, + pollInterval, forceAfter, + ), + } + mgr.closed.Store(true) + err := mgr.CheckOutbounds("auto-test") + assert.ErrorIs(t, err, ErrIsClosed) + assert.False(t, mock.checked) + }) +} + +// mockCheckerGroup implements both MutableOutboundGroup and OutboundChecker. +type mockCheckerGroup struct { + lbAdapter.MutableOutboundGroup + checked bool +} + +func (m *mockCheckerGroup) CheckOutbounds() { + m.checked = true +} + // mockURLOverrideGroup implements both MutableOutboundGroup and URLOverrideSetter. type mockURLOverrideGroup struct { lbAdapter.MutableOutboundGroup @@ -177,7 +247,7 @@ func (m *mockURLOverrideGroup) SetURLOverrides(overrides map[string]string) { m.urlOverrides = overrides } -// mockPlainGroup implements MutableOutboundGroup but NOT URLOverrideSetter. +// mockPlainGroup implements MutableOutboundGroup but NOT URLOverrideSetter or OutboundChecker. type mockPlainGroup struct { lbAdapter.MutableOutboundGroup }