Skip to content

Commit 593aad9

Browse files
author
Alex Tsibulya
committed
fix(verifier): acquire HTTP port atomically
Acquire the port in VerifyMessageProvider and VerifyProvider atomically by using the listener used for a free port acquisition for the HTTP server. This prevents a race condition when multiple pact verifiers run in parallel and compete for free ports, which could result in errors like ``` Expected server to start < 10s. Timed out waiting for http verification proxy on port 34425 - check for errors ```
1 parent c4aa28e commit 593aad9

File tree

3 files changed

+36
-30
lines changed

3 files changed

+36
-30
lines changed

dsl/pact.go

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,14 @@ func (p *Pact) VerifyProviderRaw(request types.VerifyRequest) ([]types.ProviderV
347347
// This maps the 'description' field of a message pact, to a function handler
348348
// that will implement the message producer. This function must return an object and optionally
349349
// and error. The object will be marshalled to JSON for comparison.
350-
port, err := proxy.HTTPReverseProxy(opts)
350+
listener, err := proxy.HTTPReverseProxy(opts)
351+
if err != nil {
352+
log.Printf("[ERROR] unable to start http verification proxy: %v", err)
353+
return nil, err
354+
}
355+
defer listener.Close()
356+
357+
port := listener.Addr().(*net.TCPAddr).Port
351358

352359
// Backwards compatibility, setup old provider states URL if given
353360
// Otherwise point to proxy
@@ -669,14 +676,18 @@ func (p *Pact) VerifyMessageProviderRaw(request VerifyMessageRequest) ([]types.P
669676
// and error. The object will be marshalled to JSON for comparison.
670677
mux := http.NewServeMux()
671678

672-
port, err := utils.GetFreePort()
679+
ln, err := net.Listen("tcp", "localhost:0")
673680
if err != nil {
674-
return response, fmt.Errorf("unable to allocate a port for verification: %v", err)
681+
log.Printf("[ERROR] unable to allocate a port for verification: %v", err)
682+
return nil, err
675683
}
684+
defer ln.Close()
685+
686+
log.Printf("[DEBUG] API handler starting at %s", ln.Addr())
676687

677688
// Construct verifier request
678689
verificationRequest := types.VerifyRequest{
679-
ProviderBaseURL: fmt.Sprintf("http://localhost:%d", port),
690+
ProviderBaseURL: fmt.Sprintf("http://%s", ln.Addr()),
680691
PactURLs: request.PactURLs,
681692
BrokerURL: request.BrokerURL,
682693
Tags: request.Tags,
@@ -695,25 +706,18 @@ func (p *Pact) VerifyMessageProviderRaw(request VerifyMessageRequest) ([]types.P
695706

696707
mux.HandleFunc("/", messageVerificationHandler(request.MessageHandlers, request.StateHandlers))
697708

698-
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
699-
if err != nil {
700-
log.Fatal(err)
701-
}
702-
defer ln.Close()
703-
704-
log.Printf("[DEBUG] API handler starting: port %d (%s)", port, ln.Addr())
705709
go func() {
706-
if err := http.Serve(ln, mux); err != nil {
707-
// NOTE: calling Fatalf causing test failures due to "accept tcp [::]:<port>: use of closed network connection"
710+
if err := http.Serve(ln, mux); err != nil && !strings.HasSuffix(err.Error(), "use of closed network connection") {
708711
log.Printf("[DEBUG] API handler start failed: %v", err)
709712
}
710713
}()
711714

715+
port := ln.Addr().(*net.TCPAddr).Port
716+
712717
portErr := waitForPort(port, "tcp", "localhost", p.ClientTimeout,
713718
fmt.Sprintf(`Timed out waiting for pact proxy on port %d - check for errors`, port))
714719

715720
if portErr != nil {
716-
log.Fatal("Error:", err)
717721
return response, portErr
718722
}
719723

proxy/http.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import (
1010
"net/url"
1111
"strings"
1212
"time"
13-
14-
"github.com/pact-foundation/pact-go/utils"
1513
)
1614

1715
// Middleware is a way to use composition to add functionality
@@ -72,7 +70,7 @@ func chainHandlers(mw ...Middleware) Middleware {
7270

7371
// HTTPReverseProxy provides a default setup for proxying
7472
// internal components within the framework
75-
func HTTPReverseProxy(options Options) (int, error) {
73+
func HTTPReverseProxy(options Options) (net.Listener, error) {
7674
log.Println("[DEBUG] starting new proxy with opts", options)
7775
port := options.ProxyPort
7876
var err error
@@ -86,20 +84,22 @@ func HTTPReverseProxy(options Options) (int, error) {
8684
proxy := createProxy(url, options.InternalRequestPathPrefix)
8785
proxy.Transport = customTransport{tlsConfig: options.CustomTLSConfig}
8886

89-
if port == 0 {
90-
port, err = utils.GetFreePort()
91-
if err != nil {
92-
log.Println("[ERROR] unable to start reverse proxy server:", err)
93-
return 0, err
94-
}
87+
ln, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
88+
if err != nil {
89+
return nil, err
9590
}
9691

9792
wrapper := chainHandlers(append(options.Middleware, loggingMiddleware)...)
9893

99-
log.Println("[DEBUG] starting reverse proxy on port", port)
100-
go http.ListenAndServe(fmt.Sprintf(":%d", port), wrapper(proxy)) // nolint:errcheck
94+
go func() {
95+
if err := http.Serve(ln, wrapper(proxy)); err != nil && !strings.HasSuffix(err.Error(), "use of closed network connection") {
96+
log.Printf("[ERROR] unable to start reverse proxy server: %v", err)
97+
}
98+
}()
99+
100+
log.Printf("[DEBUG] starting reverse proxy at %s", ln.Addr())
101101

102-
return port, nil
102+
return ln, nil
103103
}
104104

105105
// https://stackoverflow.com/questions/52986853/how-to-debug-httputil-newsinglehostreverseproxy

proxy/http_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package proxy
22

33
import (
4+
"net"
45
"net/http"
56
"net/http/httptest"
67
"testing"
@@ -72,19 +73,20 @@ func TestChainHandlers(t *testing.T) {
7273
func TestHTTPReverseProxy(t *testing.T) {
7374

7475
// Setup target to proxy
75-
port, err := HTTPReverseProxy(Options{
76+
ln, err := HTTPReverseProxy(Options{
7677
Middleware: []Middleware{
7778
DummyMiddleware("1"),
7879
},
7980
TargetScheme: "http",
8081
TargetAddress: "127.0.0.1:1234",
8182
})
82-
8383
if err != nil {
8484
t.Errorf("unexpected error %v", err)
8585
}
8686

87-
if port == 0 {
88-
t.Errorf("want non-zero port, got %v", port)
87+
defer ln.Close()
88+
89+
if tcpAddr, ok := ln.Addr().(*net.TCPAddr); !ok || tcpAddr.Port == 0 {
90+
t.Errorf("want non-zero port, got %v", ln.Addr())
8991
}
9092
}

0 commit comments

Comments
 (0)