Skip to content

Commit f0c529c

Browse files
authored
fix(wait): no port to wait for (testcontainers#3158)
Fix a race condition when processing HostPortStrategy.WaitUntilReady which could result in it returning "no port to wait for" when no port is specified as is the case for ForExposedPort. This could happen if the call happens very quickly after the container is started and the port is not yet available. We now retry the internal port detection until found or the internal context is cancelled. Fixes: testcontainers#2945
1 parent a897bf5 commit f0c529c

File tree

2 files changed

+51
-12
lines changed

2 files changed

+51
-12
lines changed

wait/host_port.go

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,25 @@ func (hp *HostPortStrategy) Timeout() *time.Duration {
9595
return hp.timeout
9696
}
9797

98+
// detectInternalPort returns the lowest internal port that is currently bound.
99+
// If no internal port is found, it returns the zero nat.Port value which
100+
// can be checked against an empty string.
101+
func (hp *HostPortStrategy) detectInternalPort(ctx context.Context, target StrategyTarget) (nat.Port, error) {
102+
var internalPort nat.Port
103+
inspect, err := target.Inspect(ctx)
104+
if err != nil {
105+
return internalPort, fmt.Errorf("inspect: %w", err)
106+
}
107+
108+
for port := range inspect.NetworkSettings.Ports {
109+
if internalPort == "" || port.Int() < internalPort.Int() {
110+
internalPort = port
111+
}
112+
}
113+
114+
return internalPort, nil
115+
}
116+
98117
// WaitUntilReady implements Strategy.WaitUntilReady
99118
func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error {
100119
timeout := defaultStartupTimeout()
@@ -113,26 +132,34 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT
113132
waitInterval := hp.PollInterval
114133

115134
internalPort := hp.Port
135+
i := 0
116136
if internalPort == "" {
117-
inspect, err := target.Inspect(ctx)
137+
// Port is not specified, so we need to detect it.
138+
internalPort, err = hp.detectInternalPort(ctx, target)
118139
if err != nil {
119-
return err
140+
return fmt.Errorf("detect internal port: %w", err)
120141
}
121142

122-
for port := range inspect.NetworkSettings.Ports {
123-
if internalPort == "" || port.Int() < internalPort.Int() {
124-
internalPort = port
143+
for internalPort == "" {
144+
select {
145+
case <-ctx.Done():
146+
return fmt.Errorf("detect internal port: retries: %d, last err: %w, ctx err: %w", i, err, ctx.Err())
147+
case <-time.After(waitInterval):
148+
if err := checkTarget(ctx, target); err != nil {
149+
return fmt.Errorf("detect internal port: check target: retries: %d, last err: %w", i, err)
150+
}
151+
152+
internalPort, err = hp.detectInternalPort(ctx, target)
153+
if err != nil {
154+
return fmt.Errorf("detect internal port: %w", err)
155+
}
125156
}
126157
}
127158
}
128159

129-
if internalPort == "" {
130-
return errors.New("no port to wait for")
131-
}
132-
133160
var port nat.Port
134161
port, err = target.MappedPort(ctx, internalPort)
135-
i := 0
162+
i = 0
136163

137164
for port == "" {
138165
i++
@@ -142,7 +169,7 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT
142169
return fmt.Errorf("mapped port: retries: %d, port: %q, last err: %w, ctx err: %w", i, port, err, ctx.Err())
143170
case <-time.After(waitInterval):
144171
if err := checkTarget(ctx, target); err != nil {
145-
return fmt.Errorf("check target: retries: %d, port: %q, last err: %w", i, port, err)
172+
return fmt.Errorf("mapped port: check target: retries: %d, port: %q, last err: %w", i, port, err)
146173
}
147174
port, err = target.MappedPort(ctx, internalPort)
148175
if err != nil {

wait/host_port_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,24 @@ func TestWaitForExposedPortSucceeds(t *testing.T) {
7070
port, err := nat.NewPort("tcp", strconv.Itoa(rawPort))
7171
require.NoError(t, err)
7272

73-
var mappedPortCount, execCount int
73+
var inspectCount, mappedPortCount, execCount int
7474
target := &MockStrategyTarget{
7575
HostImpl: func(_ context.Context) (string, error) {
7676
return "localhost", nil
7777
},
7878
InspectImpl: func(_ context.Context) (*container.InspectResponse, error) {
79+
defer func() { inspectCount++ }()
80+
if inspectCount == 0 {
81+
// Simulate a container that hasn't bound any ports yet.
82+
return &container.InspectResponse{
83+
NetworkSettings: &container.NetworkSettings{
84+
NetworkSettingsBase: container.NetworkSettingsBase{
85+
Ports: nat.PortMap{},
86+
},
87+
},
88+
}, nil
89+
}
90+
7991
return &container.InspectResponse{
8092
NetworkSettings: &container.NetworkSettings{
8193
NetworkSettingsBase: container.NetworkSettingsBase{

0 commit comments

Comments
 (0)