lib/graphql/retry.ex

defmodule BridgeEx.Graphql.Retry do
  @moduledoc """
  Utils for handling retrying of functions.
  """

  @spec retry(
          String.t(),
          (any() -> {:error, String.t()} | {:ok, any()}),
          Keyword.t()
        ) :: {:ok, any()} | {:error, any()}
  def retry(arg, fun, retry_options) do
    # These values should always be present
    delay = Keyword.get(retry_options, :delay)
    policy = Keyword.get(retry_options, :policy)
    timing = Keyword.get(retry_options, :timing)
    max_retries = Keyword.get(retry_options, :max_retries)

    do_retry(arg, fun, policy, delay, timing, max_retries + 1)
  end

  defp do_retry(arg, fun, _retry_policy, _delay, _timing, 1), do: fun.(arg)

  defp do_retry(_arg, _fun, _retry_policy, _delay, _timing, n) when n <= 0,
    do: {:error, :invalid_retry_value}

  defp do_retry(arg, fun, policy, delay, timing, retries) do
    case fun.(arg) do
      {:error, reason} ->
        if policy.(reason) do
          Process.sleep(calculate_new_delay(delay, timing))

          do_retry(arg, fun, policy, delay, timing, retries - 1)
        else
          {:error, reason}
        end

      val ->
        val
    end
  end

  defp calculate_new_delay(delay, :constant), do: delay

  defp calculate_new_delay(delay, :exponential) do
    delay = delay * 2
    max_delta = round(delay * 0.1)
    shift = random_uniform(2 * max_delta) - max_delta

    case delay + shift do
      n when n <= 0 -> 0
      n -> n
    end
  end

  defp random_uniform(n) when n <= 0, do: 0

  defp random_uniform(n), do: :rand.uniform(n)
end