lib/flamel/retryable.ex

defmodule Flamel.Retryable do
  @moduledoc """
  Documentation for `Flamel.Retryable`.
  """

  require Logger

  @typedoc "The number of milliseconds the next retry should happen in"
  @type interval() :: timeout()

  @doc """
  Calculate a retry interval

  """
  def calc(strategy) do
    Flamel.Retryable.Strategy.calc(strategy)
  end

  @doc """
  Create a Linear Retry Strategy

  ## Examples

      iex> Flamel.Retryable.linear(max_attempts: 10)
      %Flamel.Retryable.Linear{max_attempts: 10}
  """
  @spec linear(keyword()) :: Flamel.Retryable.Linear.t()
  def linear(args \\ []) do
    struct!(Flamel.Retryable.Linear, args)
  end

  @doc """
  Create an Exponential Retry Strategy

  ## Examples

      iex> Flamel.Retryable.exponential(multiplier: 10)
      %Flamel.Retryable.Exponential{multiplier: 10}
  """
  @spec exponential(keyword()) :: Flamel.Retryable.Exponential.t()
  def exponential(args \\ []) do
    struct!(Flamel.Retryable.Exponential, args)
  end

  @doc """
  Create an HTTP Retry Strategy.

  This strategy requires setting the HTTP status code so that
  it can adjust the retry interval based on the status.

  ## Examples

      iex> Flamel.Retryable.http(max_attempts: 10)
      %Flamel.Retryable.Http{max_attempts: 10}
  """
  @spec http(keyword()) :: Flamel.Retryable.Http.t()
  def http(args \\ []) do
    struct!(Flamel.Retryable.Http, args)
  end

  @doc """
  Executes a function based on the `Flamel.Retryable.Strategy`. The function is
  expected to return either a {:ok, result, strategy} or {:error, reason, strategy} tuple. If an error tuple is returned or an exception occurs the function will be retryed
  based on the strategy configuration.


  ## Examples

      iex> strategy = Flamel.Retryable.linear()
      iex> Flamel.Retryable.try(strategy, fn strategy -> {:ok, "success", strategy} end)
      iex> {:ok, "success", strategy}
  """

  @spec try(%{required(:halt?) => boolean()}, function()) :: term()
  def try(%{halt?: true} = strategy, _func) do
    {:error, nil, strategy}
  end

  def try(strategy, func) do
    strategy.interval
    |> Flamel.Task.delay(
      # turtles all the way down
      fn ->
        Flamel.try_and_return(fn ->
          func.(strategy)
        end)
      end
    )
    |> case do
      {:ok, result, strategy} ->
        {:ok, result, strategy}

      {:error, %{halt?: _, assigns: _} = strategy} = error ->
        Logger.error("#{__MODULE__}.execute error=#{inspect(error)}")
        try(Flamel.Retryable.calc(strategy), func)

      error ->
        Logger.error("#{__MODULE__}.execute error=#{inspect(error)}")
        try(Flamel.Retryable.calc(strategy), func)
    end
  end
end