defmodule TestcontainerEx.HttpWaitStrategy do
@moduledoc """
Considers the container as ready when a http request is successful.
"""
@timeout 60_000
@max_retries 10
@request_timeout 5_000
@doc false
def request_timeout, do: @request_timeout
@typedoc """
The HttpWaitStrategy struct
## Options
- `:endpoint` - The endpoint to request
- `:port` - The exposed port of your container
Verification Options:
- `:status_code` - Check if the request responds with the given status code
- `:match` - Run your custom matcher on the given response. A 1-arity function
taking a response as first parameter and must return a boolean
Request Options:
- `:protocol` - which protocol to use, defaults to `http`
- `:method` - the request method, one of [`:head`, `:get`, `:delete`, `:trace`, `:options`, `:post`, `:put`, `:patch`]
- `:timeout` - The timeout of the request (in milliseconds), defaults to `5000`
- `:headers` - Apply any headers to your request
"""
@type t() :: %__MODULE__{
endpoint: String.t(),
port: integer(),
protocol: String.t(),
method: :get | :post | :patch | :put | :delete | :head | :options | :trace,
timeout: integer(),
headers: [{binary(), binary()}],
status_code: integer(),
match: (map() -> boolean())
}
defstruct [
:endpoint,
:port,
# request options
protocol: "http",
method: :get,
headers: [],
timeout: @timeout,
max_retries: @max_retries,
# verification options
status_code: nil,
match: nil
]
# Public interface
@doc """
Creates a new HttpWaitStrategy to wait until a http requests succeeds.
"""
def new(endpoint, port, options \\ []) do
struct(%__MODULE__{endpoint: endpoint, port: port}, options)
end
# Private functions and implementations
defimpl TestcontainerEx.WaitStrategy do
alias TestcontainerEx.HttpWaitStrategy
@impl true
def wait_until_container_is_ready(wait_strategy, container, _conn) do
started_at = get_current_time_millis()
do_wait(wait_strategy, container, started_at)
end
defp do_wait(wait_strategy, container, started_at) do
client = build_request(wait_strategy, container)
raw_response =
Req.request(client,
url: wait_strategy.endpoint,
method: wait_strategy.method,
headers: wait_strategy.headers,
receive_timeout: 5_000
)
with {:ok, response} <- validate_response(raw_response),
:ok <- verify_status_code(wait_strategy, response),
:ok <- verify_match(wait_strategy, response) do
:ok
else
{:error, reason} ->
elapsed_ms = get_current_time_millis() - started_at
:telemetry.execute(
[:testcontainer_ex, :wait_strategy, :poll],
%{attempt: 0, elapsed_ms: elapsed_ms},
%{strategy: :http_wait, result: {:error, reason}}
)
if timed_out?(started_at, wait_strategy.timeout) do
{:error, TestcontainerEx.Error.wait_strategy_failed(:http_wait, elapsed_ms),
wait_strategy}
else
:timer.sleep(500)
do_wait(wait_strategy, container, started_at)
end
end
end
defp timed_out?(started_at, timeout),
do: get_current_time_millis() - started_at > timeout
defp get_current_time_millis, do: System.monotonic_time(:millisecond)
# Response evaluation
defp validate_response({:ok, response}), do: {:ok, response}
defp validate_response({:error, reason}), do: {:error, reason}
defp verify_status_code(wait_strategy, %{status: status_code})
when not is_nil(wait_strategy.status_code) and
status_code == wait_strategy.status_code,
do: :ok
defp verify_status_code(wait_strategy, response) when not is_nil(wait_strategy.status_code),
do:
{:error,
"Status Code does not match. Expected: #{wait_strategy.status_code} Received: #{response.status}"}
defp verify_status_code(wait_strategy, _) when is_nil(wait_strategy.status_code), do: :ok
defp verify_match(wait_strategy, response)
when not is_nil(wait_strategy.match) and is_function(wait_strategy.match) do
case wait_strategy.match.(response) do
true -> :ok
false -> {:error, "Matcher function failed"}
end
end
defp verify_match(_, _), do: :ok
# Request composition
defp build_request(wait_strategy, container) do
base_url = get_base_url(wait_strategy, container)
Req.new(
base_url: base_url,
receive_timeout: 5_000
)
end
defp get_base_url(
%HttpWaitStrategy{} = wait_strategy,
%TestcontainerEx.Container.Config{} = container
) do
port = TestcontainerEx.get_port(container, wait_strategy.port)
"#{wait_strategy.protocol}://#{TestcontainerEx.get_host(container)}:#{port}/"
end
end
end