Current section
Files
Jump to
Current section
Files
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.
"""
import Cachex.Spec
# types of accepted hooks
@hook_types [:service, :post, :pre]
#############
# Behaviour #
#############
@doc """
Returns the actions this hook is expected to listen on.
This will default to an empty list, to force the developer to opt into the
actions they receive notifications for. If all actions should be received,
you can use the `:all` atom to receive everything.
"""
@callback actions :: :all | [atom]
@doc """
Returns whether this hook is asynchronous or not.
"""
@callback async? :: boolean
@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. The `:quiet` type
is for hooks which don't listen to any broadcasts.
"""
@callback type :: :pre | :post | :service
@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(
action :: {action :: atom, args :: list},
result :: {status :: atom, value :: any},
state :: any
) ::
{:ok, any}
##################
# Implementation #
##################
@doc false
defmacro __using__(_) do
quote location: :keep, generated: true do
# force the Hook behaviours
@behaviour Cachex.Hook
# inherit server
use GenServer
use Cachex.Provision
@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: []
@doc false
def async?,
do: true
@doc false
def timeout,
do: nil
@doc false
def type do
case actions() do
[] -> :service
_ -> :post
end
end
# config overrides
defoverridable actions: 0,
async?: 0,
timeout: 0,
type: 0
#########################
# Notification Handlers #
#########################
@doc false
def handle_notify(event, result, state),
do: {:ok, state}
# listener override
defoverridable handle_notify: 3
##########################
# 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_notify, {event, result, callers}}, state) do
Process.put(:"$callers", callers)
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, new_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
##############
# Public API #
##############
@doc """
Concatenates all hooks in a cache.
"""
@spec concat(Cachex.t() | Cachex.Spec.hooks()) :: [Cachex.Spec.hook()]
def concat(hooks() = hooks) do
@hook_types
|> Enum.map(&for_type(hooks, &1))
|> Enum.concat()
end
def concat(cache(hooks: hooks)),
do: concat(hooks)
@doc """
Locates a hook module for a cache.
"""
@spec locate(Cachex.t() | Cachex.Spec.hooks(), atom(), atom()) ::
Cachex.Spec.hook() | nil
def locate(hooks, module, type \\ :all)
def locate(hooks() = hooks, module, type) do
hooks
|> concat()
|> Enum.find(&match_hook(&1, module, type))
end
def locate(cache(hooks: hooks), module, type),
do: locate(hooks, module, type)
@doc """
Retrieve all known types of hook.
"""
def types(),
do: @hook_types
###############
# Private API #
###############
# Hook lookup at runtime, instead of `hooks/2`.
for type <- @hook_types do
defp for_type(hooks() = hooks, unquote(type)),
do: hooks(hooks, unquote(type))
end
# Find a hook based on module name and module type.
defp match_hook(hook(module: module), module, :all),
do: true
defp match_hook(hook(module: module), module, type),
do: module.type() == type
defp match_hook(_hook, _module, _type),
do: false
end