diff --git a/go-gost/x/hop/hop.go b/go-gost/x/hop/hop.go index 60076eb..53cdcd3 100644 --- a/go-gost/x/hop/hop.go +++ b/go-gost/x/hop/hop.go @@ -201,11 +201,26 @@ func (p *chainHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain. return nodes[0] } - // Always go through selector for proper FailFilter evaluation, - // even when there's only one node. This ensures failed nodes - // can be filtered out properly. + // For single-node case: bypass selector/FailFilter to ensure availability. + // The marker system still works for metrics, but we don't block requests + // based on recent failures - the connection will be attempted regardless. + // This matches upstream go-gost/x behavior. + if len(nodes) == 1 { + return nodes[0] + } + + // Multi-node case: use selector with FailFilter for proper failover. + // FailFilter will exclude recently-failed nodes, allowing traffic to + // be routed to healthy alternatives. if s := p.options.selector; s != nil { - return s.Select(ctx, nodes...) + if node := s.Select(ctx, nodes...); node != nil { + return node + } + // All nodes filtered out by FailFilter - all are marked as failed. + // Return nil to signal "no healthy nodes available" to the caller. + // The handler's retry loop will handle this appropriately. + log.Debugf("all %d nodes filtered out by FailFilter, no healthy nodes available", len(nodes)) + return nil } // Fallback: return first node if no selector configured diff --git a/go-gost/x/selector/filter.go b/go-gost/x/selector/filter.go index 69befe3..c841ad6 100644 --- a/go-gost/x/selector/filter.go +++ b/go-gost/x/selector/filter.go @@ -24,11 +24,12 @@ func FailFilter[T any](maxFails int, timeout time.Duration) selector.Filter[T] { } // Filter filters dead objects. -// Note: We intentionally do NOT skip filtering when len(vs) <= 1. -// This ensures that even a single dead node gets filtered out, -// allowing the caller to know that no healthy nodes are available -// and potentially trigger failover behavior. +// For single-node case, skip filtering to ensure availability (matches upstream). +// For multi-node case, filter out failed nodes to enable failover. func (f *failFilter[T]) Filter(ctx context.Context, vs ...T) []T { + if len(vs) <= 1 { + return vs + } var l []T for _, v := range vs { maxFails := f.maxFails