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..1c8a117 100644 --- a/adapter/groups/manager.go +++ b/adapter/groups/manager.go @@ -179,6 +179,29 @@ 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() + 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 +} + // RemoveFromGroup removes an outbound/endpoint from the specified group. func (m *MutableGroupManager) RemoveFromGroup(group, tag string) error { m.mu.Lock() 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 }