diff --git a/lib/con_cache.ex b/lib/con_cache.ex index 886bc44..11fe39c 100644 --- a/lib/con_cache.ex +++ b/lib/con_cache.ex @@ -51,7 +51,8 @@ defmodule ConCache do :acquire_lock_timeout, :callback, :touch_on_read, - :lock_pids + :lock_pids, + :name ] @type t :: pid | atom | {:global, any} | {:via, atom, any} @@ -223,7 +224,12 @@ defmodule ConCache do `touch_on_read` option is set while starting the cache. """ @spec get(t, key) :: value - def get(cache_id, key), do: Operations.get(Owner.cache(cache_id), key) + def get(cache_id, key) do + case Operations.fetch(Owner.cache(cache_id), key) do + :error -> nil + {:ok, value} -> value + end + end @doc """ Stores the item into the cache. diff --git a/lib/con_cache/operations.ex b/lib/con_cache/operations.ex index 16fe10e..b4d64c7 100644 --- a/lib/con_cache/operations.ex +++ b/lib/con_cache/operations.ex @@ -20,19 +20,14 @@ defmodule ConCache.Operations do defp lock_pid(cache, key), do: elem(cache.lock_pids, :erlang.phash2(key, tuple_size(cache.lock_pids))) - def get(cache, key) do - case fetch(cache, key) do - {:ok, value} -> value - :error -> nil - end - end - - defp fetch(%ConCache{ets: ets} = cache, key) do + def fetch(%ConCache{ets: ets} = cache, key, opts \\ []) do case :ets.lookup(ets, key) do [] -> + emit(telemetry_miss(), cache, opts) :error [{^key, value}] -> + emit(telemetry_hit(), cache, opts) read_touch(cache, key) {:ok, value} @@ -42,6 +37,7 @@ defmodule ConCache.Operations do |> Enum.map(fn {^key, value} -> value end) read_touch(cache, key) + emit(telemetry_hit(), cache, opts) {:ok, values} end end @@ -177,9 +173,13 @@ defmodule ConCache.Operations do def get_or_store(cache, key, fun) do if valid_ets_type?(cache) do - case get(cache, key) do - nil -> isolated_get_or_store(cache, key, fun) - value -> value + case fetch(cache, key, emit_telemetry?: false) do + :error -> + isolated_get_or_store(cache, key, fun) + + {:ok, value} -> + emit(telemetry_hit(), cache) + value end else raise_ets_type(cache) @@ -194,13 +194,14 @@ defmodule ConCache.Operations do def dirty_get_or_store(cache, key, fun) do if valid_ets_type?(cache) do - case get(cache, key) do - nil -> + case fetch(cache, key, emit_telemetry?: false) do + :error -> new_value = fun.() dirty_put(cache, key, new_value) + emit(telemetry_miss(), cache) value(new_value) - existing -> + {:ok, existing} -> existing end else @@ -210,9 +211,11 @@ defmodule ConCache.Operations do def fetch_or_store(cache, key, fun) do if valid_ets_type?(cache) do - case fetch(cache, key) do + case fetch(cache, key, emit_telemetry?: false) do :error -> isolated_fetch_or_store(cache, key, fun) - {:ok, existing} -> {:ok, existing} + {:ok, existing} -> + emit(telemetry_hit(), cache) + {:ok, existing} end else raise_ets_type(cache) @@ -227,11 +230,12 @@ defmodule ConCache.Operations do def dirty_fetch_or_store(cache, key, fun) do if valid_ets_type?(cache) do - case fetch(cache, key) do + case fetch(cache, key, emit_telemetry?: false) do :error -> case fun.() do {:ok, new_value} -> dirty_put(cache, key, new_value) + emit(telemetry_miss(), cache) {:ok, value(new_value)} {:error, _reason} = error -> @@ -273,13 +277,27 @@ defmodule ConCache.Operations do end defp with_existing(cache, key, fun) do - case get(cache, key) do - nil -> nil - existing -> fun.(existing) + case fetch(cache, key) do + :error -> nil + {:ok, existing} -> fun.(existing) end end def touch(cache, key) do set_ttl(cache, key, :renew) end + + def telemetry_hit() do + [:con_cache, :stats, :hit] + end + + def telemetry_miss() do + [:con_cache, :stats, :miss] + end + + defp emit(event, cache, opts \\ []) do + if Keyword.get(opts, :emit_telemetry?, true) do + :telemetry.execute(event, %{}, %{cache: cache}) + end + end end diff --git a/lib/con_cache/owner.ex b/lib/con_cache/owner.ex index e88b348..12a6a2d 100644 --- a/lib/con_cache/owner.ex +++ b/lib/con_cache/owner.ex @@ -43,7 +43,8 @@ defmodule ConCache.Owner do acquire_lock_timeout: options[:acquire_lock_timeout] || 5000, callback: options[:callback], touch_on_read: options[:touch_on_read] || false, - lock_pids: List.to_tuple(ConCache.LockSupervisor.lock_pids(parent_process())) + lock_pids: List.to_tuple(ConCache.LockSupervisor.lock_pids(parent_process())), + name: options[:name] } {:ok, _} = Registry.register(ConCache, {parent_process(), __MODULE__}, cache) diff --git a/mix.exs b/mix.exs index 764c4d8..f3801ec 100644 --- a/mix.exs +++ b/mix.exs @@ -18,7 +18,7 @@ defmodule ConCache.Mixfile do def application do [ - applications: [:logger], + applications: [:logger, :telemetry], mod: {ConCache.Application, []} ] end @@ -26,7 +26,8 @@ defmodule ConCache.Mixfile do defp deps do [ {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, - {:dialyxir, "~> 1.0", only: :dev, runtime: false} + {:dialyxir, "~> 1.0", only: :dev, runtime: false}, + {:telemetry, "~> 1.0"} ] end diff --git a/mix.lock b/mix.lock index e891ce4..9cb77fd 100644 --- a/mix.lock +++ b/mix.lock @@ -7,4 +7,5 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, } diff --git a/test/con_cache_test.exs b/test/con_cache_test.exs index e9251bc..e3a6436 100644 --- a/test/con_cache_test.exs +++ b/test/con_cache_test.exs @@ -427,6 +427,7 @@ defmodule ConCacheTest do {:ok, _} = start_cache(name: name) ConCache.put(name, :a, 1) assert ConCache.get(name, :a) == 1 + assert ConCache.Owner.cache(name).name == name end end @@ -463,6 +464,85 @@ defmodule ConCacheTest do assert ConCache.get(cache, :key) == :value end + describe "telemetry" do + defmodule TestTelemetryHandler do + def handle_event(event, _measurments, metadata, _config) do + send(self(), {:telemetry_handled, event, metadata}) + end + end + + setup do + hit = ConCache.Operations.telemetry_hit() + miss = ConCache.Operations.telemetry_miss() + + :telemetry.attach_many( + "test", + [ + hit, + miss + ], + &TestTelemetryHandler.handle_event/4, + nil + ) + + {:ok, hit_event: hit, miss_event: miss} + end + + test "get/2 emits telemetry events", %{hit_event: hit, miss_event: miss, test: test} do + {:ok, cache} = ConCache.start_link(ttl_check_interval: false, name: test) + + ConCache.get(cache, :key) + assert_receive {:telemetry_handled, ^miss, %{cache: %{owner_pid: ^cache, name: ^test}}} + + ConCache.put(cache, :key, :value) + ConCache.get(cache, :key) + assert_receive {:telemetry_handled, ^hit, %{cache: %{owner_pid: ^cache, name: ^test}}} + refute_receive _ + end + + test "get_or_store/3 emits telemetry events", %{hit_event: hit, miss_event: miss} do + {:ok, cache} = ConCache.start_link(ttl_check_interval: false) + + ConCache.get_or_store(cache, :key, fn -> :value end) + assert_receive {:telemetry_handled, ^miss, %{cache: %{owner_pid: ^cache, name: nil}}} + refute_receive _ + + ConCache.get_or_store(cache, :key, fn -> :value end) + assert_receive {:telemetry_handled, ^hit, %{cache: %{owner_pid: ^cache, name: nil}}} + refute_receive _ + end + + test "get_or_store/3 emits telemetry hit after put/3 is made", %{hit_event: hit, test: test} do + assert {:ok, cache} = ConCache.start_link(ttl_check_interval: false, name: test) + + ConCache.put(cache, :key, fn -> :value end) + ConCache.get_or_store(cache, :key, fn -> :value end) + assert_receive {:telemetry_handled, ^hit, %{cache: %{owner_pid: ^cache, name: ^test}}} + refute_receive _ + end + + test "fetch_or_store/3 emits telemetry events", %{hit_event: hit, miss_event: miss} do + {:ok, cache} = ConCache.start_link(ttl_check_interval: false) + + ConCache.fetch_or_store(cache, :key, fn -> {:ok, :value} end) + assert_receive {:telemetry_handled, ^miss, %{cache: %{owner_pid: ^cache, name: nil}}} + refute_receive _ + + ConCache.fetch_or_store(cache, :key, fn -> {:ok, :value} end) + assert_receive {:telemetry_handled, ^hit, %{cache: %{owner_pid: ^cache, name: nil}}} + refute_receive _ + end + + test "fetch_or_store/3 emits telemetry hit after put/3 is made", %{hit_event: hit, test: test} do + assert {:ok, cache} = ConCache.start_link(ttl_check_interval: false, name: test) + + ConCache.put(cache, :key, fn -> {:ok, :value} end) + ConCache.get_or_store(cache, :key, fn -> {:ok, :value} end) + assert_receive {:telemetry_handled, ^hit, %{cache: %{owner_pid: ^cache, name: ^test}}} + refute_receive _ + end + end + defp start_cache(opts \\ []) do ConCache.start_link(Keyword.merge([ttl_check_interval: false], opts)) end