| defmodule Plausible.CacheTest do |
| use Plausible.DataCase, async: true |
|
|
| alias Plausible.Cache |
| import ExUnit.CaptureLog |
|
|
| defmodule ExampleCache do |
| use Plausible.Cache |
|
|
| def name(), do: __MODULE__ |
| def child_id(), do: __MODULE__ |
| def count_all(), do: 100 |
|
|
| def base_db_query() do |
| %Ecto.Query{} |
| end |
|
|
| def get_from_source("existing_key") do |
| :from_source |
| end |
|
|
| def get_from_source(_) do |
| nil |
| end |
| end |
|
|
| describe "public cache interface" do |
| test "cache process is started, but falls back to the database if cache is disabled", %{ |
| test: test |
| } do |
| {:ok, _} = |
| Supervisor.start_link( |
| [{ExampleCache, [cache_name: ExampleCache.name(), child_id: test]}], |
| strategy: :one_for_one, |
| name: :"cache_supervisor_#{test}" |
| ) |
|
|
| insert(:site, domain: "example.test") |
| refute Cache.enabled?() |
| assert Process.alive?(Process.whereis(ExampleCache.name())) |
| refute Process.whereis(ExampleCache.Warmer) |
| assert :from_source = ExampleCache.get("existing_key") |
| assert ExampleCache.size() == 0 |
| refute ExampleCache.get("other.test") |
| end |
|
|
| test "critical cache errors are logged and nil is returned" do |
| log = |
| capture_log(fn -> |
| assert ExampleCache.get("key", force?: true, cache_name: NonExistingCache) == nil |
| end) |
|
|
| assert log =~ "Error retrieving key from 'NonExistingCache'" |
| end |
|
|
| test "cache is not ready when it doesn't exist", %{test: test} do |
| refute ExampleCache.ready?(test) |
| end |
| end |
|
|
| describe "merging cache items" do |
| test "merging adds new items", %{test: test} do |
| {:ok, _} = start_test_cache(test) |
|
|
| :ok = ExampleCache.merge_items([{"item1", :item1}], cache_name: test) |
| assert :item1 == ExampleCache.get("item1", cache_name: test, force?: true) |
| end |
|
|
| test "merging no new items leaves the old cache intact", %{test: test} do |
| {:ok, _} = start_test_cache(test) |
|
|
| :ok = ExampleCache.merge_items([{"item1", :item1}], cache_name: test) |
| :ok = ExampleCache.merge_items([], cache_name: test) |
| assert :item1 == ExampleCache.get("item1", cache_name: test, force?: true) |
| end |
|
|
| test "merging removes stale items", %{test: test} do |
| {:ok, _} = start_test_cache(test) |
|
|
| :ok = ExampleCache.merge_items([{"item1", :item1}], cache_name: test) |
| :ok = ExampleCache.merge_items([{"item2", :item2}], cache_name: test) |
|
|
| refute ExampleCache.get("item1", cache_name: test, force?: true) |
| assert ExampleCache.get("item2", cache_name: test, force?: true) |
| end |
|
|
| test "merging optionally leaves stale items intact", %{test: test} do |
| {:ok, _} = start_test_cache(test) |
|
|
| :ok = ExampleCache.merge_items([{"item1", :item1}], cache_name: test) |
|
|
| :ok = |
| ExampleCache.merge_items([{"item2", :item2}], |
| cache_name: test, |
| delete_stale_items?: false |
| ) |
|
|
| assert ExampleCache.get("item1", cache_name: test, force?: true) |
| assert ExampleCache.get("item2", cache_name: test, force?: true) |
| end |
|
|
| test "merging updates changed items", %{test: test} do |
| {:ok, _} = start_test_cache(test) |
|
|
| :ok = |
| ExampleCache.merge_items([{"item1", :item1}, {"item2", :item2}], |
| cache_name: test |
| ) |
|
|
| :ok = |
| ExampleCache.merge_items([{"item1", :changed}, {"item2", :item2}], |
| cache_name: test |
| ) |
|
|
| assert :changed == ExampleCache.get("item1", cache_name: test, force?: true) |
| assert :item2 == ExampleCache.get("item2", cache_name: test, force?: true) |
| end |
|
|
| test "put puts into local cache, overwriting as appropriate", %{test: test} do |
| {:ok, _} = start_test_cache(test) |
| opts = [cache_name: test, force?: true] |
| assert ExampleCache.put("key", :item1, opts) |
| assert ExampleCache.get("key", opts) == :item1 |
| assert ExampleCache.put("key", :updated, opts) |
| assert ExampleCache.get("key", opts) == :updated |
| end |
|
|
| test "broadcast_put puts into local cache", %{test: test} do |
| {:ok, _} = start_test_cache(test) |
| :ok = ExampleCache.broadcast_put("item1", :item1, cache_name: test) |
|
|
| assert eventually(fn -> |
| {ExampleCache.get("item1", cache_name: test, force?: true) == :item1, :ok} |
| end) |
| end |
| end |
|
|
| describe "warming the cache" do |
| test "cache warmer process warms up the cache", %{test: test} do |
| test_pid = self() |
|
|
| opts = [ |
| cache_impl: ExampleCache, |
| force_start?: true, |
| warmer_fn: report_back(test_pid), |
| cache_name: test, |
| interval: 30 |
| ] |
|
|
| {:ok, _} = |
| Supervisor.start_link([{Plausible.Cache.Warmer, opts}], |
| strategy: :one_for_one, |
| name: test |
| ) |
|
|
| assert Process.whereis(Plausible.Cache.Warmer) |
|
|
| assert_receive {:cache_warmed, %{opts: got_opts}} |
| assert got_opts[:cache_name] == test |
| end |
|
|
| test "cache warmer warms periodically with an interval", %{test: test} do |
| test_pid = self() |
|
|
| opts = [ |
| cache_impl: ExampleCache, |
| force_start?: true, |
| warmer_fn: report_back(test_pid), |
| cache_name: test, |
| interval: 30 |
| ] |
|
|
| {:ok, _} = start_test_warmer(opts) |
|
|
| assert_receive {:cache_warmed, %{at: at1}}, 100 |
| assert_receive {:cache_warmed, %{at: at2}}, 100 |
| assert_receive {:cache_warmed, %{at: at3}}, 100 |
|
|
| assert is_integer(at1) |
| assert is_integer(at2) |
| assert is_integer(at3) |
|
|
| assert at1 < at2 |
| assert at3 > at2 |
| end |
| end |
|
|
| defp report_back(test_pid) do |
| fn opts -> |
| send(test_pid, {:cache_warmed, %{at: System.monotonic_time(), opts: opts}}) |
| :ok |
| end |
| end |
|
|
| defp start_test_warmer(opts) do |
| child_name_opt = {:child_name, Keyword.fetch!(opts, :cache_name)} |
| %{start: {m, f, a}} = Cache.Warmer.child_spec([child_name_opt | opts]) |
| apply(m, f, a) |
| end |
|
|
| defp start_test_cache(cache_name) do |
| %{start: {m, f, a}} = ExampleCache.child_spec(cache_name: cache_name) |
| apply(m, f, a) |
| end |
| end |
|
|