# SPDX-License-Identifier: MIT # Original by: Marco Dallagiacoma @ 2023 in https://github.com/dallagi/excontainers # Modified by: Jarl André Hübenthal @ 2023 defmodule Testcontainers.CommandWaitStrategy do @moduledoc """ Considers a container ready as soon as a command runs successfully inside it. """ require Logger @retry_delay 200 defstruct [:command, :timeout, retry_delay: @retry_delay] # Public interface @doc """ Creates a new CommandWaitStrategy. This strategy waits 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 # Private functions and implementations defimpl Testcontainers.WaitStrategy do alias Testcontainers.Docker @impl true def wait_until_container_is_ready(wait_strategy, container, conn) do started_at = get_current_time_millis() perform_recursive_wait(wait_strategy, container.container_id, conn, started_at) end # Main loop for waiting strategy defp perform_recursive_wait(wait_strategy, container_id, conn, started_at) do case execute_command_and_wait(wait_strategy, container_id, conn) do {:ok, 0} -> :ok {:ok, exit_code} -> handle_non_zero_exit(wait_strategy, container_id, exit_code, conn, started_at) error -> handle_execution_error(error, wait_strategy) end end defp execute_command_and_wait( %{command: command, timeout: timeout, retry_delay: retry_delay}, container_id, conn ) do with {:ok, exec_id} <- Docker.Api.start_exec(container_id, command, conn) do started_at = get_current_time_millis() wait_for_command_completion(exec_id, timeout, started_at, retry_delay, conn) end end defp handle_non_zero_exit(wait_strategy, container_id, exit_code, conn, started_at) do if timed_out?(started_at, wait_strategy.timeout) do {:error, strategy_timed_out(wait_strategy.timeout, started_at), wait_strategy} else log_retry_message(container_id, exit_code, wait_strategy.retry_delay) :timer.sleep(wait_strategy.retry_delay) perform_recursive_wait(wait_strategy, container_id, conn, started_at) end end defp handle_execution_error({:error, reason}, wait_strategy), do: {:error, reason, wait_strategy} defp wait_for_command_completion(exec_id, timeout, started_at, retry_delay, conn) do case Docker.Api.inspect_exec(exec_id, conn) do {:ok, %{running: true}} -> wait_unless_timeout(exec_id, timeout, started_at, retry_delay, conn) {:ok, exec_status} -> {:ok, exec_status.exit_code} end end defp wait_unless_timeout(exec_id, timeout, started_at, retry_delay, conn) do if timed_out?(started_at, timeout) do {:error, strategy_timed_out(timeout, started_at)} else :timer.sleep(retry_delay) wait_for_command_completion(exec_id, timeout, started_at, retry_delay, conn) end end defp get_current_time_millis, do: System.monotonic_time(:millisecond) defp timed_out?(started_at, timeout), do: get_current_time_millis() - started_at > timeout defp strategy_timed_out(timeout, started_at) do {:command_wait_strategy, :timeout, timeout, elapsed_time: get_current_time_millis() - started_at} end defp log_retry_message(container_id, exit_code, delay) do Logger.debug( "Command execution in container #{container_id} failed with exit_code #{exit_code}, retrying in #{delay}ms." ) end end end