diff --git a/README.md b/README.md index 035ecccd..11f76c26 100644 --- a/README.md +++ b/README.md @@ -66,10 +66,12 @@ file: def deps do [ {:nebulex, "~> 3.0"}, + #=> When using the local cache adapter {:nebulex_adapters_local, "~> 3.0"}, - {:decorator, "~> 1.4"}, #=> When using Caching Annotations - {:telemetry, "~> 1.0"}, #=> When using the Telemetry events (Nebulex stats) - {:shards, "~> 1.0"}, #=> When using :shards as backend + #=> When using Caching Annotations + {:decorator, "~> 1.4"}, + #=> When using the Telemetry events (Nebulex stats) + {:telemetry, "~> 1.0"} ] end ``` @@ -85,10 +87,6 @@ makes all dependencies optional. For example: you have to add `:telemetry` to the dependency list. See [telemetry guide][telemetry]. - * For intensive workloads, you may want to use `:shards` as the backend for - the local adapter and having partitioned tables. In such a case, you have - to add `:shards` to the dependency list. - [telemetry]: http://hexdocs.pm/nebulex/telemetry.html Then run `mix deps.get` in your shell to fetch the dependencies. If you want to @@ -96,7 +94,7 @@ use another cache adapter, just choose the proper dependency from the table above. Finally, in the cache definition, you will need to specify the `adapter:` -respective to the chosen dependency. For the local built-in cache it is: +respective to the chosen dependency. For the local cache would be: ```elixir defmodule MyApp.Cache do diff --git a/guides/getting-started.md b/guides/getting-started.md index 979a48e0..81e6aeed 100644 --- a/guides/getting-started.md +++ b/guides/getting-started.md @@ -418,9 +418,9 @@ iex> Blog.Cache.count_all(spec) _num_of_matched_entries ``` -> The previous example assumes you are using the built-in local adapter. +> The previous example assumes you are using `Nebulex.Adapters.Local` adapter. -Also, if you are using the built-in local adapter, you can use the queries +Also, if you are using `Nebulex.Adapters.Local` adapter, you can use the queries `:expired` and `:unexpired` too, like so: ```elixir @@ -443,8 +443,8 @@ _num_of_removed_entries ``` And just like `count_all/2`, you can also provide a custom query to delete only -the matched entries, or if you are using the built-in local adapter you can also -use the queries `:expired` and `:unexpired`. For example: +the matched entries, or if you are using `Nebulex.Adapters.Local` adapter, you +can also use the queries `:expired` and `:unexpired`. For example: ```elixir iex> expired_entries = Blog.Cache.delete_all(:expired) @@ -460,7 +460,7 @@ iex> Blog.Cache.delete_all(spec) _num_of_matched_entries ``` -> These examples assumes you are using the built-in local adapter. +> These examples assumes you are using `Nebulex.Adapters.Local` adapter. ### Stream all entries from cache matching the given query @@ -582,8 +582,8 @@ mix nbx.gen.cache -c Blog.NearCache -a Nebulex.Adapters.Multilevel ``` By default, the command generates a 2-level near-cache topology. The first -level or `L1` using the built-in local adapter, and the second one or `L2` -using the built-in partitioned adapter. +level or `L1` using `Nebulex.Adapters.Local` adapter, and the second one or `L2` +using `Nebulex.Adapters.Partitioned` adapter. The generated cache module `lib/blog/near_cache.ex`: diff --git a/guides/telemetry.md b/guides/telemetry.md index 059a81b8..ae7e7603 100644 --- a/guides/telemetry.md +++ b/guides/telemetry.md @@ -35,7 +35,16 @@ events: adapter before an adapter callback is executed. * Measurement: `%{system_time: System.monotonic_time()}` - * Metadata: `%{adapter_meta: map, function_name: atom, args: [term]}` + * Metadata: + + ```elixir + %{ + adapter_meta: map, + function_name: atom, + args: [term], + extra_metadata: map + } + ``` * `[:my_app, :cache, :command, :stop]` - Dispatched by the underlying cache adapter after an adapter callback has been successfully executed. @@ -48,6 +57,7 @@ events: adapter_meta: map, function_name: atom, args: [term], + extra_metadata: map, result: term } ``` @@ -64,6 +74,7 @@ events: adapter_meta: map, function_name: atom, args: [term], + extra_metadata: map, kind: :error | :exit | :throw, reason: term, stacktrace: term @@ -120,7 +131,7 @@ Telemetry.Metrics.summary( tag_values: &Map.merge(&1, %{ cache: &1.adapter_meta.cache, - adapter: &1.adapter_meta.cache.__adapter__() + adapter: &1.adapter_meta.adapter }) ) ``` @@ -133,7 +144,7 @@ transformation on the event metadata in order to get to the values we need. Each adapter is responsible for providing stats by implementing `Nebulex.Adapter.Stats` behaviour. However, Nebulex provides a simple default implementation using [Erlang counters][erl_counters], which is used by -the built-in local adapter. The local adapter uses +`Nebulex.Adapters.Local` adapter. The local adapter uses `Nebulex.Telemetry.StatsHandler` to aggregate the stats and keep them updated, therefore, it requires the Telemetry events are dispatched by the adapter, otherwise, it won't work properly. @@ -201,8 +212,7 @@ defmodule MyApp.Telemetry do last_value("my_app.cache.stats.misses", tags: [:cache]), last_value("my_app.cache.stats.writes", tags: [:cache]), last_value("my_app.cache.stats.updates", tags: [:cache]), - last_value("my_app.cache.stats.evictions", tags: [:cache]), - last_value("my_app.cache.stats.expirations", tags: [:cache]) + last_value("my_app.cache.stats.evictions", tags: [:cache]) ] end @@ -225,24 +235,24 @@ children = [ ] ``` -Now start an IEx session and call the server: +Now start an IEx session and make some cache calls: ``` -iex(1)> MyApp.Cache.get 1 +iex(1)> MyApp.Cache.get! 1 nil -iex(2)> MyApp.Cache.put 1, 1, ttl: 10 +iex(2)> MyApp.Cache.put! 1, 1, ttl: 10 :ok -iex(3)> MyApp.Cache.get 1 +iex(3)> MyApp.Cache.get! 1 1 -iex(4)> MyApp.Cache.put 2, 2 +iex(4)> MyApp.Cache.put! 2, 2 :ok -iex(5)> MyApp.Cache.delete 2 +iex(5)> MyApp.Cache.delete! 2 :ok iex(6)> Process.sleep(20) :ok -iex(7)> MyApp.Cache.get 1 +iex(7)> MyApp.Cache.get! 1 nil -iex(2)> MyApp.Cache.replace 1, 11 +iex(2)> MyApp.Cache.replace! 1, 11 true ``` @@ -251,7 +261,7 @@ and you should see something like the following output: ``` [Telemetry.Metrics.ConsoleReporter] Got new event! Event name: my_app.cache.stats -All measurements: %{evictions: 2, expirations: 1, hits: 1, misses: 2, updates: 1, writes: 2} +All measurements: %{evictions: 2, hits: 1, misses: 2, updates: 1, writes: 2} All metadata: %{cache: MyApp.Cache} Metric measurement: :hits (last_value) @@ -273,10 +283,6 @@ Tag values: %{cache: MyApp.Cache} Metric measurement: :evictions (last_value) With value: 2 Tag values: %{cache: MyApp.Cache} - -Metric measurement: :expirations (last_value) -With value: 1 -Tag values: %{cache: MyApp.Cache} ``` ### Custom metrics @@ -325,8 +331,7 @@ defp metrics do last_value("my_app.cache.stats.misses", tags: [:cache, :node]), last_value("my_app.cache.stats.writes", tags: [:cache, :node]), last_value("my_app.cache.stats.updates", tags: [:cache, :node]), - last_value("my_app.cache.stats.evictions", tags: [:cache, :node]), - last_value("my_app.cache.stats.expirations", tags: [:cache, :node]), + last_value("my_app.cache.stats.evictions", tags: [:cache, :node]) # Nebulex custom Metrics last_value("my_app.cache.size.value", tags: [:cache, :node]) @@ -339,7 +344,7 @@ If you start an IEx session like previously, you should see the new metric too: ``` [Telemetry.Metrics.ConsoleReporter] Got new event! Event name: my_app.cache.stats -All measurements: %{evictions: 0, expirations: 0, hits: 0, misses: 0, updates: 0, writes: 0} +All measurements: %{evictions: 0, hits: 0, misses: 0, updates: 0, writes: 0} All metadata: %{cache: MyApp.Cache, node: :nonode@nohost} Metric measurement: :hits (last_value) @@ -362,10 +367,6 @@ Metric measurement: :evictions (last_value) With value: 0 Tag values: %{cache: MyApp.Cache, node: :nonode@nohost} -Metric measurement: :expirations (last_value) -With value: 0 -Tag values: %{cache: MyApp.Cache, node: :nonode@nohost} - [Telemetry.Metrics.ConsoleReporter] Got new event! Event name: my_app.cache.size All measurements: %{value: 0} @@ -408,8 +409,8 @@ Then, when you run `MyApp.Multilevel.stats()` you get something like: ```elixir %Nebulex.Stats{ measurements: %{ - l1: %{evictions: 0, expirations: 0, hits: 0, misses: 0, updates: 0, writes: 0}, - l2: %{evictions: 0, expirations: 0, hits: 0, misses: 0, updates: 0, writes: 0} + l1: %{evictions: 0, hits: 0, misses: 0, updates: 0, writes: 0}, + l2: %{evictions: 0, hits: 0, misses: 0, updates: 0, writes: 0} }, metadata: %{ l1: %{ @@ -459,11 +460,6 @@ metrics in this way: measurement: &get_in(&1, [:l1, :evictions]), tags: [:cache] ), - last_value("my_app.cache.stats.l1.expirations", - event_name: "my_app.cache.stats", - measurement: &get_in(&1, [:l1, :expirations]), - tags: [:cache] - ), # L2 metrics last_value("my_app.cache.stats.l2.hits", diff --git a/lib/nebulex/adapter.ex b/lib/nebulex/adapter.ex index 11f15afd..b65e003c 100644 --- a/lib/nebulex/adapter.ex +++ b/lib/nebulex/adapter.ex @@ -3,8 +3,6 @@ defmodule Nebulex.Adapter do Specifies the minimal API required from adapters. """ - alias Nebulex.Telemetry - @typedoc "Adapter" @type t :: module @@ -20,7 +18,7 @@ defmodule Nebulex.Adapter do * `:adapter` - The defined cache adapter. """ - @type adapter_meta :: %{optional(term) => term} + @type adapter_meta() :: %{optional(term) => term} ## Callbacks @@ -33,7 +31,7 @@ defmodule Nebulex.Adapter do Initializes the adapter supervision tree by returning the children and adapter metadata. """ - @callback init(config :: keyword) :: {:ok, :supervisor.child_spec(), adapter_meta} + @callback init(config :: keyword()) :: {:ok, :supervisor.child_spec(), adapter_meta()} # Define optional callbacks @optional_callbacks __before_compile__: 1 @@ -53,7 +51,7 @@ defmodule Nebulex.Adapter do Nebulex.Adapter.lookup_meta(cache.get_dynamic_cache()) """ - @spec lookup_meta(atom | pid) :: {:ok, adapter_meta} | {:error, Nebulex.Error.t()} + @spec lookup_meta(atom() | pid()) :: {:ok, adapter_meta()} | {:error, Nebulex.Error.t()} defdelegate lookup_meta(name_or_pid), to: Nebulex.Cache.Registry, as: :lookup @doc """ @@ -62,82 +60,10 @@ defmodule Nebulex.Adapter do It expects a name or a PID representing the cache. """ - @spec with_meta(atom | pid, (adapter_meta -> term)) :: term | {:error, Nebulex.Error.t()} + @spec with_meta(atom() | pid(), (adapter_meta() -> any())) :: any() | {:error, Nebulex.Error.t()} def with_meta(name_or_pid, fun) do with {:ok, adapter_meta} <- lookup_meta(name_or_pid) do fun.(adapter_meta) end end - - # FIXME: ExCoveralls does not mark most of this section as covered - # coveralls-ignore-start - - @doc """ - Helper macro for the adapters so they can add the logic for emitting the - recommended Telemetry events. - - See the built-in adapters for more information on how to use this macro. - """ - defmacro defspan(fun, opts \\ [], do: block) do - {name, [adapter_meta | args_tl], as, [_ | as_args_tl] = as_args} = build_defspan(fun, opts) - - quote do - def unquote(name)(unquote_splicing(as_args)) - - def unquote(name)(%{telemetry: false} = unquote(adapter_meta), unquote_splicing(args_tl)) do - unquote(block) - end - - def unquote(name)(unquote_splicing(as_args)) do - metadata = %{ - adapter_meta: unquote(adapter_meta), - function_name: unquote(as), - args: unquote(as_args_tl) - } - - Telemetry.span( - unquote(adapter_meta).telemetry_prefix ++ [:command], - metadata, - fn -> - result = - unquote(name)( - Map.merge(unquote(adapter_meta), %{telemetry: false, in_span?: true}), - unquote_splicing(as_args_tl) - ) - - {result, Map.put(metadata, :result, result)} - end - ) - end - end - end - - ## Private Functions - - defp build_defspan(ast, opts) when is_list(opts) do - {name, args} = - case Macro.decompose_call(ast) do - {_, _} = parts -> parts - _ -> raise ArgumentError, "invalid syntax in defspan #{Macro.to_string(ast)}" - end - - as = Keyword.get(opts, :as, name) - as_args = build_as_args(args) - - {name, args, as, as_args} - end - - defp build_as_args(args) do - for {arg, idx} <- Enum.with_index(args) do - arg - |> Macro.to_string() - |> build_as_arg({arg, idx}) - end - end - - # sobelow_skip ["DOS.BinToAtom"] - defp build_as_arg("_" <> _, {{_e1, e2, e3}, idx}), do: {:"var#{idx}", e2, e3} - defp build_as_arg(_, {arg, _idx}), do: arg - - # coveralls-ignore-stop end diff --git a/lib/nebulex/adapter/kv.ex b/lib/nebulex/adapter/kv.ex index 4ac6c1f4..43376033 100644 --- a/lib/nebulex/adapter/kv.ex +++ b/lib/nebulex/adapter/kv.ex @@ -7,25 +7,25 @@ defmodule Nebulex.Adapter.KV do """ @typedoc "Proxy type to the adapter meta" - @type adapter_meta :: Nebulex.Adapter.adapter_meta() + @type adapter_meta() :: Nebulex.Adapter.adapter_meta() @typedoc "Proxy type to the cache key" - @type key :: Nebulex.Cache.key() + @type key() :: Nebulex.Cache.key() @typedoc "Proxy type to the cache value" - @type value :: Nebulex.Cache.value() + @type value() :: Nebulex.Cache.value() @typedoc "Proxy type to the cache options" - @type opts :: Nebulex.Cache.opts() + @type opts() :: Nebulex.Cache.opts() @typedoc "Proxy type to the cache entries" - @type entries :: Nebulex.Cache.entries() + @type entries() :: Nebulex.Cache.entries() @typedoc "TTL for a cache entry" - @type ttl :: timeout + @type ttl() :: timeout() @typedoc "Write command type" - @type on_write :: :put | :put_new | :replace + @type on_write() :: :put | :put_new | :replace @doc """ Fetches the value for a specific `key` in the cache. @@ -41,7 +41,7 @@ defmodule Nebulex.Adapter.KV do See `c:Nebulex.Cache.fetch/2`. """ - @callback fetch(adapter_meta, key, opts) :: + @callback fetch(adapter_meta(), key(), opts()) :: Nebulex.Cache.ok_error_tuple(value, Nebulex.Cache.fetch_error_reason()) @doc """ @@ -53,7 +53,7 @@ defmodule Nebulex.Adapter.KV do See `c:Nebulex.Cache.get_all/2`. """ - @callback get_all(adapter_meta, [key], opts) :: Nebulex.Cache.ok_error_tuple(map) + @callback get_all(adapter_meta(), [key()], opts()) :: Nebulex.Cache.ok_error_tuple(map()) @doc """ Puts the given `value` under `key` into the `cache`. @@ -84,8 +84,8 @@ defmodule Nebulex.Adapter.KV do See `c:Nebulex.Cache.put/3`, `c:Nebulex.Cache.put_new/3`, `c:Nebulex.Cache.replace/3`. """ - @callback put(adapter_meta, key, value, ttl, on_write, opts) :: - Nebulex.Cache.ok_error_tuple(boolean) + @callback put(adapter_meta(), key(), value(), ttl(), on_write(), opts()) :: + Nebulex.Cache.ok_error_tuple(boolean()) @doc """ Puts the given `entries` (key/value pairs) into the `cache`. @@ -117,8 +117,8 @@ defmodule Nebulex.Adapter.KV do See `c:Nebulex.Cache.put_all/2`. """ - @callback put_all(adapter_meta, entries, ttl, on_write, opts) :: - Nebulex.Cache.ok_error_tuple(boolean) + @callback put_all(adapter_meta(), entries(), ttl(), on_write(), opts()) :: + Nebulex.Cache.ok_error_tuple(boolean()) @doc """ Deletes a single entry from cache. @@ -127,7 +127,7 @@ defmodule Nebulex.Adapter.KV do See `c:Nebulex.Cache.delete/2`. """ - @callback delete(adapter_meta, key, opts) :: :ok | Nebulex.Cache.error() + @callback delete(adapter_meta(), key(), opts()) :: :ok | Nebulex.Cache.error_tuple() @doc """ Removes and returns the value associated with `key` in the cache. @@ -143,8 +143,8 @@ defmodule Nebulex.Adapter.KV do See `c:Nebulex.Cache.take/2`. """ - @callback take(adapter_meta, key, opts) :: - Nebulex.Cache.ok_error_tuple(value, Nebulex.Cache.fetch_error_reason()) + @callback take(adapter_meta(), key(), opts()) :: + Nebulex.Cache.ok_error_tuple(value(), Nebulex.Cache.fetch_error_reason()) @doc """ Updates the counter mapped to the given `key`. @@ -158,9 +158,9 @@ defmodule Nebulex.Adapter.KV do See `c:Nebulex.Cache.incr/3`. See `c:Nebulex.Cache.decr/3`. """ - @callback update_counter(adapter_meta, key, amount, ttl, default, opts) :: - Nebulex.Cache.ok_error_tuple(integer) - when amount: integer, default: integer + @callback update_counter(adapter_meta(), key(), amount, ttl(), default, opts()) :: + Nebulex.Cache.ok_error_tuple(integer()) + when amount: integer(), default: integer() @doc """ Determines if the cache contains an entry for the specified `key`. @@ -172,7 +172,7 @@ defmodule Nebulex.Adapter.KV do See `c:Nebulex.Cache.has_key?/2`. """ - @callback has_key?(adapter_meta, key, opts) :: Nebulex.Cache.ok_error_tuple(boolean) + @callback has_key?(adapter_meta(), key(), opts()) :: Nebulex.Cache.ok_error_tuple(boolean()) @doc """ Returns the remaining time-to-live for the given `key`. @@ -188,8 +188,8 @@ defmodule Nebulex.Adapter.KV do See `c:Nebulex.Cache.ttl/2`. """ - @callback ttl(adapter_meta, key, opts) :: - Nebulex.Cache.ok_error_tuple(value, Nebulex.Cache.fetch_error_reason()) + @callback ttl(adapter_meta(), key(), opts()) :: + Nebulex.Cache.ok_error_tuple(value(), Nebulex.Cache.fetch_error_reason()) @doc """ Returns `{:ok, true}` if the given `key` exists and the new `ttl` was @@ -199,7 +199,7 @@ defmodule Nebulex.Adapter.KV do See `c:Nebulex.Cache.expire/3`. """ - @callback expire(adapter_meta, key, ttl, opts) :: Nebulex.Cache.ok_error_tuple(boolean) + @callback expire(adapter_meta(), key(), ttl(), opts()) :: Nebulex.Cache.ok_error_tuple(boolean()) @doc """ Returns `{:ok, true}` if the given `key` exists and the last access time was @@ -209,5 +209,5 @@ defmodule Nebulex.Adapter.KV do See `c:Nebulex.Cache.touch/2`. """ - @callback touch(adapter_meta, key, opts) :: Nebulex.Cache.ok_error_tuple(boolean) + @callback touch(adapter_meta(), key(), opts()) :: Nebulex.Cache.ok_error_tuple(boolean()) end diff --git a/lib/nebulex/adapter/persistence.ex b/lib/nebulex/adapter/persistence.ex index 4e1dcf9a..4dfef65b 100644 --- a/lib/nebulex/adapter/persistence.ex +++ b/lib/nebulex/adapter/persistence.ex @@ -32,7 +32,7 @@ defmodule Nebulex.Adapter.Persistence do See `c:Nebulex.Cache.dump/2`. """ @callback dump(Nebulex.Adapter.adapter_meta(), Path.t(), Nebulex.Cache.opts()) :: - :ok | Nebulex.Cache.error() + :ok | Nebulex.Cache.error_tuple() @doc """ Loads a dumped cache from the given `path`. @@ -42,7 +42,7 @@ defmodule Nebulex.Adapter.Persistence do See `c:Nebulex.Cache.load/2`. """ @callback load(Nebulex.Adapter.adapter_meta(), Path.t(), Nebulex.Cache.opts()) :: - :ok | Nebulex.Cache.error() + :ok | Nebulex.Cache.error_tuple() alias Nebulex.Entry diff --git a/lib/nebulex/adapter/queryable.ex b/lib/nebulex/adapter/queryable.ex index 5a15dacb..cec464ed 100644 --- a/lib/nebulex/adapter/queryable.ex +++ b/lib/nebulex/adapter/queryable.ex @@ -17,16 +17,16 @@ defmodule Nebulex.Adapter.Queryable do The `query` value depends entirely on the adapter implementation; it could any term. Therefore, it is highly recommended to see adapters' documentation - for more information about building queries. For example, the built-in + for more information about building queries. For example, `Nebulex.Adapters.Local` adapter uses `:ets.match_spec()` for queries, as well as other pre-defined ones like `:unexpired` and `:expired`. """ @typedoc "Proxy type to the adapter meta" - @type adapter_meta :: Nebulex.Adapter.adapter_meta() + @type adapter_meta() :: Nebulex.Adapter.adapter_meta() @typedoc "Proxy type to the cache options" - @type opts :: Nebulex.Cache.opts() + @type opts() :: Nebulex.Cache.opts() @doc """ Executes the `query` according to the given `operation`. @@ -51,11 +51,11 @@ defmodule Nebulex.Adapter.Queryable do and `c:Nebulex.Cache.delete_all/2`. """ @callback execute( - adapter_meta, + adapter_meta(), operation :: :all | :count_all | :delete_all, - query :: term, - opts - ) :: Nebulex.Cache.ok_error_tuple([term] | non_neg_integer) + query :: any(), + opts() + ) :: Nebulex.Cache.ok_error_tuple([any()] | non_neg_integer()) @doc """ Streams the given `query`. @@ -69,6 +69,6 @@ defmodule Nebulex.Adapter.Queryable do See `c:Nebulex.Cache.stream/2`. """ - @callback stream(adapter_meta, query :: term, opts) :: + @callback stream(adapter_meta(), query :: any(), opts()) :: Nebulex.Cache.ok_error_tuple(Enumerable.t()) end diff --git a/lib/nebulex/adapter/stats.ex b/lib/nebulex/adapter/stats.ex index f6d44257..f802bd2d 100644 --- a/lib/nebulex/adapter/stats.ex +++ b/lib/nebulex/adapter/stats.ex @@ -5,7 +5,7 @@ defmodule Nebulex.Adapter.Stats do Each adapter is responsible for providing support for stats by implementing this behaviour. However, this module brings with a default implementation using [Erlang counters][https://erlang.org/doc/man/counters.html], with all - callbacks overridable, which is supported by the built-in adapters. + 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 @@ -31,7 +31,7 @@ defmodule Nebulex.Adapter.Stats do See `c:Nebulex.Cache.stats/1`. """ - @callback stats(Nebulex.Adapter.adapter_meta()) :: + @callback stats(Nebulex.Adapter.adapter_meta(), Nebulex.Cache.opts()) :: Nebulex.Cache.ok_error_tuple(Nebulex.Stats.t() | nil) @doc false @@ -39,19 +39,19 @@ defmodule Nebulex.Adapter.Stats do quote do @behaviour Nebulex.Adapter.Stats + import Nebulex.Adapter.Stats, only: [defspan: 2, defspan: 3] import Nebulex.Helpers @impl true - def stats(adapter_meta) do + def stats(adapter_meta, _opts) do if counter_ref = adapter_meta[:stats_counter] do stats = %Nebulex.Stats{ measurements: %{ hits: :counters.get(counter_ref, 1), misses: :counters.get(counter_ref, 2), - writes: :counters.get(counter_ref, 3), - updates: :counters.get(counter_ref, 4), - evictions: :counters.get(counter_ref, 5), - expirations: :counters.get(counter_ref, 6) + evictions: :counters.get(counter_ref, 3), + writes: :counters.get(counter_ref, 4), + updates: :counters.get(counter_ref, 5) }, metadata: %{ cache: adapter_meta[:name] || adapter_meta[:cache] @@ -66,10 +66,13 @@ defmodule Nebulex.Adapter.Stats do end end - defoverridable stats: 1 + defoverridable stats: 2 end end + alias Nebulex.Cache.Options + alias Nebulex.Telemetry + @doc """ Initializes the Erlang's counter to be used by the adapter. See the module documentation for more information about the stats default implementation. @@ -116,8 +119,131 @@ defmodule Nebulex.Adapter.Stats do def incr(nil, _stat, _incr), do: :ok def incr(ref, :hits, incr), do: :counters.add(ref, 1, incr) def incr(ref, :misses, incr), do: :counters.add(ref, 2, incr) - def incr(ref, :writes, incr), do: :counters.add(ref, 3, incr) - def incr(ref, :updates, incr), do: :counters.add(ref, 4, incr) - def incr(ref, :evictions, incr), do: :counters.add(ref, 5, incr) - def incr(ref, :expirations, incr), do: :counters.add(ref, 6, incr) + def incr(ref, :evictions, incr), do: :counters.add(ref, 3, incr) + def incr(ref, :writes, incr), do: :counters.add(ref, 4, incr) + def incr(ref, :updates, incr), do: :counters.add(ref, 5, incr) + + @doc """ + Helper macro for the adapters to add the logic for emitting the recommended + Telemetry events. See the "Telemetry events" section in `Nebulex.Cache`. + + ## Options + + * `:as` - The function name to add to the metadata of the emitted event + (`:function_name` field). This parameter is optional and defaults to + the name being spanned (`defspan`). + + ## Example + + defmodule MyAdapter do + @behaviour Nebulex.Adapter + @behaviour Nebulex.Adapter.KV + + use Nebulex.Adapter.Stats + + ## Nebulex.Adapter + + @impl true + defmacro __before_compile__(_env), do: :ok + + @impl true + def init(_opts) do + # your init logic ... + end + + ## Nebulex.Adapter.KV + + @impl true + defspan fetch(adapter_meta, key, opts) do + # your logic ... + end + + ... + end + + """ + defmacro defspan(fun, opts \\ [], do: block) do + { + name, + [adapter_meta | args_tl], + as, + [_ | as_args_tl] = as_args + } = build_defspan(fun, opts) + + opts_arg = build_opts_arg(as_args_tl) + + quote do + def unquote(name)(unquote_splicing(as_args)) + + def unquote(name)(%{telemetry: false} = unquote(adapter_meta), unquote_splicing(args_tl)) do + unquote(block) + end + + def unquote(name)(unquote_splicing(as_args)) do + opts = + (is_list(unquote(opts_arg)) && unquote(opts_arg)) + |> Kernel.||([]) + |> Keyword.take([:telemetry_event, :telemetry_metadata]) + |> Options.validate_runtime_shared_opts!() + + telemetry_event = + Keyword.get_lazy(opts, :telemetry_event, fn -> + unquote(adapter_meta).telemetry_prefix ++ [:command] + end) + + metadata = %{ + adapter_meta: unquote(adapter_meta), + function_name: unquote(as), + args: unquote(as_args_tl), + extra_metadata: Keyword.fetch!(opts, :telemetry_metadata) + } + + Telemetry.span( + telemetry_event, + metadata, + fn -> + result = + unquote(name)( + Map.merge(unquote(adapter_meta), %{telemetry: false, in_span?: true}), + unquote_splicing(as_args_tl) + ) + + {result, Map.put(metadata, :result, result)} + end + ) + end + end + end + + ## Private Functions + + defp build_defspan(ast, opts) when is_list(opts) do + {name, args} = + with :error <- Macro.decompose_call(ast) do + raise ArgumentError, "invalid syntax in defspan #{Macro.to_string(ast)}" + end + + as = Keyword.get(opts, :as, name) + as_args = build_as_args(args) + + {name, args, as, as_args} + end + + defp build_as_args(args) do + for {arg, idx} <- Enum.with_index(args) do + arg + |> Macro.to_string() + |> build_as_arg({arg, idx}) + end + end + + # sobelow_skip ["DOS.BinToAtom"] + defp build_as_arg("_" <> _, {{_e1, e2, e3}, idx}), do: {:"var#{idx}", e2, e3} + defp build_as_arg(_, {arg, _idx}), do: arg + + defp build_opts_arg(as_args_tl) do + as_args_tl + |> Enum.reverse() + |> hd() + end end diff --git a/lib/nebulex/adapter/transaction.ex b/lib/nebulex/adapter/transaction.ex index 68e3c78d..9a926689 100644 --- a/lib/nebulex/adapter/transaction.ex +++ b/lib/nebulex/adapter/transaction.ex @@ -66,7 +66,8 @@ defmodule Nebulex.Adapter.Transaction do See `c:Nebulex.Cache.transaction/2`. """ - @callback transaction(Nebulex.Adapter.adapter_meta(), Nebulex.Cache.opts(), fun) :: any + @callback transaction(Nebulex.Adapter.adapter_meta(), Nebulex.Cache.opts(), (() -> any())) :: + any() @doc """ Returns `{:ok, true}` if the current process is inside a transaction, @@ -76,7 +77,8 @@ defmodule Nebulex.Adapter.Transaction do See `c:Nebulex.Cache.in_transaction?/1`. """ - @callback in_transaction?(Nebulex.Adapter.adapter_meta()) :: Nebulex.Cache.ok_error_tuple(boolean) + @callback in_transaction?(Nebulex.Adapter.adapter_meta(), Nebulex.Cache.opts()) :: + Nebulex.Cache.ok_error_tuple(boolean()) @doc false defmacro __using__(_opts) do @@ -100,11 +102,11 @@ defmodule Nebulex.Adapter.Transaction do end @impl true - def in_transaction?(adapter_meta) do + def in_transaction?(adapter_meta, _opts) do wrap_ok do_in_transaction?(adapter_meta) end - defoverridable transaction: 3, in_transaction?: 1 + defoverridable transaction: 3, in_transaction?: 2 ## Helpers diff --git a/lib/nebulex/adapters/nil.ex b/lib/nebulex/adapters/nil.ex index 234d3bc9..531f0178 100644 --- a/lib/nebulex/adapters/nil.ex +++ b/lib/nebulex/adapters/nil.ex @@ -147,5 +147,5 @@ defmodule Nebulex.Adapters.Nil do ## Nebulex.Adapter.Stats @impl true - def stats(_), do: {:ok, %Nebulex.Stats{}} + def stats(_, _), do: {:ok, %Nebulex.Stats{}} end diff --git a/lib/nebulex/cache.ex b/lib/nebulex/cache.ex index ab9e5496..d945b37e 100644 --- a/lib/nebulex/cache.ex +++ b/lib/nebulex/cache.ex @@ -7,9 +7,12 @@ defmodule Nebulex.Cache do adapter. For example, Nebulex ships with a default adapter that implements a local generational cache. - When used, the Cache expects the `:otp_app` and `:adapter` as options. - The `:otp_app` should point to an OTP application that has the cache - configuration. For example, the Cache: + When used, the defined cache can be configured with the following + compilation time options: + + #{Nebulex.Cache.Options.compile_options_docs()} + + For example, the cache: defmodule MyApp.Cache do use Nebulex.Cache, @@ -33,36 +36,14 @@ defmodule Nebulex.Cache do for more information. In spite of this, the following configuration values are shared across all adapters: - * `:name` - The name of the Cache supervisor process. - - * `:telemetry_prefix` - It is recommend for adapters to publish events - using the `Telemetry` library. By default, the telemetry prefix is based - on the module name, so if your module is called `MyApp.Cache`, the prefix - will be `[:my_app, :cache]`. See the "Telemetry events" section to see - what events recommended for the adapters to publish.. Note that if you - have multiple caches, you should keep the `:telemetry_prefix` consistent - for each of them and use the `:cache` and/or `:name` (in case of a named - or dynamic cache) properties in the event metadata for distinguishing - between caches. - - * `:telemetry` - An optional flag to tell the adapters whether Telemetry - events should be emitted or not. Defaults to `true`. - - * `:stats` - Boolean to define whether or not the cache will provide stats. - Defaults to `false`. Each adapter is responsible for providing stats by - implementing `Nebulex.Adapter.Stats` behaviour. See the "Stats" section - below. + #{Nebulex.Cache.Options.start_link_options_docs()} ## Shared options Almost all of the cache functions outlined in this module accept the following options: - * `:dynamic_cache` - The name of the cache supervisor process. It can be - an atom or a PID. There might be 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. + #{Nebulex.Cache.Options.runtime_shared_options_docs()} ## Telemetry events @@ -134,13 +115,16 @@ defmodule Nebulex.Cache do `System.system_time()`. A Telemetry `:metadata` map including the following fields. Each cache adapter - may emit different information here. For built-in adapters, it will contain: + may emit different information here. For adapters using + `Nebulex.Adapter.Stats.defspan/2` will contain: * `:adapter_meta` - The adapter metadata. - * `:function_name` - The name of the invoked adapter function. + * `:function_name` - The name of the invoked adapter's function. * `:args` - The arguments of the invoked adapter function, omitting the - first argument, since it is the adapter metadata already included into + first argument, since the adapter's metadata is already included in the event's metadata. + * `:extra_metadata` - Additional provided metadata via the runtime option + `:telemetry_metadata`. #### `[:my_app, :cache, :command, :stop]` @@ -154,14 +138,17 @@ defmodule Nebulex.Cache do docs for `System.convert_time_unit/3`. A Telemetry `:metadata` map including the following fields. Each cache adapter - may emit different information here. For built-in adapters, it will contain: + may emit different information here. For adapters using + `Nebulex.Adapter.Stats.defspan/2` will contain: * `:adapter_meta` - The adapter metadata. - * `:function_name` - The name of the invoked adapter function. + * `:function_name` - The name of the invoked adapter's function. * `:args` - The arguments of the invoked adapter function, omitting the - first argument, since it is the adapter metadata already included into + first argument, since the adapter's metadata is already included in the event's metadata. - * `:result` - The command result. + * `:extra_metadata` - Additional provided metadata via the runtime option + `:telemetry_metadata`. + * `:result` - The command's result. #### `[:my_app, :cache, :command, :exception]` @@ -175,13 +162,16 @@ defmodule Nebulex.Cache do docs for `System.convert_time_unit/3`. A Telemetry `:metadata` map including the following fields. Each cache adapter - may emit different information here. For built-in adapters, it will contain: + may emit different information here. For adapters using + `Nebulex.Adapter.Stats.defspan/2` will contain: * `:adapter_meta` - The adapter metadata. - * `:function_name` - The name of the invoked adapter function. + * `:function_name` - The name of the invoked adapter's function. * `:args` - The arguments of the invoked adapter function, omitting the - first argument, since it is the adapter metadata already included into + first argument, since the adapter's metadata is already included in the event's metadata. + * `:extra_metadata` - Additional provided metadata via the runtime option + `:telemetry_metadata`. * `:kind` - The type of the error: `:error`, `:exit`, or `:throw`. * `:reason` - The reason of the error. * `:stacktrace` - The stacktrace. @@ -195,9 +185,8 @@ defmodule Nebulex.Cache do Stats are provided by the adapters by implementing the optional behaviour `Nebulex.Adapter.Stats`. This behaviour exposes a callback to return the - current cache stats. Nevertheless, the behaviour brings with a default - implementation using [Erlang counters][counters], which is used by the - local built-in adapter (`Nebulex.Adapters.Local`). + current cache stats. Nevertheless, the behaviour brings with a default + implementation using [Erlang counters][counters]. [counters]: https://erlang.org/doc/man/counters.html @@ -211,13 +200,13 @@ defmodule Nebulex.Cache do > Remember to check if the underlying adapter implements the `Nebulex.Adapter.Stats` behaviour. - See `c:Nebulex.Cache.stats/1` for more information. + See `c:stats/1` for more information. ## Dispatching stats via Telemetry It is possible to emit Telemetry events for the current stats via - `c:Nebulex.Cache.dispatch_stats/1`, but it has to be invoked explicitly; - Nebulex does not emit this Telemetry event automatically. But it is very + `c:dispatch_stats/1`, but it has to be invoked explicitly; Nebulex + does not emit this Telemetry event automatically. But it is very easy to emit this event using [`:telemetry_poller`][telemetry_poller]. [telemetry_poller]: https://github.com/beam-telemetry/telemetry_poller @@ -251,7 +240,7 @@ defmodule Nebulex.Cache do ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] - Supervisor.start_link(children, opts) + Supervisor.start_link(children, opts()) end See [Nebulex Telemetry Guide](http://hexdocs.pm/nebulex/telemetry.html) @@ -271,34 +260,35 @@ defmodule Nebulex.Cache do to `Nebulex.Adapters.Local`. """ - @type t :: module + @typedoc "Cache type" + @type t() :: module() @typedoc "Cache entry key" - @type key :: any + @type key() :: any() @typedoc "Cache entry value" - @type value :: any + @type value() :: any() @typedoc "Cache entries" - @type entries :: map | [{key, value}] + @type entries() :: map() | [{key(), value()}] @typedoc "Cache action options" - @type opts :: keyword + @type opts() :: keyword() - @typedoc "Proxy type for base Nebulex error" - @type nbx_error :: Nebulex.Error.t() + @typedoc "Proxy type for generic Nebulex error" + @type nbx_error_reason() :: Nebulex.Error.t() @typedoc "Fetch error reason" - @type fetch_error_reason :: Nebulex.KeyError.t() | nbx_error + @type fetch_error_reason() :: Nebulex.KeyError.t() | nbx_error_reason() @typedoc "Common error type" - @type error :: error(nbx_error) + @type error_tuple() :: error_tuple(nbx_error_reason()) @typedoc "Error type for the given reason" - @type error(reason) :: {:error, reason} + @type error_tuple(reason) :: {:error, reason} @typedoc "Ok/Error tuple with default error reasons" - @type ok_error_tuple(ok) :: ok_error_tuple(ok, nbx_error) + @type ok_error_tuple(ok) :: ok_error_tuple(ok, nbx_error_reason()) @typedoc "Ok/Error type" @type ok_error_tuple(ok, error) :: {:ok, ok} | {:error, error} @@ -332,13 +322,13 @@ defmodule Nebulex.Cache do quote do @behaviour Nebulex.Cache - {otp_app, adapter, behaviours} = Nebulex.Cache.Supervisor.compile_config(unquote(opts)) + {otp_app, adapter, behaviours, opts} = Nebulex.Cache.Supervisor.compile_config(unquote(opts)) @otp_app otp_app @adapter adapter - @opts unquote(opts) + @opts opts @default_dynamic_cache @opts[:default_dynamic_cache] || __MODULE__ - @default_key_generator @opts[:default_key_generator] || Nebulex.Caching.SimpleKeyGenerator + @default_key_generator Keyword.fetch!(@opts, :default_key_generator) @before_compile adapter end end @@ -428,11 +418,6 @@ defmodule Nebulex.Cache do _ = put_dynamic_cache(default_dynamic_cache) end end - - @impl true - def with_dynamic_cache(name, module, fun, args) do - with_dynamic_cache(name, fn -> apply(module, fun, args) end) - end end end @@ -700,7 +685,7 @@ defmodule Nebulex.Cache do @impl true def in_transaction?(opts \\ []) do - dynamic_cache opts, do: Transaction.in_transaction?(dynamic_cache) + dynamic_cache opts, do: Transaction.in_transaction?(dynamic_cache, opts) end end end @@ -711,12 +696,12 @@ defmodule Nebulex.Cache do @impl true def stats(opts \\ []) do - dynamic_cache opts, do: Stats.stats(dynamic_cache) + dynamic_cache opts, do: Stats.stats(dynamic_cache, opts) end @impl true def stats!(opts \\ []) do - dynamic_cache opts, do: Stats.stats!(dynamic_cache) + dynamic_cache opts, do: Stats.stats!(dynamic_cache, opts) end @impl true @@ -767,7 +752,7 @@ defmodule Nebulex.Cache do If the `c:init/1` callback is implemented in the cache, it will be invoked. """ @doc group: "Runtime API" - @callback config() :: keyword + @callback config() :: keyword() @doc """ Starts a supervision and return `{:ok, pid}` or just `:ok` if nothing @@ -782,10 +767,10 @@ defmodule Nebulex.Cache do for adapter-specific configuration see the adapter's documentation. """ @doc group: "Runtime API" - @callback start_link(opts) :: - {:ok, pid} - | {:error, {:already_started, pid}} - | {:error, term} + @callback start_link(opts()) :: + {:ok, pid()} + | {:error, {:already_started, pid()}} + | {:error, term()} @doc """ Shuts down the cache. @@ -800,7 +785,7 @@ defmodule Nebulex.Cache do documentation for more options. """ @doc group: "Runtime API" - @callback stop(opts) :: :ok + @callback stop(opts()) :: :ok @doc """ Returns the atom name or pid of the current cache @@ -809,7 +794,7 @@ 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() :: atom() | pid() @doc """ Sets the dynamic cache to be used in further commands @@ -848,7 +833,7 @@ defmodule Nebulex.Cache do will run on `:another_cache_name`. """ @doc group: "Runtime API" - @callback put_dynamic_cache(atom | pid) :: atom | pid + @callback put_dynamic_cache(atom() | pid()) :: atom() | pid() @doc """ Invokes the given function `fun` for the dynamic cache `name_or_pid`. @@ -862,25 +847,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, fun) :: term - - @doc """ - For the dynamic cache `name_or_pid`, invokes the given function name `fun` - from `module` with the list of arguments `args`. - - ## Example - - MyCache.with_dynamic_cache(:my_cache, Module, :some_fun, ["foo", "bar"]) - - 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, - module, - fun :: atom, - args :: [term] - ) :: term + @callback with_dynamic_cache(name_or_pid :: atom() | pid(), (() -> any())) :: any() ## Nebulex.Adapter.KV @@ -914,7 +881,7 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback fetch(key, opts) :: ok_error_tuple(value, fetch_error_reason) + @callback fetch(key(), opts()) :: ok_error_tuple(value(), fetch_error_reason()) @doc """ Same as `c:fetch/2` but raises `Nebulex.KeyError` if the cache doesn't @@ -922,7 +889,7 @@ defmodule Nebulex.Cache do the command. """ @doc group: "KV API" - @callback fetch!(key, opts) :: value + @callback fetch!(key(), opts()) :: value() @doc """ Gets a value from cache where the key matches the given `key`. @@ -956,13 +923,13 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback get(key, default :: value, opts) :: ok_error_tuple(value) + @callback get(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 + @callback get!(key(), default :: value(), opts()) :: value() @doc """ Returns a map in the shape of `{:ok, map}` with the key-value pairs of all @@ -986,13 +953,13 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback get_all(keys :: [key], opts) :: ok_error_tuple(map) + @callback get_all(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 + @callback get_all!(keys :: [key()], opts()) :: map() @doc """ Puts the given `value` under `key` into the Cache. @@ -1033,13 +1000,13 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback put(key, value, opts) :: :ok | error + @callback put(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 + @callback put!(key(), value(), opts()) :: :ok @doc """ Puts the given `entries` (key/value pairs) into the cache. It replaces @@ -1070,13 +1037,13 @@ defmodule Nebulex.Cache do the adapter's documentation. """ @doc group: "KV API" - @callback put_all(entries, opts) :: :ok | error + @callback put_all(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 + @callback put_all!(entries(), opts()) :: :ok @doc """ Puts the given `value` under `key` into the cache, only if it does not @@ -1106,13 +1073,13 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback put_new(key, value, opts) :: ok_error_tuple(boolean) + @callback put_new(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 + @callback put_new!(key(), value(), opts()) :: boolean() @doc """ Puts the given `entries` (key/value pairs) into the `cache`. It will not @@ -1146,13 +1113,13 @@ defmodule Nebulex.Cache do the adapter's documentation. """ @doc group: "KV API" - @callback put_new_all(entries, opts) :: ok_error_tuple(boolean) + @callback put_new_all(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 + @callback put_new_all!(entries(), opts()) :: boolean() @doc """ Alters the entry stored under `key`, but only if the entry already exists @@ -1190,13 +1157,13 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback replace(key, value, opts) :: ok_error_tuple(boolean) + @callback replace(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 + @callback replace!(key(), value(), opts()) :: boolean() @doc """ Deletes the entry in cache for a specific `key`. @@ -1224,13 +1191,13 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback delete(key, opts) :: :ok | error + @callback delete(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 + @callback delete!(key(), opts()) :: :ok @doc """ Removes and returns the value associated with `key` in the cache. @@ -1262,13 +1229,13 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback take(key, opts) :: ok_error_tuple(value, fetch_error_reason) + @callback take(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 + @callback take!(key(), opts()) :: value() @doc """ Determines if the cache contains an entry for the specified `key`. @@ -1296,7 +1263,7 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback has_key?(key, opts) :: ok_error_tuple(boolean) + @callback has_key?(key(), opts()) :: ok_error_tuple(boolean()) @doc """ Increments the counter stored at `key` by the given `amount`, and returns @@ -1336,13 +1303,13 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback incr(key, amount :: integer, opts) :: ok_error_tuple(integer) + @callback incr(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 + @callback incr!(key(), amount :: integer(), opts()) :: integer() @doc """ Decrements the counter stored at `key` by the given `amount`, and returns @@ -1382,13 +1349,13 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback decr(key, amount :: integer, opts) :: ok_error_tuple(integer) + @callback decr(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 + @callback decr!(key(), amount :: integer(), opts()) :: integer() @doc """ Returns the remaining time-to-live for the given `key`. @@ -1426,13 +1393,13 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback ttl(key, opts) :: ok_error_tuple(timeout, fetch_error_reason) + @callback ttl(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 + @callback ttl!(key(), opts()) :: timeout() @doc """ Returns `{:ok, true}` if the given `key` exists and the new `ttl` was @@ -1461,13 +1428,13 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback expire(key, ttl :: timeout, opts) :: ok_error_tuple(boolean) + @callback expire(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 + @callback expire!(key(), ttl :: timeout(), opts()) :: boolean() @doc """ Returns `{:ok, true}` if the given `key` exists and the last access time was @@ -1493,13 +1460,13 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback touch(key, opts) :: ok_error_tuple(boolean) + @callback touch(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 + @callback touch!(key(), opts()) :: boolean() @doc """ Gets the value from `key` and updates it, all in one pass. @@ -1554,17 +1521,17 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback get_and_update(key, (value -> {current_value, new_value} | :pop), opts) :: + @callback get_and_update(key(), (value() -> {current_value, new_value} | :pop), opts()) :: ok_error_tuple({current_value, new_value}) - when current_value: value, new_value: value + when current_value: value(), new_value: value() @doc """ Same as `c:get_and_update/3` but raises an exception if an error occurs. """ @doc group: "KV API" - @callback get_and_update!(key, (value -> {current_value, new_value} | :pop), opts) :: + @callback get_and_update!(key(), (value() -> {current_value, new_value} | :pop), opts()) :: {current_value, new_value} - when current_value: value, new_value: value + when current_value: value(), new_value: value() @doc """ Updates the cached `key` with the given function. @@ -1599,13 +1566,14 @@ defmodule Nebulex.Cache do """ @doc group: "KV API" - @callback update(key, initial :: value, (value -> value), opts) :: ok_error_tuple(value) + @callback update(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 + @callback update!(key(), initial :: value(), (value() -> value()), opts()) :: value() ## Nebulex.Adapter.Queryable @@ -1645,7 +1613,7 @@ defmodule Nebulex.Cache do The `query` value depends entirely on the adapter implementation; it could any term. Therefore, it is highly recommended to see adapters' documentation - for more information about supported queries. For example, the built-in + for more information about supported queries. For example, `Nebulex.Adapters.Local` adapter uses `:ets.match_spec()` for queries, as well as other pre-defined ones like `:unexpired` and `:expired`. @@ -1653,10 +1621,10 @@ defmodule Nebulex.Cache do * `:return` - Tells the query what to return from the matched entries. See the possible values in the "Query return option" section below. - The default depends on the adapter, for example, the default for the - built-in adapters is `:key`. This option is supported by the built-in - adapters, but it is recommended to see the adapter's documentation - to confirm its compatibility with this option. + The default depends on the adapter, for example, for + `Nebulex.Adapters.Local` adapter is the `:key`. It is recommended + to see the adapter's documentation to confirm the compatibility + with this option. See the ["Shared options"](#module-shared-options) section at the module documentation for more options. @@ -1704,9 +1672,8 @@ defmodule Nebulex.Cache do ## Query Query spec is defined by the adapter, hence, it is recommended to review - adapters documentation. For instance, the built-in `Nebulex.Adapters.Local` - adapter supports `nil | :unexpired | :expired | :ets.match_spec()` as query - value. + adapters documentation. For instance, the `Nebulex.Adapters.Local` adapter + supports `nil | :unexpired | :expired | :ets.match_spec()` as query value. ## Examples @@ -1741,13 +1708,13 @@ defmodule Nebulex.Cache do """ @doc group: "Query API" - @callback all(query :: term, opts) :: ok_error_tuple([term]) + @callback all(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 :: term, opts) :: [any] + @callback all!(query :: any(), opts()) :: [any()] @doc """ Similar to `c:all/2` but returns a lazy enumerable that emits all entries @@ -1765,12 +1732,7 @@ defmodule Nebulex.Cache do ## Options - * `:return` - Tells the query what to return from the matched entries. - See the possible values in the "Query return option" section below. - The default depends on the adapter, for example, the default for the - built-in adapters is `:key`. This option is supported by the built-in - adapters, but it is recommended to see the adapter's documentation - to confirm its compatibility with this option. + * `:return` - Same as `c:all/2`. * `:page_size` - Positive integer (>= 1) that defines the page size internally used by the adapter for paginating the results coming @@ -1849,13 +1811,13 @@ defmodule Nebulex.Cache do """ @doc group: "Query API" - @callback stream(query :: term, opts) :: ok_error_tuple(Enum.t()) + @callback stream(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 :: term, opts) :: Enum.t() + @callback stream!(query :: any(), opts()) :: Enum.t() @doc """ Deletes all entries matching the given `query`. If `query` is `nil`, @@ -1897,13 +1859,13 @@ defmodule Nebulex.Cache do See `c:all/2` for more examples, the same applies to `c:delete_all/2`. """ @doc group: "Query API" - @callback delete_all(query :: term, opts) :: ok_error_tuple(non_neg_integer) + @callback delete_all(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 :: term, opts) :: integer + @callback delete_all!(query :: any(), opts()) :: integer() @doc """ Counts all entries in cache matching the given `query`. @@ -1947,13 +1909,13 @@ defmodule Nebulex.Cache do See `c:all/2` for more examples, the same applies to `c:count_all/2`. """ @doc group: "Query API" - @callback count_all(query :: term, opts) :: ok_error_tuple(non_neg_integer) + @callback count_all(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 :: term, opts) :: integer + @callback count_all!(query :: any(), opts()) :: non_neg_integer() ## Nebulex.Adapter.Persistence @@ -1968,9 +1930,9 @@ defmodule Nebulex.Cache do This operation relies entirely on the adapter implementation, which means the options depend on each of them. For that reason, it is recommended to review - the documentation of the adapter to be used. The built-in adapters inherit - the default implementation from `Nebulex.Adapter.Persistence`, hence, review - the available options there. + the documentation of the adapter to be used. Some adapters inherit the default + implementation from `Nebulex.Adapter.Persistence`, hence, review the available + options there. See the ["Shared options"](#module-shared-options) section at the module documentation for more options. @@ -1990,13 +1952,13 @@ defmodule Nebulex.Cache do """ @doc group: "Persistence API" - @callback dump(path :: Path.t(), opts) :: :ok | error + @callback dump(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 + @callback dump!(path :: Path.t(), opts()) :: :ok @doc """ Loads a dumped cache from the given `path`. @@ -2007,9 +1969,9 @@ defmodule Nebulex.Cache do Similar to `c:dump/2`, this operation relies entirely on the adapter implementation, therefore, it is recommended to review the documentation - of the adapter to be used. Similarly, the built-in adapters inherit the - default implementation from `Nebulex.Adapter.Persistence`, hence, review - the available options there. + of the adapter to be used. Similarly, some adapters inherit the default + implementation from `Nebulex.Adapter.Persistence`, hence, review the + available options there. See the ["Shared options"](#module-shared-options) section at the module documentation for more options. @@ -2034,13 +1996,13 @@ defmodule Nebulex.Cache do """ @doc group: "Persistence API" - @callback load(path :: Path.t(), opts) :: :ok | error + @callback load(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 + @callback load!(path :: Path.t(), opts()) :: :ok ## Nebulex.Adapter.Transaction @@ -2086,7 +2048,7 @@ defmodule Nebulex.Cache do """ @doc group: "Transaction API" - @callback transaction(opts, function :: fun) :: ok_error_tuple(term, term) + @callback transaction(opts(), (() -> any())) :: ok_error_tuple(any()) @doc """ Returns `{:ok, true}` if the current process is inside a transaction, @@ -2101,7 +2063,7 @@ defmodule Nebulex.Cache do ## Examples - MyCache.in_transaction? + MyCache.in_transaction?() #=> {:ok, false} MyCache.transaction(fn -> @@ -2110,7 +2072,7 @@ defmodule Nebulex.Cache do """ @doc group: "Transaction API" - @callback in_transaction?(opts) :: ok_error_tuple(boolean) + @callback in_transaction?(opts()) :: ok_error_tuple(boolean()) ## Nebulex.Adapter.Stats @@ -2137,10 +2099,9 @@ defmodule Nebulex.Cache do {:ok, %Nebulex.Stats{ measurements: %{ - evictions: 0, - expirations: 0, hits: 0, misses: 0, + evictions: 0, updates: 0, writes: 0 }, @@ -2149,13 +2110,13 @@ defmodule Nebulex.Cache do """ @doc group: "Stats API" - @callback stats(opts) :: ok_error_tuple(Nebulex.Stats.t()) + @callback stats(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() + @callback stats!(opts()) :: Nebulex.Stats.t() @doc """ Emits a telemetry event when called with the current stats count. @@ -2165,10 +2126,9 @@ defmodule Nebulex.Cache do The telemetry `:measurements` map will include the same as `Nebulex.Stats.t()`'s measurements. For example: - * `:evictions` - Current **evictions** count. - * `:expirations` - Current **expirations** count. * `:hits` - Current **hits** count. * `:misses` - Current **misses** count. + * `:evictions` - Current **evictions** count. * `:updates` - Current **updates** count. * `:writes` - Current **writes** count. @@ -2183,8 +2143,8 @@ defmodule Nebulex.Cache do ## Options - * `:event_prefix` – The prefix of the telemetry event. - Defaults to `[:nebulex, :cache]`. + * `:telemetry_event` – The telemetry event name to dispatch the event under. + Defaults to what is configured in the `:telemetry_prefix` option. * `:metadata` – A map with additional metadata fields. Defaults to `%{}`. @@ -2197,7 +2157,7 @@ defmodule Nebulex.Cache do :ok iex> MyCache.Stats.dispatch_stats( - ...> event_prefix: [:my_cache], + ...> telemetry_event: [:my_cache], ...> metadata: %{tag: "tag1"} ...> ) :ok @@ -2207,5 +2167,5 @@ defmodule Nebulex.Cache do returning `:ok`. """ @doc group: "Stats API" - @callback dispatch_stats(opts) :: :ok | error + @callback dispatch_stats(opts()) :: :ok | error_tuple() end diff --git a/lib/nebulex/cache/options.ex b/lib/nebulex/cache/options.ex index 0d26676e..2d5ee250 100644 --- a/lib/nebulex/cache/options.ex +++ b/lib/nebulex/cache/options.ex @@ -1,47 +1,73 @@ defmodule Nebulex.Cache.Options do - @moduledoc """ - Behaviour for option definitions and validation. - """ + @moduledoc false - @base_definition [ + # Compilation time option definitions + compile_opts_defs = [ otp_app: [ - required: false, type: :atom, + required: true, doc: """ - The OTP app. + The `:otp_app` should point to an OTP application that has the cache + configuration. """ ], adapter: [ - required: false, type: :atom, + required: true, doc: """ The cache adapter module. """ ], - cache: [ - required: false, + default_dynamic_cache: [ type: :atom, + required: false, doc: """ - The defined cache module. + Defines the dynamic cache to be used by default when executing + cache commands. By default, it is set to the defined cache module. + For example, when you call `MyApp.Cache.start_link/1`, it will start + a cache with the name `MyApp.Cache`. """ ], + default_key_generator: [ + type: :atom, + required: false, + default: Nebulex.Caching.SimpleKeyGenerator, + doc: """ + Defines the default key generator module to be used by caching decorators. + """ + ] + ] + + # Start option definitions (runtime) + start_link_opts_defs = [ name: [ + type: {:custom, __MODULE__, :__validate_name__, []}, required: false, - type: :atom, doc: """ - The name of the Cache supervisor process. + The name of the Cache supervisor process. By default, it is set to the + defined cache module. When you call `MyApp.Cache.start_link/1`, it will + start a cache with the name `MyApp.Cache`. """ ], telemetry_prefix: [ - required: false, type: {:list, :atom}, + required: false, doc: """ - The telemetry prefix. + It is recommend for adapters to publish events using the `Telemetry` + library. By default, the telemetry prefix is based on the module name, + so if your module is called `MyApp.Cache`, the prefix will be + `[:my_app, :cache]`. See the "Telemetry events" section in the moduledoc + for more details and the recommended events for the adapters to publish. + + Note that if you have multiple caches (dynamic caches), you should keep + the `:telemetry_prefix` consistent for each cache and use either the + `:cache` or the `:name` property in the event metadata for distinguishing + between caches. """ ], telemetry: [ - required: false, type: :boolean, + required: false, default: true, doc: """ An optional flag to tell the adapters whether Telemetry events should be @@ -49,54 +75,123 @@ defmodule Nebulex.Cache.Options do """ ], stats: [ - required: false, type: :boolean, + required: false, default: false, doc: """ - Defines whether or not the cache will provide stats. + 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. """ ] ] - @doc false - def base_definition, do: @base_definition + # Shared option definitions (runtime) + runtime_shared_opts_defs = [ + telemetry_event: [ + type: {:list, :atom}, + required: false, + doc: """ + The telemetry event name to dispatch the event under. Defaults to what + is configured in the `:telemetry_prefix` option. See the + "Telemetry events" section for more information. + """ + ], + telemetry_metadata: [ + type: {:map, :any, :any}, + required: false, + default: %{}, + doc: """ + Extra metadata to add to the Telemetry event. These end up in the + `: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`. - @doc false - defmacro __using__(_opts) do - quote do - @behaviour Nebulex.Cache.Options + 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. + """ + ] + ] + + # Compilation time options schema + @compile_opts_schema NimbleOptions.new!(compile_opts_defs) + + # Start options schema + @start_link_opts_schema NimbleOptions.new!(start_link_opts_defs) + + # Shared options schema + @runtime_shared_opts_schema NimbleOptions.new!(runtime_shared_opts_defs) + + ## API + + @spec compile_options_docs() :: binary() + def compile_options_docs do + NimbleOptions.docs(@compile_opts_schema) + end - import unquote(__MODULE__), only: [base_definition: 0] + @spec start_link_options_docs() :: binary() + def start_link_options_docs do + NimbleOptions.docs(@start_link_opts_schema) + end + + @spec runtime_shared_options_docs() :: binary() + def runtime_shared_options_docs do + NimbleOptions.docs(@runtime_shared_opts_schema) + end - @doc false - def definition, do: unquote(@base_definition) + @spec validate_compile_opts!(keyword()) :: keyword() + def validate_compile_opts!(opts) do + validate!(opts, @compile_opts_schema) + end + + @spec validate_start_opts!(keyword()) :: keyword() + def validate_start_opts!(opts) do + _opts = + opts + |> Keyword.take(Keyword.keys(@start_link_opts_schema.schema)) + |> validate!(@start_link_opts_schema) + + opts + end - @doc false - def validate!(opts) do - opts - |> NimbleOptions.validate(__MODULE__.definition()) - |> format_error() - end + @spec validate_runtime_shared_opts!(keyword()) :: keyword() + def validate_runtime_shared_opts!(opts) do + validate!(opts, @runtime_shared_opts_schema) + end - defp format_error({:ok, opts}) do - opts - end + @spec validate!(keyword(), NimbleOptions.t()) :: keyword() + def validate!(opts, schema) do + opts + |> NimbleOptions.validate(schema) + |> format_error() + end - defp format_error({:error, %NimbleOptions.ValidationError{message: message}}) do - raise ArgumentError, message - end + defp format_error({:ok, opts}) do + opts + end - defoverridable definition: 0, validate!: 1 - end + defp format_error({:error, %NimbleOptions.ValidationError{message: message}}) do + raise ArgumentError, message end - @doc """ - Returns the option definitions. - """ - @callback definition() :: NimbleOptions.t() | NimbleOptions.schema() + @doc false + def __validate_name__(name) - @doc """ - Validates the given `opts` with the definition returned by `c:definition/0`. - """ - @callback validate!(opts :: keyword) :: keyword + def __validate_name__(name) when is_atom(name) do + {:ok, name} + end + + def __validate_name__({:via, _reg_mod, _reg_name}) do + {:ok, nil} + end end diff --git a/lib/nebulex/cache/stats.ex b/lib/nebulex/cache/stats.ex index 23c77752..2bcd8a78 100644 --- a/lib/nebulex/cache/stats.ex +++ b/lib/nebulex/cache/stats.ex @@ -1,45 +1,50 @@ defmodule Nebulex.Cache.Stats do @moduledoc false + # use Nebulex.Cache.Options + import Nebulex.Helpers + import Nebulex.Cache.Options, only: [validate_runtime_shared_opts!: 1] - alias Nebulex.Adapter + alias Nebulex.{Adapter, Telemetry} ## API @doc """ Implementation for `c:Nebulex.Cache.stats/1`. """ - def stats(name) do - Adapter.with_meta(name, & &1.adapter.stats(&1)) + def stats(name, opts) do + Adapter.with_meta(name, & &1.adapter.stats(&1, opts)) end @doc """ Implementation for `c:Nebulex.Cache.stats!/0`. """ - def stats!(name) do - unwrap_or_raise stats(name) + def stats!(name, opts) do + unwrap_or_raise stats(name, opts) end - if Code.ensure_loaded?(:telemetry) do - @doc """ - Implementation for `c:Nebulex.Cache.dispatch_stats/1`. - """ - def dispatch_stats(name, opts \\ []) do - Adapter.with_meta(name, fn %{adapter: adapter} = meta -> - with {:ok, %Nebulex.Stats{} = info} <- adapter.stats(meta) do - :telemetry.execute( - meta.telemetry_prefix ++ [:stats], - info.measurements, - Map.merge(info.metadata, opts[:metadata] || %{}) - ) - end - end) - end - else - @doc """ - Implementation for `c:Nebulex.Cache.dispatch_stats/1`. - """ - def dispatch_stats(_name, _opts \\ []), do: :ok + @doc """ + Implementation for `c:Nebulex.Cache.dispatch_stats/1`. + """ + def dispatch_stats(name, opts \\ []) do + Adapter.with_meta(name, fn %{adapter: adapter} = meta -> + with {:ok, %Nebulex.Stats{} = info} <- adapter.stats(meta, opts) do + # Validate options + opts = + opts + |> Keyword.take([:telemetry_event, :telemetry_metadata]) + |> validate_runtime_shared_opts!() + + # Resolve the telemetry event + telemetry_event = Keyword.get(opts, :telemetry_event, meta.telemetry_prefix) + + Telemetry.execute( + telemetry_event ++ [:stats], + info.measurements, + Map.put(info.metadata, :extra_metadata, Keyword.fetch!(opts, :telemetry_metadata)) + ) + end + end) end end diff --git a/lib/nebulex/cache/supervisor.ex b/lib/nebulex/cache/supervisor.ex index 1a647856..093e4ff7 100644 --- a/lib/nebulex/cache/supervisor.ex +++ b/lib/nebulex/cache/supervisor.ex @@ -1,7 +1,9 @@ defmodule Nebulex.Cache.Supervisor do @moduledoc false + use Supervisor + import Nebulex.Cache.Options import Nebulex.Helpers alias Nebulex.Telemetry @@ -9,7 +11,7 @@ defmodule Nebulex.Cache.Supervisor do @doc """ Starts the cache manager supervisor. """ - @spec start_link(module, atom, module, keyword) :: Supervisor.on_start() + @spec start_link(module(), atom(), module(), keyword()) :: Supervisor.on_start() def start_link(cache, otp_app, adapter, opts) do name = Keyword.get(opts, :name, cache) sup_opts = if name, do: [name: name], else: [] @@ -20,7 +22,7 @@ defmodule Nebulex.Cache.Supervisor do @doc """ Retrieves the runtime configuration. """ - @spec runtime_config(module, atom, keyword) :: {:ok, keyword} | :ignore + @spec runtime_config(module(), atom(), keyword()) :: {:ok, keyword()} | :ignore def runtime_config(cache, otp_app, opts) do config = otp_app @@ -29,6 +31,7 @@ defmodule Nebulex.Cache.Supervisor do |> Keyword.put(:otp_app, otp_app) |> Keyword.put_new_lazy(:telemetry_prefix, fn -> telemetry_prefix(cache) end) |> Keyword.update(:telemetry, true, &(is_boolean(&1) && &1)) + |> validate_start_opts!() cache_init(cache, config) end @@ -44,19 +47,23 @@ defmodule Nebulex.Cache.Supervisor do @doc """ Retrieves the compile time configuration. """ - @spec compile_config(keyword) :: {atom, module, [module]} + @spec compile_config(keyword()) :: {atom(), module(), [module()], keyword()} def compile_config(opts) do - otp_app = opts[:otp_app] || raise ArgumentError, "expected otp_app: to be given as argument" - adapter = opts[:adapter] || raise ArgumentError, "expected adapter: to be given as argument" + # Validate options + opts = validate_compile_opts!(opts) + + otp_app = Keyword.fetch!(opts, :otp_app) + adapter = Keyword.fetch!(opts, :adapter) behaviours = module_behaviours(adapter, "adapter") unless Nebulex.Adapter in behaviours do raise ArgumentError, - "expected :adapter option given to Nebulex.Cache to list Nebulex.Adapter as a behaviour" + "expected :adapter option given to Nebulex.Cache " <> + "to list Nebulex.Adapter as a behaviour" end - {otp_app, adapter, behaviours} + {otp_app, adapter, behaviours, opts} end ## Supervisor Callbacks diff --git a/lib/nebulex/cache/transaction.ex b/lib/nebulex/cache/transaction.ex index e050e381..846d9c33 100644 --- a/lib/nebulex/cache/transaction.ex +++ b/lib/nebulex/cache/transaction.ex @@ -11,9 +11,9 @@ defmodule Nebulex.Cache.Transaction do end @doc """ - Implementation for `c:Nebulex.Cache.in_transaction?/0`. + Implementation for `c:Nebulex.Cache.in_transaction?/1`. """ - def in_transaction?(name) do - Adapter.with_meta(name, & &1.adapter.in_transaction?(&1)) + def in_transaction?(name, opts) do + Adapter.with_meta(name, & &1.adapter.in_transaction?(&1, opts)) end end diff --git a/lib/nebulex/caching/decorators.ex b/lib/nebulex/caching/decorators.ex index eb8d6401..dfed9492 100644 --- a/lib/nebulex/caching/decorators.ex +++ b/lib/nebulex/caching/decorators.ex @@ -416,19 +416,19 @@ if Code.ensure_loaded?(Decorator.Define) do defrecordp(:keyref, :"$nbx_cache_ref", cache: nil, key: nil) @typedoc "Type spec for a key reference" - @type keyref :: record(:keyref, cache: Nebulex.Cache.t(), key: term) + @type keyref() :: record(:keyref, cache: Nebulex.Cache.t(), key: term) @typedoc "Type for on_error action" - @type on_error :: :nothing | :raise + @type on_error() :: :nothing | :raise @typedoc "Type for match function" - @type match :: (term -> boolean | {true, term}) + @type match() :: (any() -> boolean() | {true, any()}) @typedoc "Type for the key generator" - @type keygen :: module | {module, function_name :: atom, args :: [term]} + @type keygen() :: module() | {module(), function_name :: atom(), args :: [any()]} @typedoc "Type spec for the option :references" - @type references :: (term -> keyref | term) | nil | term + @type references() :: (any() -> keyref() | any()) | nil | any() ## Decorator API @@ -799,7 +799,7 @@ if Code.ensure_loaded?(Decorator.Define) do {:"$nbx_cache_ref", MyCache, "my-key"} """ - @spec cache_ref(Nebulex.Cache.t(), term) :: keyref() + @spec cache_ref(Nebulex.Cache.t(), any()) :: keyref() def cache_ref(cache \\ nil, key) do keyref(cache: cache, key: key) end @@ -1011,7 +1011,8 @@ if Code.ensure_loaded?(Decorator.Define) do **NOTE:** Internal purposes only. """ @doc group: "Internal API" - @spec eval_cacheable(module, term, references, Keyword.t(), match, on_error, fun) :: term + @spec eval_cacheable(module(), any(), references(), keyword(), match(), on_error(), fun()) :: + any() def eval_cacheable(cache, key, references, opts, match, on_error, block_fun) def eval_cacheable(cache, key, nil, opts, match, on_error, block_fun) do @@ -1081,7 +1082,15 @@ if Code.ensure_loaded?(Decorator.Define) do **NOTE:** Internal purposes only. """ @doc group: "Internal API" - @spec eval_cache_evict(boolean, boolean, module, keygen, [term] | nil, on_error, fun) :: term + @spec eval_cache_evict( + boolean(), + boolean(), + module(), + keygen(), + [any()] | nil, + on_error(), + fun() + ) :: any() def eval_cache_evict(before_invocation?, all_entries?, cache, keygen, keys, on_error, block_fun) def eval_cache_evict(true, all_entries?, cache, keygen, keys, on_error, block_fun) do @@ -1117,7 +1126,7 @@ if Code.ensure_loaded?(Decorator.Define) do **NOTE:** Internal purposes only. """ @doc group: "Internal API" - @spec eval_cache_put(module, term, term, Keyword.t(), atom, match) :: any + @spec eval_cache_put(module(), term(), term(), keyword(), atom(), match()) :: any() def eval_cache_put(cache, key, value, opts, on_error, match) def eval_cache_put(cache, keyref(cache: nil, key: key), value, opts, on_error, match) do @@ -1151,7 +1160,7 @@ if Code.ensure_loaded?(Decorator.Define) do **NOTE:** Internal purposes only. """ @doc group: "Internal API" - @spec cache_put(module, {:"$keys", term} | term, term, Keyword.t()) :: :ok + @spec cache_put(module(), {:"$keys", any()} | any(), any(), keyword()) :: :ok def cache_put(cache, key, value, opts) def cache_put(cache, {:"$keys", keys}, value, opts) do diff --git a/lib/nebulex/caching/key_generator.ex b/lib/nebulex/caching/key_generator.ex index e2a7f556..33038bf3 100644 --- a/lib/nebulex/caching/key_generator.ex +++ b/lib/nebulex/caching/key_generator.ex @@ -43,10 +43,10 @@ defmodule Nebulex.Caching.KeyGenerator do """ @typedoc "Key generator type" - @type t :: module + @type t() :: module() @doc """ Generates a key for the given `module`, `function_name`, and its `args`. """ - @callback generate(module, function_name :: atom, args :: [term]) :: term + @callback generate(module(), function_name :: atom(), args :: [any()]) :: any() end diff --git a/lib/nebulex/entry.ex b/lib/nebulex/entry.ex index 49b31b9e..99547f1c 100644 --- a/lib/nebulex/entry.ex +++ b/lib/nebulex/entry.ex @@ -18,7 +18,7 @@ defmodule Nebulex.Entry do The entry depends on the adapter completely, this struct/type aims to define the common fields. """ - @type t :: %__MODULE__{ + @type t() :: %__MODULE__{ key: any, value: any, touched: integer, @@ -39,7 +39,7 @@ defmodule Nebulex.Entry do "hello" """ - @spec encode(term, [term]) :: binary + @spec encode(any(), [any()]) :: binary() def encode(data, opts \\ []) do data |> :erlang.term_to_binary(opts) @@ -58,7 +58,7 @@ defmodule Nebulex.Entry do """ # sobelow_skip ["Misc.BinToTerm"] - @spec decode(binary, [term]) :: term + @spec decode(binary(), [any()]) :: any() def decode(data, opts \\ []) when is_binary(data) do data |> Base.decode64!() @@ -78,7 +78,7 @@ defmodule Nebulex.Entry do true """ - @spec expired?(t) :: boolean + @spec expired?(t()) :: boolean() def expired?(%__MODULE__{exp: :infinity}), do: false def expired?(%__MODULE__{exp: exp, time_unit: unit}), do: Time.now(unit) >= exp @@ -96,7 +96,7 @@ defmodule Nebulex.Entry do true """ - @spec ttl(t) :: timeout + @spec ttl(t()) :: timeout() def ttl(%__MODULE__{exp: :infinity}), do: :infinity def ttl(%__MODULE__{exp: exp, time_unit: unit}), do: exp - Time.now(unit) end diff --git a/lib/nebulex/exceptions.ex b/lib/nebulex/exceptions.ex index 0c6f4d06..f95c2e6d 100644 --- a/lib/nebulex/exceptions.ex +++ b/lib/nebulex/exceptions.ex @@ -3,13 +3,39 @@ defmodule Nebulex.Error do This exception represents cache command execution errors. For example, the command cannot be executed because the cache was not started or it does not exist, or the adapter failed while executing it. + + ## Struct fields + + The exception struct itself is opaque, that is, not all fields are public. + The following are the public fields: + + * `:cache` - the cache where the exception occurred. + + * `:module` - a custom error formatter module. When it is present, + it invokes `module.format_error(reason)` to format the error reason. + + * `:reason` - a term representing the error reason. The value of this field + can be: + + * `:registry_lookup_error` - the cache cannot be retrieved from the + registry because it was not started or it does not exist. + + * `:stats_error` - (Stats API) if stats are disabled or maybe not + supported by the underlying adapter. + + * `{:invalid_query, query}` - (Queryable API) if the underlying adapter + cannot run a given query because it is invalid or not supported. + + * `{:transaction_aborted, nodes}` - (Transaction API) if a transaction + is aborted by the underlying adapter. + """ @typedoc "Error reason type" - @type reason :: atom | {atom, term} | Exception.t() + @type reason() :: atom() | {atom(), any()} | Exception.t() @typedoc "Error type" - @type t :: %__MODULE__{cache: term, reason: reason, module: module} + @type t() :: %__MODULE__{cache: any(), reason: reason(), module: module()} # Exception struct defexception cache: nil, reason: nil, module: __MODULE__ @@ -37,6 +63,10 @@ defmodule Nebulex.Error do "not started or it does not exist" end + def format_error(:stats_error, cache) do + "stats disabled or not supported by the cache #{inspect(cache)}" + end + def format_error({:invalid_query, query}, cache) do "cache #{inspect(cache)} cannot execute invalid query #{inspect(query)}" end @@ -45,10 +75,6 @@ defmodule Nebulex.Error do "cache #{inspect(cache)} has aborted a transaction on nodes: #{inspect(nodes)}" end - def format_error(:stats_error, cache) do - "stats disabled or not supported by the cache #{inspect(cache)}" - end - def format_error(exception, cache) when is_exception(exception) do """ the following exception occurred in the cache #{inspect(cache)}. @@ -69,13 +95,26 @@ defmodule Nebulex.KeyError do This exception denotes the cache command was executed, but there was an issue with the requested key; for example, it was not found. + + ## Struct fields + + The exception struct itself is opaque, that is, not all fields are public. + The following are the public fields: + + * `:cache` - the cache where the exception occurred. + + * `:key` - the key causing the error. + + * `:reason` - the two possible reasons are: `:not_found` and `:expired`. + Defaults to `:not_found`. + """ @typedoc "Error type" - @type t :: %__MODULE__{cache: term, key: term, reason: atom} + @type t() :: %__MODULE__{cache: any(), key: any(), reason: atom()} # Exception struct - defexception [:cache, :key, :reason] + defexception cache: nil, key: nil, reason: :not_found ## API diff --git a/lib/nebulex/helpers.ex b/lib/nebulex/helpers.ex index be328a57..95e0085c 100644 --- a/lib/nebulex/helpers.ex +++ b/lib/nebulex/helpers.ex @@ -129,9 +129,6 @@ defmodule Nebulex.Helpers do end end - # FIXME: this is because coveralls does not mark this as covered - # coveralls-ignore-start - @doc false defmacro wrap_ok(call) do quote do @@ -145,6 +142,4 @@ defmodule Nebulex.Helpers do {:error, unquote(exception).exception(unquote(opts))} end end - - # coveralls-ignore-stop end diff --git a/lib/nebulex/stats.ex b/lib/nebulex/stats.ex index 957af598..1dab8009 100644 --- a/lib/nebulex/stats.ex +++ b/lib/nebulex/stats.ex @@ -11,22 +11,20 @@ defmodule Nebulex.Stats do ## Measurements - The following measurements are expected to be present and fed by the - underlying adapter: + Measurements are an extensible map, the adapters are free to add the + measurements they want. However, the following measurements are expected + to be always present:: - * `:evictions` - When a cache entry is removed. - * `:expirations` - When a cache entry is expired. * `:hits` - When a key is looked up in cache and found. * `:misses` - When a key is looked up in cache but not found. - * `:updates` - When an existing cache entry is or updated. - * `:writes` - When a cache entry is inserted or overwritten. + * `:evictions` - When a cache entry is removed. ## Metadata Despite the adapters can include any additional or custom metadata, It is recommended they include the following keys: - * `:cache` - The cache module, or the name (if an explicit name has been + * `:cache` - The cache module or the name (if an explicit name has been given to the cache). **IMPORTANT:** Since the adapter may include any additional or custom @@ -35,12 +33,17 @@ defmodule Nebulex.Stats do """ # Stats data type - defstruct measurements: %{}, + defstruct measurements: %{hits: 0, misses: 0, evictions: 0}, metadata: %{} @typedoc "Nebulex.Stats data type" @type t :: %__MODULE__{ - measurements: %{optional(atom) => term}, + measurements: %{ + required(:hits) => number, + required(:misses) => number, + required(:evictions) => number, + optional(atom) => number + }, metadata: %{optional(atom) => term} } end diff --git a/lib/nebulex/telemetry/stats_handler.ex b/lib/nebulex/telemetry/stats_handler.ex index 22dfb135..099972b7 100644 --- a/lib/nebulex/telemetry/stats_handler.ex +++ b/lib/nebulex/telemetry/stats_handler.ex @@ -2,9 +2,6 @@ defmodule Nebulex.Telemetry.StatsHandler do @moduledoc """ Telemetry handler for aggregating cache stats; it relies on the default stats implementation based on Erlang counters. See `Nebulex.Adapter.Stats`. - - This handler is used by the built-in local adapter when the option `:stats` - is set to `true`. """ alias Nebulex.Adapter.Stats @@ -32,7 +29,6 @@ defmodule Nebulex.Telemetry.StatsHandler do when action in [:fetch, :take, :ttl] do :ok = Stats.incr(ref, :misses) :ok = Stats.incr(ref, :evictions) - :ok = Stats.incr(ref, :expirations) end defp update_stats(%{ @@ -44,12 +40,20 @@ defmodule Nebulex.Telemetry.StatsHandler do :ok = Stats.incr(ref, :misses) end - defp update_stats(%{function_name: action, result: {:ok, _}, adapter_meta: %{stats_counter: ref}}) + defp update_stats(%{ + function_name: action, + result: {:ok, _}, + adapter_meta: %{stats_counter: ref} + }) when action in [:fetch, :ttl] do :ok = Stats.incr(ref, :hits) end - defp update_stats(%{function_name: :take, result: {:ok, _}, adapter_meta: %{stats_counter: ref}}) do + defp update_stats(%{ + function_name: :take, + result: {:ok, _}, + adapter_meta: %{stats_counter: ref} + }) do :ok = Stats.incr(ref, :hits) :ok = Stats.incr(ref, :evictions) end @@ -63,7 +67,11 @@ defmodule Nebulex.Telemetry.StatsHandler do :ok = Stats.incr(ref, :updates) end - defp update_stats(%{function_name: :put, result: {:ok, true}, adapter_meta: %{stats_counter: ref}}) do + defp update_stats(%{ + function_name: :put, + result: {:ok, true}, + adapter_meta: %{stats_counter: ref} + }) do :ok = Stats.incr(ref, :writes) end diff --git a/lib/nebulex/time.ex b/lib/nebulex/time.ex index c1191992..e0e960e7 100644 --- a/lib/nebulex/time.ex +++ b/lib/nebulex/time.ex @@ -44,7 +44,7 @@ defmodule Nebulex.Time do false """ - @spec timeout?(term) :: boolean + @spec timeout?(any()) :: boolean() def timeout?(timeout) do (is_integer(timeout) and timeout >= 0) or timeout == :infinity end diff --git a/mix.exs b/mix.exs index f4a4e2e1..cfe15ab2 100644 --- a/mix.exs +++ b/mix.exs @@ -48,7 +48,7 @@ defmodule Nebulex.MixProject do defp deps do [ - {:nimble_options, "~> 0.5"}, + {:nimble_options, "~> 0.5 or ~> 1.0"}, {:decorator, "~> 1.4", optional: true}, {:telemetry, "~> 1.2", optional: true}, diff --git a/mix.lock b/mix.lock index 37c3109b..14bcc16b 100644 --- a/mix.lock +++ b/mix.lock @@ -11,7 +11,7 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.31", "a93921cdc6b9b869f519213d5bc79d9e218ba768d7270d46fdcf1c01bacff9e2", [:mix], [], "hexpm", "317d367ee0335ef037a87e46c91a2269fef6306413f731e8ec11fc45a7efd059"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.29.3", "f07444bcafb302db86e4f02d8bbcd82f2e881a0dcf4f3e4740e4b8128b9353f7", [:mix], [{:earmark_parser, "~> 1.4.31", [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", "3dc6787d7b08801ec3b51e9bd26be5e8826fbf1a17e92d1ebc252e1a1c75bfe1"}, - "excoveralls": {:hex, :excoveralls, "0.16.0", "41f4cfbf7caaa3bc2cf411db6f89c1f53afedf0f1fe8debac918be1afa19c668", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "401205356482ab99fb44d9812cd14dd83b65de8e7ae454697f8b34ba02ecd916"}, + "excoveralls": {:hex, :excoveralls, "0.16.1", "0bd42ed05c7d2f4d180331a20113ec537be509da31fed5c8f7047ce59ee5a7c5", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dae763468e2008cf7075a64cb1249c97cb4bc71e236c5c2b5e5cdf1cfa2bf138"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [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", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, @@ -23,7 +23,7 @@ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mimic": {:hex, :mimic, "1.7.4", "cd2772ffbc9edefe964bc668bfd4059487fa639a5b7f1cbdf4fd22946505aa4f", [:mix], [], "hexpm", "437c61041ecf8a7fae35763ce89859e4973bb0666e6ce76d75efc789204447c3"}, - "nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"}, + "nimble_options": {:hex, :nimble_options, "1.0.1", "b448018287b22584e91b5fd9c6c0ad717cb4bcdaa457957c8d57770f56625c43", [:mix], [], "hexpm", "078b2927cd9f84555be6386d56e849b0c555025ecccf7afee00ab6a9e6f63837"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"}, diff --git a/test/dialyzer/caching_decorators.ex b/test/dialyzer/caching_decorators.ex index 0455391d..ca0299a2 100644 --- a/test/dialyzer/caching_decorators.ex +++ b/test/dialyzer/caching_decorators.ex @@ -5,20 +5,21 @@ defmodule Nebulex.Dialyzer.CachingDecorators do defmodule Account do @moduledoc false defstruct [:id, :username, :password] - @type t :: %__MODULE__{} + + @type t() :: %__MODULE__{} end @ttl :timer.seconds(3600) ## Annotated Functions - @spec get_account(integer) :: Account.t() + @spec get_account(integer()) :: Account.t() @decorate cacheable(cache: Cache, key: {Account, id}) def get_account(id) do %Account{id: id} end - @spec get_account_by_username(binary) :: Account.t() + @spec get_account_by_username(binary()) :: Account.t() @decorate cacheable( cache: Cache, key: {Account, username}, @@ -40,7 +41,7 @@ defmodule Nebulex.Dialyzer.CachingDecorators do {:ok, acct} end - @spec update_account_by_id(binary, %{optional(atom) => term}) :: {:ok, Account.t()} + @spec update_account_by_id(binary(), %{optional(atom()) => any()}) :: {:ok, 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))} @@ -52,20 +53,20 @@ defmodule Nebulex.Dialyzer.CachingDecorators do acct end - @spec delete_all_accounts(term) :: term + @spec delete_all_accounts(any()) :: any() @decorate cache_evict(cache: Cache, all_entries: true) def delete_all_accounts(filter) do filter end - @spec get_user_key(binary) :: binary + @spec get_user_key(binary()) :: binary() @decorate cacheable( cache: {__MODULE__, :dynamic_cache, [:dynamic]}, key_generator: {__MODULE__, [id]} ) def get_user_key(id), do: id - @spec update_user_key(binary) :: binary + @spec update_user_key(binary()) :: binary() @decorate cacheable(cache: Cache, key_generator: {__MODULE__, :generate_key, [id]}) def update_user_key(id), do: id diff --git a/test/nebulex/cache/supervisor_test.exs b/test/nebulex/cache/supervisor_test.exs index 974f5f42..7dc4fbc1 100644 --- a/test/nebulex/cache/supervisor_test.exs +++ b/test/nebulex/cache/supervisor_test.exs @@ -19,73 +19,96 @@ defmodule Nebulex.Cache.SupervisorTest do alias Nebulex.TestCache.Cache - test "fails on init because :ignore is returned" do - assert MyCache.start_link(ignore: true) == :ignore - end + describe "compile_config/1" do + test "error: missing :otp_app option" do + assert_raise ArgumentError, ~r"required :otp_app option not found", fn -> + Nebulex.Cache.Supervisor.compile_config(adapter: TestAdapter) + end + end - test "fails on compile_config because missing otp_app" do - assert_raise ArgumentError, "expected otp_app: to be given as argument", fn -> - Nebulex.Cache.Supervisor.compile_config(adapter: TestAdapter) + test "error: missing :adapter option" do + assert_raise ArgumentError, ~r"required :adapter option not found", fn -> + Nebulex.Cache.Supervisor.compile_config(otp_app: :nebulex) + end end - end - test "fails on compile_config because missing adapter" do - assert_raise ArgumentError, "expected adapter: to be given as argument", fn -> - Nebulex.Cache.Supervisor.compile_config(otp_app: :nebulex) + test "error: adapter was not compiled" do + msg = ~r"adapter TestAdapter was not compiled, ensure" + + assert_raise ArgumentError, msg, fn -> + Nebulex.Cache.Supervisor.compile_config(otp_app: :nebulex, adapter: TestAdapter) + end end - end - test "fails on compile_config because adapter was not compiled" do - msg = ~r"adapter TestAdapter was not compiled, ensure" + test "error: adapter doesn't implement the required behaviour" do + msg = "expected :adapter option given to Nebulex.Cache to list Nebulex.Adapter as a behaviour" + + assert_raise ArgumentError, msg, fn -> + defmodule MyAdapter do + end - assert_raise ArgumentError, msg, fn -> - Nebulex.Cache.Supervisor.compile_config(otp_app: :nebulex, adapter: TestAdapter) + defmodule MyCache2 do + use Nebulex.Cache, + otp_app: :nebulex, + adapter: MyAdapter + end + end end end - test "fails on compile_config because adapter error" do - msg = "expected :adapter option given to Nebulex.Cache to list Nebulex.Adapter as a behaviour" + describe "start_link/1" do + test "starts anonymous cache" do + assert {:ok, pid} = Cache.start_link(name: nil) + assert Process.alive?(pid) - assert_raise ArgumentError, msg, fn -> - defmodule MyAdapter do - end + assert Cache.stop(dynamic_cache: pid) == :ok + refute Process.alive?(pid) + end + + test "starts cache with via" do + {:ok, _} = Registry.start_link(keys: :unique, name: Registry.ViaTest) + name = {:via, Registry, {Registry.ViaTest, "test"}} - defmodule MyCache2 do + assert {:ok, pid} = Cache.start_link(name: name) + assert Process.alive?(pid) + + assert [{^pid, _}] = Registry.lookup(Registry.ViaTest, "test") + + assert Cache.stop(dynamic_cache: pid) == :ok + refute Process.alive?(pid) + end + + test "starts cache with custom adapter" do + defmodule CustomCache do use Nebulex.Cache, otp_app: :nebulex, - adapter: MyAdapter + adapter: Nebulex.TestCache.AdapterMock end - Nebulex.Cache.Supervisor.compile_config(otp_app: :nebulex) - end - end - - test "start cache with custom adapter" do - defmodule CustomCache do - use Nebulex.Cache, - otp_app: :nebulex, - adapter: Nebulex.TestCache.AdapterMock - end + assert {:ok, _pid} = CustomCache.start_link(child_name: :custom_cache) - assert {:ok, _pid} = CustomCache.start_link(child_name: :custom_cache) + _ = Process.flag(:trap_exit, true) - _ = Process.flag(:trap_exit, true) + assert {:error, error} = + CustomCache.start_link(name: :another_custom_cache, child_name: :custom_cache) - assert {:error, error} = - CustomCache.start_link(name: :another_custom_cache, child_name: :custom_cache) + assert_receive {:EXIT, _pid, ^error} + assert CustomCache.stop() == :ok + end - assert_receive {:EXIT, _pid, ^error} - assert CustomCache.stop() == :ok - end + test "emits telemetry event upon cache start" do + with_telemetry_handler([[:nebulex, :cache, :init]], fn -> + {:ok, _} = Cache.start_link(name: :telemetry_test) - test "emits telemetry event upon cache start" do - with_telemetry_handler([[:nebulex, :cache, :init]], fn -> - {:ok, _} = Cache.start_link(name: :telemetry_test) + assert_receive {[:nebulex, :cache, :init], _, %{cache: Cache, opts: opts}} + assert opts[:telemetry_prefix] == [:nebulex, :test_cache, :cache] + assert opts[:name] == :telemetry_test + end) + end - assert_receive {[:nebulex, :cache, :init], _, %{cache: Cache, opts: opts}} - assert opts[:telemetry_prefix] == [:nebulex, :test_cache, :cache] - assert opts[:name] == :telemetry_test - end) + test "error: fails on init because :ignore is returned" do + assert MyCache.start_link(ignore: true) == :ignore + end end ## Helpers diff --git a/test/nebulex/helpers_test.exs b/test/nebulex/helpers_test.exs index abacd3a7..1e337d44 100644 --- a/test/nebulex/helpers_test.exs +++ b/test/nebulex/helpers_test.exs @@ -12,8 +12,7 @@ defmodule Nebulex.HelpersTest do Nebulex.Adapter.Queryable, Nebulex.Adapter.Transaction, Nebulex.Adapter.Persistence, - Nebulex.Adapter.Stats, - Nebulex.Cache.Options + Nebulex.Adapter.Stats ] end diff --git a/test/nebulex/stats_test.exs b/test/nebulex/stats_test.exs index dee3d40c..8fd7eaee 100644 --- a/test/nebulex/stats_test.exs +++ b/test/nebulex/stats_test.exs @@ -13,7 +13,7 @@ defmodule Nebulex.StatsTest do ## Tests - describe "stats/0" do + describe "c:Nebulex.Cache.stats/1" do setup_with_cache Cache, stats: true test "returns an error" do @@ -39,7 +39,6 @@ defmodule Nebulex.StatsTest do misses: 4, writes: 2, evictions: 0, - expirations: 0, updates: 0 } end @@ -71,7 +70,6 @@ defmodule Nebulex.StatsTest do misses: 1, writes: 10, evictions: 1, - expirations: 1, updates: 4 } end) @@ -93,7 +91,6 @@ defmodule Nebulex.StatsTest do misses: 1, writes: 10, evictions: 2, - expirations: 0, updates: 0 } @@ -104,31 +101,9 @@ defmodule Nebulex.StatsTest do misses: 1, writes: 10, evictions: 10, - expirations: 0, updates: 0 } end - - test "expirations" do - :ok = Cache.put_all!(a: 1, b: 2) - :ok = Cache.put_all!([c: 3, d: 4], ttl: 1000) - - assert Cache.get_all!([:a, :b, :c, :d]) == %{a: 1, b: 2, c: 3, d: 4} - - :ok = Process.sleep(1100) - assert Cache.get_all!([:a, :b, :c, :d]) == %{a: 1, b: 2} - - wait_until(fn -> - assert Cache.stats!().measurements == %{ - hits: 6, - misses: 2, - writes: 4, - evictions: 2, - expirations: 2, - updates: 0 - } - end) - end end describe "cache init error" do @@ -161,23 +136,22 @@ defmodule Nebulex.StatsTest do end end - describe "dispatch_stats/1" do + describe "c:Nebulex.Cache.dispatch_stats/1" do setup_with_cache Cache, stats: true test "emits a telemetry event when called" do with_telemetry_handler(__MODULE__, [@event], fn -> - :ok = Cache.dispatch_stats(metadata: %{node: node()}) + :ok = Cache.dispatch_stats(telemetry_metadata: %{node: node()}) node = node() - assert_receive {@event, measurements, %{cache: Cache, node: ^node}} + assert_receive {@event, measurements, %{cache: Cache, extra_metadata: %{node: ^node}}} assert measurements == %{ hits: 0, misses: 0, writes: 0, evictions: 0, - expirations: 0, updates: 0 } end) @@ -192,7 +166,7 @@ defmodule Nebulex.StatsTest do end end - describe "dispatch_stats/1 with dynamic cache" do + describe "c:Nebulex.Cache.dispatch_stats/1 with custom opts" do setup_with_dynamic_cache Cache, :stats_with_dispatch, telemetry_prefix: [:my_event], @@ -200,20 +174,31 @@ defmodule Nebulex.StatsTest do test "emits a telemetry event with custom telemetry_prefix when called" do with_telemetry_handler(__MODULE__, [[:my_event, :stats]], fn -> - :ok = Cache.dispatch_stats(metadata: %{foo: :bar}) + :ok = Cache.dispatch_stats(telemetry_metadata: %{foo: :bar}) assert_receive {[:my_event, :stats], measurements, - %{cache: :stats_with_dispatch, foo: :bar}} + %{cache: :stats_with_dispatch, extra_metadata: %{foo: :bar}}} assert measurements == %{ hits: 0, misses: 0, writes: 0, evictions: 0, - expirations: 0, updates: 0 } end) end end + + describe "c:Nebulex.Adapter.Stats.defspan/2" do + test "error: invalid definition" do + assert_raise ArgumentError, "invalid syntax in defspan :invalid", fn -> + defmodule InvalidDefspan do + import Nebulex.Adapter.Stats, only: [defspan: 2] + + defspan(:invalid, do: :invalid) + end + end + end + end end diff --git a/test/nebulex/telemetry_test.exs b/test/nebulex/telemetry_test.exs index 8ef606ec..2109f35c 100644 --- a/test/nebulex/telemetry_test.exs +++ b/test/nebulex/telemetry_test.exs @@ -36,6 +36,7 @@ defmodule Nebulex.TelemetryTest do assert metadata[:adapter_meta][:cache] == Cache assert metadata[:args] == ["foo", "bar", :infinity, :put, []] assert metadata[:telemetry_span_context] |> is_reference() + assert metadata[:extra_metadata] == %{} assert_receive {@stop, measurements, %{function_name: :put} = metadata} assert measurements[:duration] > 0 @@ -43,6 +44,7 @@ defmodule Nebulex.TelemetryTest do assert metadata[:args] == ["foo", "bar", :infinity, :put, []] assert metadata[:result] == {:ok, true} assert metadata[:telemetry_span_context] |> is_reference() + assert metadata[:extra_metadata] == %{} end) end @@ -62,6 +64,7 @@ defmodule Nebulex.TelemetryTest do assert metadata[:reason] == %ArgumentError{message: "error"} assert metadata[:stacktrace] assert metadata[:telemetry_span_context] |> is_reference() + assert metadata[:extra_metadata] == %{} end) end @@ -132,4 +135,72 @@ defmodule Nebulex.TelemetryTest do end) end end + + describe "span/3 with custom event and metadata" do + @custom_prefix [:my, :custom, :event] + @custom_start @custom_prefix ++ [:start] + @custom_stop @custom_prefix ++ [:stop] + @custom_exception @custom_prefix ++ [:exception] + @custom_events [@custom_start, @custom_stop, @custom_exception] + + @custom_opts [ + telemetry_event: @custom_prefix, + telemetry_metadata: %{foo: "bar"} + ] + + setup_with_cache Cache + + test "ok: emits start and stop events" do + with_telemetry_handler(__MODULE__, @custom_events, fn -> + :ok = Cache.put("foo", "bar", @custom_opts) + + assert_receive {@custom_start, measurements, %{function_name: :put} = metadata} + assert measurements[:system_time] |> DateTime.from_unix!(:native) + assert metadata[:adapter_meta][:cache] == Cache + assert metadata[:args] == ["foo", "bar", :infinity, :put, @custom_opts] + assert metadata[:telemetry_span_context] |> is_reference() + assert metadata[:extra_metadata] == %{foo: "bar"} + + assert_receive {@custom_stop, measurements, %{function_name: :put} = metadata} + assert measurements[:duration] > 0 + assert metadata[:adapter_meta][:cache] == Cache + assert metadata[:args] == ["foo", "bar", :infinity, :put, @custom_opts] + assert metadata[:result] == {:ok, true} + assert metadata[:telemetry_span_context] |> is_reference() + assert metadata[:extra_metadata] == %{foo: "bar"} + end) + end + + test "raise: emits start and exception events" do + with_telemetry_handler(__MODULE__, @custom_events, fn -> + key = {:eval, fn -> raise ArgumentError, "error" end} + + assert_raise ArgumentError, fn -> + Cache.fetch(key, @custom_opts) + end + + assert_receive {@custom_exception, measurements, %{function_name: :fetch} = metadata} + assert measurements[:duration] > 0 + assert metadata[:adapter_meta][:cache] == Cache + assert metadata[:args] == [key, @custom_opts] + assert metadata[:kind] == :error + assert metadata[:reason] == %ArgumentError{message: "error"} + assert metadata[:stacktrace] + assert metadata[:telemetry_span_context] |> is_reference() + assert metadata[:extra_metadata] == %{foo: "bar"} + end) + end + + test "error: invalid telemetry_event" do + assert_raise ArgumentError, ~r"invalid value for :telemetry_event option", fn -> + Cache.fetch(:invalid, telemetry_event: :invalid) + end + end + + test "error: invalid telemetry_metadata" do + assert_raise ArgumentError, ~r"invalid value for :telemetry_metadata option", fn -> + Cache.fetch(:invalid, telemetry_metadata: :invalid) + end + end + end end diff --git a/test/support/fake_adapter.exs b/test/support/fake_adapter.exs index 849f6cd8..bc2131f9 100644 --- a/test/support/fake_adapter.exs +++ b/test/support/fake_adapter.exs @@ -68,7 +68,7 @@ defmodule Nebulex.FakeAdapter do ## Nebulex.Adapter.Stats @doc false - def stats(_), do: {:error, %Nebulex.Error{reason: :error}} + def stats(_, _), do: {:error, %Nebulex.Error{reason: :error}} ## Nebulex.Adapter.Transaction @@ -76,5 +76,5 @@ defmodule Nebulex.FakeAdapter do def transaction(_, _, _), do: {:error, %Nebulex.Error{reason: :error}} @doc false - def in_transaction?(_), do: {:error, %Nebulex.Error{reason: :error}} + def in_transaction?(_, _), do: {:error, %Nebulex.Error{reason: :error}} end diff --git a/test/support/test_adapter.exs b/test/support/test_adapter.exs index 6b1a23d8..f4e4aad2 100644 --- a/test/support/test_adapter.exs +++ b/test/support/test_adapter.exs @@ -40,9 +40,6 @@ defmodule Nebulex.TestAdapter do # Inherit default stats implementation use Nebulex.Adapter.Stats - use Nebulex.Cache.Options - - import Nebulex.Adapter, only: [defspan: 2] import Nebulex.Helpers alias Nebulex.Adapter.Stats @@ -56,9 +53,6 @@ defmodule Nebulex.TestAdapter do @impl true def init(opts) do - # Validate options - opts = validate!(opts) - # Required options telemetry = Keyword.fetch!(opts, :telemetry) telemetry_prefix = Keyword.fetch!(opts, :telemetry_prefix) @@ -254,15 +248,15 @@ defmodule Nebulex.TestAdapter do end @impl true - defspan in_transaction?(adapter_meta) do - super(adapter_meta) + defspan in_transaction?(adapter_meta, opts) do + super(adapter_meta, opts) end ## Nebulex.Adapter.Stats @impl true - defspan stats(adapter_meta) do - with {:ok, %Nebulex.Stats{} = stats} <- super(adapter_meta) do + defspan stats(adapter_meta, opts) do + with {:ok, %Nebulex.Stats{} = stats} <- super(adapter_meta, opts) do {:ok, %{stats | metadata: Map.put(stats.metadata, :started_at, adapter_meta.started_at)}} end end