Repository: whitfin/cachex (elixir)

Issue:
Add a simple eviction policy to enforce limits with low memory overhead
This was sort of raised in https://github.com/whitfin/cachex/issues/300, but it's something I've been wanting to do for a while.

Right now there is an LRW eviction policy, which acts as an evented system for pruning records when the cache is too large. As such it's extremely accurate on keeping the cache below bounds. This comes at a cost of message passing (as it acts as a hook).

We can provide the same, except that it runs on a timer instead of acting as a hook. This will be less accurate, but there will be far lower memory used by the actual policy. You are effectively trading lower policy memory for potential bursts in memory as your cache goes over the limit. 

It is important to document the difference between the hooks, and their tradeoffs. I'm not sure how to name the new policies, perhaps we have `Cachex.Policy.LRW.Evented` and `Cachex.Policy.LRW.Timed`, and we alias `Cachex.Policy.LRW` through to the evented version (so we can ship this in a minor version bump).

Pull request description:
Add a scheduled LRW policy for low memory use
This closes #303 and #300. 

This PR introduces a new implementation of the LRW policy which avoids the need for message passing in hooks. It operates on a (configurable) schedule to check the cache bounds. As such, it uses less memory but might have a larger delay on reclaiming space - pick your poison.

I moved `Cachex.Policy.LRW` into `Cachex.Policy.LRW.Evented` and delegated through to save the existing defaults, or people referencing that module already. The LRW logic stays inside `Cachex.Policy.LRW`, because it's used in both places. It will also be used if we ever get around to #305. 

I *think* I want to change the default away from the evented version to the scheduled version, but I need to think more on this. Not only is the policy server more efficient, it will actually help throughput of your cache (because it's not pinging the LRW hook on every write). Given that the check is once per second, it seems like it should be hard to notice the change for most use cases.

I'm not sure I like `enforce_bounds` as a public name, so need to think about that. Even something like `prune/2` might be better as a public name. I also removed some junk in the policy space which was unused in favour of just using hooks going forward. I originally had bigger plans, which is why the spec stuff was in there - but at this point I think it's just bloat. 

Patch:
diff --git a/lib/cachex/policy.ex b/lib/cachex/policy.ex
index 57f0c10..b555499 100644
--- a/lib/cachex/policy.ex
+++ b/lib/cachex/policy.ex
@@ -17,16 +17,6 @@ defmodule Cachex.Policy do
   """
   @callback hooks(Spec.limit()) :: [Spec.hook()]
 
-  @doc """
-  Returns an optional child spec to start for this policy.
-  """
-  @callback child_spec(Spec.limit()) :: Supervisor.Spec.spec()
-
-  @doc """
-  Returns the Supervisor strategy for this policy.
-  """
-  @callback strategy :: Supervisor.Spec.strategy()
-
   ##################
   # Implementation #
   ##################
@@ -37,22 +27,12 @@ defmodule Cachex.Policy do
       # include the behaviour
       @behaviour Cachex.Policy
 
-      @doc false
-      def child_spec(_limit),
-        do: []
-
       @doc false
       def hooks(_limit),
         do: []
 
-      @doc false
-      def strategy,
-        do: :one_for_one
-
       # all can be overridden
-      defoverridable hooks: 1,
-                     strategy: 0,
-                     child_spec: 1
+      defoverridable hooks: 1
     end
   end
 end
diff --git a/lib/cachex/policy/lrw.ex b/lib/cachex/policy/lrw.ex
index d754554..cefc7aa 100644
--- a/lib/cachex/policy/lrw.ex
+++ b/lib/cachex/policy/lrw.ex
@@ -1,23 +1,19 @@
 defmodule Cachex.Policy.LRW do
   @moduledoc """
-  Least recently written eviction policy for Cachex.
+  Least recently written eviction policies for Cachex.
 
-  This module provides an eviction policy for Cachex, evicting the least-recently
-  written entries from the cache. This is determined by the touched time inside
-  each cache record, which means that this eviction is almost zero cost, consuming
-  no additional memory beyond that of the running GenServer.
+  This module provides general utilities for implementing an eviction policy for
+  Cachex which will evict the least-recently written entries from the cache. This
+  is determined by the touched time inside each cache record, which means that we
+  don't have to store any additional tables to keep track of access time.
 
-  This policy accepts a reclaim space to determine how much of the cache to evict,
-  allowing the user to determine exactly how much they'd like to trim in the event
-  they've gone over the limit.
+  There are several policies implemented using this algorithm:
 
-  The `:batch_size` option can be set in the limit options to dictate how many
-  entries should be removed at once by this policy. This will default to a batch
-  size of 100 entries at a time.
+  * `Cachex.Policy.LRW.Evented`
+  * `Cachex.Policy.LRW.Scheduled`
 
-  This eviction is relatively fast, and should keep the cache below bounds at most
-  times. Note that many writes in a very short amount of time can flood the cache,
-  but it should recover given a few seconds.
+  Although the functions in this module are public, the way they function internally
+  should be treated as private and subject to change at any point.
   """
   use Cachex.Hook
   use Cachex.Policy
@@ -29,9 +25,6 @@ defmodule Cachex.Policy.LRW do
   alias Cachex.Query
   alias Cachex.Services.Informant
 
-  # actions which didn't trigger a write
-  @ignored [:error, :ignored]
-
   # compile our QLC match at runtime to avoid recalculating
   @qlc_match Query.raw(true, {:key, :touched})
 
@@ -39,54 +32,28 @@ defmodule Cachex.Policy.LRW do
   # Policy Behaviour #
   ####################
 
-  @doc """
-  Retrieves a list of hooks required to run against this policy.
-  """
-  @spec hooks(Spec.limit()) :: [Spec.hook()]
-  def hooks(limit),
-    do: [hook(module: __MODULE__, state: limit)]
+  @doc false
+  # Backwards compatibility with < v3.5.x defaults
+  defdelegate hooks(limit), to: __MODULE__.Scheduled
 
-  ######################
-  # Hook Configuration #
-  ######################
+  #############
+  # Algorithm #
+  #############
 
   @doc """
-  Returns the actions this policy should listen on.
+  Enforces cache bounds based on the provided limit.
 
-  This returns as a `MapSet` to optimize the lookups
-  on actions to O(n) in the broadcasting algorithm.
-  """
-  @spec actions :: [atom]
-  def actions,
-    do: [
-      :put,
-      :decr,
-      :incr,
-      :fetch,
-      :update,
-      :put_many,
-      :get_and_update
-    ]
+  This function will enforce cache bounds using a least recently written (LRW)
+  eviction policy. It will trigger a Janitor purge to clear expired records
+  before attempting to trim older cache entries.
 
-  @doc """
-  Returns the provisions this policy requires.
+  The `:batch_size` option can be set in the limit options to dictate how many
+  entries should be removed at once by this policy. This will default to a batch
+  size of 100 entries at a time.
   """
-  @spec provisions :: [atom]
-  def provisions,
-    do: [:cache]
-
-  ####################
-  # Server Callbacks #
-  ####################
-
-  @doc false
-  # Initializes this policy using the limit being enforced.
-  #
-  # The maximum size is stored in the state, alongside the pre-calculated
-  # number to trim down to. The batch size to use when removing records is
-  # also configurable via the provided options.
-  def init(limit(size: max_size, reclaim: reclaim, options: options)) do
-    trim_bound = round(max_size * reclaim)
+  @spec enforce(Spec.cache(), Spec.limit()) :: :ok
+  def enforce(cache() = cache, limit() = limit) do
+    limit(size: max_size, reclaim: reclaim, options: options) = limit
 
     batch_size =
       case Keyword.get(options, :batch_size, 100) do
@@ -94,69 +61,17 @@ defmodule Cachex.Policy.LRW do
         val -> val
       end
 
-    {:ok, {max_size, trim_bound, batch_size, nil}}
-  end
-
-  @doc false
-  # Handles notification of a cache action.
-  #
-  # This will check if the action can modify the size of the cache, and if so will
-  # execute the boundary enforcement to trim the size as needed.
-  #
-  # Note that this will ignore error results and only operates on actions which are
-  # able to cause a net gain in cache size (so removals are also ignored).
-  def handle_notify(_message, {status, _value}, opts)
-      when status not in @ignored,
-      do: enforce_bounds(opts) && {:ok, opts}
-
-  def handle_notify(_message, _result, opts),
-    do: {:ok, opts}
+    reclaim_bound = round(max_size * reclaim)
 
-  @doc false
-  # Receives a provisioned cache instance.
-  #
-  # The provided cache is then stored in the cache and used for cache calls going
-  # forwards, in order to skip the lookups inside the cache overseer for performance.
-  def handle_provision({:cache, cache}, {max_size, reclaim, batch, _cache}),
-    do: {:ok, {max_size, reclaim, batch, cache}}
-
-  #############
-  # Algorithm #
-  #############
-
-  # Enforces the bounds of a cache based on the provided state.
-  #
-  # Suggest stepping through this pipeline a function at a time and reading the
-  # associated comments. This pipeline controls the trimming of the cache to fit
-  # the appropriate size.
-  #
-  # We start with the current size of the cache and pass it through to a function
-  # which will calculate the a number of slots to reclaim in the cache. If the
-  # number is positive, we carry out a Janitor purge and see if that brings us
-  # back under the maximum size. If so, we're done and stop. If not, we then trim
-  # the cache using a QLC cursor through the cache, deleting entries in batches.
-  # Note that these deletions are sorted by the touch time to ensure we delete
-  # the oldest first.
-  #
-  # Upon completion, we notify the worker itself to broadcast a message to all
-  # hooks to ensure that hooks are notified of the evictions. This is only done
-  # in the case that something has actually been removed, in order to avoid edge
-  # cases and potentially breaking existing hook implementations.
-  #
-  # It's safe to always run through the pipeline here, but we check the cache
-  # size as an optimization to speed up message processing when no evictions need
-  # to happen. This is a slight speed boost, but it's noticeable - tests will fail
-  # intermittently if this is not checked in this way.
-  defp enforce_bounds({max_size, reclaim, batch, cache}) do
     case Cachex.size!(cache, const(:notify_false)) do
       cache_size when cache_size <= max_size ->
         notify_worker(0, cache)
 
       cache_size ->
         cache_size
-        |> calculate_reclaim(max_size, reclaim)
+        |> calculate_reclaim(max_size, reclaim_bound)
         |> calculate_poffset(cache)
-        |> erase_lower_bound(cache, batch)
+        |> erase_lower_bound(cache, batch_size)
         |> notify_worker(cache)
     end
   end
@@ -255,5 +170,5 @@ defmodule Cachex.Policy.LRW do
     do: Informant.broadcast(state, {:clear, [[]]}, {:ok, offset})
 
   defp notify_worker(_offset, _state),
-    do: {:ok, 0}
+    do: :ok
 end
diff --git a/lib/cachex/policy/lrw/evented.ex b/lib/cachex/policy/lrw/evented.ex
new file mode 100644
index 0000000..bedeea2
--- /dev/null
+++ b/lib/cachex/policy/lrw/evented.ex
@@ -0,0 +1,102 @@
+defmodule Cachex.Policy.LRW.Evented do
+  @moduledoc """
+  Evented least recently written eviction policy for Cachex.
+
+  This module implements an evented LRW eviction policy for Cachex, using a hook
+  to listen for new key additions to a cache and enforcing bounds in a reactive
+  way. This policy enforces cache bounds and limits far more accurately than other
+  scheduled implementations, but comes at a higher memory cost (due to the message
+  passing between hooks).
+
+  The `:batch_size` option can be set in the limit options to dictate how many
+  entries should be removed at once by this policy. This will default to a batch
+  size of 100 entries at a time.
+
+  This eviction is relatively fast, and should keep the cache below bounds at most
+  times. Note that many writes in a very short amount of time can flood the cache,
+  but it should recover given a few seconds.
+  """
+  use Cachex.Hook
+  use Cachex.Policy
+
+  # import macros
+  import Cachex.Spec
+
+  # add internal aliases
+  alias Cachex.Policy.LRW
+
+  # actions which didn't trigger a write
+  @ignored [:error, :ignored]
+
+  ####################
+  # Policy Behaviour #
+  ####################
+
+  @doc """
+  Retrieves a list of hooks required to run against this policy.
+  """
+  @spec hooks(Spec.limit()) :: [Spec.hook()]
+  def hooks(limit),
+    do: [hook(module: __MODULE__, state: limit)]
+
+  ######################
+  # Hook Configuration #
+  ######################
+
+  @doc """
+  Returns the actions this policy should listen on.
+
+  This returns as a `MapSet` to optimize the lookups
+  on actions to O(n) in the broadcasting algorithm.
+  """
+  @spec actions :: [atom]
+  def actions,
+    do: [
+      :put,
+      :decr,
+      :incr,
+      :fetch,
+      :update,
+      :put_many,
+      :get_and_update
+    ]
+
+  @doc """
+  Returns the provisions this policy requires.
+  """
+  @spec provisions :: [atom]
+  def provisions,
+    do: [:cache]
+
+  ####################
+  # Server Callbacks #
+  ####################
+
+  @doc false
+  # Initializes this policy using the limit being enforced.
+  def init(limit() = limit),
+    do: {:ok, {nil, limit}}
+
+  @doc false
+  # Handles notification of a cache action.
+  #
+  # This will check if the action can modify the size of the cache, and if so will
+  # execute the boundary enforcement to trim the size as needed.
+  #
+  # Note that this will ignore error results and only operates on actions which are
+  # able to cause a net gain in cache size (so removals are also ignored).
+  def handle_notify(_message, {status, _value}, {cache, limit} = opts)
+      when status not in @ignored,
+      do: LRW.enforce(cache, limit) && {:ok, opts}
+
+  def handle_notify(_message, _result, opts),
+    do: {:ok, opts}
+
+  @doc false
+  # Receives a provisioned cache instance.
+  #
+  # The provided cache is then stored in the cache and used for cache calls going
+  # forwards, in order to skip the lookups inside the cache overseer for performance.
+  def handle_provision({:cache, cache}, {_cache, limit}),
+    do: {:ok, {cache, limit}}
+end
diff --git a/lib/cachex/policy/lrw/scheduled.ex b/lib/cachex/policy/lrw/scheduled.ex
new file mode 100644
index 0000000..3bdb33d
--- /dev/null
+++ b/lib/cachex/policy/lrw/scheduled.ex
@@ -0,0 +1,102 @@
+defmodule Cachex.Policy.LRW.Scheduled do
+  @moduledoc """
+  Schedule least recently written eviction policy for Cachex.
+
+  This module implements an evented LRW eviction policy for Cachex, using a basic
+  timer to trigger bound enforcement in a scheduled way. This has the same bound
+  accuracy as `Cachex.Policy.LRW.Evented`, but has potential for some delay. The
+  main advantage of this implementation is a far lower memory cost due to not
+  using hook messages.
+
+  The `:batch_size` option can be set in the limit options to dictate how many
+  entries should be removed at once by this policy. This will default to a batch
+  size of 100 entries at a time.
+
+  The `:frequency` option can also be set in the limit options to specify how
+  frequently this policy will fire. This defaults to every few seconds (but may
+  change at any point).
+
+  This eviction is relatively fast, and should keep the cache below bounds at most
+  times. Note that many writes in a very short amount of time can flood the cache,
+  but it should recover given a few seconds.
+  """
+  use Cachex.Hook
+  use Cachex.Policy
+
+  # import macros
+  import Cachex.Spec
+
+  # add internal aliases
+  alias Cachex.Policy.LRW
+
+  ####################
+  # Policy Behaviour #
+  ####################
+
+  @doc """
+  Retrieves a list of hooks required to run against this policy.
+  """
+  @spec hooks(Spec.limit()) :: [Spec.hook()]
+  def hooks(limit),
+    do: [hook(module: __MODULE__, state: limit)]
+
+  ######################
+  # Hook Configuration #
+  ######################
+
+  @doc """
+  Returns the actions this policy should listen on.
+  """
+  @spec actions :: [atom]
+  def actions,
+    do: []
+
+  @doc """
+  Returns the provisions this policy requires.
+  """
+  @spec provisions :: [atom]
+  def provisions,
+    do: [:cache]
+
+  ####################
+  # Server Callbacks #
+  ####################
+
+  @doc false
+  # Initializes this policy using the limit being enforced.
+  def init(limit() = limit),
+    do: {schedule(limit), {nil, limit}}
+
+  @doc false
+  # Handles notification of a cache action.
+  #
+  # This will execute a bounds check on a cache and schedule a new check.
+  def handle_info(:policy_check, {cache, limit} = opts) do
+    unless is_nil(cache) do
+      LRW.enforce(cache, limit)
+    end
+
+    schedule(limit) && {:noreply, opts}
+  end
+
+  @doc false
+  # Receives a provisioned cache instance.
+  #
+  # The provided cache is then stored in the cache and used for cache calls going
+  # forwards, in order to skip the lookups inside the cache overseer for performance.
+  def handle_provision({:cache, cache}, {_cache, limit}),
+    do: {:ok, {cache, limit}}
+
+  ###############
+  # Private API #
+  ###############
+
+  # Schedules a check to occur after the designated interval.
+  defp schedule(limit(options: options)) do
+    options
+    |> Keyword.get(:frequency, 1000)
+    |> :erlang.send_after(self(), :policy_check)
+
+    :ok
+  end
+end
diff --git a/lib/cachex/services.ex b/lib/cachex/services.ex
index 0ad7216..a5b85cd 100644
--- a/lib/cachex/services.ex
+++ b/lib/cachex/services.ex
@@ -59,7 +59,6 @@ defmodule Cachex.Services do
     |> Enum.concat(incubator_spec(cache))
     |> Enum.concat(courier_spec(cache))
     |> Enum.concat(janitor_spec(cache))
-    |> Enum.concat(limit_spec(cache))
   end
 
   @doc """
@@ -148,30 +147,6 @@ defmodule Cachex.Services do
       }
     ]
 
-  # Creates any require limit specifications for the supervision tree.
-  #
-  # This will rarely be used in the out-of-the-box experience, it's mainly
-  # provided for use in custom limit implementations by developers.
-  defp limit_spec(cache(limit: limit(size: nil))),
-    do: []
-
-  defp limit_spec(cache(limit: limit(policy: policy) = limit)) do
-    case apply(policy, :child_spec, [limit]) do
-      [] ->
-        []
-
-      cs ->
-        [
-          %{
-            id: Supervisor,
-            start:
-              {Supervisor, :start_link,
-               [cs, [strategy: apply(policy, :strategy, [])]]}
-          }
-        ]
-    end
-  end
-
   # Creates the required Locksmith queue specification for a cache.
   #
   # This will create a queue worker instance for any transactions to be


Test patch:
diff --git a/test/cachex/policy/lrw_test.exs b/test/cachex/policy/lrw/evented_test.exs
similarity index 97%
rename from test/cachex/policy/lrw_test.exs
rename to test/cachex/policy/lrw/evented_test.exs
index e032c06..50198aa 100644
--- a/test/cachex/policy/lrw_test.exs
+++ b/test/cachex/policy/lrw/evented_test.exs
@@ -1,4 +1,4 @@
-defmodule Cachex.Policy.LRWTest do
+defmodule Cachex.Policy.LRW.EventedTest do
   use CachexCase
 
   # This test just ensures that there are no artificial limits placed on a cache
@@ -36,7 +36,7 @@ defmodule Cachex.Policy.LRWTest do
     limit =
       limit(
         size: 100,
-        policy: Cachex.Policy.LRW,
+        policy: Cachex.Policy.LRW.Evented,
         reclaim: 0.75,
         options: [batch_size: 25]
       )
@@ -112,7 +112,7 @@ defmodule Cachex.Policy.LRWTest do
     limit =
       limit(
         size: 100,
-        policy: Cachex.Policy.LRW,
+        policy: Cachex.Policy.LRW.Evented,
         reclaim: 0.3,
         options: [batch_size: -1]
       )
diff --git a/test/cachex/policy/lrw/scheduled_test.exs b/test/cachex/policy/lrw/scheduled_test.exs
new file mode 100644
index 0000000..80621da
--- /dev/null
+++ b/test/cachex/policy/lrw/scheduled_test.exs
@@ -0,0 +1,179 @@
+defmodule Cachex.Policy.LRW.ScheduledTest do
+  use CachexCase
+
+  # This test just ensures that there are no artificial limits placed on a cache
+  # by adding 5000 keys and making sure they're not evicted. It simply serves as
+  # validation that there are no bad defaults set anywhere.
+  test "evicting with no upper bound" do
+    # create a cache with no max size
+    cache = Helper.create_cache()
+
+    # retrieve the cache state
+    state = Services.Overseer.retrieve(cache)
+
+    # add 5000 keys to the cache
+    for x <- 1..5000 do
+      {:ok, true} = Cachex.put(state, x, x)
+    end
+
+    # retrieve the cache size
+    count = Cachex.size!(state)
+
+    # make sure all keys are there
+    assert(count == 5000)
+  end
+
+  # This test ensures that a cache will cap caches at a given limit by trimming
+  # caches by a given size once they cross a given threshold. We ensure that the
+  # size is trimmed properly and the oldest entries are evicted first, with the
+  # newest entries kept in the cache. Finally we make sure that all hooks are
+  # notified of the evictions that occurred.
+  test "evicting when a cache crosses a limit" do
+    # create a forwarding hook
+    hook = ForwardHook.create()
+
+    # define our cache limit
+    limit =
+      limit(
+        size: 100,
+        policy: Cachex.Policy.LRW.Scheduled,
+        reclaim: 0.75,
+        options: [batch_size: 25, frequency: 100]
+      )
+
+    # create a cache with a max size
+    cache = Helper.create_cache(hooks: [hook], limit: limit)
+
+    # retrieve the cache state
+    state = Services.Overseer.retrieve(cache)
+
+    # add 1000 keys to the cache
+    for x <- 1..100 do
+      # add the entry to the cache
+      {:ok, true} = Cachex.put(state, x, x)
+
+      # tick to make sure each has a new touch time
+      :timer.sleep(1)
+    end
+
+    # retrieve the cache size
+    size1 = Cachex.size!(cache)
+
+    # verify the cache size
+    assert(size1 == 100)
+
+    # flush all existing hook events
+    Helper.flush()
+
+    # add a new key to the cache to trigger evictions
+    {:ok, true} = Cachex.put(state, 101, 101)
+
+    # verify the cache shrinks to 25%
+    Helper.poll(250, 25, fn ->
+      Cachex.size!(state)
+    end)
+
+    # our validation step
+    validate = fn range, expected ->
+      # iterate all keys in the range
+      for x <- range do
+        # retrieve whether the key exists
+        exists = Cachex."exists?!"(state, x)
+
+        # verify whether it exists
+        assert(exists == expected)
+      end
+    end
+
+    # verify the first 76 keys are removed
+    validate.(1..76, false)
+
+    # verify the latest 25 are retained
+    validate.(77..101, true)
+
+    # finally, verify hooks are notified
+    assert_receive({{:clear, [[]]}, {:ok, 76}})
+
+    # retrieve the policy hook definition
+    cache(hooks: hooks(post: [hook1 | _])) = state
+
+    # just ensure that notifying errors to the policy doesn't cause a crash
+    Services.Informant.notify([hook1], {:action, []}, {:error, false})
+  end
+
+  # This test ensures that the cache eviction policy will evict any expired values
+  # before removing the oldest. This is to make sure that we don't remove anything
+  # without good reason. To verify this we add 50 keys with a TTL more recently
+  # than those without and cross the cache limit. We then validate that all expired
+  # keys have been purged, and no other keys have been removed as the purge takes
+  # the cache size back under the maximum size.
+  test "evicting by removing expired keys" do
+    # define our cache limit
+    limit =
+      limit(
+        size: 100,
+        policy: Cachex.Policy.LRW.Scheduled,
+        reclaim: 0.3,
+        options: [batch_size: -1, frequency: 100]
+      )
+
+    # create a cache with a max size
+    cache = Helper.create_cache(limit: limit)
+
+    # retrieve the cache state
+    state = Services.Overseer.retrieve(cache)
+
+    # set 50 keys without ttl
+    for x <- 1..50 do
+      # set the key
+      {:ok, true} = Cachex.put(state, x, x)
+
+      # tick to make sure each has a new touch time
+      :timer.sleep(1)
+    end
+
+    # set a more recent 50 keys
+    for x <- 51..100 do
+      # set the key
+      {:ok, true} = Cachex.put(state, x, x, ttl: 1)
+
+      # tick to make sure each has a new touch time
+      :timer.sleep(1)
+    end
+
+    # retrieve the cache size
+    size1 = Cachex.size!(cache)
+
+    # verify the cache size
+    assert(size1 == 100)
+
+    # add a new key to the cache to trigger evictions
+    {:ok, true} = Cachex.put(state, 101, 101)
+
+    # verify the cache shrinks to 51%
+    Helper.poll(250, 51, fn ->
+      Cachex.size!(state)
+    end)
+
+    # our validation step
+    validate = fn range, expected ->
+      # iterate all keys in the range
+      for x <- range do
+        # retrieve whether the key exists
+        exists = Cachex."exists?!"(state, x)
+
+        # verify whether it exists
+        assert(exists == expected)
+      end
+    end
+
+    # verify the first 50 keys are retained
+    validate.(1..50, true)
+
+    # verify the second 50 are removed
+    validate.(51..100, false)
+
+    # verify the last key added is retained
+    validate.(101..101, true)
+  end
+end
diff --git a/test/cachex/policy_test.exs b/test/cachex/policy_test.exs
index 9884cae..bb05c81 100644
--- a/test/cachex/policy_test.exs
+++ b/test/cachex/policy_test.exs
@@ -2,10 +2,7 @@ defmodule Cachex.PolicyTest do
   use CachexCase
 
   test "default policy implementations" do
-    # validate each default implementation provided by __using__
-    assert __MODULE__.DefaultPolicy.strategy() == :one_for_one
     assert __MODULE__.DefaultPolicy.hooks(limit()) == []
-    assert __MODULE__.DefaultPolicy.child_spec(limit()) == []
   end
 
   defmodule DefaultPolicy,
diff --git a/test/cachex/services_test.exs b/test/cachex/services_test.exs
index 0674719..7a7c8f8 100644
--- a/test/cachex/services_test.exs
+++ b/test/cachex/services_test.exs
@@ -27,31 +27,6 @@ defmodule Cachex.ServicesTest do
            ] = Services.cache_spec(cache)
   end
 
-  test "generating cache limit specifications" do
-    # generate the test cache state with a limit attached
-    name =
-      Helper.create_cache(limit: limit(size: 10, policy: __MODULE__.TestPolicy))
-
-    cache = Services.Overseer.retrieve(name)
-
-    # validate the services
-    assert [
-             %{id: Eternal, start: {Eternal, _, _}},
-             %{
-               id: Services.Locksmith.Queue,
-               start: {Services.Locksmith.Queue, _, _}
-             },
-             %{id: Services.Informant, start: {Services.Informant, _, _}},
-             %{id: Services.Incubator, start: {Services.Incubator, _, _}},
-             %{id: Services.Courier, start: {Services.Courier, _, _}},
-             %{id: Services.Janitor, start: {Services.Janitor, _, _}},
-             %{
-               id: Supervisor,
-               start: {Supervisor, _, [[%{id: __MODULE__.TestPolicy}], _]}
-             }
-           ] = Services.cache_spec(cache)
-  end
-
   test "skipping cache janitor specifications" do
     # generate the test cache state with the Janitor disabled
     name = Helper.create_cache(expiration: expiration(interval: nil))
@@ -79,14 +54,4 @@ defmodule Cachex.ServicesTest do
     assert Services.locate(cache, Services.Courier) != nil
     assert Services.locate(cache, Services.Janitor) == nil
   end
-
-  defmodule TestPolicy do
-    use Cachex.Policy
-
-    def child_spec(_limit),
-      do: [%{id: __MODULE__, start: {__MODULE__, :start_link, []}}]
-
-    def start_link,
-      do: :ignore
-  end
 end