Skip to content

Commit

Permalink
Abstract out stats registration, improve test cases
Browse files Browse the repository at this point in the history
  • Loading branch information
whitfin committed Apr 11, 2016
1 parent 95d9167 commit 30e09b2
Show file tree
Hide file tree
Showing 5 changed files with 444 additions and 190 deletions.
139 changes: 26 additions & 113 deletions lib/cachex/stats.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ defmodule Cachex.Stats do
# use the hooks
use Cachex.Hook

# we need a Logger
require Logger

# add some aliases
alias Cachex.Stats.Registry
alias Cachex.Util

@moduledoc false
Expand All @@ -14,10 +12,6 @@ defmodule Cachex.Stats do
# of what a hook can look like. This container has no knowledge of the cache
# it belongs to, it only keeps track of an internal struct.

@amount_based MapSet.new([ :clear, :purge ])
@status_based MapSet.new([ :get, :get_and_update, :expire, :expire_at, :persist, :refresh, :take, :ttl ])
@values_based MapSet.new([ :set, :del, :exists?, :update ])

@doc """
Initializes a new stats container, setting the creation date to the current time.
This state will be passed into each handler in this server.
Expand All @@ -30,56 +24,10 @@ defmodule Cachex.Stats do
We don't keep statistics on requests which errored, to avoid false positives
about what exactly is going on inside a given cache.
"""
def handle_notify(action, { status, val }, stats) when status != :error do
act = elem(action, 0)

amount_based? = find_if_required(@amount_based, act, false)
status_based? = find_if_required(@status_based, act, amount_based?)
values_based? = find_if_required(@values_based, act, amount_based? || status_based?)

set_value = cond do
status_based? -> status
values_based? -> val
amount_based? -> :total
true -> :calls
end

amount = cond do
amount_based? -> val
true -> 1
end

global_status = cond do
status_based? ->
case status do
:ok ->
[ :hitCount ]
:missing ->
[ :missCount ]
:loaded ->
[ :missCount, :loadCount ]
end
act == :exists? ->
[ val == true && :hitCount || :missCount ]
true ->
[ ]
end

global_change = case act do
action when action in [ :purge ] ->
[ :expiredCount | global_status ]
action when action in [ :del, :take, :clear ] ->
[ :evictionCount | global_status ]
action when action in [ :set, :get_and_update, :incr, :update ] ->
[ :setCount | global_status ]
_action ->
global_status
end

stats
|> increment(act, set_value, amount)
|> increment(:global, global_change, amount)
|> increment(:global, :opCount)
def handle_notify(action, { status, _val } = result, stats) when status != :error do
action
|> Kernel.elem(0)
|> Registry.register(result, stats)
|> Util.ok
end

Expand Down Expand Up @@ -122,6 +70,15 @@ defmodule Cachex.Stats do
|> add_meta(key, val)
end

# Plucks out the global component of the statistics if requested. This is just
# to maintain backwards compatibility with the previous iterations.
defp extract_global(stats, [ :global ]) do
stats
|> Map.delete(:global)
|> Map.merge(stats.global || %{ })
end
defp extract_global(stats, _other), do: stats

# Finalizes the stats to be returned in some form. Asking for different keys
# provides you with various bonus statistics about those keys. If you don't
# ask for anything specific, you get a high level overview about what's in the
Expand All @@ -130,50 +87,28 @@ defmodule Cachex.Stats do
options
|> Enum.map(fn(option) ->
case Map.get(stats, option) do
nil ->
Map.put(%{ }, option, %{ })
val ->
finalized_stats =
val
|> finalize(option)

case option do
:global ->
finalized_stats
_others ->
Map.put(%{ }, option, finalized_stats)
end
nil -> Map.new([{ option, %{ } }])
val -> Map.new([{ option, finalize(val, option) }])
end
end)
|> Enum.reduce(%{}, &(Map.merge(&2, &1)))
|> Map.merge(Map.get(stats, :meta))
|> Map.merge(stats.meta)
|> extract_global(options)
end
defp finalize(stats, :global) do
hitCount = Map.get(stats, :hitCount, 0)
missCount = Map.get(stats, :missCount, 0)
loadCount = Map.get(stats, :loadCount, 0)

totalMissCount = missCount + loadCount
hitsCount = Map.get(stats, :hitCount, 0)
missCount = Map.get(stats, :missCount, 0) + Map.get(stats, :loadCount, 0)

reqRates = case hitCount + totalMissCount do
0 ->
%{ requestCount: 0 }
reqRates = case hitsCount + missCount do
0 -> %{ }
v ->
cond do
hitCount == 0 -> %{
requestCount: v,
hitRate: 0,
missRate: 100
}
missCount == 0 -> %{
requestCount: v,
hitRate: 100,
missRate: 0
}
hitsCount == 0 -> %{ requestCount: v, hitRate: 0, missRate: 100 }
missCount == 0 -> %{ requestCount: v, hitRate: 100, missRate: 0 }
true -> %{
requestCount: v,
hitRate: hitCount / v,
missRate: totalMissCount / v
hitRate: hitsCount / v,
missRate: missCount / v
}
end
end
Expand All @@ -187,26 +122,4 @@ defmodule Cachex.Stats do
Map.has_key?(stats, option) && stats[option] || stats
end

# Looks inside a set for a value assuming it's required to still look for it.
# This is just sugar to normalize some of the unnecessary searches done in the
# event handlers defined above.
defp find_if_required(_set, _val, true), do: false
defp find_if_required(set, val, _required?), do: MapSet.member?(set, val)

# Increments a given set of statistics by a given amount. If the amount is not
# provided, we default to a value of 1. We accept a list of fields to work with
# as it's not unusual for an action to increment various fields at the same time.
defp increment(stats, action, fields, amount \\ 1) do
action_stats =
stats
|> Map.get(action, %{ })

new_action_stats =
fields
|> List.wrap
|> Enum.reduce(action_stats, &(Map.put(&2, &1, Map.get(&2, &1, 0) + amount)))

Map.put(stats, action, new_action_stats)
end

end
128 changes: 128 additions & 0 deletions lib/cachex/stats/registry.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
defmodule Cachex.Stats.Registry do
@moduledoc false
# Controls the registration of new actions taken inside the cache. This is moved
# outside due to the use of `process_action/2` which is defined many times to
# match against actions efficiently.

# add some aliases
alias Cachex.Util

@doc """
Registers an action with the stats, incrementing various fields as appropriate
and incrementing the global statistics map. Every action will increment the
global operation count, but all other changes are action-specific.
"""
def register(action, result, stats) do
action
|> process_action(result)
|> Enum.reduce(stats, &(increment(&1, &2)))
|> increment(:global, :opCount)
end

# Clearing a cache returns the number of entries removed, so we update both the
# total cleared as well as the global eviction count.
defp process_action(:clear, { _status, value }) do
[ { :clear, :total, value }, { :global, :evictionCount, value } ]
end

# Deleting a key should increment the delete count by 1 and the global eviction
# count by 1.
defp process_action(:del, { _status, value }) do
[ { :del, value, 1 }, { :global, :evictionCount, 1 } ]
end

# Checking if a key exists simply updates the global hit/miss count based on
# the value (which returns true or false depending on existence).
defp process_action(:exists?, { _status, value }) do
[ { :exists?, value, 1 }, { :global, value && :hitCount || :missCount, 1 } ]
end

# Retrieving a value will increment the global stats to represent whether the
# key existed, was missing, or was loaded.
defp process_action(:get, { status, _value }) do
[ { :get, status, 1 }, { :global, normalize_status(status), 1 } ]
end

# Purging receives the number of keys removed from the cache, so we use this
# number to increment the exiredCount in the global namespace.
defp process_action(:purge, { _status, value }) do
[ { :purge, :total, value }, { :global, :expiredCount, value } ]
end

# Sets always update the global namespace and the setCount key.
defp process_action(:set, { _status, value }) do
[ { :set, value, 1 }, { :global, :setCount, 1 } ]
end

# Taking a key will update evictions if they exist, otherwise they'll just update
# the same stats as if :get were called.
defp process_action(:take, { status, _value }) do
[ { :take, status, 1 }, { :global, normalize_status(status), 1 } ] ++ if status == :ok do
[ { :global , :evictionCount, 1 } ]
else
[]
end
end

# Calling TTL has zero effect on the global stats, so we simply increment the
# action's statistics.
defp process_action(:ttl, { status, _value }) do
[ { :ttl, status, 1 } ]
end

# An update will increment the update stats, and if it was successful we also
# register it in the global namespace as well.
defp process_action(:update, { _status, value }) do
[ { :update, value, 1 } ] ++ if value do
[ { :global , :updateCount, 1 } ]
else
[]
end
end

# Both the get_and_update and increment calls do either an update or a set depending on whether
# the key existed in the cache before the operation.
defp process_action(action, { status, _value }) when action in [ :get_and_update, :incr ] do
[ { action, status, 1 }, { :global, status == :ok && :updateCount || :setCount, 1 } ]
end

# Any TTL based changes just carry out updates inside the cache, so we increment the update count
# in the global namespace.
defp process_action(action, { _status, value }) when action in [ :expire, :expire_at, :persist, :refresh ] do
[ { action, value, 1 } ] ++ if value do
[ { :global , :updateCount, 1 } ]
else
[]
end
end

# Catch all stats should simply increment the call count by 1.
defp process_action(action, _result) do
[ { action, :calls, 1 } ]
end

# Increments a given set of statistics by a given amount. If the amount is not
# provided, we default to a value of 1. We accept a list of fields to work with
# as it's not unusual for an action to increment various fields at the same time.
defp increment({ action, fields, amount }, stats) do
increment(stats, action, List.wrap(fields), amount)
end
defp increment(stats, action, fields, amount \\ 1) do
{ _, updated_stats } = Map.get_and_update(stats, action, fn(inner_stats) ->
action_stats =
fields
|> List.wrap
|> Enum.reduce(inner_stats || %{ }, &(Util.increment_map_key(&2, &1, amount)))

{ inner_stats, action_stats }
end)
updated_stats
end

# Converts the result of a request into the type of count it should increment
# in the global statistics namespace.
defp normalize_status(:ok), do: [ :hitCount ]
defp normalize_status(:missing), do: [ :missCount ]
defp normalize_status(:loaded), do: [ :missCount, :loadCount ]

end
14 changes: 14 additions & 0 deletions lib/cachex/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,20 @@ defmodule Cachex.Util do
|> Enum.any?(&(&1 == fun_arity))
end

@doc """
Shorthand increments for a map key. If the value is not a number, it is assumed
to be 0.
"""
def increment_map_key(map, key, amount) do
{ _, updated_map } = Map.get_and_update(map, key, fn
(val) when is_number(val) ->
{ val, amount + val }
(val) ->
{ val, amount }
end)
updated_map
end

@doc """
Retrieves the last item in a Tuple. This is just shorthand around sizeof and
pulling the last element.
Expand Down
7 changes: 6 additions & 1 deletion lib/cachex/worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,12 @@ defmodule Cachex.Worker do
"""
def persist(%__MODULE__{ } = state, key, options \\ []) when is_list(options) do
do_action(state, { :persist, key, options }, fn ->
expire(state, key, nil, options)
case quietly_exists?(state, key) do
{ :ok, true } ->
state.actions.expire(state, key, nil, options)
_other_value_ ->
{ :missing, false }
end
end)
end

Expand Down

0 comments on commit 30e09b2

Please sign in to comment.