# SPDX-License-Identifier: MIT
defmodule Testcontainers.PortWaitStrategy do
@moduledoc """
Considers the container as ready when it successfully accepts connections on the specified port.
"""
require Logger
@retry_delay 200
defstruct [:ip, :port, :timeout, retry_delay: @retry_delay]
# Public interface
@doc """
Creates a new PortWaitStrategy to wait until a specified port is open and accepting connections.
"""
def new(ip, port, timeout \\ 5000, retry_delay \\ @retry_delay) do
%__MODULE__{ip: ip, port: port, timeout: timeout, retry_delay: retry_delay}
end
# Private functions and implementations
defimpl Testcontainers.WaitStrategy do
alias Testcontainers.Container
@impl true
def wait_until_container_is_ready(wait_strategy, container, _conn) do
with host_port when not is_nil(host_port) <-
Container.mapped_port(container, wait_strategy.port),
do: perform_port_check(wait_strategy, host_port)
end
defp perform_port_check(wait_strategy, host_port) do
started_at = current_time_millis()
case wait_for_open_port(wait_strategy, host_port, started_at) do
:port_is_open ->
:ok
{:error, reason} ->
{:error, reason, wait_strategy}
end
end
defp wait_for_open_port(wait_strategy, host_port, start_time) do
if reached_timeout?(wait_strategy.timeout, start_time) do
{:error, strategy_timed_out(wait_strategy.timeout, start_time)}
else
check_port_status(wait_strategy, host_port, start_time)
end
end
defp check_port_status(wait_strategy, host_port, start_time) do
if port_open?(wait_strategy.ip, host_port) do
:port_is_open
else
log_retry_message(wait_strategy, host_port)
:timer.sleep(wait_strategy.retry_delay)
wait_for_open_port(wait_strategy, host_port, start_time)
end
end
defp port_open?(ip, port, timeout \\ 1000) do
case :gen_tcp.connect(to_charlist(ip), port, [:binary, active: false], timeout) do
{:ok, socket} ->
:gen_tcp.close(socket)
true
{:error, _} ->
false
end
end
defp current_time_millis(), do: System.monotonic_time(:millisecond)
defp reached_timeout?(timeout, start_time), do: current_time_millis() - start_time > timeout
defp strategy_timed_out(timeout, start_time) do
{:port_wait_strategy, :timeout, timeout, elapsed_time: current_time_millis() - start_time}
end
defp log_retry_message(wait_strategy, host_port) do
Logger.debug(
"Port #{wait_strategy.port} (host port #{host_port}) not open on IP #{wait_strategy.ip}, retrying in #{wait_strategy.retry_delay}ms."
)
end
end
end