Skip to content

Commit

Permalink
Allow warmers to trigger hooks (#322)
Browse files Browse the repository at this point in the history
  • Loading branch information
camilleryr committed Mar 22, 2024
1 parent 620ca2c commit 5754f3e
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 27 deletions.
26 changes: 24 additions & 2 deletions lib/cachex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,10 @@ defmodule Cachex do
{:ok, cache} <- setup_env(name, options),
{:ok, pid} = Supervisor.start_link(__MODULE__, cache, name: name),
{:ok, link} = Informant.link(cache),
^link <- Overseer.update(name, link),
do: {:ok, pid}
^link <- Overseer.update(name, link) do
_ = run_warmers(cache)
{:ok, pid}
end
end

def start_link(name) when not is_atom(name),
Expand Down Expand Up @@ -1461,6 +1463,26 @@ defmodule Cachex do
end
end

# Run warmers on cache startup
#
# This will find the cache's warmer pids by getting the children from the
# Incubator server, matching those pids to the warmer by module, and then
# executing the warmer based on the warmer's `async` attribute
defp run_warmers(cache(warmers: warmers) = cache) do
parent = Services.locate(cache, Services.Incubator)
children = if parent, do: Supervisor.which_children(parent), else: []
warmer_map = Map.new(children, fn {mod, pid, _, _} -> {mod, pid} end)

for warmer(module: module, async: async) <- warmers,
pid = warmer_map[module] do
if async do
send(pid, :cachex_warmer)
else
GenServer.call(pid, :blocking_cachex_warmer, :infinity)
end
end
end

# Unwraps a command result into an unsafe form.
#
# This is used alongside the Unsafe library to generate shorthand
Expand Down
2 changes: 1 addition & 1 deletion lib/cachex/actions/warm.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ defmodule Cachex.Actions.Warm do
def execute(cache() = cache, options) do
mods = Keyword.get(options, :modules, nil)
parent = Services.locate(cache, Services.Incubator)
children = Supervisor.which_children(parent)
children = if parent, do: Supervisor.which_children(parent), else: []

handlers =
for {mod, pid, _, _} <- children, mods == nil or mod in mods do
Expand Down
51 changes: 27 additions & 24 deletions lib/cachex/warmer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,16 @@ defmodule Cachex.Warmer do
#
# Initialization will trigger an initial cache warming, and store
# the provided state for later to provide during further warming.
def init({cache, warmer(async: async, state: state)}) do
if async do
send(self(), :cachex_warmer)
else
handle_info(:cachex_warmer, {cache, state})
end

{:ok, {cache, state}}
def init({cache(name: name), warmer(state: state)}) do
{:ok, {name, state, nil}}
end

def handle_call(:blocking_cachex_warmer, _from, process_state) do
{:reply, :ok, execute_warmer(process_state)}
end

def handle_info(:cachex_warmer, process_state) do
{:noreply, execute_warmer(process_state)}
end

@doc false
Expand All @@ -90,27 +92,28 @@ defmodule Cachex.Warmer do
# cache via `Cachex.put_many/3` if returns in a Tuple tagged with the
# `:ok` atom. If `:ignore` is returned, nothing happens aside from
# scheduling the next execution of the warming to occur on interval.
def handle_info(:cachex_warmer, {cache, state} = process_state) do
defp execute_warmer({name, state, timer}) do
if timer, do: Process.cancel_timer(timer)

# execute, passing state
case execute(state) do
# no changes
:ignore ->
:ignore
Cachex.execute(name, fn cache ->
case execute(state) do
# no changes
:ignore ->
:ignore

# set pairs without options
{:ok, pairs} ->
Cachex.put_many(cache, pairs)
# set pairs without options
{:ok, pairs} ->
Cachex.put_many(cache, pairs)

# set pairs with options
{:ok, pairs, options} ->
Cachex.put_many(cache, pairs, options)
end
# set pairs with options
{:ok, pairs, options} ->
Cachex.put_many(cache, pairs, options)
end
end)

# trigger the warming to happen again after the interval
:erlang.send_after(interval(), self(), :cachex_warmer)

# repeat with the state
{:noreply, process_state}
{name, state, :erlang.send_after(interval(), self(), :cachex_warmer)}
end
end
end
Expand Down
30 changes: 30 additions & 0 deletions test/cachex/warmer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,34 @@ defmodule Cachex.WarmerTest do
# check that the key was warmed with state
assert Cachex.get!(cache, "state") == state
end

test "warmer triggers hook for sync and async warmers" do
expected_values = [{1, 1}]
expected_opts = [ttl: 1_000]

warmer_state = %{
expected_values: expected_values,
expected_opts: expected_opts
}

hook = ForwardHook.create()

# create a test warmer to pass to the cache
Helper.create_warmer(:test_warmer, 500, fn state ->
{:ok, state.expected_values, state.expected_opts}
end)

for async <- [true, false] do
# create a cache instance with a warmer and hook
Helper.create_cache(
warmers: [
warmer(module: :test_warmer, state: warmer_state, async: async)
],
hooks: [hook]
)

assert_receive {{:put_many, [^expected_values, ^expected_opts]},
{:ok, true}}
end
end
end

0 comments on commit 5754f3e

Please sign in to comment.