defmodule Xandra.RetryStrategy do @moduledoc """ A behaviour that handles how to retry failed queries. This behaviour makes it possible to customize the strategy that Xandra uses to retry failed queries. By default, Xandra does not retry failed queries, and does not provide any default retry strategy since retrying queries based on the failure reason is very tied to application logic. A module that implements the `Xandra.RetryStrategy` behaviour can be passed to several functions in the `Xandra` module: look at the documentation for `Xandra` for more information. ## Usage When a query fails and a retry strategy module was passed as an option, Xandra will: 1. invoke the `c:new/1` callback with the options passed to the failing function to initialize the given retry strategy 1. ask the retry strategy whether to retry or error out (`c:retry/3`) until either the query succeeds or `c:retry/3` says to error out The `c:new/1` and `c:retry/3` callbacks will be invoked in the same process that executed the original query. If `c:retry/3` says to retry a query, such query will be retried on a different Xandra connection than the one the query was first executed through. For more information, see the documentation for `c:retry/3`. ## Examples This is an example of a retry strategy that retries a fixed number of times before failing. It reads the allowed number of retries from the options. defmodule MyApp.CounterRetryStrategy do @behaviour Xandra.RetryStrategy def new(options) do Keyword.fetch!(options, :retry_count) end def retry(_error, _options, _retries_left = 0) do :error end def retry(_error, options, retries_left) do {:retry, options, retries_left - 1} end end Another interesting example could be a retry strategy based on downgrading consistency: for example, we could execute all queries with a high consistency (such as `:all`) at first, and in case of failure, try again with a lower consistency (such as `:quorum`), finally giving up if that fails as well. defmodule MyApp.DowngradingConsistencyRetryStrategy do @behaviour Xandra.RetryStrategy def new(_options) do :no_state end def retry(_error, options, :no_state) do case Keyword.fetch(options, :consistency) do # No consistency was specified, so we don't bother to retry. :error -> :error {:ok, :all} -> {:retry, Keyword.put(options, :consistency, :quorum), :no_state} {:ok, _other} -> :error end end end """ @type state :: term @doc """ Initializes the state of a retry strategy based on the given `options`. """ @callback new(options :: keyword) :: state @doc """ Determines whether to retry the failed query or return the error. The first argument is the error that caused the query to fail: for example, it could be a `Xandra.Error` struct with reason `:read_timeout`. This can be used to determine the retry strategy based on the failure reason. The second argument is the options given to the function that failed while executing the query. The third argument is the retry strategy state returned either by `c:new/1` (if this was the first attempt to retry) or by subsequent calls to `c:retry/3`. If `:error` is returned, the function that was trying to execute the query will return the error to the caller instead of retrying. If `{:retry, new_options, new_state}` is returned, the function that was trying to execute the query will be invoked again with the same query and `new_options` as its options. `new_state` will be used if the query fails again: in that case, `c:retry/3` will be invoked again with `new_state` as its third argument. This process will continue until either the query is executed successfully or this callback returns `:error`. Note that when `{:retry, new_options, new_state}` is returned, the query will be executed again *on a different Xandra connection*. This behaviour is particularly useful with pooled connections and especially when using `Xandra.Cluster` as the pool, since it will mean that there's a chance the retried query will be executed on a different node altogether. """ @callback retry(error :: term, options :: keyword, state) :: :error | {:retry, new_options :: keyword, new_state :: state} @doc false @spec run_with_retrying(keyword, (() -> result)) :: result when result: var def run_with_retrying(options, fun) do case Keyword.pop(options, :retry_strategy) do {nil, _options} -> fun.() {retry_strategy, options} -> run_with_retrying(options, retry_strategy, fun) end end defp run_with_retrying(options, retry_strategy, fun) do with {:error, reason} <- fun.() do {retry_state, options} = Keyword.pop_lazy(options, :retrying_state, fn -> retry_strategy.new(options) end) case retry_strategy.retry(reason, options, retry_state) do :error -> {:error, reason} {:retry, new_options, new_retry_state} -> new_options = Keyword.put(new_options, :retrying_state, new_retry_state) run_with_retrying(new_options, retry_strategy, fun) other -> raise ArgumentError, "invalid return value #{inspect(other)} from " <> "retry strategy #{inspect(retry_strategy)} " <> "with state #{inspect(retry_state)}" end end end end