From 7171d6aa589d8642e03d1c2d75cbca2a820b6201 Mon Sep 17 00:00:00 2001 From: Isaac Whitfield Date: Sun, 17 Apr 2016 15:49:31 +0100 Subject: [PATCH] Add new inspection functions for expired keys --- lib/cachex.ex | 4 ++++ lib/cachex/inspector.ex | 29 +++++++++++++++++++++++++++ lib/cachex/janitor.ex | 20 +------------------ lib/cachex/util.ex | 38 +++++++++++++++++++++++++----------- test/cachex_test/inspect.exs | 19 ++++++++++++++++++ 5 files changed, 80 insertions(+), 30 deletions(-) diff --git a/lib/cachex.ex b/lib/cachex.ex index a3ec70ce..75158c6b 100644 --- a/lib/cachex.ex +++ b/lib/cachex.ex @@ -822,6 +822,10 @@ defmodule Cachex do ## Options + * `{ :expired, :count }` - the number of keys which have expired but have not + yet been removed by TTL handlers. + * `{ :expired, :keys }` - the list of unordered keys which have expired but + have not yet been removed by TTL handlers. * `{ :memory, :bytes }` - the memory footprint of the cache in bytes. * `{ :memory, :binary }` - the memory footprint of the cache in binary format. * `:worker` - the internal state of the cache worker. This blocks the cache diff --git a/lib/cachex/inspector.ex b/lib/cachex/inspector.ex index b3dcf205..cc2cf609 100644 --- a/lib/cachex/inspector.ex +++ b/lib/cachex/inspector.ex @@ -29,6 +29,35 @@ defmodule Cachex.Inspector do { :error, "Invalid cache reference provided" } end + @doc """ + Returns information about the expired keys currently inside the cache (i.e. keys + which will be purged in the next Janitor run). + """ + def inspect(cache, :expired) do + __MODULE__.inspect(cache, { :expired, :count }) + end + def inspect(cache, { :expired, :count }) do + query = + true + |> Util.retrieve_expired_rows + + cache + |> :ets.select_count(query) + |> Util.ok + end + def inspect(cache, { :expired, :keys }) do + query = + :"$1" + |> Util.retrieve_expired_rows + + cache + |> :ets.select(query) + |> Util.ok + end + def inspect(_cache, { :expired, _unknown }) do + { :error, "Invalid expiration inspection type provided" } + end + @doc """ Requests the memory information from a cache, and converts it using the word size of the system, in order to return a number of bytes or as a binary. diff --git a/lib/cachex/janitor.ex b/lib/cachex/janitor.ex index 1f424b8c..dc506c72 100644 --- a/lib/cachex/janitor.ex +++ b/lib/cachex/janitor.ex @@ -72,25 +72,7 @@ defmodule Cachex.Janitor do process as needed. This is needed because we expose purging in the public API. """ def purge_records(cache) when is_atom(cache) do - { :ok, :ets.select_delete(cache, create_selection(true)) } - end - - # Returns a selection to return the designated values, just an easier way to - # define this in one place - not the nicest piece in the world. - defp create_selection(return) do - [ - { - { :"_", :"$1", :"$2", :"$3", :"_" }, # input (our records) - [ - { - :andalso, # guards for matching - { :"/=", :"$3", nil }, # where a TTL is set - { :"<", { :"+", :"$2", :"$3" }, now } # and the TTL has passed - } - ], - [ return ] # our output - } - ] + { :ok, :ets.select_delete(cache, retrieve_expired_rows(true)) } end # Schedules a check to occur after the designated interval. Once scheduled, diff --git a/lib/cachex/util.ex b/lib/cachex/util.ex index ab723a91..c5e6b4d1 100644 --- a/lib/cachex/util.ex +++ b/lib/cachex/util.ex @@ -22,6 +22,15 @@ defmodule Cachex.Util do |> IO.iodata_to_binary end + @doc """ + Creates a match spec for the cache using the provided rules, and returning the + provided return values. This is just shorthand for writing the same boilerplate + spec over and over again. + """ + def create_match(return, where) do + [ { { :"_", :"$1", :"$2", :"$3", :"$4" }, List.wrap(where), List.wrap(return) } ] + end + @doc """ Creates a long machine name from a provided binary name. If a hostname is given, it will be used - otherwise we default to using the local node's hostname. @@ -279,19 +288,26 @@ defmodule Cachex.Util do like finding all stored keys and all stored values. """ def retrieve_all_rows(return) do - [ + create_match(return, [ + { + :orelse, # guards for matching + { :"==", :"$3", nil }, # where a TTL is not set + { :">", { :"+", :"$2", :"$3" }, now } # or the TTL has not passed + } + ]) + end + + @doc """ + Returns a selection to return the designated value for all expired rows. + """ + def retrieve_expired_rows(return) do + create_match(return, [ { - { :"_", :"$1", :"$2", :"$3", :"$4" }, # input (our records) - [ - { - :orelse, # guards for matching - { :"==", :"$3", nil }, # where a TTL is set - { :">", { :"+", :"$2", :"$3" }, now } # and the TTL has not passed - } - ], - [ return ] # our output + :andalso, # guards for matching + { :"/=", :"$3", nil }, # where a TTL is set + { :"<", { :"+", :"$2", :"$3" }, now } # and the TTL has passed } - ] + ]) end @doc """ diff --git a/test/cachex_test/inspect.exs b/test/cachex_test/inspect.exs index f2c0ec96..d60793df 100644 --- a/test/cachex_test/inspect.exs +++ b/test/cachex_test/inspect.exs @@ -69,6 +69,25 @@ defmodule CachexTest.Inspect do assert(state_result == worker_result) end + test "inspect can return a count of expired keys", state do + inspect_result = Cachex.inspect(state.cache, :expired) + assert(inspect_result == { :ok, 0 }) + + set_result = Cachex.set(state.cache, "key", "value", ttl: 1) + assert(set_result == { :ok, true }) + + :timer.sleep(2) + + inspect_result = Cachex.inspect(state.cache, { :expired, :count }) + assert(inspect_result == { :ok, 1 }) + + inspect_result = Cachex.inspect(state.cache, { :expired, :keys }) + assert(inspect_result == { :ok, [ "key" ] }) + + inspect_result = Cachex.inspect(state.cache, { :expired, :missing }) + assert(inspect_result == { :error, "Invalid expiration inspection type provided" }) + end + test "inspect fails safely on invalid options", state do inspect_result = Cachex.inspect(state.cache, :missing_option)