From 437a3350ff44ce0fc964f89989c261958c93454a Mon Sep 17 00:00:00 2001 From: hq1 Date: Tue, 12 Mar 2024 07:58:12 +0100 Subject: [PATCH] Replace caching engine (#3878) * Dependencies: swap Cachex for ConCache * Implement Cache adapter wrapping ConCache * Implement cache stats tracker, for metrics * Use Cache.Adapter in Plausible.Cache Marking the test as not slow anymore * Use Cache Adapter when tracking sessions * Use Cache Adapter for UA parsing * Rename child identifiers - cachex is obsolete now * Test stats tracking * Update grafana metrics * Put all caches under common child specification * Try less * Shorten the function delegation path --- lib/plausible/application.ex | 27 ++--- lib/plausible/cache.ex | 65 +++++------ lib/plausible/cache/adapter.ex | 107 +++++++++++++++++++ lib/plausible/cache/stats.ex | 74 +++++++++++++ lib/plausible/ingestion/event.ex | 8 +- lib/plausible/session/cache_store.ex | 13 +-- lib/plausible/shield/country_rule_cache.ex | 2 +- lib/plausible/shield/ip_rule_cache.ex | 2 +- lib/plausible/site/cache.ex | 2 +- lib/plausible/telemetry/plausible_metrics.ex | 38 +++---- mix.exs | 4 +- mix.lock | 2 +- test/plausible/cache/stats_test.exs | 50 +++++++++ test/plausible/cache_test.exs | 28 ++++- test/plausible/site/cache_test.exs | 1 - 15 files changed, 323 insertions(+), 100 deletions(-) create mode 100644 lib/plausible/cache/adapter.ex create mode 100644 lib/plausible/cache/stats.ex create mode 100644 test/plausible/cache/stats_test.exs diff --git a/lib/plausible/application.ex b/lib/plausible/application.ex index 8f2b62215..bdc0aa9bc 100644 --- a/lib/plausible/application.ex +++ b/lib/plausible/application.ex @@ -10,6 +10,7 @@ defmodule Plausible.Application do on_full_build(do: Plausible.License.ensure_valid_license()) children = [ + Plausible.Cache.Stats, Plausible.Repo, Plausible.ClickhouseRepo, Plausible.IngestRepo, @@ -23,13 +24,15 @@ defmodule Plausible.Application do Supervisor.child_spec(Plausible.Event.WriteBuffer, id: Plausible.Event.WriteBuffer), Supervisor.child_spec(Plausible.Session.WriteBuffer, id: Plausible.Session.WriteBuffer), ReferrerBlocklist, - Supervisor.child_spec({Cachex, name: :user_agents, limit: 10_000, stats: true}, - id: :cachex_user_agents + Plausible.Cache.Adapter.child_spec(:user_agents, :cache_user_agents, + ttl_check_interval: :timer.seconds(5), + global_ttl: :timer.minutes(60) ), - Supervisor.child_spec({Cachex, name: :sessions, limit: nil, stats: true}, - id: :cachex_sessions + Plausible.Cache.Adapter.child_spec(:sessions, :cache_sessions, + ttl_check_interval: :timer.seconds(1), + global_ttl: :timer.minutes(30) ), - {Plausible.Site.Cache, []}, + {Plausible.Site.Cache, ttl_check_interval: false}, {Plausible.Cache.Warmer, [ child_name: Plausible.Site.Cache.All, @@ -44,7 +47,7 @@ defmodule Plausible.Application do interval: :timer.seconds(30), warmer_fn: :refresh_updated_recently ]}, - {Plausible.Shield.IPRuleCache, []}, + {Plausible.Shield.IPRuleCache, ttl_check_interval: false}, {Plausible.Cache.Warmer, [ child_name: Plausible.Shield.IPRuleCache.All, @@ -59,7 +62,7 @@ defmodule Plausible.Application do interval: :timer.seconds(35), warmer_fn: :refresh_updated_recently ]}, - {Plausible.Shield.CountryRuleCache, []}, + {Plausible.Shield.CountryRuleCache, ttl_check_interval: false}, {Plausible.Cache.Warmer, [ child_name: Plausible.Shield.CountryRuleCache.All, @@ -168,16 +171,6 @@ defmodule Plausible.Application do ) end - def report_cache_stats() do - case Cachex.stats(:user_agents) do - {:ok, stats} -> - Logger.info("User agent cache stats: #{inspect(stats)}") - - e -> - IO.puts("Unable to show cache stats: #{inspect(e)}") - end - end - defp setup_opentelemetry() do OpentelemetryPhoenix.setup() OpentelemetryEcto.setup([:plausible, :repo]) diff --git a/lib/plausible/cache.ex b/lib/plausible/cache.ex index 5f27bda82..17ec2d2cf 100644 --- a/lib/plausible/cache.ex +++ b/lib/plausible/cache.ex @@ -8,10 +8,10 @@ defmodule Plausible.Cache do # - Optionally override `unwrap_cache_keys/1` # - Populate the cache with `Plausible.Cache.Warmer` - Serves as a thin wrapper around Cachex, but the underlying + Serves as a wrapper around `Plausible.Cache.Adapter`, where the underlying implementation can be transparently swapped. - Even though the Cachex process is started, cache access is disabled + Even though normally the relevant Adapter processes are started, cache access is disabled during tests via the `:plausible, #{__MODULE__}, enabled: bool()` application env key. This can be overridden on case by case basis, using the child specs options. @@ -61,29 +61,33 @@ defmodule Plausible.Cache do @behaviour Plausible.Cache @modes [:all, :updated_recently] + alias Plausible.Cache.Adapter + @spec get(any(), Keyword.t()) :: any() | nil - def get(key, opts \\ []) do + def get(key, opts \\ []) when is_list(opts) do cache_name = Keyword.get(opts, :cache_name, name()) force? = Keyword.get(opts, :force?, false) if Plausible.Cache.enabled?() or force? do - case Cachex.get(cache_name, key) do - {:ok, nil} -> - nil - - {:ok, item} -> - item - - {:error, e} -> - Logger.error("Error retrieving key from '#{inspect(cache_name)}': #{inspect(e)}") - - nil - end + Adapter.get(cache_name, key) else get_from_source(key) end end + @spec get_or_store(any(), (-> any()), Keyword.t()) :: any() | nil + def get_or_store(key, fallback_fn, opts \\ []) + when is_function(fallback_fn, 0) and is_list(opts) do + cache_name = Keyword.get(opts, :cache_name, name()) + force? = Keyword.get(opts, :force?, false) + + if Plausible.Cache.enabled?() or force? do + Adapter.get(cache_name, key, fallback_fn) + else + get_from_source(key) || fallback_fn.() + end + end + def unwrap_cache_keys(items), do: items defoverridable unwrap_cache_keys: 1 @@ -117,10 +121,10 @@ defmodule Plausible.Cache do def merge_items(new_items, opts) do new_items = unwrap_cache_keys(new_items) cache_name = Keyword.get(opts, :cache_name, name()) - true = Cachex.put_many!(cache_name, new_items) + :ok = Adapter.put_many(cache_name, new_items) if Keyword.get(opts, :delete_stale_items?, true) do - {:ok, old_keys} = Cachex.keys(cache_name) + old_keys = Adapter.keys(cache_name) new = MapSet.new(Enum.into(new_items, [], fn {k, _} -> k end)) old = MapSet.new(old_keys) @@ -128,7 +132,7 @@ defmodule Plausible.Cache do old |> MapSet.difference(new) |> Enum.each(fn k -> - Cachex.del(cache_name, k) + Adapter.delete(cache_name, k) end) end @@ -139,11 +143,7 @@ defmodule Plausible.Cache do def child_spec(opts) do cache_name = Keyword.get(opts, :cache_name, name()) child_id = Keyword.get(opts, :child_id, child_id()) - - Supervisor.child_spec( - {Cachex, name: cache_name, limit: nil, stats: true}, - id: child_id - ) + Adapter.child_spec(cache_name, child_id, opts) end @doc """ @@ -154,7 +154,7 @@ defmodule Plausible.Cache do @spec ready?(atom()) :: boolean def ready?(cache_name \\ name()) do case size(cache_name) do - n when n > 0 -> + n when is_integer(n) and n > 0 -> true 0 -> @@ -165,21 +165,8 @@ defmodule Plausible.Cache do end end - @spec size() :: non_neg_integer() - def size(cache_name \\ name()) do - case Cachex.size(cache_name) do - {:ok, size} -> size - _ -> 0 - end - end - - @spec hit_rate() :: number() - def hit_rate(cache_name \\ name()) do - case Cachex.stats(cache_name) do - {:ok, stats} -> Map.get(stats, :hit_rate, 0) - _ -> 0 - end - end + defdelegate size(cache_name \\ name()), to: Plausible.Cache.Adapter + defdelegate hit_rate(cache_name \\ name()), to: Plausible.Cache.Stats @spec telemetry_event_refresh(atom(), atom()) :: list(atom()) def telemetry_event_refresh(cache_name \\ name(), mode) when mode in @modes do diff --git a/lib/plausible/cache/adapter.ex b/lib/plausible/cache/adapter.ex new file mode 100644 index 000000000..a4a4a8aeb --- /dev/null +++ b/lib/plausible/cache/adapter.ex @@ -0,0 +1,107 @@ +defmodule Plausible.Cache.Adapter do + @moduledoc """ + Interface for the underlying cache implementation. + Currently: ConCache + + Using the Adapter module directly, the user must ensure that the relevant + processes are available to use, which is normally done via the child specification. + """ + + require Logger + + @spec child_spec(atom(), atom(), Keyword.t()) :: Supervisor.child_spec() + def child_spec(name, child_id, opts \\ []) + when is_atom(name) and is_atom(child_id) and is_list(opts) do + cache_name = Keyword.get(opts, :cache_name, name) + child_id = Keyword.get(opts, :child_id, child_id) + ttl_check_interval = Keyword.get(opts, :ttl_check_interval, false) + + opts = + opts + |> Keyword.put(:name, cache_name) + |> Keyword.put(:ttl_check_interval, ttl_check_interval) + + Supervisor.child_spec( + {ConCache, opts}, + id: child_id + ) + end + + @spec size(atom()) :: non_neg_integer() | nil + def size(cache_name) do + ConCache.size(cache_name) + catch + :exit, _ -> nil + end + + @spec get(atom(), any()) :: any() + def get(cache_name, key) do + cache_name + |> ConCache.get(key) + |> Plausible.Cache.Stats.track(cache_name) + catch + :exit, _ -> + Logger.error("Error retrieving key from '#{inspect(cache_name)}'") + nil + end + + @spec get(atom(), any(), (-> any())) :: any() + def get(cache_name, key, fallback_fn) do + cache_name + |> ConCache.get_or_store(key, fn -> + {:from_fallback, fallback_fn.()} + end) + |> Plausible.Cache.Stats.track(cache_name) + catch + :exit, _ -> + Logger.error("Error retrieving key from '#{inspect(cache_name)}'") + nil + end + + @spec put(atom(), any(), any()) :: any() + def put(cache_name, key, value) do + :ok = ConCache.put(cache_name, key, value) + value + catch + :exit, _ -> + Logger.error("Error putting a key to '#{cache_name}'") + nil + end + + @spec put_many(atom(), [any()]) :: :ok + def put_many(cache_name, items) when is_list(items) do + true = :ets.insert(ConCache.ets(cache_name), items) + :ok + catch + :exit, _ -> + Logger.error("Error putting keys to '#{cache_name}'") + :ok + end + + @spec delete(atom(), any()) :: :ok + def delete(cache_name, key) do + ConCache.dirty_delete(cache_name, key) + catch + :exit, _ -> + Logger.error("Error deleting a key in '#{cache_name}'") + :ok + end + + @spec keys(atom()) :: Enumerable.t() + def keys(cache_name) do + ets = ConCache.ets(cache_name) + + Stream.resource( + fn -> :ets.first(ets) end, + fn + :"$end_of_table" -> {:halt, nil} + prev_key -> {[prev_key], :ets.next(ets, prev_key)} + end, + fn _ -> :ok end + ) + catch + :exit, _ -> + Logger.error("Error retrieving key from '#{inspect(cache_name)}'") + [] + end +end diff --git a/lib/plausible/cache/stats.ex b/lib/plausible/cache/stats.ex new file mode 100644 index 000000000..b1b240eb4 --- /dev/null +++ b/lib/plausible/cache/stats.ex @@ -0,0 +1,74 @@ +defmodule Plausible.Cache.Stats do + @moduledoc """ + Keeps track of hit/miss ratio for various caches. + """ + + use GenServer + + def start_link(opts) do + GenServer.start_link(__MODULE__, opts) + end + + def init(opts) do + table = Keyword.get(opts, :table, __MODULE__) + + ^table = + :ets.new(table, [ + :public, + :named_table, + :ordered_set, + read_concurrency: true, + write_concurrency: true + ]) + + {:ok, table} + end + + def gather(cache_name, table \\ __MODULE__) do + {:ok, + %{ + hit_rate: hit_rate(cache_name, table), + count: size(cache_name) || 0 + }} + end + + defdelegate size(cache_name), to: Plausible.Cache.Adapter + + def track(item, cache_name, table \\ __MODULE__) + + def track({:from_fallback, item}, cache_name, table) do + bump(cache_name, :miss, 1, table) + item + end + + def track(nil, cache_name, table) do + bump(cache_name, :miss, 1, table) + nil + end + + def track(item, cache_name, table) do + bump(cache_name, :hit, 1, table) + item + end + + def bump(cache_name, type, increment, table \\ __MODULE__) do + :ets.update_counter( + table, + {cache_name, type}, + increment, + {{cache_name, type}, 0} + ) + end + + def hit_rate(cache_name, table \\ __MODULE__) do + hit = :ets.lookup_element(table, {cache_name, :hit}, 2, 0) + miss = :ets.lookup_element(table, {cache_name, :miss}, 2, 0) + hit_miss = hit + miss + + if hit_miss == 0 do + 0.0 + else + hit / hit_miss * 100 + end + end +end diff --git a/lib/plausible/ingestion/event.ex b/lib/plausible/ingestion/event.ex index f11c435ac..3211055c5 100644 --- a/lib/plausible/ingestion/event.ex +++ b/lib/plausible/ingestion/event.ex @@ -388,11 +388,9 @@ defmodule Plausible.Ingestion.Event do end defp parse_user_agent(%Request{user_agent: user_agent}) when is_binary(user_agent) do - case Cachex.fetch(:user_agents, user_agent, &UAInspector.parse/1) do - {:ok, user_agent} -> user_agent - {:commit, user_agent} -> user_agent - _ -> nil - end + Plausible.Cache.Adapter.get(:user_agents, user_agent, fn -> + UAInspector.parse(user_agent) + end) end defp parse_user_agent(request), do: request diff --git a/lib/plausible/session/cache_store.ex b/lib/plausible/session/cache_store.ex index 6c29407d5..830453528 100644 --- a/lib/plausible/session/cache_store.ex +++ b/lib/plausible/session/cache_store.ex @@ -19,27 +19,22 @@ defmodule Plausible.Session.CacheStore do defp find_session(_domain, nil), do: nil defp find_session(event, user_id) do - from_cache = Cachex.get(:sessions, {event.site_id, user_id}) + from_cache = Plausible.Cache.Adapter.get(:sessions, {event.site_id, user_id}) case from_cache do - {:ok, nil} -> + nil -> nil - {:ok, session} -> + session -> if Timex.diff(event.timestamp, session.timestamp, :minutes) <= 30 do session end - - {:error, e} -> - Sentry.capture_message("Cachex error", extra: %{error: e}) - nil end end defp persist_session(session) do key = {session.site_id, session.user_id} - Cachex.put(:sessions, key, session, ttl: :timer.minutes(30)) - session + Plausible.Cache.Adapter.put(:sessions, key, session) end defp update_session(session, event) do diff --git a/lib/plausible/shield/country_rule_cache.ex b/lib/plausible/shield/country_rule_cache.ex index ec42232b7..453e0226e 100644 --- a/lib/plausible/shield/country_rule_cache.ex +++ b/lib/plausible/shield/country_rule_cache.ex @@ -19,7 +19,7 @@ defmodule Plausible.Shield.CountryRuleCache do def name(), do: @cache_name @impl true - def child_id(), do: :cachex_country_blocklist + def child_id(), do: :cache_country_blocklist @impl true def count_all() do diff --git a/lib/plausible/shield/ip_rule_cache.ex b/lib/plausible/shield/ip_rule_cache.ex index 9b941ba19..7644a9477 100644 --- a/lib/plausible/shield/ip_rule_cache.ex +++ b/lib/plausible/shield/ip_rule_cache.ex @@ -19,7 +19,7 @@ defmodule Plausible.Shield.IPRuleCache do def name(), do: @cache_name @impl true - def child_id(), do: :cachex_ip_blocklist + def child_id(), do: :cache_ip_blocklist @impl true def count_all() do diff --git a/lib/plausible/site/cache.ex b/lib/plausible/site/cache.ex index 2c9e850a5..67944cca1 100644 --- a/lib/plausible/site/cache.ex +++ b/lib/plausible/site/cache.ex @@ -37,7 +37,7 @@ defmodule Plausible.Site.Cache do def name(), do: @cache_name @impl true - def child_id(), do: :cachex_sites + def child_id(), do: :cache_sites @impl true def count_all() do diff --git a/lib/plausible/telemetry/plausible_metrics.ex b/lib/plausible/telemetry/plausible_metrics.ex index c5cb11265..80e78d25b 100644 --- a/lib/plausible/telemetry/plausible_metrics.ex +++ b/lib/plausible/telemetry/plausible_metrics.ex @@ -88,29 +88,23 @@ defmodule Plausible.PromEx.Plugins.PlausibleMetrics do end @doc """ - Add telemetry events for Cachex user agents and sessions + Fire telemetry events for various caches """ def execute_cache_metrics do - {:ok, user_agents_stats} = Cachex.stats(:user_agents) - {:ok, sessions_stats} = Cachex.stats(:sessions) + {:ok, user_agents_stats} = Plausible.Cache.Stats.gather(:user_agents) + {:ok, sessions_stats} = Plausible.Cache.Stats.gather(:sessions) - user_agents_hit_rate = Map.get(user_agents_stats, :hit_rate, 0.0) - sessions_hit_rate = Map.get(sessions_stats, :hit_rate, 0.0) - - {:ok, user_agents_count} = Cachex.size(:user_agents) - {:ok, sessions_count} = Cachex.size(:sessions) - - :telemetry.execute([:prom_ex, :plugin, :cachex, :user_agents], %{ - count: user_agents_count, - hit_rate: user_agents_hit_rate + :telemetry.execute([:prom_ex, :plugin, :cache, :user_agents], %{ + count: user_agents_stats.count, + hit_rate: user_agents_stats.hit_rate }) - :telemetry.execute([:prom_ex, :plugin, :cachex, :sessions], %{ - count: sessions_count, - hit_rate: sessions_hit_rate + :telemetry.execute([:prom_ex, :plugin, :cache, :sessions], %{ + count: sessions_stats.count, + hit_rate: sessions_stats.hit_rate }) - :telemetry.execute([:prom_ex, :plugin, :cachex, :sites], %{ + :telemetry.execute([:prom_ex, :plugin, :cache, :sites], %{ count: Site.Cache.size(), hit_rate: Site.Cache.hit_rate() }) @@ -144,32 +138,32 @@ defmodule Plausible.PromEx.Plugins.PlausibleMetrics do [ last_value( metric_prefix ++ [:cache, :sessions, :size], - event_name: [:prom_ex, :plugin, :cachex, :sessions], + event_name: [:prom_ex, :plugin, :cache, :sessions], measurement: :count ), last_value( metric_prefix ++ [:cache, :user_agents, :size], - event_name: [:prom_ex, :plugin, :cachex, :user_agents], + event_name: [:prom_ex, :plugin, :cache, :user_agents], measurement: :count ), last_value( metric_prefix ++ [:cache, :user_agents, :hit_ratio], - event_name: [:prom_ex, :plugin, :cachex, :user_agents], + event_name: [:prom_ex, :plugin, :cache, :user_agents], measurement: :hit_rate ), last_value( metric_prefix ++ [:cache, :sessions, :hit_ratio], - event_name: [:prom_ex, :plugin, :cachex, :sessions], + event_name: [:prom_ex, :plugin, :cache, :sessions], measurement: :hit_rate ), last_value( metric_prefix ++ [:cache, :sites, :size], - event_name: [:prom_ex, :plugin, :cachex, :sites], + event_name: [:prom_ex, :plugin, :cache, :sites], measurement: :count ), last_value( metric_prefix ++ [:cache, :sites, :hit_ratio], - event_name: [:prom_ex, :plugin, :cachex, :sites], + event_name: [:prom_ex, :plugin, :cache, :sites], measurement: :hit_rate ) ] diff --git a/mix.exs b/mix.exs index 95c76fdf5..f434ad6ca 100644 --- a/mix.exs +++ b/mix.exs @@ -67,7 +67,6 @@ defmodule Plausible.MixProject do {:bamboo_mua, "~> 0.1.4"}, {:bcrypt_elixir, "~> 3.0"}, {:bypass, "~> 2.1", only: [:dev, :test, :small_test]}, - {:cachex, "~> 3.4"}, {:ecto_ch, "~> 0.3"}, {:cloak, "~> 1.1"}, {:cloak_ecto, "~> 1.2"}, @@ -140,7 +139,8 @@ defmodule Plausible.MixProject do {:ex_aws, "~> 2.5"}, {:ex_aws_s3, "~> 2.5"}, {:sweet_xml, "~> 0.7.4"}, - {:testcontainers, "~> 1.6", only: [:test, :small_test]} + {:testcontainers, "~> 1.6", only: [:test, :small_test]}, + {:con_cache, "~> 1.0"} ] end diff --git a/mix.lock b/mix.lock index a985822b3..2669598ce 100644 --- a/mix.lock +++ b/mix.lock @@ -8,7 +8,6 @@ "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, - "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"}, "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "ch": {:hex, :ch, "0.2.4", "d510fbb5542d009f7c5b00bb1ecab73307b6066d9fb9b220600257d462cba67f", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "8f065d15aaf912ae8da56c9ca5298fb2d1a09108d006de589bcf8c2b39a7e2bb"}, @@ -19,6 +18,7 @@ "combination": {:hex, :combination, "0.0.3", "746aedca63d833293ec6e835aa1f34974868829b1486b1e1cb0685f0b2ae1f41", [:mix], [], "hexpm", "72b099f463df42ef7dc6371d250c7070b57b6c5902853f69deb894f79eda18ca"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, + "con_cache": {:hex, :con_cache, "1.0.0", "6405e2bd5d5005334af72939432783562a8c35a196c2e63108fe10bb97b366e6", [:mix], [], "hexpm", "4d1f5cb1a67f3c1a468243dc98d10ac83af7f3e33b7e7c15999dc2c9bc0a551e"}, "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, diff --git a/test/plausible/cache/stats_test.exs b/test/plausible/cache/stats_test.exs new file mode 100644 index 000000000..557ab6f48 --- /dev/null +++ b/test/plausible/cache/stats_test.exs @@ -0,0 +1,50 @@ +defmodule Plausible.Cache.StatsTest do + use Plausible.DataCase, async: true + + alias Plausible.Cache.Stats + + test "when tracking is not initialized, stats are 0" do + assert {:ok, %{hit_rate: +0.0, count: 0}} = Stats.gather(:foo) + end + + test "when cache is started, stats are 0", %{test: test} do + {:ok, _} = Stats.start_link(name: test, table: test) + assert {:ok, %{hit_rate: +0.0, count: 0}} = Stats.gather(test, test) + end + + test "tracking changes hit ratio", %{test: test} do + {:ok, _} = Stats.start_link(name: test, table: test) + + Stats.track(nil, test, test) + assert {:ok, %{hit_rate: +0.0, count: 0}} = Stats.gather(test, test) + + Stats.track(:not_nil, test, test) + assert {:ok, %{hit_rate: 50.0, count: 0}} = Stats.gather(test, test) + + Stats.track(:not_nil, test, test) + Stats.track(:not_nil, test, test) + assert {:ok, %{hit_rate: 75.0, count: 0}} = Stats.gather(test, test) + + Stats.track(nil, test, test) + Stats.track(nil, test, test) + assert {:ok, %{hit_rate: 50.0, count: 0}} = Stats.gather(test, test) + end + + test "bump by custom number", %{test: test} do + {:ok, _} = Stats.start_link(name: test, table: test) + Stats.bump(test, :miss, 10, test) + assert {:ok, %{hit_rate: +0.0, count: 0}} = Stats.gather(test, test) + Stats.bump(test, :hit, 90, test) + assert {:ok, %{hit_rate: 90.0, count: 0}} = Stats.gather(test, test) + end + + test "count comes from cache adapter", %{test: test} do + {:ok, _} = Stats.start_link(name: test, table: test) + %{start: {m, f, a}} = Plausible.Cache.Adapter.child_spec(test, test) + {:ok, _} = apply(m, f, a) + + Plausible.Cache.Adapter.put(test, :key, :value) + assert Stats.size(test) == 1 + assert {:ok, %{hit_rate: +0.0, count: 1}} = Stats.gather(test, test) + end +end diff --git a/test/plausible/cache_test.exs b/test/plausible/cache_test.exs index 0e66c25f6..2190f09c8 100644 --- a/test/plausible/cache_test.exs +++ b/test/plausible/cache_test.exs @@ -50,7 +50,7 @@ defmodule Plausible.CacheTest do assert ExampleCache.get("key", force?: true, cache_name: NonExistingCache) == nil end) - assert log =~ "Error retrieving key from 'NonExistingCache': :no_cache" + assert log =~ "Error retrieving key from 'NonExistingCache'" end test "cache is not ready when it doesn't exist", %{test: test} do @@ -58,6 +58,32 @@ defmodule Plausible.CacheTest do end end + describe "stats tracking" do + test "get affects hit rate", %{test: test} do + {:ok, _} = start_test_cache(test) + :ok = ExampleCache.merge_items([{"item1", :item1}], cache_name: test) + assert ExampleCache.get("item1", cache_name: test, force?: true) + assert {:ok, %{hit_rate: 100.0}} = Plausible.Cache.Stats.gather(test) + refute ExampleCache.get("item2", cache_name: test, force?: true) + assert {:ok, %{hit_rate: 50.0}} = Plausible.Cache.Stats.gather(test) + end + + test "get_or_store affects hit rate", %{test: test} do + {:ok, _} = start_test_cache(test) + + :ok = ExampleCache.merge_items([{"item1", :item1}], cache_name: test) + assert ExampleCache.get("item1", cache_name: test, force?: true) + + assert "value" == + ExampleCache.get_or_store("item2", fn -> "value" end, + cache_name: test, + force?: true + ) + + assert {:ok, %{hit_rate: 50.0}} = Plausible.Cache.Stats.gather(test) + end + end + describe "merging cache items" do test "merging adds new items", %{test: test} do {:ok, _} = start_test_cache(test) diff --git a/test/plausible/site/cache_test.exs b/test/plausible/site/cache_test.exs index de2cb6ca3..7c5a889f7 100644 --- a/test/plausible/site/cache_test.exs +++ b/test/plausible/site/cache_test.exs @@ -323,7 +323,6 @@ defmodule Plausible.Site.CacheTest do @items1 for i <- 1..200_000, do: {i, nil, :batch1} @items2 for _ <- 1..200_000, do: {Enum.random(1..400_000), nil, :batch2} @max_seconds 2 - @tag :slow test "merging large sets is expected to be under #{@max_seconds} seconds", %{test: test} do {:ok, _} = start_test_cache(test)