File size: 6,097 Bytes
c9c49ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
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