From 652b054625c0f2a27f28863998cb67d8faa36526 Mon Sep 17 00:00:00 2001 From: Clemens Hoffmann Date: Tue, 14 Apr 2026 14:38:23 +0200 Subject: [PATCH 01/13] Fix data race in registry heartbeat-based pruning test The background goroutine that keeps an endpoint alive during pruning was not fully drained before the spec ended. This caused a race with the next spec's BeforeEach overwriting the shared `configObj` variable. Wait for the goroutine to exit via a `stoppedChan` before returning. --- .../gorouter/registry/registry_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/code.cloudfoundry.org/gorouter/registry/registry_test.go b/src/code.cloudfoundry.org/gorouter/registry/registry_test.go index 5e828186b..1db3933f9 100644 --- a/src/code.cloudfoundry.org/gorouter/registry/registry_test.go +++ b/src/code.cloudfoundry.org/gorouter/registry/registry_test.go @@ -824,10 +824,11 @@ var _ = Describe("RouteRegistry", func() { r.Register("hb-partial-prune.example.com", staleEndpoint) doneChan := make(chan struct{}) - defer close(doneChan) + stoppedChan := make(chan struct{}) // Keep the fresh endpoint alive during pruning go func() { + defer close(stoppedChan) for { select { case <-doneChan: @@ -838,6 +839,10 @@ var _ = Describe("RouteRegistry", func() { } } }() + defer func() { + close(doneChan) + <-stoppedChan + }() captureCountBefore := reporter.CaptureEndpointsPerPoolCallCount() From 4d084ddb355f5d48374cb198604947a4ba197d54 Mon Sep 17 00:00:00 2001 From: Clemens Hoffmann Date: Tue, 14 Apr 2026 14:45:36 +0200 Subject: [PATCH 02/13] Fix flaky router and integration tests by setting explicit Eventually timeout The router and integration test suites relied on Gomega's default Eventually timeout of 1s, which is too short under CI load with parallel test procs. Set it to 5s to match other test suites. --- src/code.cloudfoundry.org/gorouter/integration/init_test.go | 1 + src/code.cloudfoundry.org/gorouter/router/router_suite_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/src/code.cloudfoundry.org/gorouter/integration/init_test.go b/src/code.cloudfoundry.org/gorouter/integration/init_test.go index 8521e035c..43a9d5d6c 100644 --- a/src/code.cloudfoundry.org/gorouter/integration/init_test.go +++ b/src/code.cloudfoundry.org/gorouter/integration/init_test.go @@ -48,6 +48,7 @@ var _ = SynchronizedBeforeSuite(func() []byte { json.Unmarshal(data, &res) gorouterPath = res.Gorouter testAssets = res.Test + SetDefaultEventuallyTimeout(5 * time.Second) SetDefaultEventuallyPollingInterval(100 * time.Millisecond) SetDefaultConsistentlyDuration(1 * time.Second) SetDefaultConsistentlyPollingInterval(10 * time.Millisecond) diff --git a/src/code.cloudfoundry.org/gorouter/router/router_suite_test.go b/src/code.cloudfoundry.org/gorouter/router/router_suite_test.go index 2fbfc4ff4..a0b4d6588 100644 --- a/src/code.cloudfoundry.org/gorouter/router/router_suite_test.go +++ b/src/code.cloudfoundry.org/gorouter/router/router_suite_test.go @@ -26,6 +26,7 @@ var _ = SynchronizedBeforeSuite(func() []byte { dropsonde.InitializeWithEmitter(fakeEmitter) return nil }, func([]byte) { + SetDefaultEventuallyTimeout(5 * time.Second) SetDefaultEventuallyPollingInterval(100 * time.Millisecond) SetDefaultConsistentlyDuration(1 * time.Second) SetDefaultConsistentlyPollingInterval(10 * time.Millisecond) From e8810a7e217ed317d7d072e2ed39718075c78426 Mon Sep 17 00:00:00 2001 From: Clemens Hoffmann Date: Tue, 14 Apr 2026 15:51:50 +0200 Subject: [PATCH 03/13] Use global defaultEventuallyTimeout 10s --- src/code.cloudfoundry.org/gorouter/integration/init_test.go | 2 +- src/code.cloudfoundry.org/gorouter/router/router_suite_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/code.cloudfoundry.org/gorouter/integration/init_test.go b/src/code.cloudfoundry.org/gorouter/integration/init_test.go index 43a9d5d6c..294eb315a 100644 --- a/src/code.cloudfoundry.org/gorouter/integration/init_test.go +++ b/src/code.cloudfoundry.org/gorouter/integration/init_test.go @@ -48,7 +48,7 @@ var _ = SynchronizedBeforeSuite(func() []byte { json.Unmarshal(data, &res) gorouterPath = res.Gorouter testAssets = res.Test - SetDefaultEventuallyTimeout(5 * time.Second) + SetDefaultEventuallyTimeout(10 * time.Second) SetDefaultEventuallyPollingInterval(100 * time.Millisecond) SetDefaultConsistentlyDuration(1 * time.Second) SetDefaultConsistentlyPollingInterval(10 * time.Millisecond) diff --git a/src/code.cloudfoundry.org/gorouter/router/router_suite_test.go b/src/code.cloudfoundry.org/gorouter/router/router_suite_test.go index a0b4d6588..060f2590a 100644 --- a/src/code.cloudfoundry.org/gorouter/router/router_suite_test.go +++ b/src/code.cloudfoundry.org/gorouter/router/router_suite_test.go @@ -26,7 +26,7 @@ var _ = SynchronizedBeforeSuite(func() []byte { dropsonde.InitializeWithEmitter(fakeEmitter) return nil }, func([]byte) { - SetDefaultEventuallyTimeout(5 * time.Second) + SetDefaultEventuallyTimeout(10 * time.Second) SetDefaultEventuallyPollingInterval(100 * time.Millisecond) SetDefaultConsistentlyDuration(1 * time.Second) SetDefaultConsistentlyPollingInterval(10 * time.Millisecond) From 4cba3d20faed571f9347c84ed6a805f085feef2d Mon Sep 17 00:00:00 2001 From: Clemens Hoffmann Date: Tue, 14 Apr 2026 15:56:45 +0200 Subject: [PATCH 04/13] Add timeout to health check HTTP client in router test The `testHealthCheckEndpoint` helper used an HTTP client with no timeout. If a TCP connection hangs instead of being immediately refused, the `Consistently` callback blocks indefinitely, causing the test proc to stall and Ginkgo to report a suite-level timeout. --- src/code.cloudfoundry.org/gorouter/router/router_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code.cloudfoundry.org/gorouter/router/router_test.go b/src/code.cloudfoundry.org/gorouter/router/router_test.go index 80e46605b..8ee161810 100644 --- a/src/code.cloudfoundry.org/gorouter/router/router_test.go +++ b/src/code.cloudfoundry.org/gorouter/router/router_test.go @@ -229,7 +229,7 @@ var _ = Describe("Router", func() { tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } - client := http.Client{Transport: tr} + client := http.Client{Transport: tr, Timeout: 500 * time.Millisecond} resp, err := client.Do(req) if err != nil { return 0, err From 7aac5ce726c4ec734986497788315e4b5b5be66d Mon Sep 17 00:00:00 2001 From: Clemens Hoffmann Date: Tue, 14 Apr 2026 16:11:16 +0200 Subject: [PATCH 05/13] Replace Eventually(...).ShouldNot with Consistently(...).ShouldNot These assertions verify that something does NOT happen, which is exactly what Consistently is for. Using Eventually would wait up to the full timeout (10s) before passing, causing unnecessary slowdown. --- src/code.cloudfoundry.org/gorouter/integration/main_test.go | 2 +- .../gorouter/router/health_listener_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/code.cloudfoundry.org/gorouter/integration/main_test.go b/src/code.cloudfoundry.org/gorouter/integration/main_test.go index cdc0c79ea..dec54edab 100644 --- a/src/code.cloudfoundry.org/gorouter/integration/main_test.go +++ b/src/code.cloudfoundry.org/gorouter/integration/main_test.go @@ -1288,7 +1288,7 @@ var _ = Describe("Router Integration", func() { It("doesn't start the route fetcher", func() { gorouterSession = startGorouterSession(cfgFile) - Eventually(gorouterSession).ShouldNot(Say("setting-up-routing-api")) + Consistently(gorouterSession).ShouldNot(Say("setting-up-routing-api")) stopGorouter(gorouterSession) }) }) diff --git a/src/code.cloudfoundry.org/gorouter/router/health_listener_test.go b/src/code.cloudfoundry.org/gorouter/router/health_listener_test.go index c4a2f15e2..f5e5803c2 100644 --- a/src/code.cloudfoundry.org/gorouter/router/health_listener_test.go +++ b/src/code.cloudfoundry.org/gorouter/router/health_listener_test.go @@ -182,7 +182,7 @@ var _ = Describe("HealthListener", func() { router.stopping = true }) It("does not log an error message", func() { - Eventually(logger).ShouldNot(gbytes.Say("health-listener-failed")) + Consistently(logger).ShouldNot(gbytes.Say("health-listener-failed")) }) }) }) From affc36e930b6756a2716a9ef38732efeb7a707f5 Mon Sep 17 00:00:00 2001 From: Clemens Hoffmann Date: Tue, 14 Apr 2026 16:30:49 +0200 Subject: [PATCH 06/13] Fix flaky keep-alive and 100-Continue router tests Increase EndpointTimeout from 500ms to 2s for the HTTP keep-alive tests. The 500ms timeout left only 125ms of margin for request processing between sleeps, which is insufficient under CI load. Add a 10s read deadline on the TCP connection in the 100-Continue test to prevent hanging if the gorouter is slow to respond. --- src/code.cloudfoundry.org/gorouter/router/router_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/code.cloudfoundry.org/gorouter/router/router_test.go b/src/code.cloudfoundry.org/gorouter/router/router_test.go index 8ee161810..f685d3838 100644 --- a/src/code.cloudfoundry.org/gorouter/router/router_test.go +++ b/src/code.cloudfoundry.org/gorouter/router/router_test.go @@ -685,6 +685,7 @@ var _ = Describe("Router", func() { conn, err := net.DialTimeout("tcp", host, 10*time.Second) Expect(err).ToNot(HaveOccurred()) defer conn.Close() + conn.SetDeadline(time.Now().Add(10 * time.Second)) fmt.Fprintf(conn, "POST / HTTP/1.1\r\n"+ "Host: %s\r\n"+ @@ -838,6 +839,10 @@ var _ = Describe("Router", func() { }) Context("HTTP keep-alive", func() { + BeforeEach(func() { + config.EndpointTimeout = 2 * time.Second + backendIdleTimeout = config.EndpointTimeout + }) It("reuses the same connection on subsequent calls", func() { app := test.NewGreetApp([]route.Uri{"keepalive." + test_util.LocalhostDNS}, config.Port, mbusClient, nil) app.RegisterAndListen() From deca2e3b5e873bac5c8a984130e6da958d9fd990 Mon Sep 17 00:00:00 2001 From: Clemens Hoffmann Date: Thu, 16 Apr 2026 11:23:23 +0200 Subject: [PATCH 07/13] fix: Update requestTimeout in keep-alive test to match EndpointTimeout The keep-alive BeforeEach only updated backendIdleTimeout but not requestTimeout. Since initializeRouter passes requestTimeout to the proxy config, the proxy still used the default 500ms timeout while the slow app slept for (2s*3)/4 = 1.5s, causing every request to time out with a 502. --- src/code.cloudfoundry.org/gorouter/router/router_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/code.cloudfoundry.org/gorouter/router/router_test.go b/src/code.cloudfoundry.org/gorouter/router/router_test.go index f685d3838..e5532e068 100644 --- a/src/code.cloudfoundry.org/gorouter/router/router_test.go +++ b/src/code.cloudfoundry.org/gorouter/router/router_test.go @@ -842,6 +842,7 @@ var _ = Describe("Router", func() { BeforeEach(func() { config.EndpointTimeout = 2 * time.Second backendIdleTimeout = config.EndpointTimeout + requestTimeout = config.EndpointTimeout }) It("reuses the same connection on subsequent calls", func() { app := test.NewGreetApp([]route.Uri{"keepalive." + test_util.LocalhostDNS}, config.Port, mbusClient, nil) From cd8b3e48ecccb5c63ed2ddc5888e686c6435ad34 Mon Sep 17 00:00:00 2001 From: Clemens Hoffmann Date: Thu, 16 Apr 2026 13:30:47 +0200 Subject: [PATCH 08/13] fix: Guard println with nil check to prevent proc crash on nil error The println("Error", err.Error()) calls unconditionally dereference err before the Expect assertion. If TLS certificate validation races and err is nil, this causes a nil pointer panic that crashes the parallel proc and triggers "Ginkgo timed out waiting for all parallel procs to report back". --- src/code.cloudfoundry.org/gorouter/router/router_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/code.cloudfoundry.org/gorouter/router/router_test.go b/src/code.cloudfoundry.org/gorouter/router/router_test.go index e5532e068..e73e89caf 100644 --- a/src/code.cloudfoundry.org/gorouter/router/router_test.go +++ b/src/code.cloudfoundry.org/gorouter/router/router_test.go @@ -2173,7 +2173,9 @@ var _ = Describe("Router", func() { tlsClientConfig.Certificates = []tls.Certificate{*clientCert} resp, err := client.Do(req) - println("Error", err.Error()) + if err != nil { + println("Error", err.Error()) + } Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(ContainSubstring("remote error: tls: bad certificate"))) Expect(resp).To(BeNil()) @@ -2203,7 +2205,9 @@ var _ = Describe("Router", func() { tlsClientConfig.Certificates = []tls.Certificate{*clientCert} resp, err := client.Do(req) - println("Error", err.Error()) + if err != nil { + println("Error", err.Error()) + } Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(ContainSubstring("remote error: tls: bad certificate"))) Expect(resp).To(BeNil()) From f28688cc1f30c4834be489974deb991500a0e2a8 Mon Sep 17 00:00:00 2001 From: Clemens Hoffmann Date: Thu, 16 Apr 2026 13:40:02 +0200 Subject: [PATCH 09/13] fix: Add 10s timeout to all HTTP clients in router test suite HTTP clients without timeouts can hang indefinitely on stalled connections, crashing the parallel proc and causing "Ginkgo timed out waiting for all parallel procs to report back". - Set http.DefaultClient.Timeout = 10s in SynchronizedBeforeSuite - Add Timeout: 10s to all 17 custom http.Client instances in router_test.go and health_listener_test.go --- .../gorouter/router/health_listener_test.go | 5 +- .../gorouter/router/router_suite_test.go | 1 + .../gorouter/router/router_test.go | 50 ++++++++++--------- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/code.cloudfoundry.org/gorouter/router/health_listener_test.go b/src/code.cloudfoundry.org/gorouter/router/health_listener_test.go index f5e5803c2..0eccc0dfa 100644 --- a/src/code.cloudfoundry.org/gorouter/router/health_listener_test.go +++ b/src/code.cloudfoundry.org/gorouter/router/health_listener_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -129,7 +130,7 @@ var _ = Describe("HealthListener", func() { tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } - client := http.Client{Transport: tr} + client := http.Client{Transport: tr, Timeout: 10 * time.Second} resp, err := client.Do(req) Expect(err).ToNot(HaveOccurred()) Expect(resp).ToNot(BeNil()) @@ -154,7 +155,7 @@ var _ = Describe("HealthListener", func() { tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } - client := http.Client{Transport: tr} + client := http.Client{Transport: tr, Timeout: 10 * time.Second} resp, err := client.Do(req) Expect(err).ToNot(HaveOccurred()) Expect(resp).ToNot(BeNil()) diff --git a/src/code.cloudfoundry.org/gorouter/router/router_suite_test.go b/src/code.cloudfoundry.org/gorouter/router/router_suite_test.go index 060f2590a..946afbeb1 100644 --- a/src/code.cloudfoundry.org/gorouter/router/router_suite_test.go +++ b/src/code.cloudfoundry.org/gorouter/router/router_suite_test.go @@ -26,6 +26,7 @@ var _ = SynchronizedBeforeSuite(func() []byte { dropsonde.InitializeWithEmitter(fakeEmitter) return nil }, func([]byte) { + http.DefaultClient.Timeout = 10 * time.Second SetDefaultEventuallyTimeout(10 * time.Second) SetDefaultEventuallyPollingInterval(100 * time.Millisecond) SetDefaultConsistentlyDuration(1 * time.Second) diff --git a/src/code.cloudfoundry.org/gorouter/router/router_test.go b/src/code.cloudfoundry.org/gorouter/router/router_test.go index e73e89caf..82bf9616c 100644 --- a/src/code.cloudfoundry.org/gorouter/router/router_test.go +++ b/src/code.cloudfoundry.org/gorouter/router/router_test.go @@ -514,7 +514,7 @@ var _ = Describe("Router", func() { req, err = http.NewRequest("GET", app.Endpoint(), nil) Expect(err).ToNot(HaveOccurred()) - client := http.Client{} + client := http.Client{Timeout: 10 * time.Second} _, err = client.Do(req) Expect(err).To(HaveOccurred()) }) @@ -610,7 +610,7 @@ var _ = Describe("Router", func() { tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } - client := http.Client{Transport: tr} + client := http.Client{Transport: tr, Timeout: 10 * time.Second} resp, err := client.Do(req) Expect(err).ToNot(HaveOccurred()) Expect(resp).ToNot(BeNil()) @@ -652,7 +652,7 @@ var _ = Describe("Router", func() { r, err := http.NewRequest("PUT", url, buf) Expect(err).ToNot(HaveOccurred()) - client := http.Client{} + client := http.Client{Timeout: 10 * time.Second} resp, err := client.Do(r) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) @@ -824,7 +824,7 @@ var _ = Describe("Router", func() { tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } - client := http.Client{Transport: tr} + client := http.Client{Transport: tr, Timeout: 10 * time.Second} resp, err := client.Do(req) Expect(err).ToNot(HaveOccurred()) @@ -1037,7 +1037,7 @@ var _ = Describe("Router", func() { var client http.Client BeforeEach(func() { - client = http.Client{} + client = http.Client{Timeout: 10 * time.Second} }) JustBeforeEach(func() { @@ -1094,7 +1094,7 @@ var _ = Describe("Router", func() { requestTimeout = 1 * time.Second backendIdleTimeout = 3 * time.Second appResponseTime = 2 * time.Second - client = http.Client{} + client = http.Client{Timeout: 10 * time.Second} }) JustBeforeEach(func() { app := newSlowApp( @@ -1156,7 +1156,7 @@ var _ = Describe("Router", func() { requestTimeout = 3 * time.Second backendIdleTimeout = 1 * time.Second appResponseTime = 2 * time.Second - client = http.Client{} + client = http.Client{Timeout: 10 * time.Second} }) JustBeforeEach(func() { app := newSlowApp( @@ -1344,9 +1344,10 @@ var _ = Describe("Router", func() { RootCAs: rootCAs, } - httpClient = &http.Client{Transport: &http.Transport{ - TLSClientConfig: tlsClientConfig, - }} + httpClient = &http.Client{ + Transport: &http.Transport{TLSClientConfig: tlsClientConfig}, + Timeout: 10 * time.Second, + } }) JustBeforeEach(func() { @@ -1649,9 +1650,10 @@ var _ = Describe("Router", func() { tlsClientConfig := &tls.Config{ RootCAs: rootCAs, } - client := &http.Client{Transport: &http.Transport{ - TLSClientConfig: tlsClientConfig, - }} + client := &http.Client{ + Transport: &http.Transport{TLSClientConfig: tlsClientConfig}, + Timeout: 10 * time.Second, + } app := test.NewGreetApp([]route.Uri{"test." + test_util.LocalhostDNS}, config.Port, mbusClient, nil) app.RegisterAndListen() @@ -1694,9 +1696,10 @@ var _ = Describe("Router", func() { tlsClientConfig = &tls.Config{ RootCAs: rootCAs, } - client = &http.Client{Transport: &http.Transport{ - TLSClientConfig: tlsClientConfig, - }} + client = &http.Client{ + Transport: &http.Transport{TLSClientConfig: tlsClientConfig}, + Timeout: 10 * time.Second, + } }) It("serves ssl traffic", func() { @@ -1735,9 +1738,10 @@ var _ = Describe("Router", func() { RootCAs: rootCAs, } tlsClientConfig.CipherSuites = []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256} - client = &http.Client{Transport: &http.Transport{ - TLSClientConfig: tlsClientConfig, - }} + client = &http.Client{ + Transport: &http.Transport{TLSClientConfig: tlsClientConfig}, + Timeout: 10 * time.Second, + } }) It("serves ssl traffic", func() { @@ -2027,7 +2031,7 @@ var _ = Describe("Router", func() { }, } - client := http.Client{Transport: tr} + client := http.Client{Transport: tr, Timeout: 10 * time.Second} resp, err := client.Do(req) Expect(err).To(MatchError(ContainSubstring("remote error: tls: handshake failure"))) Expect(resp).To(BeNil()) @@ -2263,7 +2267,7 @@ var _ = Describe("Router", func() { }, } - client := http.Client{Transport: tr} + client := http.Client{Transport: tr, Timeout: 10 * time.Second} resp, err := client.Do(req) Expect(err).ToNot(HaveOccurred()) defer resp.Body.Close() @@ -2295,7 +2299,7 @@ var _ = Describe("Router", func() { }, } - client := http.Client{Transport: tr} + client := http.Client{Transport: tr, Timeout: 10 * time.Second} resp, err := client.Do(req) Expect(err).To(HaveOccurred()) Expect(resp).To(BeNil()) @@ -2355,7 +2359,7 @@ var _ = Describe("Router", func() { RootCAs: certPool, }, } - client := http.Client{Transport: tr} + client := http.Client{Transport: tr, Timeout: 10 * time.Second} req, err := http.NewRequest("GET", fmt.Sprintf("https://myapp.%s:%d/", test_util.LocalhostDNS, config.SSLPort), nil) Expect(err).NotTo(HaveOccurred()) _, err = client.Do(req) From 1bf516c7f10f48ce7c6d749eebc2ca3318777230 Mon Sep 17 00:00:00 2001 From: Clemens Hoffmann Date: Thu, 16 Apr 2026 14:01:35 +0200 Subject: [PATCH 10/13] fix: Add nil check before err.Error() in SSL port refusal test net.Dial err.Error() was called without checking err != nil first. Under parallel test execution, port reuse can cause the dial to succeed unexpectedly, making err nil and triggering a panic that crashes the proc. --- src/code.cloudfoundry.org/gorouter/router/router_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/code.cloudfoundry.org/gorouter/router/router_test.go b/src/code.cloudfoundry.org/gorouter/router/router_test.go index 82bf9616c..4eda0eebe 100644 --- a/src/code.cloudfoundry.org/gorouter/router/router_test.go +++ b/src/code.cloudfoundry.org/gorouter/router/router_test.go @@ -1840,6 +1840,7 @@ var _ = Describe("Router", func() { It("refuses connections to the SSL port", func() { _, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", config.SSLPort)) + Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("connection refused")) }) }) From cb7ee1c6ec08d73e50a94a22b1bbc132fde0c980 Mon Sep 17 00:00:00 2001 From: Clemens Hoffmann Date: Thu, 16 Apr 2026 14:22:48 +0200 Subject: [PATCH 11/13] fix: Stop router before NATS in AfterEach to prevent proc crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cleanup order stopped NATS before the router, causing the subscriber's ClosedCB to fire log.Fatal → os.Exit(1), which kills the test proc without reporting results back to Ginkgo. Reversing the order ensures the router and its subscriber disconnect gracefully before the NATS server shuts down. --- src/code.cloudfoundry.org/gorouter/router/router_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/code.cloudfoundry.org/gorouter/router/router_test.go b/src/code.cloudfoundry.org/gorouter/router/router_test.go index 4eda0eebe..b9217227d 100644 --- a/src/code.cloudfoundry.org/gorouter/router/router_test.go +++ b/src/code.cloudfoundry.org/gorouter/router/router_test.go @@ -130,13 +130,13 @@ var _ = Describe("Router", func() { }) AfterEach(func() { - if natsRunner != nil { - natsRunner.Stop() - } - if router != nil { router.Stop() } + + if natsRunner != nil { + natsRunner.Stop() + } }) Describe("Route Services Server", func() { From 961e6163d163238de8664caeccfa1548a4cb4a08 Mon Sep 17 00:00:00 2001 From: Clemens Hoffmann Date: Thu, 16 Apr 2026 15:12:35 +0200 Subject: [PATCH 12/13] fix: Use dynamic port for debug server in integration tests The debug server address was hardcoded to 127.0.0.1:17017, causing port collisions when multiple parallel Ginkgo procs each start a gorouter binary. This led to "bind: address already in use" failures in the integration suite, and cascading proc crashes in the router suite. --- src/code.cloudfoundry.org/gorouter/integration/main_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code.cloudfoundry.org/gorouter/integration/main_test.go b/src/code.cloudfoundry.org/gorouter/integration/main_test.go index dec54edab..846974a43 100644 --- a/src/code.cloudfoundry.org/gorouter/integration/main_test.go +++ b/src/code.cloudfoundry.org/gorouter/integration/main_test.go @@ -635,7 +635,7 @@ var _ = Describe("Router Integration", func() { BeforeEach(func() { testState = NewTestState() - testState.cfg.DebugAddr = "127.0.0.1:17017" + testState.cfg.DebugAddr = fmt.Sprintf("127.0.0.1:%d", test_util.NextAvailPort()) testState.StartGorouterOrFail() gorouterSession = testState.gorouterSession From f9a2edf59c0c43f0780bc287776ac51b4d3d3e42 Mon Sep 17 00:00:00 2001 From: Clemens Hoffmann Date: Thu, 16 Apr 2026 15:44:47 +0200 Subject: [PATCH 13/13] fix: Use OS-assigned ports to prevent cross-suite port collisions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every test suite ran in a fresh process, resetting the static port counter to the same base (25000 + procID). When suites ran sequentially, ports from a prior suite (e.g. integration) could still be in TCP TIME_WAIT, causing net.Listen to fail in the next suite (e.g. router). This triggered log.Fatal → os.Exit(1), crashing the Ginkgo proc. Replace the static interleaved port scheme with OS-assigned ports via net.Listen("tcp", "127.0.0.1:0"). The OS guarantees the port is free and avoids recently-used ports in TIME_WAIT. --- .../gorouter/test_util/ports.go | 33 ++++++------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/code.cloudfoundry.org/gorouter/test_util/ports.go b/src/code.cloudfoundry.org/gorouter/test_util/ports.go index 4e0622c0b..aaeec5d70 100644 --- a/src/code.cloudfoundry.org/gorouter/test_util/ports.go +++ b/src/code.cloudfoundry.org/gorouter/test_util/ports.go @@ -1,31 +1,18 @@ package test_util import ( - "sync" - - . "github.com/onsi/ginkgo/v2" -) - -var ( - lastPortUsed uint16 - portLock sync.Mutex - once sync.Once + "net" ) +// NextAvailPort asks the OS for a free port by binding to :0, then closing +// the listener and returning the assigned port. This avoids cross-suite port +// collisions that occur when multiple suites reuse the same static port range. func NextAvailPort() uint16 { - portLock.Lock() - defer portLock.Unlock() - - if lastPortUsed == 0 { - once.Do(func() { - const portRangeStart = 25000 - // #nosec G115 - if we have negative or > 65k parallel ginkgo threads there's something worse happening - lastPortUsed = portRangeStart + uint16(GinkgoParallelProcess()) - }) + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + panic("NextAvailPort: " + err.Error()) } - - suiteCfg, _ := GinkgoConfiguration() - // #nosec G115 - if we have negative or > 65k parallel ginkgo threads there's something worse happening - lastPortUsed += uint16(suiteCfg.ParallelTotal) - return lastPortUsed + defer l.Close() + // #nosec G115 - ephemeral ports are always in uint16 range + return uint16(l.Addr().(*net.TCPAddr).Port) }