From 2127c5bcd8028dba2643474cc493a1a30534f735 Mon Sep 17 00:00:00 2001 From: Solomon Jacobs Date: Tue, 18 Nov 2025 16:43:35 +0100 Subject: [PATCH] Fix flakes in tests using `freeAddress` This commit addresses the following error: ``` time=2025-11-14T16:14:59.573Z level=ERROR source=main.go:559 msg="Listen error" err="listen tcp 127.0.0.1:36357: bind: address already in use" ``` `freeAddress` is called multiple times within one test, but also in parallel in multiple tests. `freeAddress` immediately releases the port despite it being used later in a test. This causes ports to errorenously to be reused. We replace `net.Listen` in order to avoid calling `syscall.Listen`. This way the port can be reused by AM, without the kernel assuming that port can be reused by other processes specifiying port `0`. Fixes: #4742 Signed-off-by: Solomon Jacobs --- test/cli/acceptance.go | 39 +++++++++++++++++++++++----------- test/with_api_v2/acceptance.go | 39 +++++++++++++++++++++++----------- 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/test/cli/acceptance.go b/test/cli/acceptance.go index fcc603783a..b6933f36bf 100644 --- a/test/cli/acceptance.go +++ b/test/cli/acceptance.go @@ -20,7 +20,6 @@ import ( "fmt" "io" "maps" - "net" "net/http" "os" "os/exec" @@ -99,20 +98,36 @@ func NewAcceptanceTest(t *testing.T, opts *AcceptanceOpts) *AcceptanceTest { } // freeAddress returns a new listen address not currently in use. -func freeAddress() string { - // Let the OS allocate a free address, close it and hope - // it is still free when starting Alertmanager. - l, err := net.Listen("tcp4", "localhost:0") +func (t *AcceptanceTest) freeAddress() string { + // Let the OS allocate a free address. + fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0) if err != nil { panic(err) } - defer func() { - if err := l.Close(); err != nil { - panic(err) + t.Cleanup(func() { + // Keep the fd open until the end of test. This ensures the port is not + // used by other process binding to port 0. Any process can still bind + // to the port by specifying it explicitly. + if err := syscall.Close(fd); err != nil { + t.Fatalf("Failed to close fd: %v", fd) } - }() + }) + + if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { + panic(err) + } + + addr := syscall.SockaddrInet4{Port: 0, Addr: [4]byte{127, 0, 0, 1}} + if err := syscall.Bind(fd, &addr); err != nil { + panic(err) + } + + boundAddr, err := syscall.Getsockname(fd) + if err != nil { + panic(err) + } - return l.Addr().String() + return fmt.Sprintf("127.0.0.1:%d", boundAddr.(*syscall.SockaddrInet4).Port) } // AmtoolOk verifies that the "amtool" file exists in the correct location for testing, @@ -156,8 +171,8 @@ func (t *AcceptanceTest) AlertmanagerCluster(conf string, size int) *Alertmanage am.confFile = cf am.UpdateConfig(conf) - am.apiAddr = freeAddress() - am.clusterAddr = freeAddress() + am.apiAddr = t.freeAddress() + am.clusterAddr = t.freeAddress() transport := httptransport.New(am.apiAddr, t.opts.RoutePrefix+"/api/v2/", nil) am.clientV2 = apiclient.New(transport, strfmt.Default) diff --git a/test/with_api_v2/acceptance.go b/test/with_api_v2/acceptance.go index ac1a152689..d53aeb6f8d 100644 --- a/test/with_api_v2/acceptance.go +++ b/test/with_api_v2/acceptance.go @@ -17,7 +17,6 @@ import ( "bytes" "context" "fmt" - "net" "os" "os/exec" "path/filepath" @@ -89,20 +88,36 @@ func NewAcceptanceTest(t *testing.T, opts *AcceptanceOpts) *AcceptanceTest { } // freeAddress returns a new listen address not currently in use. -func freeAddress() string { - // Let the OS allocate a free address, close it and hope - // it is still free when starting Alertmanager. - l, err := net.Listen("tcp4", "localhost:0") +func (t *AcceptanceTest) freeAddress() string { + // Let the OS allocate a free address. + fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0) if err != nil { panic(err) } - defer func() { - if err := l.Close(); err != nil { - panic(err) + t.Cleanup(func() { + // Keep the fd open until the end of test. This ensures the port is not + // used by other process binding to port 0. Any process can still bind + // to the port by specifying it explicitly. + if err := syscall.Close(fd); err != nil { + t.Fatalf("Failed to close fd: %v", fd) } - }() + }) + + if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { + panic(err) + } + + addr := syscall.SockaddrInet4{Port: 0, Addr: [4]byte{127, 0, 0, 1}} + if err := syscall.Bind(fd, &addr); err != nil { + panic(err) + } + + boundAddr, err := syscall.Getsockname(fd) + if err != nil { + panic(err) + } - return l.Addr().String() + return fmt.Sprintf("127.0.0.1:%d", boundAddr.(*syscall.SockaddrInet4).Port) } // Do sets the given function to be executed at the given time. @@ -134,8 +149,8 @@ func (t *AcceptanceTest) AlertmanagerCluster(conf string, size int) *Alertmanage am.confFile = cf am.UpdateConfig(conf) - am.apiAddr = freeAddress() - am.clusterAddr = freeAddress() + am.apiAddr = t.freeAddress() + am.clusterAddr = t.freeAddress() transport := httptransport.New(am.apiAddr, t.opts.RoutePrefix+"/api/v2/", nil) am.clientV2 = apiclient.New(transport, strfmt.Default)