Foundation.Poller runs a function repeatedly until it completes, fails, or times out. It is designed for long-running workflows where you need to check status periodically.

Basic Polling

alias Foundation.Poller
alias Foundation.Backoff

{:ok, result} = Poller.run(
  fn attempt ->
    case check_job_status(job_id) do
      {:ok, :completed, data} -> {:ok, data}
      {:ok, :pending}         -> {:retry, :pending}
      {:ok, :failed, reason}  -> {:error, reason}
    end
  end,
  backoff: Backoff.Policy.new(strategy: :exponential, base_ms: 500, max_ms: 10_000),
  timeout_ms: 60_000,
  max_attempts: 20
)

Step Function Return Values

The step function controls polling flow through its return value:

ReturnBehavior
{:ok, value}Polling stops, returns {:ok, value}
{:error, reason}Polling stops, returns {:error, reason}
{:retry, reason}Polls again after backoff delay
{:retry, reason, delay_ms}Polls again after the given delay (overrides backoff)
:retryPolls again after backoff delay
Any other valueTreated as {:ok, value}

Backoff Options

# Backoff policy struct
Poller.run(step_fun, backoff: Backoff.Policy.new(strategy: :linear, base_ms: 1_000))

# Shorthand tuple
Poller.run(step_fun, backoff: {:exponential, 500, 10_000})

# Custom function (receives attempt number)
Poller.run(step_fun, backoff: fn attempt -> min(1_000 * attempt, 30_000) end)

# No backoff (immediate retries)
Poller.run(step_fun, backoff: :none)

Timeout and Attempt Limits

Poller.run(step_fun,
  timeout_ms: 120_000,     # stop after 2 minutes
  max_attempts: 50          # stop after 50 attempts
)

Both default to :infinity if not specified.

Async Polling

Run polling in a Task for concurrent workflows:

task = Poller.async(fn attempt ->
  case check_status(job_id) do
    :done -> {:ok, :completed}
    :pending -> :retry
  end
end, backoff: {:exponential, 1_000, 30_000}, timeout_ms: 300_000)

# Do other work...

{:ok, result} = Poller.await(task)

Poller.await/2 converts task exits into error tuples, so it will not raise.

Exception Handling

By default, exceptions are caught and returned as {:error, exception}. You can provide a custom handler:

Poller.run(step_fun,
  exception_handler: fn exception ->
    Logger.error("Polling failed: #{Exception.message(exception)}")
    {:error, exception}
  end
)