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
This commit is contained in:
hq1 2024-03-12 07:58:12 +01:00 committed by GitHub
parent bb8a272a36
commit 437a3350ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 323 additions and 100 deletions

View File

@ -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])

View File

@ -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

107
lib/plausible/cache/adapter.ex vendored Normal file
View File

@ -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

74
lib/plausible/cache/stats.ex vendored Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
)
]

View File

@ -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

View File

@ -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"},

50
test/plausible/cache/stats_test.exs vendored Normal file
View File

@ -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

View File

@ -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)

View File

@ -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)