lib/circuit.ex

defmodule Virgil.Circuit do
  @moduledoc """
  Circuit implementation
  """

  @type t :: %__MODULE__{
          name: atom(),
          error_threshold: integer(),
          reset_timeout: integer(),
          failures: integer(),
          state: :open | :closed | :half_open
        }

  defstruct name: nil,
            error_threshold: 0,
            reset_timeout: 0,
            failures: 0,
            state: nil

  @callback run(any()) :: {:ok, any()} | {:error, any()}

  defmacro __using__(opts) do
    quote do
      @behaviour Virgil.Circuit

      opts = unquote(opts)

      require Logger

      alias Virgil.{
        Config,
        Handler
      }

      alias __MODULE__, as: Circuit

      @error_threshold opts[:error_threshold] || Config.default_error_threshold()
      @reset_timeout opts[:reset_timeout] || Config.default_reset_timeout()

      @spec execute(any()) :: {:ok, any()} | {:error, any()}
      def execute(params) do
        {:ok, is_closed?} =
          circuit()
          |> circuit_manager().adapter().is_closed?()

        if is_closed? do
          Logger.info("[#{__MODULE__}] Running circuit")

          params
          |> run()
          |> handle_response()
        else
          Logger.info("[#{__MODULE__}] Circuit is not closed")
        end
      end

      @spec error_threshold :: integer()
      def error_threshold, do: @error_threshold

      @spec reset_timeout :: integer()
      def reset_timeout, do: @reset_timeout

      def circuit do
        %Virgil.Circuit{
          name: __MODULE__,
          failures: 0,
          state: :closed,
          error_threshold: @error_threshold,
          reset_timeout: @reset_timeout
        }
      end

      defp circuit_manager, do: Config.circuit_manager()

      defp handle_response({:ok, response}) do
        :telemetry.execute(
          [:virgil, :circuit, :success],
          %{circuit_response: response},
          %{
            circuit: __MODULE__
          }
        )
      end

      defp handle_response({:error, response}) do
        :telemetry.execute(
          [:virgil, :circuit, :failure],
          %{circuit_response: response},
          %{
            circuit: __MODULE__
          }
        )
      end
    end
  end
end