# 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