# SPDX-License-Identifier: MIT
# Original by: Marco Dallagiacoma @ 2023 in https://github.com/dallagi/excontainers
# Modified by: Jarl André Hübenthal @ 2023
defmodule Testcontainers.WaitStrategy.CommandWaitStrategy do
@moduledoc """
Considers container as ready as soon as a command runs successfully inside the container.
"""
@retry_delay 200
defstruct [:command, :timeout, retry_delay: @retry_delay]
@doc """
Creates a new CommandWaitStrategy to wait until the given command executes successfully inside the container.
"""
def new(command, timeout \\ 5000, retry_delay \\ @retry_delay),
do: %__MODULE__{command: command, timeout: timeout, retry_delay: retry_delay}
end
defimpl Testcontainers.WaitStrategy,
for: Testcontainers.WaitStrategy.CommandWaitStrategy do
alias Testcontainers.Docker.Exec
require Logger
def wait_until_container_is_ready(wait_strategy, id_or_name) do
# Capture the start time of the process
started_at = current_time_millis()
# Call the recursive function
recursive_wait(wait_strategy, id_or_name, started_at)
end
# Recursive function with breaking conditions
defp recursive_wait(wait_strategy, id_or_name, started_at) do
case exec_and_wait(
id_or_name,
wait_strategy.command,
wait_strategy.timeout,
wait_strategy.retry_delay
) do
{:ok, 0} ->
:ok
{:ok, other_exit_code} ->
if out_of_time(started_at, wait_strategy.timeout) do
{:error, strategy_timed_out(wait_strategy.timeout, started_at), wait_strategy}
else
delay = max(0, wait_strategy.retry_delay)
Logger.debug(
"Command execution in container #{id_or_name} failed with exit_code #{other_exit_code}, retrying in #{delay}ms."
)
:timer.sleep(delay)
recursive_wait(wait_strategy, id_or_name, started_at)
end
{:error, reason} ->
{:error, reason, wait_strategy}
end
end
def exec_and_wait(container_id, command, timeout, retry_delay) do
{:ok, exec_id} = exec(container_id, command)
started_at = current_time_millis()
case wait_for_exec_result(exec_id, timeout, started_at, retry_delay) do
{:ok, exec_info} -> {:ok, exec_info.exit_code}
{:error, error} -> {:error, error}
end
end
def exec(container_id, command) do
with {:ok, exec_id} <- Exec.create(container_id, command),
:ok <- Exec.start(exec_id) do
{:ok, exec_id}
end
end
defp wait_for_exec_result(
exec_id,
timeout_ms,
started_at,
retry_delay
) do
case Exec.inspect(exec_id) do
{:ok, %{running: true}} ->
do_wait_unless_timed_out(exec_id, timeout_ms, started_at, retry_delay)
{:ok, finished_exec_status} ->
{:ok, finished_exec_status}
end
end
defp do_wait_unless_timed_out(exec_id, timeout, started_at, retry_delay) do
if out_of_time(started_at, timeout) do
{:error, strategy_timed_out(timeout, started_at)}
else
delay = max(0, retry_delay)
:timer.sleep(delay)
wait_for_exec_result(exec_id, timeout, started_at, retry_delay)
end
end
defp current_time_millis, do: System.monotonic_time(:millisecond)
defp out_of_time(started_at, timeout_ms), do: current_time_millis() - started_at > timeout_ms
defp strategy_timed_out(timeout, started_at) when is_number(timeout) and is_number(started_at),
do:
{:command_wait_strategy, :timeout, timeout,
elapsed_time: current_time_millis() - started_at}
end