From bef00dffefd425d3e4eab81001d4ec56a6d882d2 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Thu, 16 Jul 2026 16:44:48 +0800 Subject: [PATCH] e2e: add node liveness probe tests TCP probe + FailFilter: dead node pre-marked, all requests succeed. LowestLatency strategy: two probed nodes, selector picks fastest. --- tests/e2e/probe_test.go | 90 +++++++++++++++++++++ tests/e2e/testdata/probe/lowestlatency.yaml | 49 +++++++++++ tests/e2e/testdata/probe/tcp.yaml | 44 ++++++++++ 3 files changed, 183 insertions(+) create mode 100644 tests/e2e/probe_test.go create mode 100644 tests/e2e/testdata/probe/lowestlatency.yaml create mode 100644 tests/e2e/testdata/probe/tcp.yaml diff --git a/tests/e2e/probe_test.go b/tests/e2e/probe_test.go new file mode 100644 index 0000000..7db11bf --- /dev/null +++ b/tests/e2e/probe_test.go @@ -0,0 +1,90 @@ +package e2e + +import ( + "context" + "fmt" + "io" + "testing" + "time" + + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" +) + +type ProbeSuite struct { + suite.Suite + ctx context.Context + echoC testcontainers.Container + echoIP string +} + +func (s *ProbeSuite) 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 *ProbeSuite) TearDownSuite() { + if s.echoC != nil { + s.echoC.Terminate(s.ctx) + } +} + +func (s *ProbeSuite) 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) +} + +// TestTCPProbeFailover verifies that the TCP probe detects a dead node and +// marks it before any real traffic, so the FailFilter excludes it. With the +// dead node pre-marked, every request succeeds immediately. +func (s *ProbeSuite) TestTCPProbeFailover() { + gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/probe/tcp.yaml", "8080/tcp") + s.Require().NoError(err) + defer gostC.Terminate(s.ctx) + + // The probe fires immediately at startup, so the dead node is already + // marked by the time we send requests. + for range 10 { + code, body := s.proxyRequest(gostC, "8080") + s.Require().Equal(0, code, "every request must succeed; dead node pre-marked by probe") + s.Require().Contains(body, "hello-gost") + } +} + +// TestLowestLatencyProbe verifies that the lowestlatency strategy works with +// probed nodes. Both nodes are live; the strategy selects the one with the +// lowest measured latency. +func (s *ProbeSuite) TestLowestLatencyProbe() { + gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/probe/lowestlatency.yaml", "8080/tcp") + s.Require().NoError(err) + defer gostC.Terminate(s.ctx) + + // Give the initial probe a moment to fire so both nodes show healthy. + time.Sleep(200 * time.Millisecond) + + for range 10 { + code, body := s.proxyRequest(gostC, "8080") + s.Require().Equal(0, code, "all requests must succeed with lowestlatency strategy") + s.Require().Contains(body, "hello-gost") + } +} + +func TestProbeSuite(t *testing.T) { + suite.Run(t, new(ProbeSuite)) +} diff --git a/tests/e2e/testdata/probe/lowestlatency.yaml b/tests/e2e/testdata/probe/lowestlatency.yaml new file mode 100644 index 0000000..c3c71c8 --- /dev/null +++ b/tests/e2e/testdata/probe/lowestlatency.yaml @@ -0,0 +1,49 @@ +services: +- name: proxy + addr: :8080 + handler: + type: http + chain: my-chain + listener: + type: tcp + +# Two live relays — both forward to the echo server. +- name: relay-a + addr: 127.0.0.1:18081 + handler: + type: http + listener: + type: tcp + +- name: relay-b + addr: 127.0.0.1:18082 + handler: + type: http + listener: + type: tcp + +chains: +- name: my-chain + hops: + - name: hop-1 + selector: + strategy: lowestlatency + maxFails: 1 + failTimeout: 10s + nodes: + - name: node-a + addr: 127.0.0.1:18081 + connector: + type: http + probe: + type: tcp + addr: 127.0.0.1:18081 + interval: 5s + - name: node-b + addr: 127.0.0.1:18082 + connector: + type: http + probe: + type: tcp + addr: 127.0.0.1:18082 + interval: 5s diff --git a/tests/e2e/testdata/probe/tcp.yaml b/tests/e2e/testdata/probe/tcp.yaml new file mode 100644 index 0000000..19be3b6 --- /dev/null +++ b/tests/e2e/testdata/probe/tcp.yaml @@ -0,0 +1,44 @@ +services: +- name: proxy + addr: :8080 + handler: + type: http + chain: my-chain + listener: + type: tcp + +# Live relay: forwards to the echo server. +- name: relay + addr: 127.0.0.1:18081 + handler: + type: http + listener: + type: tcp + +chains: +- name: my-chain + hops: + - name: hop-1 + selector: + strategy: round + maxFails: 1 + failTimeout: 10s + nodes: + - name: node-live + addr: 127.0.0.1:18081 + connector: + type: http + probe: + type: tcp + addr: 127.0.0.1:18081 + interval: 5s + # dead node: probed, marked as failed by the probe goroutine, + # then excluded by FailFilter before any real traffic tries it. + - name: node-dead + addr: 127.0.0.1:18082 + connector: + type: http + probe: + type: tcp + addr: 127.0.0.1:18082 + interval: 5s