lib/cachex/hook.ex

defmodule Cachex.Hook do
  @moduledoc """
  Module controlling hook behaviour definitions.

  This module defines the hook implementations for Cachex, allowing the user to
  add hooks into the command execution. This means that users can build plugin
  style listeners in order to do things like logging. Hooks can be registered
  to execute either before or after the Cachex command, and can be blocking as
  needed.
  """

  #############
  # Behaviour #
  #############

  @doc """
  Returns the actions this hook is expected to listen on.

  This will default to the atom `:all`, which signals that all actions should
  be reported to the hook. If not this atom, an enumerable of atoms should be
  returned.
  """
  @callback actions :: :all | [atom]

  @doc """
  Returns whether this hook is asynchronous or not.
  """
  @callback async? :: boolean

  @doc """
  Returns an enumerable of provisions this hook requires.

  The current provisions available to a hook are:

    * `cache` - a cache instance used to make cache calls from inside a hook
      with zero overhead.

  This should always return an enumerable of atoms; in the case of no required
  provisions an empty enumerable should be returned.
  """
  @callback provisions :: [atom]

  @doc """
  Returns the timeout for all calls to this hook.

  This will be applied to hooks regardless of whether they're synchronous or
  not; a behaviour change which shipped in v3.0 initially.
  """
  @callback timeout :: nil | integer

  @doc """
  Returns the type of this hook.

  This should return `:post` to fire after a cache action has occurred, and
  return `:pre` if it should fire before the action occurs.
  """
  @callback type :: :pre | :post

  @doc """
  Handles a cache notification.

  The first argument is the action being taken along with arguments, with the
  second argument being the results of the action (this can be nil for hooks)
  which fire before the action is executed.
  """
  @callback handle_notify(tuple, tuple, any) :: {:ok, any}

  @doc """
  Handles a provisioning call.

  The provided argument will be a Tuple dictating the type of value being
  provisioned along with the value itself. This can be used to listen on
  states required for hook executions (such as cache records).
  """
  @callback handle_provision({atom, any}, any) :: {:ok, any}

  ##################
  # Implementation #
  ##################

  @doc false
  defmacro __using__(_) do
    quote location: :keep do
      # force the Hook behaviours
      @behaviour Cachex.Hook

      # inherit server
      use GenServer

      @doc false
      def init(args),
        do: {:ok, args}

      @doc false
      def child_spec(args),
        do: super(args)

      # allow overriding of init
      defoverridable init: 1

      #################
      # Configuration #
      #################

      @doc false
      def actions,
        do: :all

      @doc false
      def async?,
        do: true

      @doc false
      def provisions,
        do: []

      @doc false
      def timeout,
        do: nil

      @doc false
      def type,
        do: :post

      # config overrides
      defoverridable actions: 0,
                     async?: 0,
                     provisions: 0,
                     timeout: 0,
                     type: 0

      #########################
      # Notification Handlers #
      #########################

      @doc false
      def handle_notify(event, result, state),
        do: {:ok, state}

      @doc false
      def handle_provision(provisions, state),
        do: {:ok, state}

      # listener override
      defoverridable handle_notify: 3,
                     handle_provision: 2

      ##########################
      # Private Implementation #
      ##########################

      @doc false
      def handle_info({:cachex_reset, args}, state) do
        {:ok, new_state} = init(args)
        {:noreply, new_state}
      end

      @doc false
      def handle_info({:cachex_provision, provisions}, state) do
        {:ok, new_state} = handle_provision(provisions, state)
        {:noreply, new_state}
      end

      @doc false
      def handle_info({:cachex_notify, {event, result}}, state) do
        case timeout() do
          nil ->
            {:ok, new_state} = handle_notify(event, result, state)
            {:noreply, new_state}

          val ->
            task =
              Task.async(fn ->
                handle_notify(event, result, state)
              end)

            case Task.yield(task, val) || Task.shutdown(task) do
              {:ok, {:ok, new_state}} -> {:noreply, state}
              nil -> {:noreply, state}
            end
        end
      end

      @doc false
      def handle_call({:cachex_notify, _message} = message, _ctx, state) do
        {:noreply, new_state} = handle_info(message, state)
        {:reply, :ok, new_state}
      end
    end
  end
end