Repository: whitfin/cachex (elixir)

Issue:
Feature Request: Cache Warmer that doesn't block startup
I'm using a Cachex.Warmer in a code path that is tangential to the application that I'm working on. So I don't want to take a hit on startup time by waiting for the cache to be ready. If I serve a couple requests in the meantime I am not worried. I see that this was originally possible in the initial version of https://github.com/whitfin/cachex/pull/244 but it was later removed.

Pull request description:
Add support for non-synchronous warmers
This fixes #280.

This adds a new option inside a `warmer` named `:async` which defaults to `false`. If you set this to `true`, the warmer startup will be asychronous and not block the cache startup.

Patch:
diff --git a/.credo.exs b/.credo.exs
index f61ea5b..d4351ef 100644
--- a/.credo.exs
+++ b/.credo.exs
@@ -67,14 +67,13 @@
         {Credo.Check.Refactor.ABCSize, max_size: 50},
         {Credo.Check.Refactor.CaseTrivialMatches, false},
         {Credo.Check.Refactor.CondStatements},
-        {Credo.Check.Refactor.CyclomaticComplexity},
         {Credo.Check.Refactor.FunctionArity},
         {Credo.Check.Refactor.MatchInCondition},
         {Credo.Check.Refactor.NegatedConditionsInUnless},
         {Credo.Check.Refactor.NegatedConditionsWithElse},
         {Credo.Check.Refactor.Nesting},
         {Credo.Check.Refactor.PipeChainStart},
-        {Credo.Check.Refactor.CyclomaticComplexity},
+        {Credo.Check.Refactor.CyclomaticComplexity, max_complexity: 10},
         {Credo.Check.Refactor.NegatedConditionsInUnless},
         {Credo.Check.Refactor.NegatedConditionsWithElse},
         {Credo.Check.Refactor.Nesting},
diff --git a/lib/cachex/services/incubator.ex b/lib/cachex/services/incubator.ex
index 294fdb5..534dc3c 100644
--- a/lib/cachex/services/incubator.ex
+++ b/lib/cachex/services/incubator.ex
@@ -34,6 +34,9 @@ defmodule Cachex.Services.Incubator do
   ###############
 
   # Generates a Supervisor specification for a hook.
-  defp spec(warmer(module: module, state: state), cache),
-    do: %{id: module, start: {GenServer, :start_link, [module, {cache, state}]}}
+  defp spec(warmer(module: module) = warmer, cache),
+    do: %{
+      id: module,
+      start: {GenServer, :start_link, [module, {cache, warmer}]}
+    }
 end
diff --git a/lib/cachex/spec.ex b/lib/cachex/spec.ex
index 53e2aa6..33f1ccd 100644
--- a/lib/cachex/spec.ex
+++ b/lib/cachex/spec.ex
@@ -97,7 +97,8 @@ defmodule Cachex.Spec do
   @type warmer ::
           record(:warmer,
             module: atom,
-            state: any
+            state: any,
+            async: boolean
           )
 
   ###########
@@ -251,7 +252,8 @@ defmodule Cachex.Spec do
   """
   defrecord :warmer,
     module: nil,
-    state: nil
+    state: nil,
+    async: false
 
   ###############
   # Record Docs #
diff --git a/lib/cachex/spec/validator.ex b/lib/cachex/spec/validator.ex
index e4b29c9..6f76c86 100644
--- a/lib/cachex/spec/validator.ex
+++ b/lib/cachex/spec/validator.ex
@@ -92,9 +92,8 @@ defmodule Cachex.Spec.Validator do
     check3 = check2 and nillable?(name, &(is_atom(&1) or is_pid(&1)))
     check4 = check3 and nillable?(module.timeout(), &is_positive_integer/1)
 
-    check5 =
-      check4 and
-        (enum?(module.actions(), &is_atom/1) or module.actions() == :all)
+    action = check4 and module.actions()
+    check5 = check4 and (enum?(action, &is_atom/1) or action == :all)
 
     check6 = check5 and enum?(module.provisions(), &is_atom/1)
     check7 = check6 and module.type() in [:post, :pre]
diff --git a/lib/cachex/warmer.ex b/lib/cachex/warmer.ex
index 816afee..8b01e79 100644
--- a/lib/cachex/warmer.ex
+++ b/lib/cachex/warmer.ex
@@ -70,9 +70,14 @@ defmodule Cachex.Warmer do
       #
       # Initialization will trigger an initial cache warming, and store
       # the provided state for later to provide during further warming.
-      def init(state) do
-        handle_info(:cachex_warmer, state)
-        {:ok, state}
+      def init({cache, warmer(async: async, state: state)}) do
+        if async do
+          send(self(), :cachex_warmer)
+        else
+          handle_info(:cachex_warmer, {cache, state})
+        end
+
+        {:ok, {cache, state}}
       end
 
       @doc false
@@ -102,7 +107,7 @@ defmodule Cachex.Warmer do
         # trigger the warming to happen again after the interval
         :erlang.send_after(interval(), self(), :cachex_warmer)
 
-        # no reply, and the state persist
+        # repeat with the state
         {:noreply, process_state}
       end
     end


Test patch:
diff --git a/test/cachex/warmer_test.exs b/test/cachex/warmer_test.exs
index 5d14444..6d1939c 100644
--- a/test/cachex/warmer_test.exs
+++ b/test/cachex/warmer_test.exs
@@ -43,6 +43,21 @@ defmodule Cachex.WarmerTest do
     assert Cachex.empty?(!cache)
   end
 
+  test "warmers which aren't blocking" do
+    # create a test warmer to pass to the cache
+    Helper.create_warmer(:async_warmer, 50, fn _ ->
+      :timer.sleep(3000)
+      {:ok, [{1, 1}]}
+    end)
+
+    # create a cache instance with a warmer
+    warmer = warmer(module: :async_warmer, async: true)
+    cache = Helper.create_cache(warmers: [warmer])
+
+    # check that the key was not warmed
+    assert Cachex.get!(cache, 1) == nil
+  end
+
   test "providing warmers with states" do
     # create a test warmer to pass to the cache
     Helper.create_warmer(:state_warmer, 50, fn state ->