• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

whitfin / cachex / 64ffdb79131a3eb9e15e483ea81eedd63160a005-PR-426

30 Oct 2025 04:39AM UTC coverage: 99.042% (-1.0%) from 100.0%
64ffdb79131a3eb9e15e483ea81eedd63160a005-PR-426

Pull #426

github

whitfin
Remove tagging from inspection and commands
Pull Request #426: Simplify and naturalize API signatures and return types

68 of 74 new or added lines in 25 files covered. (91.89%)

2 existing lines in 2 files now uncovered.

827 of 835 relevant lines covered (99.04%)

1054.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.31
/lib/cachex/stats.ex
1
defmodule Cachex.Stats do
2
  @moduledoc """
3
  Hook module to control the gathering of cache statistics.
4

5
  This implementation of statistics tracking uses a hook to run asynchronously
6
  against a cache (so that it doesn't impact those who don't want it). It executes
7
  as a post hook and provides a solid example of what a hook can/should look like.
8

9
  This hook has zero knowledge of the cache it belongs to; it keeps track of an
10
  internal set of statistics based on the provided messages. This means that it
11
  can also be mocked easily using raw server calls to `handle_notify/3`.
12
  """
13
  use Cachex.Hook
14
  alias Cachex.Hook
15

16
  # need our macros
17
  import Cachex.Error
18
  import Cachex.Spec
19

20
  # add our aliases
21
  alias Cachex.Options
22

23
  # update increments
24
  @update_calls [
25
    :expire,
26
    :expire_at,
27
    :persist,
28
    :refresh,
29
    :touch,
30
    :update
31
  ]
32

33
  @doc """
34
  Retrieves the latest statistics for a cache.
35
  """
36
  @spec for_cache(cache :: Cachex.t()) :: {:ok, map()} | {:error, atom()}
37
  def for_cache(cache() = cache) do
38
    case Hook.locate(cache, __MODULE__) do
25✔
39
      nil ->
1✔
40
        error(:stats_disabled)
41

42
      hook(name: name) ->
43
        GenServer.call(name, :retrieve)
24✔
44
    end
45
  end
46

47
  ####################
48
  # Server Callbacks #
49
  ####################
50

51
  @doc false
52
  def actions,
248✔
53
    do: :all
54

55
  @doc false
56
  # Initializes this hook with a new stats container.
57
  #
58
  # The `:creationDate` field is set inside the `:meta` field to contain the date
59
  # at which the statistics container was first created (which is more of less
60
  # equivalent to the start time of the cache).
61
  def init(_options),
22✔
62
    do: {:ok, %{meta: %{creation_date: now()}}}
63

64
  @doc false
65
  # Retrieves the current stats container.
66
  #
67
  # This will just return the internal state to the calling process.
68
  def handle_call(:retrieve, _ctx, stats),
69
    do: {:reply, {:ok, stats}, stats}
70

71
  @doc false
72
  # Registers an action against the stats container.
73
  #
74
  # This clause will match against any failed requests and short-circuit to
75
  # avoid artificially adding errors to the statistics. In future it might
76
  # be that we want to track this, so this might change at some point.
77
  #
78
  # coveralls-ignore-start
79
  def handle_notify(_action, {:error, _result}, stats),
80
    do: {:ok, stats}
81

82
  # coveralls-ignore-stop
83

84
  @doc false
85
  # Registers an action against the stats container.
86
  #
87
  # This will increment the call count for every action taken on a cache, as
88
  # well as incrementing the operation count (although this could be computed
89
  # from the call counts).
90
  #
91
  # It will then pass the statistics on to `register_action/3` in order to
92
  # allow call specific statistics to be incremented. Note that the order of
93
  # `register_action/3` is naively ordered to try and optimize for frequency.
94
  def handle_notify({call, _args} = action, result, stats) do
94✔
95
    stats
96
    |> increment([:calls, call], 1)
97
    |> increment([:operations], 1)
98
    |> register_action(action, result)
99
    |> wrap(:ok)
100
  end
101

102
  ################
103
  # Registration #
104
  ################
105

106
  # Handles registration of `get()` command calls.
107
  #
108
  # This will increment the hits/misses of the stats container, based on
109
  # whether the value pulled back is `nil` or not (as `nil` is treated as
110
  # a missing value through Cachex as of v3).
111
  defp register_action(stats, {:get, _args}, {_tag, nil}),
112
    do: increment(stats, [:misses], 1)
3✔
113

114
  defp register_action(stats, {:get, _args}, {_tag, _value}),
115
    do: increment(stats, [:hits], 1)
6✔
116

117
  # Handles registration of `put()` command calls.
118
  #
119
  # These calls will just increment the `:writes` count of the statistics
120
  # container, but only if the write succeeded (as determined by the value).
121
  defp register_action(stats, {:put, _args}, {_tag, true}),
122
    do: increment(stats, [:writes], 1)
29✔
123

124
  # Handles registration of `put_many()` command calls.
125
  #
126
  # This is the same as the `put()` handler except that it will count the
127
  # number of pairs being processed when incrementing the `:writes` key.
128
  defp register_action(stats, {:put_many, [pairs | _]}, {_tag, true}),
129
    do: increment(stats, [:writes], length(pairs))
1✔
130

131
  # Handles registration of `del()` command calls.
132
  #
133
  # Cache deletions will increment the `:evictions` key count, based on
134
  # whether the call succeeded (i.e. the result value is truthy).
135
  defp register_action(stats, {:del, _args}, true),
136
    do: increment(stats, [:evictions], 1)
2✔
137

138
  # Handles registration of `purge()` command calls.
139
  #
140
  # A purge call will increment the `:evictions` key using the count of
141
  # purged keys as the number to increment by. The `:expirations` key
142
  # will also be incremented in the same way, to surface TTL deletions.
143
  defp register_action(stats, {:purge, _args}, count) do
144
    stats
145
    |> increment([:expirations], count)
146
    |> increment([:evictions], count)
1✔
147
  end
148

149
  # Handles registration of `fetch()` command calls.
150
  #
151
  # This will delegate through to `register_fetch/2` as the logic is
152
  # more complicated, and this will keep down the noise of head matches.
153
  defp register_action(stats, {:fetch, _args}, {label, _value}),
154
    do: register_fetch(stats, label)
4✔
155

156
  # Handles registration of `incr()` command calls.
157
  #
158
  # This delegates through to `register_increment/4` as the logic is a
159
  # little more complicated, and this will keep down the noise of matches.
160
  defp register_action(stats, {:incr, _args} = action, result),
161
    do: register_increment(stats, action, result, -1)
2✔
162

163
  # Handles registration of `decr()` command calls.
164
  #
165
  # This delegates through to `register_increment/4` as the logic is a
166
  # little more complicated, and this will keep down the noise of matches.
167
  defp register_action(stats, {:decr, _args} = action, result),
168
    do: register_increment(stats, action, result, 1)
2✔
169

170
  # Handles registration of `update()` command calls.
171
  #
172
  # This will increment the `:updates` key if the value signals that the
173
  # update was successful, otherwise nothing will be modified.
174
  defp register_action(stats, {:update, _args}, {_tag, true}),
UNCOV
175
    do: increment(stats, [:updates], 1)
×
176

177
  # Handles registration of `clear()` command calls.
178
  #
179
  # This operates in the same way as the `del()` call statistics, except that
180
  # a count is received in the result, and is used to increment by instead.
181
  defp register_action(stats, {:clear, _args}, count),
182
    do: increment(stats, [:evictions], count)
1✔
183

184
  # Handles registration of `exists?()` command calls.
185
  #
186
  # The result boolean will determine whether this increments the `:hits` or
187
  # `:misses` key of the main statistics container (true/false respectively).
188
  defp register_action(stats, {:exists?, _args}, true),
189
    do: increment(stats, [:hits], 1)
1✔
190

191
  defp register_action(stats, {:exists?, _args}, false),
192
    do: increment(stats, [:misses], 1)
1✔
193

194
  # Handles registration of `take()` command calls.
195
  #
196
  # Take calls are a little complicated because they need to increment the
197
  # global eviction count (due to removal) but also increment the global
198
  # hit/miss count, in addition to the status in the `:take` namespace.
199
  defp register_action(stats, {:take, _args}, nil),
200
    do: increment(stats, [:misses], 1)
1✔
201

202
  defp register_action(stats, {:take, _args}, _result) do
203
    stats
204
    |> increment([:hits], 1)
205
    |> increment([:evictions], 1)
1✔
206
  end
207

208
  # Handles registration of `invoke()` command calls.
209
  #
210
  # This will increment a custom invocations map to track custom command calls.
211
  defp register_action(stats, {:invoke, _args}, {:error, :invalid_command}),
NEW
212
    do: stats
×
213

214
  defp register_action(stats, {:invoke, [cmd | _args]}, _any),
215
    do: increment(stats, [:invocations, cmd], 1)
2✔
216

217
  # Handles registration of updating command calls.
218
  #
219
  # All of the matches calls (dictated by @update_calls) will increment the main
220
  # `:updates` key in the statistics map only if the value is received as `true`.
221
  defp register_action(stats, {action, _args}, true)
222
       when action in @update_calls,
223
       do: increment(stats, [:updates], 1)
2✔
224

225
  # No-op to avoid crashing on other statistics.
226
  defp register_action(stats, _action, _result),
227
    do: stats
35✔
228

229
  ########################
230
  # Registration Helpers #
231
  ########################
232

233
  # Handles tracking `fetch()` results based on the result tag.
234
  #
235
  # If there's an `:ok`, the value existed and so the `:hits` stat needs to
236
  # be incremented. If not, we need to increment the `:misses` count. In the
237
  # case of a miss, we also need to check for `:commit` vs `:ignore` to know
238
  # whether we should be updating the `:writes` key too.
239
  defp register_fetch(stats, :ok),
240
    do: increment(stats, [:hits], 1)
1✔
241

242
  defp register_fetch(stats, :commit) do
243
    stats
244
    |> register_fetch(:ignore)
245
    |> increment([:writes], 1)
2✔
246
  end
247

248
  defp register_fetch(stats, :ignore) do
249
    stats
250
    |> increment([:fetches], 1)
251
    |> increment([:misses], 1)
3✔
252
  end
253

254
  # Handles increment calls coming via `incr()` or `decr()`.
255
  #
256
  # The logic is the same for both, except for the provided offset (which is
257
  # basically just a sign flip). It's split out as it's a little more involved
258
  # than a basic stat count as we need to reverse the arguments to determine if
259
  # there was a new write or an update (based on the initial/amount arguments).
260
  defp register_increment(stats, _call, {:error, _reason}, _offset),
NEW
261
    do: stats
×
262

263
  defp register_increment(stats, {_type, args}, value, offset) do
264
    amount = Enum.at(args, 1, 1)
4✔
265
    options = Enum.at(args, 2, [])
4✔
266

267
    matcher = value + amount * offset
4✔
268

269
    case Options.get(options, :default, &is_integer/1, 0) do
4✔
270
      ^matcher ->
271
        increment(stats, [:writes], 1)
2✔
272

273
      _anything_else ->
274
        increment(stats, [:updates], 1)
2✔
275
    end
276
  end
277

278
  ##########################
279
  # Registration Utilities #
280
  ##########################
281

282
  # Increments statistics in the statistics container.
283
  #
284
  # This accepts a list of fields to specify the path to the key to increment,
285
  # much like the `update_in` provided in more recent versions of Elixir.
286
  defp increment(stats, [head], amount),
287
    do: Map.update(stats, head, amount, &(&1 + amount))
254✔
288

289
  defp increment(stats, [head | tail], amount) do
290
    Map.put(
96✔
291
      stats,
292
      head,
293
      case Map.get(stats, head) do
294
        nil -> increment(%{}, tail, amount)
23✔
295
        map -> increment(map, tail, amount)
73✔
296
      end
297
    )
298
  end
299
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc