Files
gost/tests/e2e/selector_test.go
ginuerzh 66ef4a3e14 tests(e2e): cover the parallel selector node limit
Add an e2e case for the parallel selector configured with maxParallel: 1.
With only one node racing per connection the dead node is no longer
masked by the live one, so the requests alternate between success and
failure - that alternation is what proves the limit is applied.

Requires go-gost/x with the parallel selector node limit (gost#916).
gost's go.mod still pins v0.19.5, so this test only goes green once
that release lands and the dependency is bumped.
2026-10-21 08:54:22 +00:00

137 lines
4.2 KiB
Go

package e2e
import (
"context"
"fmt"
"io"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type SelectorSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *SelectorSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
func (s *SelectorSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
// proxyRequest sends one request through the gost proxy and returns the curl
// exit code and response body.
func (s *SelectorSuite) proxyRequest(gostC testcontainers.Container, port string) (int, string) {
cmd := []string{
"curl", "-s",
"-x", fmt.Sprintf("http://127.0.0.1:%s", port),
fmt.Sprintf("http://%s:5678", s.echoIP),
}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
return code, string(body)
}
// runFailover starts a gost container from cfg and verifies that, despite a
// dead node in the hop, requests converge to the live node after the selector
// marks the dead node and skips it. At most the initial marking attempt may
// fail; every request after must reach the echo server.
func (s *SelectorSuite) runFailover(cfg, port string) {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, cfg, port+"/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
const n = 12
var failures int
lastOK := false
for range n {
code, body := s.proxyRequest(gostC, port)
ok := code == 0 && strings.Contains(body, "hello-gost")
if !ok {
failures++
}
lastOK = ok
}
s.Require().LessOrEqual(failures, 1,
"selector did not filter the dead node; too many requests failed")
s.Require().True(lastOK, "requests did not converge to the live node")
}
// TestRoundRobinFailover exercises the round-robin strategy combined with the
// fail filter: the dead node is tried once, marked, and then skipped.
func (s *SelectorSuite) TestRoundRobinFailover() {
s.runFailover("testdata/selector/roundrobin.yaml", "8080")
}
// TestFIFOFailover exercises the fifo (sticky) strategy: it always picks the
// first node until it fails, then falls through to the secondary node.
func (s *SelectorSuite) TestFIFOFailover() {
s.runFailover("testdata/selector/fifo.yaml", "8080")
}
// TestBackupFailover exercises the backup filter: with the primary node dead,
// the selector falls back to the backup node.
func (s *SelectorSuite) TestBackupFailover() {
s.runFailover("testdata/selector/backup.yaml", "8080")
}
// TestParallelSelector exercises the parallel strategy: it dials all nodes
// concurrently and uses the first that connects, so a dead node never blocks
// the request.
func (s *SelectorSuite) TestParallelSelector() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/selector/parallel.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
code, body := s.proxyRequest(gostC, "8080")
s.Require().Equal(0, code)
s.Require().Contains(body, "hello-gost")
}
// TestParallelSelectorLimit exercises the parallel strategy with a node
// limit: only one node takes part in each race, so the dead node is no
// longer masked by the live one. With the round-robin mode the requests
// alternate between the live node (succeeds) and the dead node (fails).
func (s *SelectorSuite) TestParallelSelectorLimit() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/selector/parallel_limit.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
code, body := s.proxyRequest(gostC, "8080")
s.Require().Equal(0, code)
s.Require().Contains(body, "hello-gost", "first race should pick the live node")
code, body = s.proxyRequest(gostC, "8080")
s.Require().Equal(0, code)
s.Require().NotContains(body, "hello-gost", "second race should pick the dead node")
code, body = s.proxyRequest(gostC, "8080")
s.Require().Equal(0, code)
s.Require().Contains(body, "hello-gost", "third race should pick the live node again")
}
func TestSelectorSuite(t *testing.T) {
suite.Run(t, new(SelectorSuite))
}