From 10a0380ccb1ff7414ad039cc599f68a930110579 Mon Sep 17 00:00:00 2001 From: cabol Date: Fri, 31 Mar 2023 16:12:28 +0200 Subject: [PATCH] [#189] All commands optionally support a dynamic cache as the first argument --- .formatter.exs | 6 +- guides/creating-new-adapter.md | 4 +- lib/nebulex/adapter/kv.ex | 24 +- lib/nebulex/adapter/persistence.ex | 8 +- lib/nebulex/adapter/queryable.ex | 11 +- lib/nebulex/adapter/stats.ex | 20 +- lib/nebulex/adapter/transaction.ex | 53 +- lib/nebulex/adapters/nil.ex | 2 +- lib/nebulex/cache.ex | 1159 +++++++++++++++++------- lib/nebulex/cache/kv.ex | 4 +- lib/nebulex/cache/options.ex | 16 +- lib/nebulex/cache/persistence.ex | 2 +- lib/nebulex/cache/queryable.ex | 2 +- lib/nebulex/cache/registry.ex | 2 +- lib/nebulex/cache/stats.ex | 2 +- lib/nebulex/cache/supervisor.ex | 2 +- lib/nebulex/cache/transaction.ex | 2 +- lib/nebulex/cache/utils.ex | 33 + lib/nebulex/caching/decorators.ex | 2 +- lib/nebulex/{helpers.ex => utils.ex} | 29 +- test/nebulex/cache/supervisor_test.exs | 4 +- test/nebulex/helpers_test.exs | 14 +- test/nebulex/stats_test.exs | 4 +- test/shared/cache/kv_test.exs | 12 +- test/shared/cache/transaction_test.exs | 63 +- test/support/test_adapter.exs | 8 +- 26 files changed, 1005 insertions(+), 483 deletions(-) create mode 100644 lib/nebulex/cache/utils.ex rename lib/nebulex/{helpers.ex => utils.ex} (78%) diff --git a/.formatter.exs b/.formatter.exs index 9f1a7935..51b98387 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,12 +1,12 @@ locals_without_parens = [ - # Nebulex.Helpers + # Nebulex.Utils unwrap_or_raise: 1, wrap_ok: 1, wrap_error: 1, wrap_error: 2, - # Nebulex.Cache - dynamic_cache: 2, + # Nebulex.Cache.Utils + defcacheapi: 2, # Nebulex.Caching cache_ref: 1, diff --git a/guides/creating-new-adapter.md b/guides/creating-new-adapter.md index 09679a39..13980cf2 100644 --- a/guides/creating-new-adapter.md +++ b/guides/creating-new-adapter.md @@ -256,7 +256,7 @@ defmodule NebulexMemoryAdapter do @behaviour Nebulex.Adapter @behaviour Nebulex.Adapter.Queryable - import Nebulex.Helpers + import Nebulex.Utils @impl Nebulex.Adapter defmacro __before_compile__(_env), do: :ok @@ -308,7 +308,7 @@ defmodule NebulexMemoryAdapter do @behaviour Nebulex.Adapter.KV @behaviour Nebulex.Adapter.Queryable - import Nebulex.Helpers + import Nebulex.Utils @impl Nebulex.Adapter defmacro __before_compile__(_env), do: :ok diff --git a/lib/nebulex/adapter/kv.ex b/lib/nebulex/adapter/kv.ex index 43376033..bf56735a 100644 --- a/lib/nebulex/adapter/kv.ex +++ b/lib/nebulex/adapter/kv.ex @@ -49,7 +49,7 @@ defmodule Nebulex.Adapter.KV do specified `keys`. For every key that does not hold a value or does not exist, it is ignored and not added into the returned map. - Returns `{:error, reason}` if an error occurs while executing the command. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. See `c:Nebulex.Cache.get_all/2`. """ @@ -64,7 +64,7 @@ defmodule Nebulex.Adapter.KV do Returns `{:ok, true}` if the `value` with key `key` is successfully inserted, otherwise, `{:ok, false}` is returned. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. ## OnWrite @@ -97,7 +97,7 @@ defmodule Nebulex.Adapter.KV do Returns `{:ok, true}` if all the keys were inserted. If no key was inserted (at least one key already existed), `{:ok, false}` is returned. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. ## OnWrite @@ -123,7 +123,7 @@ defmodule Nebulex.Adapter.KV do @doc """ Deletes a single entry from cache. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. See `c:Nebulex.Cache.delete/2`. """ @@ -138,8 +138,8 @@ defmodule Nebulex.Adapter.KV do If `key` is not present in the cache, `{:error, Nebulex.KeyError.t()}` is returned. - Returns `{:error, Nebulex.Error.t()}` if any other error occurs while - executing the command. + Returns `{:error, Nebulex.Error.t()}` if any other error occurs executing + the command. See `c:Nebulex.Cache.take/2`. """ @@ -153,7 +153,7 @@ defmodule Nebulex.Adapter.KV do If `amount` < 0, the counter is decremented by the given `amount`. If `amount` == 0, the counter is not updated. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. See `c:Nebulex.Cache.incr/3`. See `c:Nebulex.Cache.decr/3`. @@ -168,7 +168,7 @@ defmodule Nebulex.Adapter.KV do More formally, returns `{:ok, true}` if the cache contains the given `key`. If the cache doesn't contain `key`, `{:ok, :false}` is returned. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. See `c:Nebulex.Cache.has_key?/2`. """ @@ -183,8 +183,8 @@ defmodule Nebulex.Adapter.KV do If `key` is not present in the cache, `{:error, Nebulex.KeyError.t()}` is returned. - Returns `{:error, Nebulex.Error.t()}` if any other error occurs while - executing the command. + Returns `{:error, Nebulex.Error.t()}` if any other error occurs executing + the command. See `c:Nebulex.Cache.ttl/2`. """ @@ -195,7 +195,7 @@ defmodule Nebulex.Adapter.KV do Returns `{:ok, true}` if the given `key` exists and the new `ttl` was successfully updated, otherwise, `{:ok, false}` is returned. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. See `c:Nebulex.Cache.expire/3`. """ @@ -205,7 +205,7 @@ defmodule Nebulex.Adapter.KV do Returns `{:ok, true}` if the given `key` exists and the last access time was successfully updated, otherwise, `{:ok, false}` is returned. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. See `c:Nebulex.Cache.touch/2`. """ diff --git a/lib/nebulex/adapter/persistence.ex b/lib/nebulex/adapter/persistence.ex index 4dfef65b..d763a0d1 100644 --- a/lib/nebulex/adapter/persistence.ex +++ b/lib/nebulex/adapter/persistence.ex @@ -27,7 +27,8 @@ defmodule Nebulex.Adapter.Persistence do @doc """ Dumps a cache to the given file `path`. - Returns `:ok` if successful, or `{:error, reason}` if an error occurs. + Returns `:ok` if successful, or `{:error, Nebulex.Error.t()}` + if an error occurs. See `c:Nebulex.Cache.dump/2`. """ @@ -37,7 +38,8 @@ defmodule Nebulex.Adapter.Persistence do @doc """ Loads a dumped cache from the given `path`. - Returns `:ok` if successful, or `{:error, reason}` if an error occurs. + Returns `:ok` if successful, or `{:error, Nebulex.Error.t()}` + if an error occurs. See `c:Nebulex.Cache.load/2`. """ @@ -51,7 +53,7 @@ defmodule Nebulex.Adapter.Persistence do quote do @behaviour Nebulex.Adapter.Persistence - import Nebulex.Helpers + import Nebulex.Utils, only: [wrap_error: 2] @impl true def dump(%{cache: cache}, path, opts) do diff --git a/lib/nebulex/adapter/queryable.ex b/lib/nebulex/adapter/queryable.ex index cec464ed..cb29f74c 100644 --- a/lib/nebulex/adapter/queryable.ex +++ b/lib/nebulex/adapter/queryable.ex @@ -33,10 +33,10 @@ defmodule Nebulex.Adapter.Queryable do This callback returns: - * `{:ok, query_result}` - the query is valid, then it is executed - and the result returned. + * `{:ok, query_result}` - The query was successfully executed, + the matched entries are returned. - * `{:error, reason}` - an error occurred while executing the command. + * `{:error, Nebulex.Error.t()}` - An error occurred executing the command. ## Operations @@ -62,10 +62,9 @@ defmodule Nebulex.Adapter.Queryable do This callback returns: - * `{:ok, stream}` - the query is valid, then the stream is built - and returned. + * `{:ok, stream}` - The query is valid, then the stream is returned. - * `{:error, reason}` - an error occurred while executing the command. + * `{:error, Nebulex.Error.t()}` - An error occurred executing the command. See `c:Nebulex.Cache.stream/2`. """ diff --git a/lib/nebulex/adapter/stats.ex b/lib/nebulex/adapter/stats.ex index f802bd2d..85c6c356 100644 --- a/lib/nebulex/adapter/stats.ex +++ b/lib/nebulex/adapter/stats.ex @@ -7,11 +7,11 @@ defmodule Nebulex.Adapter.Stats do using [Erlang counters][https://erlang.org/doc/man/counters.html], with all callbacks overridable. - See `Nebulex.Adapters.Local` for more information about how this can be used - from the adapter, and also [Nebulex Telemetry Guide][telemetry_guide] to learn - how to use the Cache with Telemetry. + See `Nebulex.Adapters.Local` adapter for more information about how to use + this implementation. Besides, see also the [Nebulex Telemetry][nbx_telemetry] + guide. - [telemetry_guide]: http://hexdocs.pm/nebulex/telemetry.html + [nbx_telemetry]: http://hexdocs.pm/nebulex/telemetry.html """ @doc """ @@ -19,12 +19,10 @@ defmodule Nebulex.Adapter.Stats do This function returns: - * `{:ok, Nebulex.Stats.t()}` - stats are enabled and available - for the cache. + * `{:ok, Nebulex.Stats.t()}` - If stats are enabled and supported by the + cache. - * `{:ok, nil}` - the stats are disabled for the cache. - - * `{:error, reason}` - an error occurred while executing the command. + * `{:error, Nebulex.Error.t()}` - An error occurred executing the command. The adapter may also include additional custom measurements, as well as metadata. @@ -40,7 +38,7 @@ defmodule Nebulex.Adapter.Stats do @behaviour Nebulex.Adapter.Stats import Nebulex.Adapter.Stats, only: [defspan: 2, defspan: 3] - import Nebulex.Helpers + import Nebulex.Utils, only: [wrap_error: 2] @impl true def stats(adapter_meta, _opts) do @@ -93,7 +91,7 @@ defmodule Nebulex.Adapter.Stats do @spec init(keyword) :: :counters.counters_ref() | nil def init(opts) do case Keyword.get(opts, :stats, false) do - true -> :counters.new(6, [:write_concurrency]) + true -> :counters.new(5, [:write_concurrency]) false -> nil end end diff --git a/lib/nebulex/adapter/transaction.ex b/lib/nebulex/adapter/transaction.ex index 9a926689..0b5ba7b3 100644 --- a/lib/nebulex/adapter/transaction.ex +++ b/lib/nebulex/adapter/transaction.ex @@ -35,45 +35,46 @@ defmodule Nebulex.Adapter.Transaction do Locking only the involved key (recommended): - MyCache.transaction [keys: [:counter]], fn -> - counter = MyCache.get(:counter) - MyCache.set(:counter, counter + 1) - end - - MyCache.transaction [keys: [:alice, :bob]], fn -> - alice = MyCache.get(:alice) - bob = MyCache.get(:bob) - MyCache.set(:alice, %{alice | balance: alice.balance + 100}) - MyCache.set(:bob, %{bob | balance: bob.balance + 100}) - end + MyCache.transaction( + fn -> + counter = MyCache.get(:counter) + MyCache.set(:counter, counter + 1) + end, + [keys: [:counter]] + ) + + MyCache.transaction( + fn -> + alice = MyCache.get(:alice) + bob = MyCache.get(:bob) + MyCache.set(:alice, %{alice | balance: alice.balance + 100}) + MyCache.set(:bob, %{bob | balance: bob.balance + 100}) + end, + [keys: [:alice, :bob]] + ) """ @doc """ Runs the given function inside a transaction. - A successful transaction returns the value returned by the function wrapped - in a tuple as `{:ok, value}`. - - In case the transaction cannot be executed, then `{:error, reason}` is - returned. + If an Elixir exception occurs, the exception will bubble up from the + transaction function. If the transaction is aborted, + `{:error, Nebulex.Error.t()}` is returned. - If an unhandled error/exception occurs, the error will bubble up from the - transaction function. - - If `transaction/2` is called inside another transaction, the function is - simply executed without wrapping the new transaction call in any way. + A successful transaction returns the value returned by the function wrapped + in a tuple as `{:ok, any()}`. See `c:Nebulex.Cache.transaction/2`. """ - @callback transaction(Nebulex.Adapter.adapter_meta(), Nebulex.Cache.opts(), (() -> any())) :: - any() + @callback transaction(Nebulex.Adapter.adapter_meta(), fun(), Nebulex.Cache.opts()) :: + Nebulex.Cache.ok_error_tuple(any()) @doc """ Returns `{:ok, true}` if the current process is inside a transaction, otherwise, `{:ok, false}` is returned. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. See `c:Nebulex.Cache.in_transaction?/1`. """ @@ -85,10 +86,10 @@ defmodule Nebulex.Adapter.Transaction do quote do @behaviour Nebulex.Adapter.Transaction - import Nebulex.Helpers + import Nebulex.Utils, only: [wrap_ok: 1, wrap_error: 2] @impl true - def transaction(%{cache: cache, pid: pid} = adapter_meta, opts, fun) do + def transaction(%{cache: cache, pid: pid} = adapter_meta, fun, opts) do adapter_meta |> do_in_transaction?() |> do_transaction( diff --git a/lib/nebulex/adapters/nil.ex b/lib/nebulex/adapters/nil.ex index 531f0178..9295c367 100644 --- a/lib/nebulex/adapters/nil.ex +++ b/lib/nebulex/adapters/nil.ex @@ -70,7 +70,7 @@ defmodule Nebulex.Adapters.Nil do # Inherit default transaction implementation use Nebulex.Adapter.Transaction - import Nebulex.Helpers + import Nebulex.Utils, only: [wrap_error: 2] ## Nebulex.Adapter diff --git a/lib/nebulex/cache.ex b/lib/nebulex/cache.ex index d945b37e..e69b7c22 100644 --- a/lib/nebulex/cache.ex +++ b/lib/nebulex/cache.ex @@ -23,7 +23,6 @@ defmodule Nebulex.Cache do Could be configured with: config :my_app, MyApp.Cache, - backend: :shards, gc_interval: :timer.hours(12), max_size: 1_000_000, allocated_memory: 2_000_000_000, @@ -246,9 +245,66 @@ defmodule Nebulex.Cache do See [Nebulex Telemetry Guide](http://hexdocs.pm/nebulex/telemetry.html) for more information. + ## Dynamic caches + + Nebulex allows you to start multiple processes from the same cache module. + This is typically useful when you want to have different cache instances + but access them through the same cache module. + + When you list a cache in your supervision tree, such as `MyApp.Cache`, + it will start a supervision tree with a process named `MyApp.Cache` + under the hood. By default, the process has the same name as the cache + module itself. Hence, every time you invoke a function in `MyApp.Cache`, + such as `MyApp.Cache.put/3`, Nebulex will execute the command in the + cache process named `MyApp.Cache`. + + However, with Nebulex you can start multiple processes from the same cache. + The only requirement is that they must have different process names, like + this: + + children = [ + MyApp.Cache, + {MyApp.Cache, name: MyApp.UsersCache} + ] + + Now you have two cache instances running: one is named `MyApp.Cache` and the + other one is named `MyApp.UsersCache`. You can tell Nebulex which process + you want to use in your cache operations by calling: + + MyApp.Cache.put_dynamic_cache(MyApp.Cache) + MyApp.Cache.put_dynamic_cache(MyApp.UsersCache) + + Once you call `MyApp.Cache.put_dynamic_cache(name)`, all invocations made on + `MyApp.Cache` will use the cache instance denoted by `name`. + + Nebulex also provides a handy cache function for running commands when using + dynamic caches: `c:with_dynamic_cache/2`. + + MyApp.Cache.with_dynamic_cache(MyApp.UsersCache, fn -> + # all commands here will use MyApp.UsersCache + MyApp.Cache.put("u1", "joe") + ... + end) + + While these functions are handy, you may want to have the ability to pass + the dynamic cache directly to the command, avoiding the boilerplate logic + of using `c:put_dynamic_cache/1` or `c:with_dynamic_cache/2`. From **v3.0**, + all Cache API commands expose an extended callback version that admits a + dynamic cache at the first argument, so you can directly interact with a + cache instance. + + MyApp.Cache.put(MyApp.UsersCache, "u1", "joe", ttl: :timer.hours(1)) + MyApp.Cache.get(MyApp.UsersCache, "u1", nil, []) + MyApp.Cache.delete(MyApp.UsersCache, "u1", []) + + This is another handy way to work with multiple cache instances through + the same cache module. + ## Distributed topologies - Nebulex provides the following adapters for distributed topologies: + One of the goals of Nebulex is also to provide the ability to set up + distributed cache topologies, but this feature will depend on the adapters. + However, there are available adapters already for this: * `Nebulex.Adapters.Partitioned` - Partitioned cache topology. * `Nebulex.Adapters.Replicated` - Replicated cache topology. @@ -257,7 +313,7 @@ defmodule Nebulex.Cache do These adapters work more as wrappers for an existing local adapter and provide the distributed topology on top of it. Optionally, you can set the adapter for the primary cache storage with the option `:primary_storage_adapter`. Defaults - to `Nebulex.Adapters.Local`. + to `Nebulex.Adapters.Local`. See the adapters docs for information. """ @typedoc "Cache type" @@ -269,6 +325,9 @@ defmodule Nebulex.Cache do @typedoc "Cache entry value" @type value() :: any() + @typedoc "Dynamic cache value" + @type dynamic_cache() :: atom() | pid() + @typedoc "Cache entries" @type entries() :: map() | [{key(), value()}] @@ -293,12 +352,16 @@ defmodule Nebulex.Cache do @typedoc "Ok/Error type" @type ok_error_tuple(ok, error) :: {:ok, ok} | {:error, error} + ## API + + import __MODULE__.Utils + @doc false defmacro __using__(opts) do quote do unquote(prelude(opts)) unquote(base_defs()) - unquote(entry_defs()) + unquote(kv_defs()) if Nebulex.Adapter.Queryable in behaviours do unquote(queryable_defs()) @@ -335,26 +398,6 @@ defmodule Nebulex.Cache do defp base_defs do quote do - ## Helpers - - import Nebulex.Helpers, only: [kw_pop_first_lazy: 3] - - @doc """ - Helper macro to resolve the dynamic cache. - """ - defmacro dynamic_cache(opts, do: block) do - quote do - {dynamic_cache, opts} = - kw_pop_first_lazy( - unquote(opts), - :dynamic_cache, - fn -> get_dynamic_cache() end - ) - - unquote(block) - end - end - ## Config and metadata @impl true @@ -388,9 +431,12 @@ defmodule Nebulex.Cache do @impl true def stop(opts \\ []) do - dynamic_cache opts do - Supervisor.stop(dynamic_cache, :normal, Keyword.get(opts, :timeout, 5000)) - end + stop(get_dynamic_cache(), opts) + end + + @impl true + def stop(name, opts) do + Supervisor.stop(name, :normal, Keyword.get(opts, :timeout, 5000)) end # Iniline common instructions @@ -421,184 +467,79 @@ defmodule Nebulex.Cache do end end - defp entry_defs do + defp kv_defs do quote do alias Nebulex.Cache.KV - @impl true - def fetch(key, opts \\ []) do - dynamic_cache opts, do: KV.fetch(dynamic_cache, key, opts) - end + defcacheapi fetch(key, opts \\ []), to: KV - @impl true - def fetch!(key, opts \\ []) do - dynamic_cache opts, do: KV.fetch!(dynamic_cache, key, opts) - end + defcacheapi fetch!(key, opts \\ []), to: KV - @impl true - def get(key, default \\ nil, opts \\ []) do - dynamic_cache opts, do: KV.get(dynamic_cache, key, default, opts) - end + defcacheapi get(key, default \\ nil, opts \\ []), to: KV - @impl true - def get!(key, default \\ nil, opts \\ []) do - dynamic_cache opts, do: KV.get!(dynamic_cache, key, default, opts) - end + defcacheapi get!(key, default \\ nil, opts \\ []), to: KV - @impl true - def get_all(keys, opts \\ []) do - dynamic_cache opts, do: KV.get_all(dynamic_cache, keys, opts) - end + defcacheapi get_all(keys, opts \\ []), to: KV - @impl true - def get_all!(keys, opts \\ []) do - dynamic_cache opts, do: KV.get_all!(dynamic_cache, keys, opts) - end + defcacheapi get_all!(keys, opts \\ []), to: KV - @impl true - def put(key, value, opts \\ []) do - dynamic_cache opts, do: KV.put(dynamic_cache, key, value, opts) - end + defcacheapi put(key, value, opts \\ []), to: KV - @impl true - def put!(key, value, opts \\ []) do - dynamic_cache opts, do: KV.put!(dynamic_cache, key, value, opts) - end + defcacheapi put!(key, value, opts \\ []), to: KV - @impl true - def put_new(key, value, opts \\ []) do - dynamic_cache opts, do: KV.put_new(dynamic_cache, key, value, opts) - end + defcacheapi put_new(key, value, opts \\ []), to: KV - @impl true - def put_new!(key, value, opts \\ []) do - dynamic_cache opts, do: KV.put_new!(dynamic_cache, key, value, opts) - end + defcacheapi put_new!(key, value, opts \\ []), to: KV - @impl true - def replace(key, value, opts \\ []) do - dynamic_cache opts, do: KV.replace(dynamic_cache, key, value, opts) - end + defcacheapi replace(key, value, opts \\ []), to: KV - @impl true - def replace!(key, value, opts \\ []) do - dynamic_cache opts, do: KV.replace!(dynamic_cache, key, value, opts) - end + defcacheapi replace!(key, value, opts \\ []), to: KV - @impl true - def put_all(entries, opts \\ []) do - dynamic_cache opts, do: KV.put_all(dynamic_cache, entries, opts) - end + defcacheapi put_all(entries, opts \\ []), to: KV - @impl true - def put_all!(entries, opts \\ []) do - dynamic_cache opts, do: KV.put_all!(dynamic_cache, entries, opts) - end + defcacheapi put_all!(entries, opts \\ []), to: KV - @impl true - def put_new_all(entries, opts \\ []) do - dynamic_cache opts, do: KV.put_new_all(dynamic_cache, entries, opts) - end + defcacheapi put_new_all(entries, opts \\ []), to: KV - @impl true - def put_new_all!(entries, opts \\ []) do - dynamic_cache opts, do: KV.put_new_all!(dynamic_cache, entries, opts) - end + defcacheapi put_new_all!(entries, opts \\ []), to: KV - @impl true - def delete(key, opts \\ []) do - dynamic_cache opts, do: KV.delete(dynamic_cache, key, opts) - end + defcacheapi delete(key, opts \\ []), to: KV - @impl true - def delete!(key, opts \\ []) do - dynamic_cache opts, do: KV.delete!(dynamic_cache, key, opts) - end + defcacheapi delete!(key, opts \\ []), to: KV - @impl true - def take(key, opts \\ []) do - dynamic_cache opts, do: KV.take(dynamic_cache, key, opts) - end + defcacheapi take(key, opts \\ []), to: KV - @impl true - def take!(key, opts \\ []) do - dynamic_cache opts, do: KV.take!(dynamic_cache, key, opts) - end + defcacheapi take!(key, opts \\ []), to: KV - @impl true - def has_key?(key, opts \\ []) do - dynamic_cache opts, do: KV.has_key?(dynamic_cache, key, opts) - end + defcacheapi has_key?(key, opts \\ []), to: KV - @impl true - def get_and_update(key, fun, opts \\ []) do - dynamic_cache opts, do: KV.get_and_update(dynamic_cache, key, fun, opts) - end + defcacheapi get_and_update(key, fun, opts \\ []), to: KV - @impl true - def get_and_update!(key, fun, opts \\ []) do - dynamic_cache opts, do: KV.get_and_update!(dynamic_cache, key, fun, opts) - end + defcacheapi get_and_update!(key, fun, opts \\ []), to: KV - @impl true - def update(key, initial, fun, opts \\ []) do - dynamic_cache opts, do: KV.update(dynamic_cache, key, initial, fun, opts) - end + defcacheapi update(key, initial, fun, opts \\ []), to: KV - @impl true - def update!(key, initial, fun, opts \\ []) do - dynamic_cache opts, do: KV.update!(get_dynamic_cache(), key, initial, fun, opts) - end + defcacheapi update!(key, initial, fun, opts \\ []), to: KV - @impl true - def incr(key, amount \\ 1, opts \\ []) do - dynamic_cache opts, do: KV.incr(dynamic_cache, key, amount, opts) - end + defcacheapi incr(key, amount \\ 1, opts \\ []), to: KV - @impl true - def incr!(key, amount \\ 1, opts \\ []) do - dynamic_cache opts, do: KV.incr!(dynamic_cache, key, amount, opts) - end + defcacheapi incr!(key, amount \\ 1, opts \\ []), to: KV - @impl true - def decr(key, amount \\ 1, opts \\ []) do - dynamic_cache opts, do: KV.decr(dynamic_cache, key, amount, opts) - end + defcacheapi decr(key, amount \\ 1, opts \\ []), to: KV - @impl true - def decr!(key, amount \\ 1, opts \\ []) do - dynamic_cache opts, do: KV.decr!(dynamic_cache, key, amount, opts) - end + defcacheapi decr!(key, amount \\ 1, opts \\ []), to: KV - @impl true - def ttl(key, opts \\ []) do - dynamic_cache opts, do: KV.ttl(dynamic_cache, key, opts) - end + defcacheapi ttl(key, opts \\ []), to: KV - @impl true - def ttl!(key, opts \\ []) do - dynamic_cache opts, do: KV.ttl!(dynamic_cache, key, opts) - end + defcacheapi ttl!(key, opts \\ []), to: KV - @impl true - def expire(key, ttl, opts \\ []) do - dynamic_cache opts, do: KV.expire(dynamic_cache, key, ttl, opts) - end + defcacheapi expire(key, ttl, opts \\ []), to: KV - @impl true - def expire!(key, ttl, opts \\ []) do - dynamic_cache opts, do: KV.expire!(dynamic_cache, key, ttl, opts) - end + defcacheapi expire!(key, ttl, opts \\ []), to: KV - @impl true - def touch(key, opts \\ []) do - dynamic_cache opts, do: KV.touch(dynamic_cache, key, opts) - end + defcacheapi touch(key, opts \\ []), to: KV - @impl true - def touch!(key, opts \\ []) do - dynamic_cache opts, do: KV.touch!(dynamic_cache, key, opts) - end + defcacheapi touch!(key, opts \\ []), to: KV end end @@ -606,45 +547,21 @@ defmodule Nebulex.Cache do quote do alias Nebulex.Cache.Queryable - @impl true - def all(query \\ nil, opts \\ []) do - dynamic_cache opts, do: Queryable.all(dynamic_cache, query, opts) - end + defcacheapi all(query \\ nil, opts \\ []), to: Queryable - @impl true - def all!(query \\ nil, opts \\ []) do - dynamic_cache opts, do: Queryable.all!(dynamic_cache, query, opts) - end + defcacheapi all!(query \\ nil, opts \\ []), to: Queryable - @impl true - def count_all(query \\ nil, opts \\ []) do - dynamic_cache opts, do: Queryable.count_all(dynamic_cache, query, opts) - end + defcacheapi count_all(query \\ nil, opts \\ []), to: Queryable - @impl true - def count_all!(query \\ nil, opts \\ []) do - dynamic_cache opts, do: Queryable.count_all!(dynamic_cache, query, opts) - end + defcacheapi count_all!(query \\ nil, opts \\ []), to: Queryable - @impl true - def delete_all(query \\ nil, opts \\ []) do - dynamic_cache opts, do: Queryable.delete_all(dynamic_cache, query, opts) - end + defcacheapi delete_all(query \\ nil, opts \\ []), to: Queryable - @impl true - def delete_all!(query \\ nil, opts \\ []) do - dynamic_cache opts, do: Queryable.delete_all!(dynamic_cache, query, opts) - end + defcacheapi delete_all!(query \\ nil, opts \\ []), to: Queryable - @impl true - def stream(query \\ nil, opts \\ []) do - dynamic_cache opts, do: Queryable.stream(dynamic_cache, query, opts) - end + defcacheapi stream(query \\ nil, opts \\ []), to: Queryable - @impl true - def stream!(query \\ nil, opts \\ []) do - dynamic_cache opts, do: Queryable.stream!(dynamic_cache, query, opts) - end + defcacheapi stream!(query \\ nil, opts \\ []), to: Queryable end end @@ -652,25 +569,13 @@ defmodule Nebulex.Cache do quote do alias Nebulex.Cache.Persistence - @impl true - def dump(path, opts \\ []) do - dynamic_cache opts, do: Persistence.dump(dynamic_cache, path, opts) - end + defcacheapi dump(path, opts \\ []), to: Persistence - @impl true - def dump!(path, opts \\ []) do - dynamic_cache opts, do: Persistence.dump!(dynamic_cache, path, opts) - end + defcacheapi dump!(path, opts \\ []), to: Persistence - @impl true - def load(path, opts \\ []) do - dynamic_cache opts, do: Persistence.load(dynamic_cache, path, opts) - end + defcacheapi load(path, opts \\ []), to: Persistence - @impl true - def load!(path, opts \\ []) do - dynamic_cache opts, do: Persistence.load!(dynamic_cache, path, opts) - end + defcacheapi load!(path, opts \\ []), to: Persistence end end @@ -678,15 +583,9 @@ defmodule Nebulex.Cache do quote do alias Nebulex.Cache.Transaction - @impl true - def transaction(opts \\ [], fun) do - dynamic_cache opts, do: Transaction.transaction(dynamic_cache, opts, fun) - end + defcacheapi transaction(fun, opts \\ []), to: Transaction - @impl true - def in_transaction?(opts \\ []) do - dynamic_cache opts, do: Transaction.in_transaction?(dynamic_cache, opts) - end + defcacheapi in_transaction?(opts \\ []), to: Transaction end end @@ -694,20 +593,11 @@ defmodule Nebulex.Cache do quote do alias Nebulex.Cache.Stats - @impl true - def stats(opts \\ []) do - dynamic_cache opts, do: Stats.stats(dynamic_cache, opts) - end + defcacheapi stats(opts \\ []), to: Stats - @impl true - def stats!(opts \\ []) do - dynamic_cache opts, do: Stats.stats!(dynamic_cache, opts) - end + defcacheapi stats!(opts \\ []), to: Stats - @impl true - def dispatch_stats(opts \\ []) do - dynamic_cache opts, do: Stats.dispatch_stats(dynamic_cache, opts) - end + defcacheapi dispatch_stats(opts \\ []), to: Stats end end @@ -787,6 +677,16 @@ defmodule Nebulex.Cache do @doc group: "Runtime API" @callback stop(opts()) :: :ok + @doc """ + Same as `c:stop/1` but stops the cache instance given in the first argument + `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + """ + @doc group: "Runtime API" + @callback stop(dynamic_cache(), opts()) :: :ok + @doc """ Returns the atom name or pid of the current cache (based on Ecto dynamic repo). @@ -794,34 +694,32 @@ defmodule Nebulex.Cache do See also `c:put_dynamic_cache/1`. """ @doc group: "Runtime API" - @callback get_dynamic_cache() :: atom() | pid() + @callback get_dynamic_cache() :: dynamic_cache() @doc """ Sets the dynamic cache to be used in further commands (based on Ecto dynamic repo). - There might be cases where we want to have different cache instances but + There are cases where you may want to have different cache instances but access them through the same cache module. By default, when you call `MyApp.Cache.start_link/1`, it will start a cache with the name `MyApp.Cache`. But it is also possible to start multiple caches by using a different name for each of them: MyApp.Cache.start_link(name: :cache1) - MyApp.Cache.start_link(name: :cache2, backend: :shards) + MyApp.Cache.start_link(name: :cache2) You can also start caches without names by explicitly setting the name to `nil`: - MyApp.Cache.start_link(name: nil, backend: :shards) + MyApp.Cache.start_link(name: nil) > **NOTE:** There may be adapters requiring the `:name` option anyway, therefore, it is highly recommended to see the adapter's documentation you want to use. - However, once the cache is started, it is not possible to interact directly - with it, since all operations through `MyApp.Cache` are sent by default to - the cache named `MyApp.Cache`. But you can change the default cache at - compile-time: + All operations through `MyApp.Cache` are sent by default to the cache named + `MyApp.Cache`. But you can change the default cache at compile-time: use Nebulex.Cache, default_dynamic_cache: :cache_name @@ -831,9 +729,15 @@ defmodule Nebulex.Cache do From this moment on, all future commands performed by the current process will run on `:another_cache_name`. + + Additionally, all cache commands optionally support passing the wanted + dynamic cache (name or PID) as the first argument so you can o directly + interact with a cache instance. See the + ["Dynamic caches"](#module-dynamic-caches) section at the module + documentation for more information. """ @doc group: "Runtime API" - @callback put_dynamic_cache(atom() | pid()) :: atom() | pid() + @callback put_dynamic_cache(dynamic_cache()) :: dynamic_cache() @doc """ Invokes the given function `fun` for the dynamic cache `name_or_pid`. @@ -847,7 +751,7 @@ defmodule Nebulex.Cache do See `c:get_dynamic_cache/0` and `c:put_dynamic_cache/1`. """ @doc group: "Runtime API" - @callback with_dynamic_cache(name_or_pid :: atom() | pid(), (() -> any())) :: any() + @callback with_dynamic_cache(dynamic_cache(), fun()) :: any() ## Nebulex.Adapter.KV @@ -872,7 +776,6 @@ defmodule Nebulex.Cache do iex> MyCache.put("foo", "bar") :ok - iex> MyCache.fetch("foo") {:ok, "bar"} @@ -883,6 +786,21 @@ defmodule Nebulex.Cache do @doc group: "KV API" @callback fetch(key(), opts()) :: ok_error_tuple(value(), fetch_error_reason()) + @doc """ + Same as `c:fetch/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Example + + MyCache.fetch(MyCache1, "key", []) + + """ + @doc group: "KV API" + @callback fetch(dynamic_cache(), key(), opts()) :: ok_error_tuple(value(), fetch_error_reason()) + @doc """ Same as `c:fetch/2` but raises `Nebulex.KeyError` if the cache doesn't contain `key`, or `Nebulex.Error` if any other error occurs while executing @@ -891,6 +809,14 @@ defmodule Nebulex.Cache do @doc group: "KV API" @callback fetch!(key(), opts()) :: value() + @doc """ + Same as `c:fetch/3` but raises `Nebulex.KeyError` if the cache doesn't + contain `key`, or `Nebulex.Error` if any other error occurs while executing + the command. + """ + @doc group: "KV API" + @callback fetch!(dynamic_cache(), key(), opts()) :: value() + @doc """ Gets a value from cache where the key matches the given `key`. @@ -911,7 +837,6 @@ defmodule Nebulex.Cache do iex> MyCache.put("foo", "bar") :ok - iex> MyCache.get("foo") {:ok, "bar"} @@ -925,18 +850,40 @@ defmodule Nebulex.Cache do @doc group: "KV API" @callback get(key(), default :: value(), opts()) :: ok_error_tuple(value()) + @doc """ + Same as `c:get/3`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Example + + MyCache.get(MyCache1, "key", nil, []) + + """ + @doc group: "KV API" + @callback get(dynamic_cache(), key(), default :: value(), opts()) :: ok_error_tuple(value()) + @doc """ Same as `c:get/3` but raises an exception if an error occurs. """ @doc group: "KV API" @callback get!(key(), default :: value(), opts()) :: value() + @doc """ + Same as `c:get!/4` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback get!(dynamic_cache(), key(), default :: value(), opts()) :: value() + @doc """ Returns a map in the shape of `{:ok, map}` with the key-value pairs of all specified `keys`. For every key that does not hold a value or does not exist, it is ignored and not added into the returned map. - Returns `{:error, reason}` if an error occurs while executing the command. + Returns `{:error, Nebulex.Error.t()}` if an error occurs while executing + the command. ## Options @@ -947,7 +894,6 @@ defmodule Nebulex.Cache do iex> MyCache.put_all([a: 1, c: 3]) :ok - iex> MyCache.get_all([:a, :b, :c]) {:ok, %{a: 1, c: 3}} @@ -955,12 +901,33 @@ defmodule Nebulex.Cache do @doc group: "KV API" @callback get_all(keys :: [key()], opts()) :: ok_error_tuple(map()) + @doc """ + Same as `c:get_all/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Example + + MyCache.get_all(MyCache1, [:a, :b, :c], []) + + """ + @doc group: "KV API" + @callback get_all(dynamic_cache(), keys :: [key()], opts()) :: ok_error_tuple(map()) + @doc """ Same as `c:get_all/2` but raises an exception if an error occurs. """ @doc group: "KV API" @callback get_all!(keys :: [key()], opts()) :: map() + @doc """ + Same as `c:get_all!/3` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback get_all!(dynamic_cache(), keys :: [key()], opts()) :: map() + @doc """ Puts the given `value` under `key` into the Cache. @@ -968,7 +935,8 @@ defmodule Nebulex.Cache do time to live associated with the key is discarded on successful `put` operation. - Returns `:ok` if successful, or `{:error, reason}` if an error occurs. + Returns `:ok` if successful, or `{:error, Nebulex.Error.t()}` + if an error occurs. ## Options @@ -995,24 +963,48 @@ defmodule Nebulex.Cache do iex> MyCache.put("foo", "bar", ttl: :timer.minutes(1)) :ok - iex> MyCache.put("foo", "bar", ttl: :timer.seconds(1)) + iex> MyCache.put("foo", "bar", ttl: :timer.seconds(30)) :ok """ @doc group: "KV API" @callback put(key(), value(), opts()) :: :ok | error_tuple() + @doc """ + Same as `c:put/3`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Example + + MyCache.put(MyCache1, "foo", "bar", []) + + MyCache.put(MyCache2, "foo", "bar", ttl: :timer.hours(1)) + + """ + @doc group: "KV API" + @callback put(dynamic_cache(), key(), value(), opts()) :: :ok | error_tuple() + @doc """ Same as `c:put/3` but raises an exception if an error occurs. """ @doc group: "KV API" @callback put!(key(), value(), opts()) :: :ok + @doc """ + Same as `c:put!/4` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback put!(dynamic_cache(), key(), value(), opts()) :: :ok + @doc """ Puts the given `entries` (key/value pairs) into the cache. It replaces existing values with new values (just as regular `put`). - Returns `:ok` if successful, or `{:error, reason}` if an error occurs. + Returns `:ok` if successful, or `{:error, Nebulex.Error.t()}` + if an error occurs. ## Options @@ -1028,7 +1020,7 @@ defmodule Nebulex.Cache do iex> MyCache.put_all(apples: 3, bananas: 1) :ok - iex> MyCache.put_all(%{apples: 2, oranges: 1}, ttl: 10_000) + iex> MyCache.put_all(%{apples: 2, oranges: 1}, ttl: :timer.hours(1)) :ok **NOTE:** Ideally, this operation should be atomic, so all given keys are @@ -1039,12 +1031,35 @@ defmodule Nebulex.Cache do @doc group: "KV API" @callback put_all(entries(), opts()) :: :ok | error_tuple() + @doc """ + Same as `c:put_all/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Example + + MyCache.put_all(MyCache1, [apples: 3, bananas: 1], []) + + MyCache.put_all(MyCache1, %{apples: 2, oranges: 1}, ttl: :timer.hours(1)) + + """ + @doc group: "KV API" + @callback put_all(dynamic_cache(), entries(), opts()) :: :ok | error_tuple() + @doc """ Same as `c:put_all/2` but raises an exception if an error occurs. """ @doc group: "KV API" @callback put_all!(entries(), opts()) :: :ok + @doc """ + Same as `c:put_all!/3` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback put_all!(dynamic_cache(), entries(), opts()) :: :ok + @doc """ Puts the given `value` under `key` into the cache, only if it does not already exist. @@ -1052,7 +1067,7 @@ defmodule Nebulex.Cache do Returns `{:ok, true}` if a value was set, otherwise, `{:ok, false}` is returned. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. ## Options @@ -1067,20 +1082,42 @@ defmodule Nebulex.Cache do iex> MyCache.put_new("foo", "bar") {:ok, true} - - iex> MyCache.put_new("foo", "bar") + iex> MyCache.put_new("foo", "bar", ttt: :timer.hours(1)) {:ok, false} """ @doc group: "KV API" @callback put_new(key(), value(), opts()) :: ok_error_tuple(boolean()) + @doc """ + Same as `c:put_new/3`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Example + + MyCache.put_new(MyCache1, "foo", "bar", []) + + MyCache.put_new(MyCache1, "foo", "bar", ttt: :timer.hours(1)) + + """ + @doc group: "KV API" + @callback put_new(dynamic_cache(), key(), value(), opts()) :: ok_error_tuple(boolean()) + @doc """ Same as `c:put_new/3` but raises an exception if an error occurs. """ @doc group: "KV API" @callback put_new!(key(), value(), opts()) :: boolean() + @doc """ + Same as `c:put_new!/4` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback put_new!(dynamic_cache(), key(), value(), opts()) :: boolean() + @doc """ Puts the given `entries` (key/value pairs) into the `cache`. It will not perform any operation at all even if just a single key already exists. @@ -1088,7 +1125,7 @@ defmodule Nebulex.Cache do Returns `{:ok, true}` if all entries were successfully set, or `{:ok, false}` if no key was set (at least one key already existed). - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. ## Options @@ -1103,8 +1140,7 @@ defmodule Nebulex.Cache do iex> MyCache.put_new_all(apples: 3, bananas: 1) {:ok, true} - - iex> MyCache.put_new_all(%{apples: 3, oranges: 1}, ttl: 10_000) + iex> MyCache.put_new_all(%{apples: 3, oranges: 1}, ttl: :timer.hours(1)) {:ok, false} **NOTE:** Ideally, this operation should be atomic, so all given keys are @@ -1115,12 +1151,35 @@ defmodule Nebulex.Cache do @doc group: "KV API" @callback put_new_all(entries(), opts()) :: ok_error_tuple(boolean()) + @doc """ + Same as `c:put_new_all/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Example + + MyCache.put_new_all(MyCache1, [apples: 3, bananas: 1], []) + + MyCache.put_new_all(MyCache1, %{apples: 3, oranges: 1}, ttl: 10_000) + + """ + @doc group: "KV API" + @callback put_new_all(dynamic_cache(), entries(), opts()) :: ok_error_tuple(boolean()) + @doc """ Same as `c:put_new_all/2` but raises an exception if an error occurs. """ @doc group: "KV API" @callback put_new_all!(entries(), opts()) :: boolean() + @doc """ + Same as `c:put_new_all!/3` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback put_new_all!(dynamic_cache(), entries(), opts()) :: boolean() + @doc """ Alters the entry stored under `key`, but only if the entry already exists into the Cache. @@ -1128,7 +1187,7 @@ defmodule Nebulex.Cache do Returns `{:ok, true}` if a value was set, otherwise, `{:ok, false}` is returned. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. ## Options @@ -1143,10 +1202,8 @@ defmodule Nebulex.Cache do iex> MyCache.replace("foo", "bar") {:ok, false} - iex> MyCache.put_new("foo", "bar") {:ok, true} - iex> MyCache.replace("foo", "bar2") {:ok, true} @@ -1159,16 +1216,39 @@ defmodule Nebulex.Cache do @doc group: "KV API" @callback replace(key(), value(), opts()) :: ok_error_tuple(boolean()) + @doc """ + Same as `c:replace/3`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Example + + MyCache.replace(MyCache1, "foo", "bar", []) + + MyCache.replace(MyCache1, "foo", "bar", ttl: :timer.hours(1)) + + """ + @doc group: "KV API" + @callback replace(dynamic_cache(), key(), value(), opts()) :: ok_error_tuple(boolean()) + @doc """ Same as `c:replace/3` but raises an exception if an error occurs. """ @doc group: "KV API" @callback replace!(key(), value(), opts()) :: boolean() + @doc """ + Same as `c:replace!/4` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback replace!(dynamic_cache(), key(), value(), opts()) :: boolean() + @doc """ Deletes the entry in cache for a specific `key`. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. ## Options @@ -1179,10 +1259,8 @@ defmodule Nebulex.Cache do iex> MyCache.put(:a, 1) :ok - iex> MyCache.delete(:a) :ok - iex> MyCache.get!(:a) nil @@ -1193,12 +1271,33 @@ defmodule Nebulex.Cache do @doc group: "KV API" @callback delete(key(), opts()) :: :ok | error_tuple() + @doc """ + Same as `c:delete/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Example + + iex> MyCache.delete(MyCache1, :a, []) + + """ + @doc group: "KV API" + @callback delete(dynamic_cache(), key(), opts()) :: :ok | error_tuple() + @doc """ Same as `c:delete/2` but raises an exception if an error occurs. """ @doc group: "KV API" @callback delete!(key(), opts()) :: :ok + @doc """ + Same as `c:delete!/3` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback delete!(dynamic_cache(), key(), opts()) :: :ok + @doc """ Removes and returns the value associated with `key` in the cache. @@ -1220,7 +1319,6 @@ defmodule Nebulex.Cache do iex> MyCache.put(:a, 1) :ok - iex> MyCache.take(:a) {:ok, 1} @@ -1231,19 +1329,40 @@ defmodule Nebulex.Cache do @doc group: "KV API" @callback take(key(), opts()) :: ok_error_tuple(value(), fetch_error_reason()) + @doc """ + Same as `c:take/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.take(MyCache1, :a, []) + + """ + @doc group: "KV API" + @callback take(dynamic_cache(), key(), opts()) :: ok_error_tuple(value(), fetch_error_reason()) + @doc """ Same as `c:take/2` but raises an exception if an error occurs. """ @doc group: "KV API" @callback take!(key(), opts()) :: value() + @doc """ + Same as `c:take!/3` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback take!(dynamic_cache(), key(), opts()) :: value() + @doc """ Determines if the cache contains an entry for the specified `key`. More formally, returns `{:ok, true}` if the cache contains the given `key`. If the cache doesn't contain `key`, `{:ok, :false}` is returned. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. ## Options @@ -1254,7 +1373,6 @@ defmodule Nebulex.Cache do iex> MyCache.put(:a, 1) :ok - iex> MyCache.has_key?(:a) {:ok, true} @@ -1265,6 +1383,21 @@ defmodule Nebulex.Cache do @doc group: "KV API" @callback has_key?(key(), opts()) :: ok_error_tuple(boolean()) + @doc """ + Same as `c:has_key?/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.has_key?(MyCache1, :a, []) + + """ + @doc group: "KV API" + @callback has_key?(dynamic_cache(), key(), opts()) :: ok_error_tuple(boolean()) + @doc """ Increments the counter stored at `key` by the given `amount`, and returns the current count in the shape of `{:ok, count}`. @@ -1272,7 +1405,7 @@ defmodule Nebulex.Cache do If `amount < 0` (negative), the value is decremented by that `amount` instead. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. ## Options @@ -1291,10 +1424,8 @@ defmodule Nebulex.Cache do iex> MyCache.incr(:a) {:ok, 1} - iex> MyCache.incr(:a, 2) {:ok, 3} - iex> MyCache.incr(:a, -1) {:ok, 2} @@ -1305,12 +1436,33 @@ defmodule Nebulex.Cache do @doc group: "KV API" @callback incr(key(), amount :: integer(), opts()) :: ok_error_tuple(integer()) + @doc """ + Same as `c:incr/3`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.incr(MyCache1, :a, 1, []) + + """ + @doc group: "KV API" + @callback incr(dynamic_cache(), key(), amount :: integer(), opts()) :: ok_error_tuple(integer()) + @doc """ Same as `c:incr/3` but raises an exception if an error occurs. """ @doc group: "KV API" @callback incr!(key(), amount :: integer(), opts()) :: integer() + @doc """ + Same as `c:incr!/4` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback incr!(dynamic_cache(), key(), amount :: integer(), opts()) :: integer() + @doc """ Decrements the counter stored at `key` by the given `amount`, and returns the current count in the shape of `{:ok, count}`. @@ -1318,7 +1470,7 @@ defmodule Nebulex.Cache do If `amount < 0` (negative), the value is incremented by that `amount` instead (opposite to `incr/3`). - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. ## Options @@ -1337,10 +1489,8 @@ defmodule Nebulex.Cache do iex> MyCache.decr(:a) {:ok, -1} - iex> MyCache.decr(:a, 2) {:ok, -3} - iex> MyCache.decr(:a, -1) {:ok, -2} @@ -1351,12 +1501,33 @@ defmodule Nebulex.Cache do @doc group: "KV API" @callback decr(key(), amount :: integer(), opts()) :: ok_error_tuple(integer()) + @doc """ + Same as `c:decr/3`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.decr(MyCache1, :a, 1, []) + + """ + @doc group: "KV API" + @callback decr(dynamic_cache(), key(), amount :: integer(), opts()) :: ok_error_tuple(integer()) + @doc """ Same as `c:decr/3` but raises an exception if an error occurs. """ @doc group: "KV API" @callback decr!(key(), amount :: integer(), opts()) :: integer() + @doc """ + Same as `c:decr!/4` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback decr!(dynamic_cache(), key(), amount :: integer(), opts()) :: integer() + @doc """ Returns the remaining time-to-live for the given `key`. @@ -1378,13 +1549,10 @@ defmodule Nebulex.Cache do iex> MyCache.put(:a, 1, ttl: 5000) :ok - iex> MyCache.put(:b, 2) :ok - iex> MyCache.ttl(:a) {:ok, _remaining_ttl} - iex> MyCache.ttl(:b) {:ok, :infinity} @@ -1395,17 +1563,38 @@ defmodule Nebulex.Cache do @doc group: "KV API" @callback ttl(key(), opts()) :: ok_error_tuple(timeout(), fetch_error_reason()) + @doc """ + Same as `c:ttl/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.ttl(MyCache1, :a, []) + + """ + @doc group: "KV API" + @callback ttl(dynamic_cache(), key(), opts()) :: ok_error_tuple(timeout(), fetch_error_reason()) + @doc """ Same as `c:ttl/2` but raises an exception if an error occurs. """ @doc group: "KV API" @callback ttl!(key(), opts()) :: timeout() + @doc """ + Same as `c:ttl!/3` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback ttl!(dynamic_cache(), key(), opts()) :: timeout() + @doc """ Returns `{:ok, true}` if the given `key` exists and the new `ttl` was successfully updated, otherwise, `{:ok, false}` is returned. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. ## Options @@ -1416,10 +1605,8 @@ defmodule Nebulex.Cache do iex> MyCache.put(:a, 1) :ok - - iex> MyCache.expire(:a, 5) + iex> MyCache.expire(:a, :timer.hours(1)) {:ok, true} - iex> MyCache.expire(:a, :infinity) {:ok, true} @@ -1430,17 +1617,38 @@ defmodule Nebulex.Cache do @doc group: "KV API" @callback expire(key(), ttl :: timeout(), opts()) :: ok_error_tuple(boolean()) + @doc """ + Same as `c:expire/3`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.expire(MyCache1, :a, :timer.hours(1), []) + + """ + @doc group: "KV API" + @callback expire(dynamic_cache(), key(), ttl :: timeout(), opts()) :: ok_error_tuple(boolean()) + @doc """ Same as `c:expire/3` but raises an exception if an error occurs. """ @doc group: "KV API" @callback expire!(key(), ttl :: timeout(), opts()) :: boolean() + @doc """ + Same as `c:expire!/4` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback expire!(dynamic_cache(), key(), ttl :: timeout(), opts()) :: boolean() + @doc """ Returns `{:ok, true}` if the given `key` exists and the last access time was successfully updated, otherwise, `{:ok, false}` is returned. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. ## Options @@ -1451,7 +1659,6 @@ defmodule Nebulex.Cache do iex> MyCache.put(:a, 1) :ok - iex> MyCache.touch(:a) {:ok, true} @@ -1462,12 +1669,33 @@ defmodule Nebulex.Cache do @doc group: "KV API" @callback touch(key(), opts()) :: ok_error_tuple(boolean()) + @doc """ + Same as `c:touch/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.touch(MyCache1, :a, []) + + """ + @doc group: "KV API" + @callback touch(dynamic_cache(), key(), opts()) :: ok_error_tuple(boolean()) + @doc """ Same as `c:touch/2` but raises an exception if an error occurs. """ @doc group: "KV API" @callback touch!(key(), opts()) :: boolean() + @doc """ + Same as `c:touch!/3` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback touch!(dynamic_cache(), key(), opts()) :: boolean() + @doc """ Gets the value from `key` and updates it, all in one pass. @@ -1482,7 +1710,7 @@ defmodule Nebulex.Cache do * `{:ok, {current_value, new_value}}` - The `current_value` is the current cached value and `new_value` the updated one returned by `fun`. - * `{:error, reason}` - an error occurred while executing the command. + * `{:error, Nebulex.Error.t()}` - An error occurred executing the command. ## Options @@ -1525,6 +1753,27 @@ defmodule Nebulex.Cache do ok_error_tuple({current_value, new_value}) when current_value: value(), new_value: value() + @doc """ + Same as `c:get_and_update/3`, but the command is executed on the cache + instance given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.get_and_update(MyCache1, :a, &{&1, "value!"}, []) + + """ + @doc group: "KV API" + @callback get_and_update( + dynamic_cache(), + key(), + (value() -> {current_value, new_value} | :pop), + opts() + ) :: ok_error_tuple({current_value, new_value}) + when current_value: value(), new_value: value() + @doc """ Same as `c:get_and_update/3` but raises an exception if an error occurs. """ @@ -1533,6 +1782,18 @@ defmodule Nebulex.Cache do {current_value, new_value} when current_value: value(), new_value: value() + @doc """ + Same as `c:get_and_update!/4` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback get_and_update!( + dynamic_cache(), + key(), + (value() -> {current_value, new_value} | :pop), + opts() + ) :: {current_value, new_value} + when current_value: value(), new_value: value() + @doc """ Updates the cached `key` with the given function. @@ -1545,7 +1806,7 @@ defmodule Nebulex.Cache do * `{:ok, value}` - The value associated to the given `key` has been updated. - * `{:error, reason}` - an error occurred while executing the command. + * `{:error, Nebulex.Error.t()}` - An error occurred executing the command. ## Options @@ -1560,7 +1821,6 @@ defmodule Nebulex.Cache do iex> MyCache.update(:a, 1, &(&1 * 2)) {:ok, 1} - iex> MyCache.update(:a, 1, &(&1 * 2)) {:ok, 2} @@ -1569,32 +1829,63 @@ defmodule Nebulex.Cache do @callback update(key(), initial :: value(), (value() -> value()), opts()) :: ok_error_tuple(value()) + @doc """ + Same as `c:update/4`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.update(MyCache1, :a, 1, &(&1 * 2), []) + + """ + @doc group: "KV API" + @callback update(dynamic_cache(), key(), initial :: value(), (value() -> value()), opts()) :: + ok_error_tuple(value()) + @doc """ Same as `c:update/4` but raises an exception if an error occurs. """ @doc group: "KV API" @callback update!(key(), initial :: value(), (value() -> value()), opts()) :: value() + @doc """ + Same as `c:update!/5` but raises an exception if an error occurs. + """ + @doc group: "KV API" + @callback update!(dynamic_cache(), key(), initial :: value(), (value() -> value()), opts()) :: + value() + ## Nebulex.Adapter.Queryable @optional_callbacks all: 2, + all: 3, all!: 2, + all!: 3, count_all: 2, + count_all: 3, count_all!: 2, + count_all!: 3, delete_all: 2, + delete_all: 3, delete_all!: 2, + delete_all!: 3, stream: 2, - stream!: 2 + stream: 3, + stream!: 2, + stream!: 3 @doc """ Fetches all entries from cache matching the given `query`. This function returns: - * `{:ok, matched_entries}` - the query is valid, then it is executed - and the matched entries are returned. + * `{:ok, matched_entries}` - The query was successfully executed, + the matched entries are returned. - * `{:error, reason}` - an error occurred while executing the command. + * `{:error, Nebulex.Error.t()}` - An error occurred executing the command. ## Query values @@ -1710,21 +2001,42 @@ defmodule Nebulex.Cache do @doc group: "Query API" @callback all(query :: any(), opts()) :: ok_error_tuple([any()]) + @doc """ + Same as `c:all/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.all(MyCache1, nil, []) + + """ + @doc group: "Query API" + @callback all(dynamic_cache(), query :: any(), opts()) :: ok_error_tuple([any()]) + @doc """ Same as `c:all/2` but raises an exception if an error occurs. """ @doc group: "Query API" @callback all!(query :: any(), opts()) :: [any()] + @doc """ + Same as `c:all!/3` but raises an exception if an error occurs. + """ + @doc group: "Query API" + @callback all!(dynamic_cache(), query :: any(), opts()) :: [any()] + @doc """ Similar to `c:all/2` but returns a lazy enumerable that emits all entries from the cache matching the given `query`. This function returns: - * `{:ok, Enum.t()}` - the query is valid, then the stream is returned. + * `{:ok, Enum.t()}` - The query is valid, then the stream is returned. - * `{:error, reason}` - an error occurred while executing the command. + * `{:error, Nebulex.Error.t()}` - An error occurred executing the command. ## Query values @@ -1813,22 +2125,43 @@ defmodule Nebulex.Cache do @doc group: "Query API" @callback stream(query :: any(), opts()) :: ok_error_tuple(Enum.t()) + @doc """ + Same as `c:stream/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.stream(MyCache1, nil, []) + + """ + @doc group: "Query API" + @callback stream(dynamic_cache(), query :: any(), opts()) :: ok_error_tuple(Enum.t()) + @doc """ Same as `c:stream/2` but raises an exception if an error occurs. """ @doc group: "Query API" @callback stream!(query :: any(), opts()) :: Enum.t() + @doc """ + Same as `c:stream!/3` but raises an exception if an error occurs. + """ + @doc group: "Query API" + @callback stream!(dynamic_cache(), query :: any(), opts()) :: Enum.t() + @doc """ Deletes all entries matching the given `query`. If `query` is `nil`, then all entries in the cache are deleted. This function returns: - * `{:ok, deleted_count}` - the query is valid, then the matched entries - are deleted and the `deleted_count` is returned. + * `{:ok, deleted_count}` - The query was successfully executed, + the deleted entries count is returned. - * `{:error, reason}` - an error occurred while executing the command. + * `{:error, Nebulex.Error.t()}` - An error occurred executing the command. ## Query values @@ -1861,12 +2194,33 @@ defmodule Nebulex.Cache do @doc group: "Query API" @callback delete_all(query :: any(), opts()) :: ok_error_tuple(non_neg_integer()) + @doc """ + Same as `c:delete_all/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.delete_all(MyCache1, nil, []) + + """ + @doc group: "Query API" + @callback delete_all(dynamic_cache(), query :: any(), opts()) :: ok_error_tuple(non_neg_integer()) + @doc """ Same as `c:delete_all/2` but raises an exception if an error occurs. """ @doc group: "Query API" @callback delete_all!(query :: any(), opts()) :: integer() + @doc """ + Same as `c:delete_all!/3` but raises an exception if an error occurs. + """ + @doc group: "Query API" + @callback delete_all!(dynamic_cache(), query :: any(), opts()) :: integer() + @doc """ Counts all entries in cache matching the given `query`. @@ -1875,10 +2229,10 @@ defmodule Nebulex.Cache do This function returns: - * `{:ok, count}` - the query is valid, then the `count` of the - matched entries returned. + * `{:ok, count}` - The query was successfully executed, the `count` of the + matched entries is returned. - * `{:error, reason}` - an error occurred while executing the command. + * `{:error, Nebulex.Error.t()}` - An error occurred executing the command. ## Query values @@ -1911,20 +2265,42 @@ defmodule Nebulex.Cache do @doc group: "Query API" @callback count_all(query :: any(), opts()) :: ok_error_tuple(non_neg_integer()) + @doc """ + Same as `c:count_all/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.count_all(MyCache1, nil, []) + + """ + @doc group: "Query API" + @callback count_all(dynamic_cache(), query :: any(), opts()) :: ok_error_tuple(non_neg_integer()) + @doc """ Same as `c:count_all/2` but raises an exception if an error occurs. """ @doc group: "Query API" @callback count_all!(query :: any(), opts()) :: non_neg_integer() + @doc """ + Same as `c:count_all!/3` but raises an exception if an error occurs. + """ + @doc group: "Query API" + @callback count_all!(dynamic_cache(), query :: any(), opts()) :: non_neg_integer() + ## Nebulex.Adapter.Persistence - @optional_callbacks dump: 2, dump!: 2, load: 2, load!: 2 + @optional_callbacks dump: 2, dump: 3, dump!: 2, dump!: 3, load: 2, load: 3, load!: 2, load!: 3 @doc """ Dumps a cache to the given file `path`. - Returns `:ok` if successful, or `{:error, reason}` if an error occurs. + Returns `:ok` if successful, or `{:error, Nebulex.Error.t()}` + if an error occurs. ## Options @@ -1954,16 +2330,38 @@ defmodule Nebulex.Cache do @doc group: "Persistence API" @callback dump(path :: Path.t(), opts()) :: :ok | error_tuple() + @doc """ + Same as `c:dump/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.dump(MyCache1, "my_cache", []) + + """ + @doc group: "Persistence API" + @callback dump(dynamic_cache(), path :: Path.t(), opts()) :: :ok | error_tuple() + @doc """ Same as `c:dump/2` but raises an exception if an error occurs. """ @doc group: "Persistence API" @callback dump!(path :: Path.t(), opts()) :: :ok + @doc """ + Same as `c:dump!/3` but raises an exception if an error occurs. + """ + @doc group: "Persistence API" + @callback dump!(dynamic_cache(), path :: Path.t(), opts()) :: :ok + @doc """ Loads a dumped cache from the given `path`. - Returns `:ok` if successful, or `{:error, reason}` if an error occurs. + Returns `:ok` if successful, or `{:error, Nebulex.Error.t()}` + if an error occurs. ## Options @@ -1998,27 +2396,48 @@ defmodule Nebulex.Cache do @doc group: "Persistence API" @callback load(path :: Path.t(), opts()) :: :ok | error_tuple() + @doc """ + Same as `c:load/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.load(MyCache1, "my_cache", []) + + """ + @doc group: "Persistence API" + @callback load(dynamic_cache(), path :: Path.t(), opts()) :: :ok | error_tuple() + @doc """ Same as `c:load/2` but raises an exception if an error occurs. """ @doc group: "Persistence API" @callback load!(path :: Path.t(), opts()) :: :ok + @doc """ + Same as `c:load!/3` but raises an exception if an error occurs. + """ + @doc group: "Persistence API" + @callback load!(dynamic_cache(), path :: Path.t(), opts()) :: :ok + ## Nebulex.Adapter.Transaction - @optional_callbacks transaction: 2, in_transaction?: 1 + @optional_callbacks transaction: 2, transaction: 3, in_transaction?: 1, in_transaction?: 2 @doc """ Runs the given function inside a transaction. + If an Elixir exception occurs, the exception will bubble up from the + transaction function. If the transaction is aborted, + `{:error, Nebulex.Error.t()}` is returned. + A successful transaction returns the value returned by the function wrapped in a tuple as `{:ok, value}`. - In case the transaction cannot be executed, then `{:error, reason}` is - returned. - - If an unhandled error/exception occurs, the error will bubble up from the - transaction function. + ### Nested transactions If `transaction/2` is called inside another transaction, the function is simply executed without wrapping the new transaction call in any way. @@ -2030,31 +2449,58 @@ defmodule Nebulex.Cache do ## Examples - MyCache.transaction fn -> + MyCache.transaction(fn -> alice = MyCache.get(:alice) bob = MyCache.get(:bob) MyCache.put(:alice, %{alice | balance: alice.balance + 100}) MyCache.put(:bob, %{bob | balance: bob.balance + 100}) - end + end) Locking only the involved key (recommended): - MyCache.transaction [keys: [:alice, :bob]], fn -> - alice = MyCache.get(:alice) - bob = MyCache.get(:bob) - MyCache.put(:alice, %{alice | balance: alice.balance + 100}) - MyCache.put(:bob, %{bob | balance: bob.balance + 100}) - end + MyCache.transaction( + fn -> + alice = MyCache.get(:alice) + bob = MyCache.get(:bob) + MyCache.put(:alice, %{alice | balance: alice.balance + 100}) + MyCache.put(:bob, %{bob | balance: bob.balance + 100}) + end, + [keys: [:alice, :bob]] + ) """ @doc group: "Transaction API" - @callback transaction(opts(), (() -> any())) :: ok_error_tuple(any()) + @callback transaction(fun(), opts()) :: ok_error_tuple(any()) + + @doc """ + Same as `c:transaction/2`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.transaction( + MyCache1, + fn -> + alice = MyCache.get(:alice) + bob = MyCache.get(:bob) + MyCache.put(:alice, %{alice | balance: alice.balance + 100}) + MyCache.put(:bob, %{bob | balance: bob.balance + 100}) + end, + [keys: [:alice, :bob]] + ) + + """ + @doc group: "Transaction API" + @callback transaction(dynamic_cache(), fun(), opts()) :: ok_error_tuple(any()) @doc """ Returns `{:ok, true}` if the current process is inside a transaction, otherwise, `{:ok, false}` is returned. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. ## Options @@ -2074,26 +2520,47 @@ defmodule Nebulex.Cache do @doc group: "Transaction API" @callback in_transaction?(opts()) :: ok_error_tuple(boolean()) - ## Nebulex.Adapter.Stats + @doc """ + Same as `c:in_transaction?/1`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. - @optional_callbacks stats: 1, stats!: 1, dispatch_stats: 1 + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. - @doc """ - Returns current stats values. + ## Examples - This function returns: + MyCache.in_transaction?(MyCache1, []) + + """ + @doc group: "Transaction API" + @callback in_transaction?(dynamic_cache(), opts()) :: ok_error_tuple(boolean()) + + ## Nebulex.Adapter.Stats - * `{:ok, Nebulex.Stats.t()}` - stats are enabled and available - for the cache. + @optional_callbacks stats: 1, + stats: 2, + stats!: 1, + stats!: 2, + dispatch_stats: 1, + dispatch_stats: 2 - * `{:error, reason}` - an error occurred while executing the command. + @doc """ + Returns `{:ok, Nebulex.Stats.t()}` if stats are enabled and supported by the + cache. In case an error occurs, `{:error, Nebulex.Error.t()}` is returned. + + **IMPORTANT:** Each adapter is responsible for providing stats by implementing + `Nebulex.Adapter.Stats` behaviour. See the ["Stats"](#module-stats) section + for more information. ## Options See the ["Shared options"](#module-shared-options) section at the module documentation for more options. - ## Example + ## Examples + + This example assumes the adapter uses the default implementation provided by + `Nebulex.Adapter.Stats`: iex> MyCache.stats() {:ok, @@ -2112,16 +2579,37 @@ defmodule Nebulex.Cache do @doc group: "Stats API" @callback stats(opts()) :: ok_error_tuple(Nebulex.Stats.t()) + @doc """ + Same as `c:stats/1`, but the command is executed on the cache instance + given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.stats(MyCache1, []) + + """ + @doc group: "Stats API" + @callback stats(dynamic_cache(), opts()) :: ok_error_tuple(Nebulex.Stats.t()) + @doc """ Same as `c:stats/1` but raises an exception if an error occurs. """ @doc group: "Stats API" @callback stats!(opts()) :: Nebulex.Stats.t() + @doc """ + Same as `c:stats!/2` but raises an exception if an error occurs. + """ + @doc group: "Stats API" + @callback stats!(dynamic_cache(), opts()) :: Nebulex.Stats.t() + @doc """ Emits a telemetry event when called with the current stats count. - Returns `{:error, reason}` if an error occurs. + Returns `{:error, Nebulex.Error.t()}` if an error occurs. The telemetry `:measurements` map will include the same as `Nebulex.Stats.t()`'s measurements. For example: @@ -2168,4 +2656,19 @@ defmodule Nebulex.Cache do """ @doc group: "Stats API" @callback dispatch_stats(opts()) :: :ok | error_tuple() + + @doc """ + Same as `c:dispatch_stats/1`, but the command is executed on the cache + instance given at the first argument `dynamic_cache`. + + See the ["Dynamic caches"](#module-dynamic-caches) section at the + module documentation for more information. + + ## Examples + + MyCache.dispatch_stats(MyCache1, []) + + """ + @doc group: "Stats API" + @callback dispatch_stats(dynamic_cache(), opts()) :: :ok | error_tuple() end diff --git a/lib/nebulex/cache/kv.ex b/lib/nebulex/cache/kv.ex index 0a2eb557..1c7acfd2 100644 --- a/lib/nebulex/cache/kv.ex +++ b/lib/nebulex/cache/kv.ex @@ -1,7 +1,7 @@ defmodule Nebulex.Cache.KV do @moduledoc false - import Nebulex.Helpers + import Nebulex.Utils, only: [unwrap_or_raise: 1] alias Nebulex.{Adapter, Time} @@ -251,7 +251,7 @@ defmodule Nebulex.Cache.KV do @doc """ Implementation for `c:Nebulex.Cache.update/4`. """ - def update(name, key, initial, fun, opts) do + def update(name, key, initial, fun, opts) when is_function(fun, 1) do Adapter.with_meta(name, fn %{adapter: adapter} = adapter_meta -> value = case adapter.fetch(adapter_meta, key, opts) do diff --git a/lib/nebulex/cache/options.ex b/lib/nebulex/cache/options.ex index 2d5ee250..8d291e24 100644 --- a/lib/nebulex/cache/options.ex +++ b/lib/nebulex/cache/options.ex @@ -82,8 +82,8 @@ defmodule Nebulex.Cache.Options do Flag to define whether or not the cache will provide stats. Each adapter is responsible for providing stats by implementing - `Nebulex.Adapter.Stats` behaviour. See the "Stats" section for - more information. + `Nebulex.Adapter.Stats` behaviour. See the ["Stats"](#module-stats) + section for more information. """ ] ] @@ -108,18 +108,6 @@ defmodule Nebulex.Cache.Options do `:extra_metadata` metadata key of these events. See the "Telemetry events" section for more information. """ - ], - dynamic_cache: [ - type: {:or, [:atom, :pid]}, - required: false, - doc: """ - The name or PID of the cache supervisor process to use for the invoked - cache command. Defaults to `c:get_dynamic_cache/0`. - - There are cases where we want to have different cache instances but access - them through the same cache module. This option tells the executed cache - command what cache instance to use dynamically in runtime. - """ ] ] diff --git a/lib/nebulex/cache/persistence.ex b/lib/nebulex/cache/persistence.ex index aedda6df..cb8be4c6 100644 --- a/lib/nebulex/cache/persistence.ex +++ b/lib/nebulex/cache/persistence.ex @@ -1,7 +1,7 @@ defmodule Nebulex.Cache.Persistence do @moduledoc false - import Nebulex.Helpers + import Nebulex.Utils, only: [unwrap_or_raise: 1] alias Nebulex.Adapter diff --git a/lib/nebulex/cache/queryable.ex b/lib/nebulex/cache/queryable.ex index 90324895..1cfef6f3 100644 --- a/lib/nebulex/cache/queryable.ex +++ b/lib/nebulex/cache/queryable.ex @@ -1,7 +1,7 @@ defmodule Nebulex.Cache.Queryable do @moduledoc false - import Nebulex.Helpers + import Nebulex.Utils, only: [unwrap_or_raise: 1] alias Nebulex.Adapter diff --git a/lib/nebulex/cache/registry.ex b/lib/nebulex/cache/registry.ex index 34900d1d..e80a394a 100644 --- a/lib/nebulex/cache/registry.ex +++ b/lib/nebulex/cache/registry.ex @@ -3,7 +3,7 @@ defmodule Nebulex.Cache.Registry do use GenServer - import Nebulex.Helpers + import Nebulex.Utils, only: [wrap_error: 2] ## API diff --git a/lib/nebulex/cache/stats.ex b/lib/nebulex/cache/stats.ex index 2bcd8a78..f6b4fc68 100644 --- a/lib/nebulex/cache/stats.ex +++ b/lib/nebulex/cache/stats.ex @@ -3,7 +3,7 @@ defmodule Nebulex.Cache.Stats do # use Nebulex.Cache.Options - import Nebulex.Helpers + import Nebulex.Utils, only: [unwrap_or_raise: 1] import Nebulex.Cache.Options, only: [validate_runtime_shared_opts!: 1] alias Nebulex.{Adapter, Telemetry} diff --git a/lib/nebulex/cache/supervisor.ex b/lib/nebulex/cache/supervisor.ex index 093e4ff7..457ed494 100644 --- a/lib/nebulex/cache/supervisor.ex +++ b/lib/nebulex/cache/supervisor.ex @@ -4,7 +4,7 @@ defmodule Nebulex.Cache.Supervisor do use Supervisor import Nebulex.Cache.Options - import Nebulex.Helpers + import Nebulex.Utils alias Nebulex.Telemetry diff --git a/lib/nebulex/cache/transaction.ex b/lib/nebulex/cache/transaction.ex index 846d9c33..b9563a35 100644 --- a/lib/nebulex/cache/transaction.ex +++ b/lib/nebulex/cache/transaction.ex @@ -6,7 +6,7 @@ defmodule Nebulex.Cache.Transaction do @doc """ Implementation for `c:Nebulex.Cache.transaction/2`. """ - def transaction(name, fun, opts) do + def transaction(name, fun, opts) when is_function(fun, 0) do Adapter.with_meta(name, & &1.adapter.transaction(&1, fun, opts)) end diff --git a/lib/nebulex/cache/utils.ex b/lib/nebulex/cache/utils.ex new file mode 100644 index 00000000..66d18de8 --- /dev/null +++ b/lib/nebulex/cache/utils.ex @@ -0,0 +1,33 @@ +defmodule Nebulex.Cache.Utils do + @moduledoc false + + @doc """ + Helper macro for defining the functions implementing the Cache API. + """ + defmacro defcacheapi(fun, to: target) do + {name, args} = Macro.decompose_call(fun) + all_args = defcacheapi_all_args(args) + + quote do + @impl true + def unquote(name)(unquote_splicing(args)) do + unquote(name)( + get_dynamic_cache(), + unquote_splicing(all_args) + ) + end + + @impl true + def unquote(name)(dynamic_cache, unquote_splicing(all_args)) do + unquote(target).unquote(name)(dynamic_cache, unquote_splicing(all_args)) + end + end + end + + defp defcacheapi_all_args(args) do + Enum.map(args, fn + {:\\, _, [arg, _]} -> arg + arg -> arg + end) + end +end diff --git a/lib/nebulex/caching/decorators.ex b/lib/nebulex/caching/decorators.ex index dfed9492..cd221740 100644 --- a/lib/nebulex/caching/decorators.ex +++ b/lib/nebulex/caching/decorators.ex @@ -407,7 +407,7 @@ if Code.ensure_loaded?(Decorator.Define) do use Decorator.Define, cacheable: 1, cache_evict: 1, cache_put: 1 - import Nebulex.Helpers + import Nebulex.Utils, only: [get_option: 4, get_option: 5] import Record ## Types diff --git a/lib/nebulex/helpers.ex b/lib/nebulex/utils.ex similarity index 78% rename from lib/nebulex/helpers.ex rename to lib/nebulex/utils.ex index 95e0085c..dd678fb4 100644 --- a/lib/nebulex/helpers.ex +++ b/lib/nebulex/utils.ex @@ -1,5 +1,5 @@ -defmodule Nebulex.Helpers do - # Module for general purpose helpers. +defmodule Nebulex.Utils do + # Module for general purpose utilities. @moduledoc false ## API @@ -12,7 +12,7 @@ defmodule Nebulex.Helpers do ## Examples - iex> Nebulex.Helpers.get_option( + iex> Nebulex.Utils.get_option( ...> [keys: [1, 2, 3]], ...> :keys, ...> "a list with at least one element", @@ -20,7 +20,7 @@ defmodule Nebulex.Helpers do ...> ) [1, 2, 3] - iex> Nebulex.Helpers.get_option( + iex> Nebulex.Utils.get_option( ...> [], ...> :keys, ...> "a list with at least one element", @@ -28,7 +28,7 @@ defmodule Nebulex.Helpers do ...> ) nil - iex> Nebulex.Helpers.get_option( + iex> Nebulex.Utils.get_option( ...> [keys: 123], ...> :keys, ...> "a list with at least one element", @@ -85,13 +85,13 @@ defmodule Nebulex.Helpers do ## Examples - iex> Nebulex.Helpers.camelize_and_concat([Foo, :bar]) + iex> Nebulex.Utils.camelize_and_concat([Foo, :bar]) Foo.Bar - iex> Nebulex.Helpers.camelize_and_concat([Foo, "bar"]) + iex> Nebulex.Utils.camelize_and_concat([Foo, "bar"]) Foo.Bar - iex> Nebulex.Helpers.camelize_and_concat([Foo, "Bar", 1]) + iex> Nebulex.Utils.camelize_and_concat([Foo, "Bar", 1]) :"Elixir.Foo.Bar.1" """ @@ -102,19 +102,6 @@ defmodule Nebulex.Helpers do |> Module.concat() end - @doc """ - Similar to `Keyword.pop_first/3`, but lazily returns and removes the first - value associated with key in the keyword list. - """ - @spec kw_pop_first_lazy(keyword, term, (() -> term)) :: {term, keyword} - @compile {:inline, kw_pop_first_lazy: 3} - def kw_pop_first_lazy(keywords, key, fun) when is_list(keywords) and is_atom(key) do - case :lists.keytake(key, 1, keywords) do - {:value, {^key, value}, rest} -> {value, rest} - false -> {fun.(), keywords} - end - end - ## Macros @doc false diff --git a/test/nebulex/cache/supervisor_test.exs b/test/nebulex/cache/supervisor_test.exs index 7dc4fbc1..161c9df0 100644 --- a/test/nebulex/cache/supervisor_test.exs +++ b/test/nebulex/cache/supervisor_test.exs @@ -61,7 +61,7 @@ defmodule Nebulex.Cache.SupervisorTest do assert {:ok, pid} = Cache.start_link(name: nil) assert Process.alive?(pid) - assert Cache.stop(dynamic_cache: pid) == :ok + assert Cache.stop(pid, []) == :ok refute Process.alive?(pid) end @@ -74,7 +74,7 @@ defmodule Nebulex.Cache.SupervisorTest do assert [{^pid, _}] = Registry.lookup(Registry.ViaTest, "test") - assert Cache.stop(dynamic_cache: pid) == :ok + assert Cache.stop(pid, []) == :ok refute Process.alive?(pid) end diff --git a/test/nebulex/helpers_test.exs b/test/nebulex/helpers_test.exs index 1e337d44..e25a8511 100644 --- a/test/nebulex/helpers_test.exs +++ b/test/nebulex/helpers_test.exs @@ -1,12 +1,12 @@ -defmodule Nebulex.HelpersTest do +defmodule Nebulex.UtilsTest do use ExUnit.Case, async: true - doctest Nebulex.Helpers + doctest Nebulex.Utils - alias Nebulex.Helpers + alias Nebulex.Utils describe "module_behaviours/2" do test "ok: returns implemented modules" do - assert Helpers.module_behaviours(Nebulex.TestAdapter, "module") == [ + assert Utils.module_behaviours(Nebulex.TestAdapter, "module") == [ Nebulex.Adapter, Nebulex.Adapter.KV, Nebulex.Adapter.Queryable, @@ -18,20 +18,20 @@ defmodule Nebulex.HelpersTest do test "error: invalid module" do assert_raise ArgumentError, fn -> - Helpers.module_behaviours(InvalidModule, "module") + Utils.module_behaviours(InvalidModule, "module") end end end describe "assert_behaviour/3" do test "ok: returns implemented modules" do - assert Helpers.assert_behaviour(Nebulex.TestAdapter, Nebulex.Adapter) == Nebulex.TestAdapter + assert Utils.assert_behaviour(Nebulex.TestAdapter, Nebulex.Adapter) == Nebulex.TestAdapter end end test "error: behaviour not implemented" do assert_raise ArgumentError, fn -> - Helpers.assert_behaviour(Nebulex.TestAdapter, XYZ) + Utils.assert_behaviour(Nebulex.TestAdapter, XYZ) end end end diff --git a/test/nebulex/stats_test.exs b/test/nebulex/stats_test.exs index 8fd7eaee..4b2dae41 100644 --- a/test/nebulex/stats_test.exs +++ b/test/nebulex/stats_test.exs @@ -119,13 +119,13 @@ defmodule Nebulex.StatsTest do describe "disabled stats:" do setup_with_cache Cache, stats: false - test "stats/0 returns nil" do + test "stats/0 raises an exception" do assert_raise Nebulex.Error, ~r"stats disabled or not supported by the cache", fn -> Cache.stats!() end end - test "dispatch_stats/1 is skipped" do + test "dispatch_stats/1 returns an error" do with_telemetry_handler(__MODULE__, [@event], fn -> assert {:error, %Nebulex.Error{reason: :stats_error}} = Cache.dispatch_stats() end) diff --git a/test/shared/cache/kv_test.exs b/test/shared/cache/kv_test.exs index a6f660d8..d7805114 100644 --- a/test/shared/cache/kv_test.exs +++ b/test/shared/cache/kv_test.exs @@ -34,17 +34,17 @@ defmodule Nebulex.Cache.KVTest do end end - test "with :dynamic_cache option", %{cache: cache} = ctx do + test "with dynamic_cache", %{cache: cache} = ctx do if name = Map.get(ctx, :name) do - assert cache.put("foo", "bar", dynamic_cache: name) == :ok - assert cache.fetch!("foo", dynamic_cache: name) == "bar" - assert cache.delete("foo", dynamic_cache: name) == :ok + assert cache.put(name, "foo", "bar", []) == :ok + assert cache.fetch!(name, "foo", []) == "bar" + assert cache.delete(name, "foo", []) == :ok end end - test "with :dynamic_cache option raise and exception", %{cache: cache} do + test "with dynamic_cache raises an exception", %{cache: cache} do assert_raise Nebulex.Error, ~r"could not lookup", fn -> - cache.put!("foo", "bar", dynamic_cache: :invalid) + cache.put!(:invalid, "foo", "bar", []) end end end diff --git a/test/shared/cache/transaction_test.exs b/test/shared/cache/transaction_test.exs index b1b7c090..9566badf 100644 --- a/test/shared/cache/transaction_test.exs +++ b/test/shared/cache/transaction_test.exs @@ -5,29 +5,33 @@ defmodule Nebulex.Cache.TransactionTest do describe "transaction" do test "ok: single transaction", %{cache: cache} do assert cache.transaction(fn -> - with :ok <- cache.put(1, 11), - 11 <- cache.fetch!(1), - :ok <- cache.delete(1) do - cache.get!(1) - end + :ok = cache.put!(1, 11) + + 11 = cache.fetch!(1) + + :ok = cache.delete!(1) + + cache.get!(1) end) == {:ok, nil} end test "ok: nested transaction", %{cache: cache} do assert cache.transaction( - [keys: [1]], fn -> cache.transaction( - [keys: [2]], fn -> - with :ok <- cache.put(1, 11), - 11 <- cache.fetch!(1), - :ok <- cache.delete(1) do - cache.get!(1) - end - end + :ok = cache.put!(1, 11) + + 11 = cache.fetch!(1) + + :ok = cache.delete!(1) + + cache.get!(1) + end, + keys: [2] ) - end + end, + keys: [1] ) == {:ok, {:ok, nil}} end @@ -36,12 +40,14 @@ defmodule Nebulex.Cache.TransactionTest do assert cache.fetch!(:test) == ["old value"] assert cache.transaction( - [keys: [:test]], fn -> ["old value"] = value = cache.fetch!(:test) - :ok = cache.put(:test, ["new value" | value]) + + :ok = cache.put!(:test, ["new value" | value]) + cache.fetch!(:test) - end + end, + keys: [:test] ) == {:ok, ["new value", "old value"]} assert cache.fetch!(:test) == ["new value", "old value"] @@ -50,11 +56,13 @@ defmodule Nebulex.Cache.TransactionTest do test "error: exception is raised", %{cache: cache} do assert_raise MatchError, fn -> cache.transaction(fn -> - with :ok <- cache.put(1, 11), - 11 <- cache.fetch!(1), - :ok <- cache.delete(1) do - :ok = cache.get(1) - end + :ok = cache.put!(1, 11) + + 11 = cache.fetch!(1) + + :ok = cache.delete!(1) + + :ok = cache.get(1) end) end end @@ -66,12 +74,13 @@ defmodule Nebulex.Cache.TransactionTest do _ = cache.put_dynamic_cache(name) cache.transaction( - [keys: [key], retries: 1], fn -> :ok = cache.put(key, true) Process.sleep(1100) - end + end, + keys: [key], + retries: 1 ) end) @@ -80,8 +89,9 @@ defmodule Nebulex.Cache.TransactionTest do assert_raise Nebulex.Error, ~r"cache #{inspect(name)} has aborted a transaction", fn -> {:error, %Nebulex.Error{} = reason} = cache.transaction( - [keys: [key], retries: 1], - fn -> cache.get(key) end + fn -> cache.get(key) end, + keys: [key], + retries: 1 ) raise reason @@ -95,6 +105,7 @@ defmodule Nebulex.Cache.TransactionTest do cache.transaction(fn -> :ok = cache.put(1, 11, return: :key) + {:ok, true} = cache.in_transaction?() end) end diff --git a/test/support/test_adapter.exs b/test/support/test_adapter.exs index f4e4aad2..bab5d4e8 100644 --- a/test/support/test_adapter.exs +++ b/test/support/test_adapter.exs @@ -40,7 +40,7 @@ defmodule Nebulex.TestAdapter do # Inherit default stats implementation use Nebulex.Adapter.Stats - import Nebulex.Helpers + import Nebulex.Utils alias Nebulex.Adapter.Stats alias __MODULE__.{Entry, KV} @@ -243,8 +243,8 @@ defmodule Nebulex.TestAdapter do ## Nebulex.Adapter.Transaction @impl true - defspan transaction(adapter_meta, opts, fun) do - super(adapter_meta, opts, fun) + defspan transaction(adapter_meta, fun, opts) do + super(adapter_meta, fun, opts) end @impl true @@ -272,7 +272,7 @@ defmodule Nebulex.TestAdapter.KV do use GenServer - import Nebulex.Helpers, only: [wrap_error: 2] + import Nebulex.Utils, only: [wrap_error: 2] alias Nebulex.Telemetry alias Nebulex.Telemetry.StatsHandler