defmodule ArcadiaCloud.Provisioning.Steps.WaitDropletActive do @moduledoc """ Polls a droplet (created by a prior CreateDroplet step) until its status is "active". Reads `droplet_id` from saga context. No compensation — waiting has no side effect to undo. If the saga rolls back, the prior CreateDroplet step's compensate destroys the droplet regardless of whether it ever reached active. """ @behaviour ArcadiaCloud.Provisioning.Step alias ArcadiaCloud.DigitalOcean.Client alias ArcadiaCloud.Provisioning.SagaState @poll_interval_ms 5_000 @poll_max_attempts 96 @impl true def name, do: "wait_droplet_active" @impl true def execute(state) do case SagaState.get_output(state, :droplet_id) do nil -> {:error, :no_droplet_id_in_context} droplet_id -> poll(state, droplet_id, 1) end end defp poll(_state, _droplet_id, attempt) when attempt > @poll_max_attempts do {:error, :droplet_active_timeout} end defp poll(state, droplet_id, attempt) do case Client.get_droplet(droplet_id) do {:ok, %{"status" => "active"} = droplet} -> public_ip = extract_public_ip(droplet) {:ok, SagaState.put_output(state, :droplet_public_ip, public_ip)} {:ok, %{"status" => status}} when status in ["new", "off"] -> Process.sleep(@poll_interval_ms) poll(state, droplet_id, attempt + 1) {:ok, %{"status" => other}} -> {:error, {:unexpected_droplet_status, other}} {:error, reason} -> {:error, reason} end end defp extract_public_ip(droplet) do droplet |> get_in(["networks", "v4"]) |> List.wrap() |> Enum.find(%{}, &(&1["type"] == "public")) |> Map.get("ip_address") end end