Skip to content

Commit

Permalink
[#157] Admit an MFA tuple as :cache options in the caching decorators
Browse files Browse the repository at this point in the history
  • Loading branch information
cabol committed Jun 1, 2022
1 parent 20f2c04 commit a2f73d2
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 57 deletions.
128 changes: 81 additions & 47 deletions lib/nebulex/caching.ex
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,9 @@ if Code.ensure_loaded?(Decorator.Define) do
options:
* `:cache` - Defines what cache to use (required). Raises `ArgumentError`
if the option is not present. Can be either a cache, or an MFA tuple referencing a function returning the cache.
if the option is not present. It can be also a MFA tuple to resolve the
cache dynamically in runtime by calling it. See "The :cache option"
section below for more information.
* `:key` - Defines the cache access key (optional). It overrides the
`:key_generator` option. If this option is not present, a default
Expand Down Expand Up @@ -255,7 +257,34 @@ if Code.ensure_loaded?(Decorator.Define) do
or exception executing a cache command is ignored and the annotated
function is executed normally.
## The `:key_generator` option
### The `:cache` option
The cache option can be the de defined cache module or an MFA tuple to
resolve the cache dynamically in runtime. When it is an MFA tuple, the
MFA is invoked passing the calling module, function name, and arguments
by default, and the MFA arguments are passed as extra arguments.
For example:
@decorate cacheable(cache: {MyApp.Cache, :cache, []}, key: var)
def some_function(var) do
# Some logic ...
end
The annotated function above will call `MyApp.Cache.cache(mod, fun, args)`
to resolve the cache in runtime, where `mod` is the calling module, `fun`
the calling function name, and `args` the calling arguments.
Also, we can define the function passing some extra arguments, like so:
@decorate cacheable(cache: {MyApp.Cache, :cache, ["extra"]}, key: var)
def some_function(var) do
# Some logic ...
end
In this case, the MFA will be invoked by adding the extra arguments, like:
`MyApp.Cache.cache(mod, fun, args, "extra")`.
### The `:key_generator` option
The possible values for the `:key_generator` are:
Expand Down Expand Up @@ -314,11 +343,6 @@ if Code.ensure_loaded?(Decorator.Define) do
Repo.get!(User, id)
end
@decorate cacheable(cache: {MyApp.Accounts, :get_dynamic_cache, []}, key: {User, id}, opts: [ttl: @ttl])
def get_user_from_dynamic_cache!(id) do
Repo.get!(User, id)
end
@decorate cacheable(
cache: Cache,
key: {User, username},
Expand Down Expand Up @@ -355,8 +379,6 @@ if Code.ensure_loaded?(Decorator.Define) do
|> User.changeset(attrs)
|> Repo.insert()
end
def get_dynamic_cache, Application.fetch_env!(:my_app, :cache)
end
See [Cache Usage Patters Guide](http://hexdocs.pm/nebulex/cache-usage-patterns.html).
Expand Down Expand Up @@ -544,34 +566,54 @@ if Code.ensure_loaded?(Decorator.Define) do
## Private Functions

defp caching_action(action, attrs, block, context) do
cache = attrs[:cache] || raise ArgumentError, "expected cache: to be given as argument"
_cache = attrs[:cache] || raise ArgumentError, "expected cache: to be given as argument"
match_var = attrs[:match] || quote(do: fn _ -> true end)
opts_var = attrs[:opts] || []

keygen_block = keygen_block(attrs, context)
args =
context.args
|> Enum.reduce([], &walk/2)
|> Enum.reverse()

cache_block = cache_block(attrs, args, context)
keygen_block = keygen_block(attrs, args, context)
action_block = action_block(action, block, attrs, keygen_block, on_error_opt(attrs))

quote do
cache = unquote(cache)
cache = unquote(cache_block)
opts = unquote(opts_var)
match = unquote(match_var)

cache =
case cache do
{m, f, args} -> apply(m, f, args)
cache -> cache
end

unquote(action_block)
end
end

defp keygen_block(attrs, ctx) do
args =
ctx.args
|> Enum.reduce([], &walk/2)
|> Enum.reverse()
defp walk({:\\, _, [ast, _]}, acc) do
walk(ast, acc)
end

defp walk({:=, _, [_, ast]}, acc) do
walk(ast, acc)
end

defp walk({var, [line: _], nil} = ast, acc) do
case "#{var}" do
"_" <> _ -> acc
_ -> [ast | acc]
end
end

defp walk(_ast, acc) do
acc
end

defp cache_block(attrs, args, ctx) do
attrs
|> Keyword.get(:cache)
|> cache_call(ctx, args)
end

defp keygen_block(attrs, args, ctx) do
cond do
key = Keyword.get(attrs, :key) ->
quote(do: unquote(key))
Expand All @@ -581,12 +623,6 @@ if Code.ensure_loaded?(Decorator.Define) do

true ->
quote do
cache =
case cache do
{m, f, args} -> apply(m, f, args)
cache -> cache
end

cache.__default_key_generator__().generate(
unquote(ctx.module),
unquote(ctx.name),
Expand All @@ -596,37 +632,35 @@ if Code.ensure_loaded?(Decorator.Define) do
end
end

defp walk({:\\, _, [ast, _]}, acc) do
walk(ast, acc)
end

defp walk({:=, _, [_, ast]}, acc) do
walk(ast, acc)
end

defp walk({var, [line: _], nil} = ast, acc) do
case "#{var}" do
"_" <> _ -> acc
_ -> [ast | acc]
# MFA cache: `{module, function, args}`
defp cache_call({:{}, _, [mod, fun, cache_args]}, ctx, args) do
quote do
unquote(mod).unquote(fun)(
unquote(ctx.module),
unquote(ctx.name),
unquote(args),
unquote_splicing(cache_args)
)
end
end

defp walk(_ast, acc) do
acc
# Module implementing the cache behaviour (default)
defp cache_call({_, _, _} = cache, _ctx, _args) do
quote(do: unquote(cache))
end

# MFA key-generator: `{module, function, args}`
defp keygen_call({:{}, _, [mod, fun, args]}, _ctx, _keygen_args) do
defp keygen_call({:{}, _, [mod, fun, keygen_args]}, _ctx, _args) do
quote do
unquote(mod).unquote(fun)(unquote_splicing(args))
unquote(mod).unquote(fun)(unquote_splicing(keygen_args))
end
end

# Key-generator tuple `{module, args}`, where the `module` implements
# the key-generator behaviour
defp keygen_call({{_, _, _} = mod, args}, ctx, _keygen_args) when is_list(args) do
defp keygen_call({{_, _, _} = mod, keygen_args}, ctx, _args) when is_list(keygen_args) do
quote do
unquote(mod).generate(unquote(ctx.module), unquote(ctx.name), unquote(args))
unquote(mod).generate(unquote(ctx.module), unquote(ctx.name), unquote(keygen_args))
end
end

Expand Down
19 changes: 19 additions & 0 deletions test/dialyzer/caching_decorators.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ defmodule Nebulex.Dialyzer.CachingDecorators do
filter
end

@spec get_user_key(integer) :: binary
@decorate cacheable(
cache: {__MODULE__, :dynamic_cache, [:dynamic]},
key_generator: {__MODULE__, [id]}
)
def get_user_key(id), do: id

@spec update_user_key(integer) :: binary
@decorate cacheable(cache: Cache, key_generator: {__MODULE__, :generate_key, [id]})
def update_user_key(id), do: id

## Helpers

defp match({:ok, updated}), do: {true, updated}
defp match({:error, _}), do: false

def generate(mod, fun, args), do: :erlang.phash2({mod, fun, args})

def generate_key(args), do: :erlang.phash2(args)

def dynamic_cache(_, _, _, _), do: Cache
end
89 changes: 79 additions & 10 deletions test/nebulex/caching_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,6 @@ defmodule Nebulex.CachingTest do
assert Cache.get(0) == "hello"
end

test "dynamic" do
refute Cache.get(0)
assert get_without_args_dynamic() == "hello"
assert Cache.get(0) == "hello"
end

test "with side effects and returning false (issue #111)" do
refute Cache.get("side-effect")
assert get_false_with_side_effect(false) == false
Expand Down Expand Up @@ -457,14 +451,55 @@ defmodule Nebulex.CachingTest do
end
end

describe "option :cache with MFA" do
test "cacheable annotation" do
refute Cache.get("foo")
assert get_mfa_cache_without_extra_args("foo") == "foo"
assert Cache.get("foo") == "foo"
end

test "cache_put annotation" do
:ok = Cache.put("foo", "bar")

assert update_mfa_cache_without_extra_args("bar bar") == "bar bar"
assert Cache.get("foo") == "bar bar"
end

test "cache_evict annotation" do
:ok = Cache.put("foo", "bar")

assert delete_mfa_cache_without_extra_args("bar bar") == "bar bar"
refute Cache.get("foo")
end
end

describe "option :cache with MFA and extra args" do
test "cacheable annotation" do
refute Cache.get("foo")
assert get_mfa_cache_with_extra_args("foo") == "foo"
assert Cache.get("foo") == "foo"
end

test "cache_put annotation" do
:ok = Cache.put("foo", "bar")

assert update_mfa_cache_with_extra_args("bar bar") == "bar bar"
assert Cache.get("foo") == "bar bar"
end

test "cache_evict annotation" do
:ok = Cache.put("foo", "bar")

assert delete_mfa_cache_with_extra_args("bar bar") == "bar bar"
refute Cache.get("foo")
end
end

## Annotated Functions

@decorate cacheable(cache: Cache)
def get_without_args, do: "hello"

@decorate cacheable(cache: {Nebulex.CachingTest, :get_dynamic_cache, []})
def get_without_args_dynamic, do: "hello"

@decorate cacheable(cache: Cache, key: x)
def get_by_x(x, y \\ "y") do
case x do
Expand Down Expand Up @@ -665,6 +700,38 @@ defmodule Nebulex.CachingTest do
x
end

@decorate cacheable(cache: {__MODULE__, :cache_with_extra_args, ["extra_arg"]}, key: var)
def get_mfa_cache_with_extra_args(var) do
var
end

@decorate cacheable(cache: {__MODULE__, :cache_without_extra_args, []}, key: var)
def get_mfa_cache_without_extra_args(var) do
var
end

@decorate cache_put(cache: {__MODULE__, :cache_with_extra_args, ["extra_arg"]}, key: "foo")
def update_mfa_cache_with_extra_args(var) do
var
end

@decorate cache_put(cache: {__MODULE__, :cache_without_extra_args, []}, key: "foo")
def update_mfa_cache_without_extra_args(var) do
var
end

@decorate cache_evict(cache: {__MODULE__, :cache_with_extra_args, ["extra_arg"]}, key: "foo")
def delete_mfa_cache_with_extra_args(var) do
var
end

@decorate cache_evict(cache: {__MODULE__, :cache_without_extra_args, []}, key: "foo")
def delete_mfa_cache_without_extra_args(var) do
var
end

## Helpers

# Custom key-generator function
def generate_key(arg), do: arg

Expand All @@ -673,7 +740,9 @@ defmodule Nebulex.CachingTest do
:erlang.phash2({module, function_name, args})
end

def get_dynamic_cache, do: Cache
def cache_with_extra_args(_mod, _fun, _args, _extra_arg), do: Cache

def cache_without_extra_args(_mod, _fun, _args), do: Cache

## Private Functions

Expand Down

0 comments on commit a2f73d2

Please sign in to comment.