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()) on_full_build(do: Plausible.License.ensure_valid_license())
children = [ children = [
Plausible.Cache.Stats,
Plausible.Repo, Plausible.Repo,
Plausible.ClickhouseRepo, Plausible.ClickhouseRepo,
Plausible.IngestRepo, Plausible.IngestRepo,
@ -23,13 +24,15 @@ defmodule Plausible.Application do
Supervisor.child_spec(Plausible.Event.WriteBuffer, id: Plausible.Event.WriteBuffer), Supervisor.child_spec(Plausible.Event.WriteBuffer, id: Plausible.Event.WriteBuffer),
Supervisor.child_spec(Plausible.Session.WriteBuffer, id: Plausible.Session.WriteBuffer), Supervisor.child_spec(Plausible.Session.WriteBuffer, id: Plausible.Session.WriteBuffer),
ReferrerBlocklist, ReferrerBlocklist,
Supervisor.child_spec({Cachex, name: :user_agents, limit: 10_000, stats: true}, Plausible.Cache.Adapter.child_spec(:user_agents, :cache_user_agents,
id: :cachex_user_agents ttl_check_interval: :timer.seconds(5),
global_ttl: :timer.minutes(60)
), ),
Supervisor.child_spec({Cachex, name: :sessions, limit: nil, stats: true}, Plausible.Cache.Adapter.child_spec(:sessions, :cache_sessions,
id: :cachex_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, {Plausible.Cache.Warmer,
[ [
child_name: Plausible.Site.Cache.All, child_name: Plausible.Site.Cache.All,
@ -44,7 +47,7 @@ defmodule Plausible.Application do
interval: :timer.seconds(30), interval: :timer.seconds(30),
warmer_fn: :refresh_updated_recently warmer_fn: :refresh_updated_recently
]}, ]},
{Plausible.Shield.IPRuleCache, []}, {Plausible.Shield.IPRuleCache, ttl_check_interval: false},
{Plausible.Cache.Warmer, {Plausible.Cache.Warmer,
[ [
child_name: Plausible.Shield.IPRuleCache.All, child_name: Plausible.Shield.IPRuleCache.All,
@ -59,7 +62,7 @@ defmodule Plausible.Application do
interval: :timer.seconds(35), interval: :timer.seconds(35),
warmer_fn: :refresh_updated_recently warmer_fn: :refresh_updated_recently
]}, ]},
{Plausible.Shield.CountryRuleCache, []}, {Plausible.Shield.CountryRuleCache, ttl_check_interval: false},
{Plausible.Cache.Warmer, {Plausible.Cache.Warmer,
[ [
child_name: Plausible.Shield.CountryRuleCache.All, child_name: Plausible.Shield.CountryRuleCache.All,
@ -168,16 +171,6 @@ defmodule Plausible.Application do
) )
end 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 defp setup_opentelemetry() do
OpentelemetryPhoenix.setup() OpentelemetryPhoenix.setup()
OpentelemetryEcto.setup([:plausible, :repo]) OpentelemetryEcto.setup([:plausible, :repo])

View File

@ -8,10 +8,10 @@ defmodule Plausible.Cache do
# - Optionally override `unwrap_cache_keys/1` # - Optionally override `unwrap_cache_keys/1`
# - Populate the cache with `Plausible.Cache.Warmer` # - 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. 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. 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. 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 @behaviour Plausible.Cache
@modes [:all, :updated_recently] @modes [:all, :updated_recently]
alias Plausible.Cache.Adapter
@spec get(any(), Keyword.t()) :: any() | nil @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()) cache_name = Keyword.get(opts, :cache_name, name())
force? = Keyword.get(opts, :force?, false) force? = Keyword.get(opts, :force?, false)
if Plausible.Cache.enabled?() or force? do if Plausible.Cache.enabled?() or force? do
case Cachex.get(cache_name, key) do Adapter.get(cache_name, key)
{:ok, nil} ->
nil
{:ok, item} ->
item
{:error, e} ->
Logger.error("Error retrieving key from '#{inspect(cache_name)}': #{inspect(e)}")
nil
end
else else
get_from_source(key) get_from_source(key)
end end
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 def unwrap_cache_keys(items), do: items
defoverridable unwrap_cache_keys: 1 defoverridable unwrap_cache_keys: 1
@ -117,10 +121,10 @@ defmodule Plausible.Cache do
def merge_items(new_items, opts) do def merge_items(new_items, opts) do
new_items = unwrap_cache_keys(new_items) new_items = unwrap_cache_keys(new_items)
cache_name = Keyword.get(opts, :cache_name, name()) 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 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)) new = MapSet.new(Enum.into(new_items, [], fn {k, _} -> k end))
old = MapSet.new(old_keys) old = MapSet.new(old_keys)
@ -128,7 +132,7 @@ defmodule Plausible.Cache do
old old
|> MapSet.difference(new) |> MapSet.difference(new)
|> Enum.each(fn k -> |> Enum.each(fn k ->
Cachex.del(cache_name, k) Adapter.delete(cache_name, k)
end) end)
end end
@ -139,11 +143,7 @@ defmodule Plausible.Cache do
def child_spec(opts) do def child_spec(opts) do
cache_name = Keyword.get(opts, :cache_name, name()) cache_name = Keyword.get(opts, :cache_name, name())
child_id = Keyword.get(opts, :child_id, child_id()) child_id = Keyword.get(opts, :child_id, child_id())
Adapter.child_spec(cache_name, child_id, opts)
Supervisor.child_spec(
{Cachex, name: cache_name, limit: nil, stats: true},
id: child_id
)
end end
@doc """ @doc """
@ -154,7 +154,7 @@ defmodule Plausible.Cache do
@spec ready?(atom()) :: boolean @spec ready?(atom()) :: boolean
def ready?(cache_name \\ name()) do def ready?(cache_name \\ name()) do
case size(cache_name) do case size(cache_name) do
n when n > 0 -> n when is_integer(n) and n > 0 ->
true true
0 -> 0 ->
@ -165,21 +165,8 @@ defmodule Plausible.Cache do
end end
end end
@spec size() :: non_neg_integer() defdelegate size(cache_name \\ name()), to: Plausible.Cache.Adapter
def size(cache_name \\ name()) do defdelegate hit_rate(cache_name \\ name()), to: Plausible.Cache.Stats
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
@spec telemetry_event_refresh(atom(), atom()) :: list(atom()) @spec telemetry_event_refresh(atom(), atom()) :: list(atom())
def telemetry_event_refresh(cache_name \\ name(), mode) when mode in @modes do 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 end
defp parse_user_agent(%Request{user_agent: user_agent}) when is_binary(user_agent) do 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 Plausible.Cache.Adapter.get(:user_agents, user_agent, fn ->
{:ok, user_agent} -> user_agent UAInspector.parse(user_agent)
{:commit, user_agent} -> user_agent end)
_ -> nil
end
end end
defp parse_user_agent(request), do: request 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(_domain, nil), do: nil
defp find_session(event, user_id) do 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 case from_cache do
{:ok, nil} -> nil ->
nil nil
{:ok, session} -> session ->
if Timex.diff(event.timestamp, session.timestamp, :minutes) <= 30 do if Timex.diff(event.timestamp, session.timestamp, :minutes) <= 30 do
session session
end end
{:error, e} ->
Sentry.capture_message("Cachex error", extra: %{error: e})
nil
end end
end end
defp persist_session(session) do defp persist_session(session) do
key = {session.site_id, session.user_id} key = {session.site_id, session.user_id}
Cachex.put(:sessions, key, session, ttl: :timer.minutes(30)) Plausible.Cache.Adapter.put(:sessions, key, session)
session
end end
defp update_session(session, event) do defp update_session(session, event) do

View File

@ -19,7 +19,7 @@ defmodule Plausible.Shield.CountryRuleCache do
def name(), do: @cache_name def name(), do: @cache_name
@impl true @impl true
def child_id(), do: :cachex_country_blocklist def child_id(), do: :cache_country_blocklist
@impl true @impl true
def count_all() do def count_all() do

View File

@ -19,7 +19,7 @@ defmodule Plausible.Shield.IPRuleCache do
def name(), do: @cache_name def name(), do: @cache_name
@impl true @impl true
def child_id(), do: :cachex_ip_blocklist def child_id(), do: :cache_ip_blocklist
@impl true @impl true
def count_all() do def count_all() do

View File

@ -37,7 +37,7 @@ defmodule Plausible.Site.Cache do
def name(), do: @cache_name def name(), do: @cache_name
@impl true @impl true
def child_id(), do: :cachex_sites def child_id(), do: :cache_sites
@impl true @impl true
def count_all() do def count_all() do

View File

@ -88,29 +88,23 @@ defmodule Plausible.PromEx.Plugins.PlausibleMetrics do
end end
@doc """ @doc """
Add telemetry events for Cachex user agents and sessions Fire telemetry events for various caches
""" """
def execute_cache_metrics do def execute_cache_metrics do
{:ok, user_agents_stats} = Cachex.stats(:user_agents) {:ok, user_agents_stats} = Plausible.Cache.Stats.gather(:user_agents)
{:ok, sessions_stats} = Cachex.stats(:sessions) {:ok, sessions_stats} = Plausible.Cache.Stats.gather(:sessions)
user_agents_hit_rate = Map.get(user_agents_stats, :hit_rate, 0.0) :telemetry.execute([:prom_ex, :plugin, :cache, :user_agents], %{
sessions_hit_rate = Map.get(sessions_stats, :hit_rate, 0.0) count: user_agents_stats.count,
hit_rate: user_agents_stats.hit_rate
{: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, :cachex, :sessions], %{ :telemetry.execute([:prom_ex, :plugin, :cache, :sessions], %{
count: sessions_count, count: sessions_stats.count,
hit_rate: sessions_hit_rate hit_rate: sessions_stats.hit_rate
}) })
:telemetry.execute([:prom_ex, :plugin, :cachex, :sites], %{ :telemetry.execute([:prom_ex, :plugin, :cache, :sites], %{
count: Site.Cache.size(), count: Site.Cache.size(),
hit_rate: Site.Cache.hit_rate() hit_rate: Site.Cache.hit_rate()
}) })
@ -144,32 +138,32 @@ defmodule Plausible.PromEx.Plugins.PlausibleMetrics do
[ [
last_value( last_value(
metric_prefix ++ [:cache, :sessions, :size], metric_prefix ++ [:cache, :sessions, :size],
event_name: [:prom_ex, :plugin, :cachex, :sessions], event_name: [:prom_ex, :plugin, :cache, :sessions],
measurement: :count measurement: :count
), ),
last_value( last_value(
metric_prefix ++ [:cache, :user_agents, :size], metric_prefix ++ [:cache, :user_agents, :size],
event_name: [:prom_ex, :plugin, :cachex, :user_agents], event_name: [:prom_ex, :plugin, :cache, :user_agents],
measurement: :count measurement: :count
), ),
last_value( last_value(
metric_prefix ++ [:cache, :user_agents, :hit_ratio], 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 measurement: :hit_rate
), ),
last_value( last_value(
metric_prefix ++ [:cache, :sessions, :hit_ratio], metric_prefix ++ [:cache, :sessions, :hit_ratio],
event_name: [:prom_ex, :plugin, :cachex, :sessions], event_name: [:prom_ex, :plugin, :cache, :sessions],
measurement: :hit_rate measurement: :hit_rate
), ),
last_value( last_value(
metric_prefix ++ [:cache, :sites, :size], metric_prefix ++ [:cache, :sites, :size],
event_name: [:prom_ex, :plugin, :cachex, :sites], event_name: [:prom_ex, :plugin, :cache, :sites],
measurement: :count measurement: :count
), ),
last_value( last_value(
metric_prefix ++ [:cache, :sites, :hit_ratio], metric_prefix ++ [:cache, :sites, :hit_ratio],
event_name: [:prom_ex, :plugin, :cachex, :sites], event_name: [:prom_ex, :plugin, :cache, :sites],
measurement: :hit_rate measurement: :hit_rate
) )
] ]

View File

@ -67,7 +67,6 @@ defmodule Plausible.MixProject do
{:bamboo_mua, "~> 0.1.4"}, {:bamboo_mua, "~> 0.1.4"},
{:bcrypt_elixir, "~> 3.0"}, {:bcrypt_elixir, "~> 3.0"},
{:bypass, "~> 2.1", only: [:dev, :test, :small_test]}, {:bypass, "~> 2.1", only: [:dev, :test, :small_test]},
{:cachex, "~> 3.4"},
{:ecto_ch, "~> 0.3"}, {:ecto_ch, "~> 0.3"},
{:cloak, "~> 1.1"}, {:cloak, "~> 1.1"},
{:cloak_ecto, "~> 1.2"}, {:cloak_ecto, "~> 1.2"},
@ -140,7 +139,8 @@ defmodule Plausible.MixProject do
{:ex_aws, "~> 2.5"}, {:ex_aws, "~> 2.5"},
{:ex_aws_s3, "~> 2.5"}, {:ex_aws_s3, "~> 2.5"},
{:sweet_xml, "~> 0.7.4"}, {:sweet_xml, "~> 0.7.4"},
{:testcontainers, "~> 1.6", only: [:test, :small_test]} {:testcontainers, "~> 1.6", only: [:test, :small_test]},
{:con_cache, "~> 1.0"}
] ]
end 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"}, "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"}, "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"}, "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"}, "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "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"}, "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"}, "combination": {:hex, :combination, "0.0.3", "746aedca63d833293ec6e835aa1f34974868829b1486b1e1cb0685f0b2ae1f41", [:mix], [], "hexpm", "72b099f463df42ef7dc6371d250c7070b57b6c5902853f69deb894f79eda18ca"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, "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"}, "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": {: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"}, "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 assert ExampleCache.get("key", force?: true, cache_name: NonExistingCache) == nil
end) end)
assert log =~ "Error retrieving key from 'NonExistingCache': :no_cache" assert log =~ "Error retrieving key from 'NonExistingCache'"
end end
test "cache is not ready when it doesn't exist", %{test: test} do test "cache is not ready when it doesn't exist", %{test: test} do
@ -58,6 +58,32 @@ defmodule Plausible.CacheTest do
end end
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 describe "merging cache items" do
test "merging adds new items", %{test: test} do test "merging adds new items", %{test: test} do
{:ok, _} = start_test_cache(test) {: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} @items1 for i <- 1..200_000, do: {i, nil, :batch1}
@items2 for _ <- 1..200_000, do: {Enum.random(1..400_000), nil, :batch2} @items2 for _ <- 1..200_000, do: {Enum.random(1..400_000), nil, :batch2}
@max_seconds 2 @max_seconds 2
@tag :slow
test "merging large sets is expected to be under #{@max_seconds} seconds", %{test: test} do test "merging large sets is expected to be under #{@max_seconds} seconds", %{test: test} do
{:ok, _} = start_test_cache(test) {:ok, _} = start_test_cache(test)