Skip to content

Commit

Permalink
[#128] Make :keys option available for cache_put annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
cabol committed Jul 18, 2021
1 parent ca7e0c4 commit 6f07bb6
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 36 deletions.
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,28 +166,32 @@ defmodule MyApp.Accounts do

@decorate cache_put(
cache: Cache,
keys: [{User, usr.id}, {User, usr.username}],
match: &match_update/1
keys: [{User, user.id}, {User, user.username}],
match: &match_update/1,
opts: [ttl: @ttl]
)
def update_user(%User{} = usr, attrs) do
usr
def update_user(%User{} = user, attrs) do
user
|> User.changeset(attrs)
|> Repo.update()
end

defp match_update({:ok, usr}), do: {true, usr}
defp match_update({:error, _}), do: false

@decorate cache_evict(cache: Cache, keys: [{User, usr.id}, {User, usr.username}])
def delete_user(%User{} = usr) do
Repo.delete(usr)
@decorate cache_evict(
cache: Cache,
keys: [{User, user.id}, {User, user.username}]
)
def delete_user(%User{} = user) do
Repo.delete(user)
end

def create_user(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end

defp match_update({:ok, value}), do: {true, value}
defp match_update({:error, _}), do: false
end
```

Expand Down
2 changes: 1 addition & 1 deletion lib/nebulex/adapters/partitioned.ex
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ defmodule Nebulex.Adapters.Partitioned do
# Keyslot module for selecting nodes
keyslot =
opts
|> Keyword.get(:keyslot, __MODULE__)
|> get_option(:keyslot, "an atom", &is_atom/1, __MODULE__)
|> assert_behaviour(Nebulex.Adapter.Keyslot, "keyslot")

# Maybe task supervisor for distributed tasks
Expand Down
96 changes: 82 additions & 14 deletions lib/nebulex/caching.ex
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,11 @@ if Code.ensure_loaded?(Decorator.Define) do
It is also possible passing options to the cache, like so:
@decorate cacheable(cache: Cache, key: {Account, email}, opts: [ttl: 300_000])
@decorate cacheable(
cache: Cache,
key: {Account, email},
opts: [ttl: 300_000]
)
def get_account(email, include_users?) do
# the logic for retrieving the account ...
end
Expand Down Expand Up @@ -173,7 +177,10 @@ if Code.ensure_loaded?(Decorator.Define) do
# the logic for deleting the account ...
end
@decorate cacheable(cache: Cache, keys: [{Account, acct.id}, {Account, acct.email}])
@decorate cacheable(
cache: Cache,
keys: [{Account, acct.id}, {Account, acct.email}]
)
def delete_account(%Account{} = acct) do
# the logic for deleting the account ...
end
Expand Down Expand Up @@ -265,7 +272,11 @@ if Code.ensure_loaded?(Decorator.Define) do
Repo.get!(User, id)
end
@decorate cacheable(cache: Cache, key: {User, username}, opts: [ttl: @ttl])
@decorate cacheable(
cache: Cache,
key: {User, username},
opts: [ttl: @ttl]
)
def get_user_by_username(username) do
Repo.get_by(User, [username: username])
end
Expand All @@ -284,7 +295,10 @@ if Code.ensure_loaded?(Decorator.Define) do
defp match_update({:ok, usr}), do: {true, usr}
defp match_update({:error, _}), do: false
@decorate cache_evict(cache: Cache, keys: [{User, usr.id}, {User, usr.username}])
@decorate cache_evict(
cache: Cache,
keys: [{User, usr.id}, {User, usr.username}]
)
def delete_user(%User{} = usr) do
Repo.delete(usr)
end
Expand All @@ -301,6 +315,8 @@ if Code.ensure_loaded?(Decorator.Define) do

use Decorator.Define, cacheable: 1, cache_evict: 1, cache_put: 1

import Nebulex.Helpers

alias Nebulex.Caching

@doc """
Expand Down Expand Up @@ -365,6 +381,10 @@ if Code.ensure_loaded?(Decorator.Define) do
## Options
* `:keys` - The set of cached keys to be updated with the returned value
on function completion. It overrides `:key` and `:key_generator`
options.
See the "Shared options" section at the module documentation.
## Examples
Expand All @@ -377,12 +397,27 @@ if Code.ensure_loaded?(Decorator.Define) do
@ttl :timer.hours(1)
@decorate cache_put(cache: Cache, key: id, opts: [ttl: @ttl])
def update!(id, attrs \\ %{}) do
def update!(id, attrs \\\\ %{}) do
# your logic (maybe write data to the SoR)
end
@decorate cache_put(cache: Cache, key: id, match: &match_fun/1, opts: [ttl: @ttl])
def update(id, attrs \\ %{}) do
@decorate cache_put(
cache: Cache,
key: id,
match: &match_fun/1,
opts: [ttl: @ttl]
)
def update(id, attrs \\\\ %{}) do
# your logic (maybe write data to the SoR)
end
@decorate cache_put(
cache: Cache,
keys: [object.name, object.id],
match: &match_fun/1,
opts: [ttl: @ttl]
)
def update_object(object) do
# your logic (maybe write data to the SoR)
end
Expand All @@ -407,7 +442,7 @@ if Code.ensure_loaded?(Decorator.Define) do
## Options
* `:keys` - Defines the set of keys to be evicted from cache on function
completion. It overrides the `:key_generator` option.
completion. It overrides `:key` and `:key_generator` options.
* `:all_entries` - Defines if all entries must be removed on function
completion. Defaults to `false`.
Expand Down Expand Up @@ -516,9 +551,16 @@ if Code.ensure_loaded?(Decorator.Define) do
end
end

defp action_block(:cache_put, block, _attrs, keygen) do
defp action_block(:cache_put, block, attrs, keygen) do
keys = get_keys(attrs)

key =
if is_list(keys) and length(keys) > 0,
do: {:"$keys", keys},
else: keygen

quote do
Caching.eval_match(unquote(block), match, cache, unquote(keygen), opts)
Caching.eval_match(unquote(block), match, cache, unquote(key), opts)
end
end

Expand All @@ -542,7 +584,7 @@ if Code.ensure_loaded?(Decorator.Define) do
end

defp eviction_block(attrs, keygen) do
keys = Keyword.get(attrs, :keys)
keys = get_keys(attrs)
all_entries? = attrs[:all_entries] || false

cond do
Expand All @@ -563,8 +605,17 @@ if Code.ensure_loaded?(Decorator.Define) do
end
end

defp get_keys(attrs) do
get_option(
attrs,
:keys,
"a list with at least one element",
&((is_list(&1) and length(&1) > 0) or is_nil(&1))
)
end

@doc """
This function is for internal purposes.
This function is for internal purposes only.
**NOTE:** Workaround to avoid dialyzer warnings when using declarative
annotation-based caching via decorators.
Expand All @@ -573,16 +624,33 @@ if Code.ensure_loaded?(Decorator.Define) do
def eval_match(result, match, cache, key, opts) do
case match.(result) do
{true, value} ->
:ok = cache.put(key, value, opts)
:ok = Caching.cache_put(cache, key, value, opts)
result

true ->
:ok = cache.put(key, result, opts)
:ok = Caching.cache_put(cache, key, result, opts)
result

false ->
result
end
end

@doc """
Convenience function for cache_put annotation.
**NOTE:** Internal purposes only.
"""
@spec cache_put(module, {:"$keys", term} | term, term, Keyword.t()) :: :ok
def cache_put(cache, key, value, opts)

def cache_put(cache, {:"$keys", keys}, value, opts) do
entries = for k <- keys, do: {k, value}
cache.put_all(entries, opts)
end

def cache_put(cache, key, value, opts) do
cache.put(key, value, opts)
end
end
end
8 changes: 4 additions & 4 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
"benchee_json": {:hex, :benchee_json, "1.0.0", "cc661f4454d5995c08fe10dd1f2f72f229c8f0fb1c96f6b327a8c8fc96a91fe5", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "da05d813f9123505f870344d68fb7c86a4f0f9074df7d7b7e2bb011a63ec231c"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
"credo": {:hex, :credo, "1.5.5", "e8f422026f553bc3bebb81c8e8bf1932f498ca03339856c7fec63d3faac8424b", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dd8623ab7091956a855dc9f3062486add9c52d310dfd62748779c4315d8247de"},
"credo": {:hex, :credo, "1.5.6", "e04cc0fdc236fefbb578e0c04bd01a471081616e741d386909e527ac146016c6", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4b52a3e558bd64e30de62a648518a5ea2b6e3e5d2b164ef5296244753fc7eb17"},
"decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ex2ms": {:hex, :ex2ms, "1.6.0", "f39bbd9ff1b0f27b3f707bab2d167066dd8965e7df1149b962d94c74615d0e09", [:mix], [], "hexpm", "0d1ab5e08421af5cd69146efb408dbb1ff77f38a2f4df5f086f2512dc8cf65bf"},
"ex_doc": {:hex, :ex_doc, "0.24.2", "e4c26603830c1a2286dae45f4412a4d1980e1e89dc779fcd0181ed1d5a05c8d9", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e134e1d9e821b8d9e4244687fb2ace58d479b67b282de5158333b0d57c6fb7da"},
"excoveralls": {:hex, :excoveralls, "0.14.0", "4b562d2acd87def01a3d1621e40037fdbf99f495ed3a8570dfcf1ab24e15f76d", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "94f17478b0cca020bcd85ce7eafea82d2856f7ed022be777734a2f864d36091a"},
"excoveralls": {:hex, :excoveralls, "0.14.1", "14140e4ef343f2af2de33d35268c77bc7983d7824cb945e6c2af54235bc2e61f", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4a588f9f8cf9dc140cc1f3d0ea4d849b2f76d5d8bee66b73c304bb3d3689c8b0"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
Expand All @@ -21,10 +21,10 @@
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mock": {:hex, :mock, "0.3.6", "e810a91fabc7adf63ab5fdbec5d9d3b492413b8cda5131a2a8aa34b4185eb9b4", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "bcf1d0a6826fb5aee01bae3d74474669a3fa8b2df274d094af54a25266a1ebd2"},
"mock": {:hex, :mock, "0.3.7", "75b3bbf1466d7e486ea2052a73c6e062c6256fb429d6797999ab02fa32f29e03", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "4da49a4609e41fd99b7836945c26f373623ea968cfb6282742bcb94440cf7e5c"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"shards": {:hex, :shards, "1.0.1", "1bdbbf047db27f3c3eb800a829d4a47062c84d5543cbfebcfc4c14d038bf9220", [:make, :rebar3], [], "hexpm", "2c57788afbf053c4024366772892beee89b8b72e884e764fb0a075dfa7442041"},
Expand Down
28 changes: 22 additions & 6 deletions test/dialyzer/caching_test.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ defmodule Nebulex.Dialyzer.CachingTest do
@type t :: %__MODULE__{}
end

@ttl :timer.seconds(3600)

## Annotated Functions

@spec get_account(integer) :: Account.t()
Expand All @@ -16,21 +18,32 @@ defmodule Nebulex.Dialyzer.CachingTest do
%Account{id: id}
end

@spec get_account_by_username(String.t()) :: Account.t()
@decorate cacheable(cache: Cache, key: {Account, username})
@spec get_account_by_username(binary) :: Account.t()
@decorate cacheable(cache: Cache, key: {Account, username}, opts: [ttl: @ttl])
def get_account_by_username(username) do
%Account{username: username}
end

@spec update_account(Account.t()) :: Account.t()
@decorate cache_put(cache: Cache, key: {Account, acct.id})
def update_account(acct) do
acct
@decorate cache_put(
cache: Cache,
keys: [{Account, acct.id}, {Account, acct.username}],
match: &match/1,
opts: [ttl: @ttl]
)
def update_account(%Account{} = acct) do
{:ok, acct}
end

@spec update_account_by_id(binary, %{optional(atom) => term}) :: Account.t()
@decorate cache_put(cache: Cache, key: {Account, id}, match: &match/1, opts: [ttl: @ttl])
def update_account_by_id(id, attrs) do
{:ok, struct(Account, Map.put(attrs, :id, id))}
end

@spec delete_account(Account.t()) :: Account.t()
@decorate cache_evict(cache: Cache, keys: [{Account, acct.id}, {Account, acct.username}])
def delete_account(acct) do
def delete_account(%Account{} = acct) do
acct
end

Expand All @@ -39,4 +52,7 @@ defmodule Nebulex.Dialyzer.CachingTest do
def delete_all_accounts(filter) do
filter
end

defp match({:ok, updated}), do: {true, updated}
defp match({:error, _}), do: false
end
12 changes: 11 additions & 1 deletion test/nebulex/adapters/partitioned_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ defmodule Nebulex.Adapters.PartitionedTest do
assert Regex.match?(~r"keyslot UnloadedKeyslot was not compiled", msg)
end

test "fails because invalid keyslot module" do
test "fails because keyslot module does not implement expected behaviour" do
assert {:error, {%ArgumentError{message: msg}, _}} =
Partitioned.start_link(
name: :invalid_keyslot,
Expand All @@ -77,6 +77,16 @@ defmodule Nebulex.Adapters.PartitionedTest do
behaviour = "Nebulex.Adapter.Keyslot"
assert Regex.match?(~r"expected #{mod} to implement the behaviour #{behaviour}", msg)
end

test "fails because invalid keyslot option" do
assert {:error, {%ArgumentError{message: msg}, _}} =
Partitioned.start_link(
name: :invalid_keyslot,
keyslot: "invalid"
)

assert Regex.match?(~r"expected keyslot: to be an atom, got: \"invalid\"", msg)
end
end

describe "partitioned cache" do
Expand Down
21 changes: 21 additions & 0 deletions test/nebulex/caching_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,20 @@ defmodule Nebulex.CachingTest do
assert update_without_args() == "hello"
assert Cache.get(0) == "hello"
end

test "with multiple keys and ttl" do
assert set_keys(x: 1, y: 2, z: 3) == :ok

assert update_with_multiple_keys(:x, :y) == {:ok, {"x", "y"}}
assert Cache.get(:x) == {"x", "y"}
assert Cache.get(:y) == {"x", "y"}
assert Cache.get(:z) == 3

:ok = Process.sleep(1100)
refute Cache.get(:x)
refute Cache.get(:y)
assert Cache.get(:z) == 3
end
end

describe "cache_evict" do
Expand Down Expand Up @@ -363,6 +377,13 @@ defmodule Nebulex.CachingTest do
_ -> :error
end

@decorate cache_put(cache: Cache, keys: [x, y], match: &match_fun/1, opts: [ttl: 1000])
def update_with_multiple_keys(x, y) do
{:ok, {to_string(x), to_string(y)}}
rescue
_ -> :error
end

@decorate cache_evict(cache: Cache)
def evict_without_args, do: "hello"

Expand Down

0 comments on commit 6f07bb6

Please sign in to comment.