Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstract out provisioning across all component types #340

Merged
merged 4 commits into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 3 additions & 8 deletions lib/cachex/actions/warm.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ defmodule Cachex.Actions.Warm do
# The only reason to call this command is the case in which you already
# know the backing state of your cache has been updated and you need to
# immediately refresh your warmed entries.
alias Cachex.Services

# we need our imports
import Cachex.Spec

##############
Expand All @@ -25,14 +22,12 @@ defmodule Cachex.Actions.Warm do
set of warmer modules. The list of modules which had a warming triggered will
be returned in the result of this call.
"""
def execute(cache() = cache, options) do
def execute(cache(warmers: warmers), options) do
mods = Keyword.get(options, :modules, nil)
parent = Services.locate(cache, Services.Incubator)
children = if parent, do: Supervisor.which_children(parent), else: []

handlers =
for {mod, pid, _, _} <- children, mods == nil or mod in mods do
send(pid, :cachex_warmer) && mod
for warmer(module: mod) <- warmers, mods == nil or mod in mods do
send(mod, :cachex_warmer) && mod
end

{:ok, handlers}
Expand Down
43 changes: 3 additions & 40 deletions lib/cachex/hook.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,6 @@ defmodule Cachex.Hook do
"""
@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.

Expand All @@ -65,27 +52,19 @@ defmodule Cachex.Hook do
"""
@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
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),
Expand All @@ -110,10 +89,6 @@ defmodule Cachex.Hook do
def async?,
do: true

@doc false
def provisions,
do: []

@doc false
def timeout,
do: nil
Expand All @@ -125,7 +100,6 @@ defmodule Cachex.Hook do
# config overrides
defoverridable actions: 0,
async?: 0,
provisions: 0,
timeout: 0,
type: 0

Expand All @@ -137,13 +111,8 @@ defmodule Cachex.Hook do
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
defoverridable handle_notify: 3

##########################
# Private Implementation #
Expand All @@ -155,12 +124,6 @@ defmodule Cachex.Hook do
{: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
Expand Down
2 changes: 1 addition & 1 deletion lib/cachex/policy/lrw/scheduled.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ defmodule Cachex.Policy.LRW.Scheduled do
@doc false
# Receives a provisioned cache instance.
#
# The provided cache is then stored in the cache and used for cache calls going
# The provided cache is then stored in the state and used for cache calls going
# forwards, in order to skip the lookups inside the cache overseer for performance.
def handle_provision({:cache, cache}, {_cache, limit}),
do: {:ok, {cache, limit}}
Expand Down
79 changes: 79 additions & 0 deletions lib/cachex/provision.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
defmodule Cachex.Provision do
@moduledoc """
Module controlling provisioning behaviour definitions.

This module defines the provisioning implementation for Cachex, allowing
components such as hooks and warmers to tap into state changes in the cache
table. By implementing `handle_provision/2` these components can be provided
with new versions of state as they're created.
"""

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

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

The current provisions available are:

* `cache` - a cache instance used to make cache calls with lower 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 """
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, generated: true do
# use the provision behaviour
@behaviour Cachex.Provision

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

@doc false
def provisions,
do: []

# config overrides
defoverridable provisions: 0

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

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

# listener override
defoverridable handle_provision: 2

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

@doc false
def handle_info({:cachex_provision, provision}, state) do
{:ok, new_state} = handle_provision(provision, state)
{:noreply, new_state}
end
end
end
end
2 changes: 1 addition & 1 deletion lib/cachex/services/incubator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@ defmodule Cachex.Services.Incubator do
defp spec(warmer(module: module) = warmer, cache),
do: %{
id: module,
start: {GenServer, :start_link, [module, {cache, warmer}]}
start: {GenServer, :start_link, [module, {cache, warmer}, [name: module]]}
}
end
17 changes: 2 additions & 15 deletions lib/cachex/services/overseer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ defmodule Cachex.Services.Overseer do

# add service aliases
alias Services.Overseer
alias Services.Steward

# constants for manager/table names
@manager_name :cachex_overseer_manager
Expand Down Expand Up @@ -135,13 +136,7 @@ defmodule Cachex.Services.Overseer do

register(name, nstate)

with hooks(pre: pre_hooks, post: post_hooks) <- cache(nstate, :hooks) do
pre_hooks
|> Enum.concat(post_hooks)
|> Enum.filter(&requires_state?/1)
|> Enum.map(&hook(&1, :name))
|> Enum.each(&send(&1, {:cachex_provision, {:cache, nstate}}))
end
Steward.provide(nstate, {:cache, nstate})

nstate
end)
Expand Down Expand Up @@ -180,12 +175,4 @@ defmodule Cachex.Services.Overseer do
end
end
end

###############
# Private API #
###############

# Verifies if a hook has a cache provisioned.
defp requires_state?(hook(module: module)),
do: :cache in module.provisions()
end
58 changes: 58 additions & 0 deletions lib/cachex/services/steward.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
defmodule Cachex.Services.Steward do
@moduledoc """
Service module overseeing cache provisions.

This module controls state provision to Cachex components, such as hooks
and warmers. In previous versions of Cachex provisions were handled under
the `Cachex.Hook` behaviour, but the introduction of warmers meant that it
should be handled in a separate location.

This service module will handle the provision of state to relevant components
attached to a cache, without the caller having to think about it.
"""
import Cachex.Spec

# recognised
@provisions [
:cache
]

##############
# Public API #
##############

@doc """
Provides an state pair to relevant components.

This will send updated state to all interest components, but does not
wait for a response before returning. As provisions are handled in a
base implementation, we can be sure of safe implementation here.
"""
@spec provide(Cachex.Spec.cache(), {atom, any}) :: :ok
def provide(cache() = cache, {key, _} = provision) when key in @provisions do
cache(hooks: hooks(pre: pre_hooks, post: post_hooks)) = cache
cache(warmers: warmers) = cache

provisioned =
warmers
|> Enum.concat(pre_hooks)
|> Enum.concat(post_hooks)
|> Enum.map(&map_names/1)

for {name, mod} <- provisioned, key in mod.provisions() do
send(name, {:cachex_provision, provision})
end
end

##############
# Private API #
##############

# Map a hook into the name and module tuple
defp map_names(hook(name: name, module: module)),
do: {name, module}

# Map a warmer into the name and module tuple
defp map_names(warmer(module: module)),
do: {module, module}
end
2 changes: 1 addition & 1 deletion lib/cachex/spec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ defmodule Cachex.Spec do
#############

# a list of accepted service suffixes for a cache instance
@services [:courier, :eternal, :janitor, :locksmith, :stats]
@services [:courier, :eternal, :janitor, :locksmith, :stats, :steward]

#############
# Typespecs #
Expand Down
17 changes: 16 additions & 1 deletion lib/cachex/warmer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,25 @@ defmodule Cachex.Warmer do
defmacro __using__(_) do
quote location: :keep, generated: true do
use GenServer
use Cachex.Provision

import Cachex.Spec

# enforce the behaviour
@behaviour Cachex.Warmer

@doc """
Return the provisions warmers require.
"""
def provisions,
do: [:cache]

@doc false
# Initializes the warmer from a provided state.
#
# Initialization will trigger an initial cache warming, and store
# the provided state for later to provide during further warming.
def init({cache(name: cache), warmer(state: state)}) do
def init({cache() = cache, warmer(state: state)}) do
{:ok, {cache, state, nil}}
end

Expand Down Expand Up @@ -120,6 +127,14 @@ defmodule Cachex.Warmer do
# pass the new state
{:noreply, new_state}
end

@doc false
# Receives a provisioned cache instance.
#
# The provided cache is then stored in the state and used for cache calls going
# forwards, in order to skip the lookups inside the cache overseer for performance.
def handle_provision({:cache, cache}, {_cache, state, timer}),
do: {:ok, {cache, state, timer}}
end
end
end
22 changes: 22 additions & 0 deletions test/cachex/services/steward_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule Cachex.Services.StewardTest do
use CachexCase

test "provisioning cache state" do
# bind our hook
ForwardHook.bind(
steward_forward_hook_provisions: [
provisions: [:cache]
]
)

# create our hook with the provisions forwarded through to it
hook = ForwardHook.create(:steward_forward_hook_provisions)

# start a new cache using our forwarded hook
cache = Helper.create_cache(hooks: [hook])
cache = Services.Overseer.retrieve(cache)

# the provisioned value should match
assert_receive({:cache, ^cache})
end
end