Repository: whitfin/cachex
Pull request #363 (2024-09-10 21:17:01)

Migrate interval/0 to :interval
This PR migrates `interval/0` in `Cachex.Warmer` to `:interval` in `warmer()`. This simplifies the module behaviour, and is more Elixir-y. Hardcoding the interval in the module itself didn't really make sense.

This also makes the interval completely optional, so you can choose to manually warm instead. This makes warming far more flexible for use cases we haven't thought about. 


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f6e4f28..7d6c363 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -82,6 +82,26 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
+  docs:
+    name: Documentation
+    runs-on: ubuntu-latest
+    container:
+      image: elixir:1.17
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Setup Environment
+        run: |
+          epmd -daemon
+          mix local.hex --force
+          mix local.rebar --force
+          mix deps.get
+
+      - name: Build Documentation
+        run: |
+          mix docs 2>&1 | tee output.txt
+          ! grep warning output.txt
+
   lint:
     name: Linting
     runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index 7c2b975..b299d0a 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ children = [
 ]
 ```
 
-If you wish to start a cache manually (for example, in `iex`), you can use `Cachex.start_link/2`:
+If you wish to start a cache manually (for example, in `iex`), you can use `Cachex.start_link/1`:
 
 ```elixir
 Cachex.start_link(name: :my_cache)
@@ -76,7 +76,7 @@ Caches also accept several options at startup to toggle various behaviour. These
 |      warmers     |    list of `warmer()`    |           A list of cache warmers to enable on the cache.
 
 
-For further information or examples on these features and options, please see the [documentation](docs).
+For further information or examples on these features and options, please see the [documentation](https://hexdocs.pm/cachex).
 
 ## Benchmarks
 
diff --git a/docs/README.md b/docs/README.md
deleted file mode 100644
index 3059a72..0000000
--- a/docs/README.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Cachex Documentation
-
-- [Getting Started](../README.md#installation)
-    - [Starting A Cache](../README.md#usage)
-    - [Supported Options](../README.md#options)
-- [Action Blocks](features/action-blocks.md)
-    - [Execution Blocks](features/action-blocks.md#execution-blocks)
-    - [Transaction Blocks](features/action-blocks.md#transaction-blocks)
-- [Cache Limits](features/cache-limits.md)
-    - [Configuration](features/cache-limits.md#configuration)
-    - [Policies](features/cache-limits.md#policies)
-- [Cache Warming](features/cache-warming)
-    - [Reactive Warming](features/cache-warming/reactive-warming.md)
-        - [Overview](features/cache-warming/reactive-warming.md#overview)
-        - [Courier](features/cache-warming/reactive-warming.md#courier)
-        - [Expirations](features/cache-warming/reactive-warming.md#expirations)
-        - [Use Cases](features/cache-warming/reactive-warming.md#use-cases)
-    - [Proactive Warming](features/cache-warming/proactive-warming.md)
-        - [Overview](features/cache-warming/proactive-warming.md#overview)
-        - [Definition](features/cache-warming/proactive-warming.md#definition)
-        - [Use Cases](features/cache-warming/proactive-warming.md#use-cases)
-- [Custom Commands](features/custom-commands.md)
-    - [Defining Commands](features/custom-commands.md#defining-commands)
-    - [Invoking A Command](features/custom-commands.md#invoking-a-command)
-- [Disk Interaction](features/disk-interaction.md)
-- [Distributed Caches](features/distributed-caches.md)
-    - [Overview](features/distributed-caches.md#overview)
-    - [Local Actions](features/distributed-caches.md#local-actions)
-    - [Disabled Actions](features/distributed-caches.md#disabled-actions)
-- [Execution Hooks](features/execution-hooks.md)
-    - [Creating Hooks](features/execution-hooks.md#creating-hooks)
-    - [Provisions](features/execution-hooks.md#provisions)
-- [Streaming Caches](features/streaming-caches.md)
-    - [Complex Streaming](features/streaming-caches.md#complex-streaming)
-- [TTL Implementation](features/ttl-implementation.md)
-    - [Janitor Processes](features/ttl-implementation.md#janitor-processes)
-    - [Lazy Expiration](features/ttl-implementation.md#lazy-expiration)
-- [Migrations](migrations)
-    - [Migrating To v3.x](migrations/migrating-to-v3.md)
-    - [Migrating To v2.x](migrations/migrating-to-v2.md)
-- [Benchmarks](../README.md#benchmarks)
-- [Contributions](../README.md#contributions)
diff --git a/docs/features/cache-warming/proactive-warming.md b/docs/features/cache-warming/proactive-warming.md
index 826c48a..418c725 100644
--- a/docs/features/cache-warming/proactive-warming.md
+++ b/docs/features/cache-warming/proactive-warming.md
@@ -1,14 +1,12 @@
 # Proactive Warming
 
-## Overview
+Introduced alongside Cachex v3, cache warmers act as an eager way to populate a cache. Rather than waiting for a cache miss to retrieve a value, values will be pulled up front to ensure that there is never a miss. This can be viewed as being proactive, whereas `Cachex.fetch/4` can be seen as reactive. As such, this is a better tool for those who know what data will be requested, rather than those dealing with arbitrary data.
 
-Introduced alongside Cachex v3, cache warmers act as an eager fallback. Rather than waiting for a cache miss to retrieve a value, values will be pulled up front to ensure that there is never a miss. This can be viewed as being proactive, whereas a fallback is reactive. As such, this is a better use case for those who know what data will be requested, rather than those dealing with arbitrary data.
+Warmers are deliberately easy to create, as anything complicated belongs outside of Cachex itself. A warmer is simply a module which implements the `Cachex.Warmer` behaviour, consisting of just a single callback at the time of writing (please see the `Cachex.Warmer` documentation to verify). A warmer should expose `execute/1` which actually implements the cache warming. The easiest way to explain a warmer is to implement one, so let's implement a warmer which reads from a database via the module `DatabaseWarmer`.
 
-Warmers are deliberately easy to create, as anything complicated belongs outside of Cachex itself. A warmer is simply a module which implements the `Cachex.Warmer` behaviour, which consists of just two callbacks at the time of writing (please see the `Cachex.Warmer` documentation to verify). The two callbacks are simply `interval/0` which returns a millisecond integer defining how often the warmer should execute, and `execute/1` which actually implements the cache warming. The easiest way to explain a warmer is to implement one, so let's do so; we'll implement a warmer which reads from a database via the module `DatabaseWarmer`.
+## Defining a Warmer
 
-## Definition
-
-First of all, let's define our warmer on a cache at startup:
+First of all, let's define our warmer on a cache at startup. This is done by passing a list of `warmer()` records inside the `:warmers` option of `Cachex.start_link/1`:
 
 ```elixir
 # for warmer()
@@ -18,6 +16,7 @@ import Cachex.Spec
 Cachex.start_link(:my_cache, [
   warmers: [
     warmer(
+      interval: :timer.seconds(30),
       module: MyProject.DatabaseWarmer,
       state: connection
     )
@@ -25,25 +24,19 @@ Cachex.start_link(:my_cache, [
 ])
 ```
 
-These are generally the only two fields you'll have to set in a `warmer()~ record; a `:module` tag to define the module, and a `:state` field to define the state to be provided to the warmer (used later). The state in this case is a connection handle to our database, since we'll need that for queries we're trying to warm.
+The fields above are generally the only three fields you'll have to set in a `warmer()` record. The `:module` tag defines the module implementing the `Cachex.Warmer` behaviour, the `:state` field defines the state to be provided to the warmer (used later), and the `:interval` controls the frequency with which the warmer executes (in milliseconds). In previous versions of Cachex the `:interval` option was part of the module behaviour, but this was changed to be more flexible as of Cachex v4.x.
 
-In terms of other useful options, you may pass a `:name` to use as the warmer's process name, which will default to the PID used by the process. You can also use the `:required` flag to signal whether it is necessary for a warmer to fully execute before your cache is deemed available. This defaults to `true`, but can easily be set to `false` if you're happy for your data to load asynchronously.
+In terms of some of the other options, you may pass a `:name` to use as the warmer's process name (which defaults to the warmer's PID). You can also use the `:required` flag to signal whether it is necessary for a warmer to fully execute before your cache is deemed "available". This defaults to `true` but can easily be set to `false` if you're happy for your data to load asynchronously. The `:required` flag in Cachex v4.x is the same as `async: false` in Cachex v3.x.
 
-With our cache created, all that remains is to implement our `DatabaseWarmer` module which implements the warmer behaviour:
+With our cache created all that remains is to create the `MyProject.DatabaseWarmer` module, which will implement our warmer behaviour:
 
 ```elixir
 defmodule MyProject.DatabaseWarmer do
   @moduledoc """
-  Dummy warmer which caches database rows every 30s.
+  Dummy warmer which caches database rows.
   """
   use Cachex.Warmer
 
-  @doc """
-  Returns the interval for this warmer.
-  """
-  def interval,
-    do: :timer.seconds(30)
-
   @doc """
   Executes this cache warmer with a connection.
   """
@@ -66,13 +59,15 @@ defmodule MyProject.DatabaseWarmer do
 end
 ```
 
-There are a couple of things going on here; first of all the `interval/0` is stating that we should execute every 30s (including on cache startup). Every 30 seconds, the `execute/1` callback is fired using the connection we passed at cache startup. We query the database to get all of the matching rows back, and then handle the response. In the case of an error (or really, any situation you don't want to write the results), we return `:ignore` which signals that the warmer was basically a no-op. In a successful execution, we need to map the results to one of two forms: `{ :ok, pairs }` or `{ :ok, pairs, options }`. The pairs/options act as the same format one would pass to `Cachex.put_many/3`, so check out those docs if you need to. These pairs are basically `{ key, value }`, so in our case we're caching row identifiers -> rows in our cache. Not particularly useful, but it'll do for now.
+There are a couple of things going on here. When the `execute/1` callback is fired, we use the stored connection to query the database and map all rows back into the cache table. In case of an error, we use the `:ignore` value to signal that the warmer won't be writing anything to the table.
+
+When formatting results to place into the cache table, you must provide your results in the form of either `{ :ok, pairs }` or `{ :ok, pairs, options }`. These pairs and options should match the same formats that you'd use when calling `Cachex.put_many/3`, so check out the documentation if you need to. In our example above these pairs are simply storing `row.id -> row` in our cache. Not particularly useful, but it'll do for now!
 
-This demonstrates that a single warmer can warm a whole bunch of records in a single pass; this is especially useful when fetching remote data, as otherwise you'd need a warmer for every piece coming back from a request. This would be sufficiently complicated that you'd likely just roll your own warming instead, and so Cachex tries to negate this aspect by the addition of `put_many/3` in v3.x.
+Although simple, this example demonstrates that a single warmer can populate many records in a single pass. This is particularly useful when fetching remote data, instead of using a warmer for every row in a database. This would be sufficiently complicated that you'd likely just roll your own warming instead, and so Cachex tries to negate this aspect by the addition of `put_many/3` in v3.x.
 
-## Use Cases
+## Example Use Cases
 
-To demonstrate this, we'll use the same examples from the fallback documentation, which is acting as a cache of an API call to `/api/v1/packages` which returns a list of packages. In case of a cache miss, a fallback will fetch that API and put it in the cache for future calls. With a warmer we can actually go a lot further for this use case:
+To demonstrate this, we'll use the same examples from the [Reactive Warming](reactive-warming.md) documentation, which is acting as a cache of an API call to `/api/v1/packages` which returns a list of packages. In case of a cache miss, reactive warming will call the API and put it in the cache for future calls. With a warmer we can actually go a lot further for this use case:
 
 ```elixir
 # need our records
@@ -81,12 +76,16 @@ import Cachex.Spec
 # initialize our cache with a database connection
 Cachex.start_link(:my_cache, [
   warmers: [
-    warmer(module: MyProject.PackageWarmer, state: connection)
+    warmer(
+      interval: :timer.minutes(5),
+      module: MyProject.PackageWarmer,
+      state: connection
+    )
   ]
 ])
 ```
 
-And then we define our warmer to do the same thing; pull the packages from the database every 5 minutes. It should be noted that a fallback runs **at most** every 5 minutes, whereas a warmer will run **always** every 5 minutes.
+And then we define our warmer to do the same thing; pull the packages from the database every 5 minutes. It should be noted that reactive warming runs **at most** every 5 minutes, whereas a proactive warmer will **always** run every 5 minutes with a provided interval.
 
 ```elixir
 defmodule MyProject.PackageWarmer do
@@ -95,12 +94,6 @@ defmodule MyProject.PackageWarmer do
   """
   use Cachex.Warmer
 
-  @doc """
-  Returns the interval for this warmer.
-  """
-  def interval,
-    do: :timer.minutes(5)
-
   @doc """
   Executes this cache warmer with a connection.
   """
@@ -119,6 +112,44 @@ defmodule MyProject.PackageWarmer do
 end
 ```
 
-Using the same amount of database calls, on the same frequency, we have not only populated `"/api/v1/packages"` to return the list of packages, but we have also populated the entire API `"/api/v1/packages/{id}"` to return the single package referenced in the path. This is a much more optimized solution for this type of caching, as you can explode out your key writes with a single cache action, and no extra database requests.
+Using the same amount of database calls, on the same frequency, we have not only populated `"/api/v1/packages"` to return the list of packages, but we have also populated the entire API `"/api/v1/packages/{id}"` to return the single package referenced in the path. This is a much more optimized solution for this type of caching, as you can explode out your key writes with a single cache action, while requiring no extra database requests.
+
+Somewhat obviously these warmers can only be used if you know what types of data you're expecting to be cached. If you're dealing with seeded data (i.e. from a user) you probably can't use warmers, and should be looking at reactive warming instead. You must also consider how relevant the data is that you're caching; if you only care about it for a short period of time, you likely don't want a warmer as they run for the lifetime of the cache.
+
+## Triggered Warming
+
+In some cases you may not wish to use automated interval warming, such as if your data is static and changes rarely or maybe doesn't change at all. For this case Cachex v4.x allows the `:interval` to be set to `nil`, which will only run your warmer a single time on cache startup. It also introduces `Cachex.warm/2` to allow the developer to manually warm a cache and implement their own warming schedules.
+
+When using manual warming your cache definition is much the same as before, with the only change being dropping the `:interval` option from the `warmer()` record:
+
+```elixir
+# need our records
+import Cachex.Spec
+
+# initialize our cache with a database connection
+Cachex.start_link(:my_cache, [
+  warmers: [
+    warmer(
+      module: MyProject.PackageWarmer,
+      state: connection
+    )
+  ]
+])
+```
+
+Cachex will run this warmer a single time on cache startup, and will then never run this warmer again without it being explicitly requested. In this case the developer will have to manually trigger the warmer via `Cachex.warm/2`:
+
+```elixir
+# warm the cache manually
+Cachex.warm(:my_cache)
+
+# warm the cache manually and block until complete
+Cachex.warm(:my_cache, wait: true)
+
+# warm the cache manually, but only with specific warmers
+Cachex.warm(:my_cache, only: [MyProject.PackageWarmer])
+```
+
+To extend the previous example to benefit from this type of warming, imagine that our previous package listing is part of a CRUD API which also includes package creation and deletion. In this scenario you could manually warm your cache after a package is either created or removed, rather than run it every 5 minutes (even if nothing has changed in the meantime!).
 
-Obviously, these warmers can only be used if you know what types of data you're expecting to be cached. If you're dealing with seeded data (i.e. from a user), you probably can't use warmers, and should be looking at fallbacks instead. You must also consider how relevant the data is that you're caching; if you only care about it for a short period of time, you likely don't want a warmer as they run for the lifetime of the cache.
+It should also be noted that `Cachex.warm/2` is still available even if you _have_ specified the `:interval` option. If you have a high cache interval of something like `:timer.hours(24)` and you want to trigger an earlier warming, you can always `iex` into your node and run a cache warming manually.
diff --git a/docs/features/cache-warming/reactive-warming.md b/docs/features/cache-warming/reactive-warming.md
index cc6205a..81f1c46 100644
--- a/docs/features/cache-warming/reactive-warming.md
+++ b/docs/features/cache-warming/reactive-warming.md
@@ -1,8 +1,12 @@
 # Reactive Warming
 
-## Overview
+The concept of reactive caching is the idea that a local cache is backed by another layer (for example something remote), meaning that on a cache miss you have another layer to look into to retrieve the data you want. As this lazily loads data into a cache, it's an effective tool for data which remains active for only a short period of time.
 
-The concept of fallback caching is the idea that a local cache is backed by another layer (typically something remote), meaning that on a cache miss you have another layer to look into to retrieve the data you want. Fallback functions can be defined on a cache at startup or on a call at runtime, and will only be executed if the key you're trying to retrieve doesn't exist locally. If you set a fallback at cache start up and then also pass one at call time, the call time definition takes precedence and will be executed instead. Each fallback receives a single argument by default; the key which resulted in a cache miss. Usually this will be available to you, but it allows for abstract fallback handling.
+Reactive warmers are very memory efficient as they lazily load data on demand, instead of eagerly in anticipation of data access. This means that only actively required data is loaded into a cache. Pairing this with Cachex's expiration controls is a very common and effective way of modeling your caches.
+
+## Defining a Fallback
+
+Fallback functions can be defined on a cache at startup or on a call at runtime, and will only be executed if the key you're trying to retrieve doesn't exist locally. If you set a function at cache startup and then also pass one at call time, the call time definition takes precedence and will be executed instead. Each function receives a single argument by default; the key which resulted in a cache miss:
 
 ```elixir
 # need our records
@@ -15,7 +19,7 @@ Cachex.start_link(:my_cache, [ fallback: fallback(default: &do_something/1) ])
 Cachex.fetch(:my_cache, "key", &do_something/1)
 ```
 
-There are also some cases in which you'll need a state to operate fallbacks, for example if you're caching responses from a database. In this case you might wish to have a database connection passed to your fallback to allow you to query it, and so Cachex allows you to pass a state at cache start which will be provided as a second argument to all fallback executions.
+There are also some cases in which you'll need a state to operate fallbacks, for example if you're caching responses from a database. In this case you might wish to have a database connection passed to your fallback to allow you to query it, and so Cachex allows you to pass a state at cache start to be provided as a second argument to all fallback executions.
 
 ```elixir
 # need our records
@@ -33,50 +37,11 @@ Cachex.fetch(:my_cache, "key", fn(key, db_conn) ->
 end)
 ```
 
-In order to provide some degree of control over error handling, Cachex allows for `{ :commit | :ignore, value }` syntax being returned from a fallback. Rather than just returning a value in your fallback, you can return a Tuple tagged with either `:commit` or `:ignore`. Values tagged with `:ignore` will just be returned without being stored in the cache, and those tagged with `:commit` will be returned after being stored in the cache. If you don't use a tagged Tuple return value, it will be assumed you're committing the value (for backwards compatibility). In future this may change to enforce using a Tuple in order to reduce the amount of conditionals.
-
-## Courier
-
-As of v3, fallbacks changed quite significantly to provide the guarantee that only a single fallback will fire for a given key, even if more processes ask for the same key before the fallback is complete. The internal `Courier` service will queue these requests up, then resolve them all with the results retrieved by the first. This ensures that you don't have stray processes calling for the same thing (which is especially bad if they're talking to a database, etc.). You can think of this as a per-key queue at a high level, with a short circuit involved to avoid executing too often.
-
-To fully understand this, consider this code in Cachex v2.x (even if it is a little contrived):
-
-```elixir
-for i <- 0..2 do
-  Cachex.get(:my_cache, "key", fallback: fn _ ->
-    :timer.sleep(5000 + i)
-    i
-  end)
-end
-```
-
-As the fallbacks each take 5 seconds, you have 3 cache misses and therefore 3 processes each waiting 5 seconds (as the second and third calls are fired before the first call has resolved). This isn't great, because if your fallback is a database, you'd hit it 3 times here, asking for the same thing each time. The result of the code above would be that `"key"` has a value of `1`, then `2`, then `3` as each fallback returns and clobbers what was there previously.
-
-The new `Courier` service in Cachex v3 will actually queue the second and third calls to fire after the first one, rather than firing them all at once. What's even better; the moment the first call resolves, the second and third will immediately resolve with the same results. This ensures that your fallback only fires a single time, regardless of the number of processes awaiting the result. This change in behaviour means that the code above would result in `"key"` having a single value of `1` as the second and third never fire. Although this results in a behaviour change above, it should basically never affect you in the same way as the code above is deliberately designed to highlight the changes.
+When formatting results to place into the cache table, you must provide your results in the form of either `{ status, pairs }` or `{ status, pairs, options }`. These pairs and options should match the same formats that you'd use when calling `Cachex.put_many/3`, so check out the documentation if you need to.
 
-## Expirations
-
-Sometimes you might want to set an expiration based on a value retrieved via a fallback execution. For this case, as of Cachex v3.6.0, you can (finally) provide expiration options in your returned `:commit` tuple.
-
-```elixir
-Cachex.fetch(:my_cache, "key", fn ->
-  { :commit, do_something(), expire: :timer.seconds(60) }
-end)
-```
+In order to provide some degree of control over error handling, Cachex allows for either `:commit` or `:ignore` as a status. Values tagged with `:ignore` will just be returned without being stored in the cache, and those tagged with `:commit` will be returned after being stored in the cache. If you don't use a tagged Tuple return value, it will be assumed you're committing the value (for backwards compatibility).
 
-This inlining is faster than previous solutions, while maintaining correctness and being easy to use. If you are using an older version of Cachex, you can conditionally assign an expiration based on the return value of your cache call in the case of a cache `:commit`:
-
-```elixir
-# retrieve the value from the cache, match if loaded
-with { :commit, value } = res <- Cachex.fetch(:my_cache, "key") do
-  # if so, set the key to expire after 5 minutes and return
-  Cachex.expire(:my_cache, "key", :timer.minutes(5)) && res
-end
-```
-
-Also note that if your cache has a defined default TTL, it will be applied to fallback values automatically.
-
-## Use Cases
+## Example Use Cases
 
 Fallbacks allow you to build very simple bindings using a cache in order to reduce overhead on your backing systems. A very common use case is to use a `Cachex` instance with fallbacks to a remote system to lower the jumps across network. With effective use of expirations and fallbacks, you can ensure that your application doesn't receive stale data and yet minimizes network overhead and the number of remote operations.
 
@@ -99,3 +64,22 @@ end)
 ```
 
 This allows you to easily lower the pressure on backing systems with very little code; a few lines can improve your API performance dramatically. The nice part about fallbacks is that they can easily be used with arbitrary data; something you can't predict in advance. Use cases based around input from a user (for example), are very well suited to using reactive caching (particularly because the data is only relevant when the user is currently active).
+
+## Fallback Contention
+
+As of Cachex v3.x fallbacks have changed quite significantly to provide the guarantee that only a single fallback will fire for a given key, even if more processes ask for the same key before the fallback is complete. The internal `Cachex.Services.Courier` service will queue these requests up, and then resolve them all with the results retrieved by the first. This ensures that you don't have stray processes calling for the same thing (which is especially bad if they're talking to a database, etc.). You can think of this as a per-key queue at a high level, with a short circuit involved to avoid executing too often.
+
+To fully understand this with an example, consider this code (even if it is a little contrived):
+
+```elixir
+for i <- 0..2 do
+  Cachex.fetch(:my_cache, "key", fn _ ->
+    :timer.sleep(5000 + i)
+    i
+  end)
+end
+```
+
+As each fallback function will take 5 seconds to execute, there will be 3 cache misses and therefore 3 processes each waiting 5 seconds (as the second and third calls are fired before the first call has resolved). This isn't ideal; in cases where you call a remote API or database you'd open 3 connections, asking for the same thing 3 times. The result of the code above would be that `"key"` has a value of `1`, then `2`, then `3` as each fallback returns and clobbers what was there previously.
+
+The `Cachex.Service.Courier` will instead queue the second and third calls to fire _after_ the first one, rather than executing them all at once. Even better; the moment the first call resolves, the second and third will immediately resolve with the same results. This ensures that your function will only fire a single time, regardless of the number of processes awaiting the result. This helps with consistency in your application, while also reducing the overhead behind reactive caching.
diff --git a/docs/migrations/migrating-to-v3.md b/docs/migrations/migrating-to-v3.md
index a0ed7a7..0d7a6df 100644
--- a/docs/migrations/migrating-to-v3.md
+++ b/docs/migrations/migrating-to-v3.md
@@ -4,7 +4,7 @@ There are many breaking changes bundled up in v3, and so this guide serves as a
 
 ## Startup Options
 
-In order to reduce a lot of the option parsing involved in Cachex, the options given to `Cachex.start_link/2` and `Cachex.start/2` have changed pretty drastically. The easiest way to see the differences is to look again at the documentation for these functions, but here's a very high level summary of changes (if these are things you use, please do check the docs):
+In order to reduce a lot of the option parsing involved in Cachex, the options given to `Cachex.start_link/1` and `Cachex.start/1` have changed pretty drastically. The easiest way to see the differences is to look again at the documentation for these functions, but here's a very high level summary of changes (if these are things you use, please do check the docs):
 
 - The `:commands` option now expects a list of `command` records.
 - The `:default_ttl`, `:disable_ode` and `:ttl_interval` are now passed as an `expiration` record in the `:expiration` option (and have had their names changed).
diff --git a/lib/cachex/router/jump.ex b/lib/cachex/router/jump.ex
index 535d282..240088b 100644
--- a/lib/cachex/router/jump.ex
+++ b/lib/cachex/router/jump.ex
@@ -9,7 +9,7 @@ defmodule Cachex.Router.Jump do
 
   The initialization of this router accepts a `:nodes` option which enables
   the user to define the nodes to route amongst. If this is not provided the
-  router will default to detecting a cluster via `Node.self/0` and `Node.list/2`.
+  router will default to detecting a cluster via `Node.self/0` and `Node.list/1`.
 
   For more information on the algorithm backing this router, please see the
   appropriate [publication](https://arxiv.org/pdf/1406.2294).
@@ -26,7 +26,7 @@ defmodule Cachex.Router.Jump do
 
       The `:nodes` option allows a user to provide a list of nodes to treat
       as a cluster. If this is not provided, the cluster will be inferred
-      by using `Node.self/1` and `Node.list/2`.
+      by using `Node.self/0` and `Node.list/1`.
 
   """
   @spec init(cache :: Cachex.t(), options :: Keyword.t()) :: [atom]
diff --git a/lib/cachex/router/mod.ex b/lib/cachex/router/mod.ex
index a083bc5..0539b45 100644
--- a/lib/cachex/router/mod.ex
+++ b/lib/cachex/router/mod.ex
@@ -9,7 +9,7 @@ defmodule Cachex.Router.Mod do
 
   The initialization of this router accepts a `:nodes` option which enables
   the user to define the nodes to route amongst. If this is not provided the
-  router will default to detecting a cluster via `Node.self/0` and `Node.list/2`.
+  router will default to detecting a cluster via `Node.self/0` and `Node.list/1`.
   """
   use Cachex.Router
   alias Cachex.Router
@@ -23,7 +23,7 @@ defmodule Cachex.Router.Mod do
 
       The `:nodes` option allows a user to provide a list of nodes to treat
       as a cluster. If this is not provided, the cluster will be inferred
-      by using `Node.self/1` and `Node.list/2`.
+      by using `Node.self/0` and `Node.list/1`.
 
   """
   @spec init(cache :: Cachex.t(), options :: Keyword.t()) :: [atom]
diff --git a/lib/cachex/router/ring.ex b/lib/cachex/router/ring.ex
index 4a6f47f..7405538 100644
--- a/lib/cachex/router/ring.ex
+++ b/lib/cachex/router/ring.ex
@@ -32,7 +32,7 @@ defmodule Cachex.Router.Ring do
 
       The `:nodes` option allows a user to provide a list of nodes to treat
       as a cluster. If this is not provided, the cluster will be inferred
-      by using `Node.self/1` and `Node.list/2`.
+      by using `Node.self/0` and `Node.list/1`.
 
     * `:monitor`
 
diff --git a/lib/cachex/spec.ex b/lib/cachex/spec.ex
index 1d1eab1..228722f 100644
--- a/lib/cachex/spec.ex
+++ b/lib/cachex/spec.ex
@@ -117,6 +117,7 @@ defmodule Cachex.Spec do
   @type warmer ::
           record(:warmer,
             required: boolean,
+            interval: integer | nil,
             module: atom,
             state: any,
             name: GenServer.server()
@@ -289,6 +290,7 @@ defmodule Cachex.Spec do
   """
   defrecord :warmer,
     required: true,
+    interval: nil,
     module: nil,
     state: nil,
     name: nil
diff --git a/lib/cachex/spec/validator.ex b/lib/cachex/spec/validator.ex
index 79e03fc..ab96929 100644
--- a/lib/cachex/spec/validator.ex
+++ b/lib/cachex/spec/validator.ex
@@ -150,9 +150,12 @@ defmodule Cachex.Spec.Validator do
   #
   # This will validate that the provided module correctly implements the
   # behaviour of `Cachex.Warmer` via function checking.
-  def valid?(:warmer, warmer(module: module, name: name, required: required)) do
+  def valid?(:warmer, warmer() = warmer) do
+    warmer(module: module, name: name, interval: interval, required: required) =
+      warmer
+
     check1 = behaviour?(module, Cachex.Warmer)
-    check2 = check1 and {:interval, 0} in module.__info__(:functions)
+    check2 = check1 and nillable?(interval, &is_positive_integer/1)
     check3 = check2 and {:execute, 1} in module.__info__(:functions)
     check4 = check3 and nillable?(name, &(is_atom(&1) or is_pid(&1)))
     check5 = check4 and is_boolean(required)
diff --git a/lib/cachex/warmer.ex b/lib/cachex/warmer.ex
index 7436c7c..0a56732 100644
--- a/lib/cachex/warmer.ex
+++ b/lib/cachex/warmer.ex
@@ -3,7 +3,7 @@ defmodule Cachex.Warmer do
   Module controlling cache warmer behaviour definitions.
 
   This module defines the cache warming implementation for Cachex, allowing the
-  user to register warmers against a cache to populate the tables on an interval.
+  user to register warmers against a cache to populate the tables periodically.
   Doing this allows for easy pulling against expensive values (such as those from
   a backing database or remote server), without risking heavy usage.
 
@@ -13,8 +13,7 @@ defmodule Cachex.Warmer do
   desired data in the cache.
 
   Warmers are fired on a schedule, and are exposed via a very simple behaviour of
-  just an interval and a block to execute on the interval. It should be noted that
-  this is a moving interval, and it resets after execution has completed.
+  just a block to execute periodically.
   """
 
   #############
@@ -22,16 +21,7 @@ defmodule Cachex.Warmer do
   #############
 
   @doc """
-  Returns the interval this warmer will execute on.
-
-  This must be an integer representing a count of milliseconds to wait before
-  the next execution of the warmer. Anything else will cause either invalidation
-  errors on cache startup, or crashes at runtime.
-  """
-  @callback interval :: integer
-
-  @doc """
-  Executes actions to warm a cache instance on interval.
+  Executes actions to warm a cache instance.
 
   This can either return values to set in the cache, or the atom `:ignore` to
   signal that there's nothing to be set at this point in time. Values to be set
@@ -79,8 +69,8 @@ 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({cache() = cache, warmer(state: state)}) do
-        {:ok, {cache, state, nil}}
+      def init({cache() = cache, warmer(interval: interval, state: state)}) do
+        {:ok, {cache, state, interval, nil}}
       end
 
       @doc false
@@ -101,7 +91,7 @@ defmodule Cachex.Warmer do
       # cache via `Cachex.put_many/3` if returns in a Tuple tagged with the
       # `:ok` atom. If `:ignore` is returned, nothing happens aside from
       # scheduling the next execution of the warming to occur on interval.
-      def handle_info(:cachex_warmer, {cache, state, timer}) do
+      def handle_info(:cachex_warmer, {cache, state, interval, timer}) do
         # clean our any existing timers
         if timer, do: Process.cancel_timer(timer)
 
@@ -121,11 +111,14 @@ defmodule Cachex.Warmer do
         end
 
         # trigger the warming to happen again after the interval
-        new_timer = :erlang.send_after(interval(), self(), :cachex_warmer)
-        new_state = {cache, state, new_timer}
+        timer =
+          case interval do
+            nil -> nil
+            val -> :erlang.send_after(val, self(), :cachex_warmer)
+          end
 
         # pass the new state
-        {:noreply, new_state}
+        {:noreply, {cache, state, interval, timer}}
       end
 
       @doc false
@@ -133,8 +126,8 @@ defmodule Cachex.Warmer do
       #
       # The provided cache is then stored in the state 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, state, timer}),
-        do: {:ok, {cache, state, timer}}
+      def handle_provision({:cache, cache}, {_cache, state, interval, timer}),
+        do: {:ok, {cache, state, interval, timer}}
     end
   end
 end
diff --git a/mix.exs b/mix.exs
index 9ec7058..3483bb2 100644
--- a/mix.exs
+++ b/mix.exs
@@ -27,9 +27,9 @@ defmodule Cachex.Mixfile do
       elixir: "~> 1.7",
       deps: deps(),
       docs: [
+        main: "README",
         source_ref: "v#{@version}",
         source_url: @url_github,
-        main: "getting-started",
         extra_section: "guides",
         extras: [
           "docs/features/cache-warming/reactive-warming.md",
@@ -44,7 +44,7 @@ defmodule Cachex.Mixfile do
           "docs/features/ttl-implementation.md",
           "docs/migrations/migrating-to-v3.md",
           "docs/migrations/migrating-to-v2.md",
-          "docs/getting-started.md"
+          "README.md"
         ],
         groups_for_extras: [
           Features: Path.wildcard("docs/features/*.md"),


Test patch:
diff --git a/test/cachex/actions/warm_test.exs b/test/cachex/actions/warm_test.exs
index f7c57e1..338f40e 100644
--- a/test/cachex/actions/warm_test.exs
+++ b/test/cachex/actions/warm_test.exs
@@ -5,7 +5,7 @@ defmodule Cachex.Actions.WarmTest do
   # after manually clearing it but checking again before the schedule.
   test "manually warming a cache" do
     # create a test warmer to pass to the cache
-    TestUtils.create_warmer(:manual_warmer1, :timer.hours(1), fn _ ->
+    TestUtils.create_warmer(:manual_warmer1, fn _ ->
       {:ok, [{1, 1}]}
     end)
 
@@ -39,7 +39,7 @@ defmodule Cachex.Actions.WarmTest do
 
   test "manually warming a cache and awaiting results" do
     # create a test warmer to pass to the cache
-    TestUtils.create_warmer(:manual_warmer2, :timer.hours(1), fn _ ->
+    TestUtils.create_warmer(:manual_warmer2, fn _ ->
       {:ok, [{1, 1}]}
     end)
 
@@ -71,7 +71,7 @@ defmodule Cachex.Actions.WarmTest do
   # provided list, and therefore no cache warming actually executes.
   test "manually warming a cache using specific warmers" do
     # create a test warmer to pass to the cache
-    TestUtils.create_warmer(:manual_warmer3, :timer.hours(1), fn _ ->
+    TestUtils.create_warmer(:manual_warmer3, fn _ ->
       {:ok, [{1, 1}]}
     end)
 
diff --git a/test/cachex/options_test.exs b/test/cachex/options_test.exs
index 7f92569..6c0b674 100644
--- a/test/cachex/options_test.exs
+++ b/test/cachex/options_test.exs
@@ -423,7 +423,7 @@ defmodule Cachex.OptionsTest do
     name = TestUtils.create_name()
 
     # define our warmer to pass through to the cache
-    TestUtils.create_warmer(:options_test_warmer, 50, fn _ ->
+    TestUtils.create_warmer(:options_test_warmer, fn _ ->
       :ignore
     end)
 
diff --git a/test/cachex/spec/validator_test.exs b/test/cachex/spec/validator_test.exs
index be235d9..52663c8 100644
--- a/test/cachex/spec/validator_test.exs
+++ b/test/cachex/spec/validator_test.exs
@@ -232,7 +232,7 @@ defmodule Cachex.Spec.ValidatorTest do
 
   test "validation of warmer records" do
     # create a warmer for validation
-    TestUtils.create_warmer(:validator_warmer, 1000, fn _ ->
+    TestUtils.create_warmer(:validator_warmer, fn _ ->
       :ignore
     end)
 
@@ -242,14 +242,18 @@ defmodule Cachex.Spec.ValidatorTest do
     warmer3 = warmer(module: :missing)
     warmer4 = warmer(module: __MODULE__, required: nil)
     warmer5 = warmer(module: __MODULE__, name: 1)
+    warmer6 = warmer(module: :validator_warmer, interval: 10)
+    warmer7 = warmer(module: :validator_warmer, interval: -1)
 
     # ensure the first is valid
     assert Validator.valid?(:warmer, warmer1)
+    assert Validator.valid?(:warmer, warmer6)
 
     # the others are all invalid
     refute Validator.valid?(:warmer, warmer2)
     refute Validator.valid?(:warmer, warmer3)
     refute Validator.valid?(:warmer, warmer4)
     refute Validator.valid?(:warmer, warmer5)
+    refute Validator.valid?(:warmer, warmer7)
   end
 end
diff --git a/test/cachex/test/utils.ex b/test/cachex/test/utils.ex
index daae7aa..671ec05 100644
--- a/test/cachex/test/utils.ex
+++ b/test/cachex/test/utils.ex
@@ -83,18 +83,14 @@ defmodule Cachex.Test.Utils do
   @doc false
   # Creates a warmer module.
   #
-  # This will name the module after the provided name, and create it with
-  # the provided interval definition and execution. This is a shorthand to
+  # This will name the module after the provided name. This is a shorthand to
   # avoid having to define modules all over the codebase, but does not change
   # the outcomes in any way (still a new module being defined).
-  defmacro create_warmer(name, interval, execution) do
+  defmacro create_warmer(name, execution) do
     quote do
       defmodule unquote(name) do
         use Cachex.Warmer
 
-        def interval,
-          do: unquote(interval)
-
         def execute(state),
           do: apply(unquote(execution), [state])
       end
diff --git a/test/cachex/warmer_test.exs b/test/cachex/warmer_test.exs
index 88027f1..63bdd58 100644
--- a/test/cachex/warmer_test.exs
+++ b/test/cachex/warmer_test.exs
@@ -3,7 +3,7 @@ defmodule Cachex.WarmerTest do
 
   test "warmers which set basic values" do
     # create a test warmer to pass to the cache
-    TestUtils.create_warmer(:basic_warmer, 50, fn _ ->
+    TestUtils.create_warmer(:basic_warmer, fn _ ->
       {:ok, [{1, 1}]}
     end)
 
@@ -16,7 +16,7 @@ defmodule Cachex.WarmerTest do
 
   test "warmers which set values with options" do
     # create a test warmer to pass to the cache
-    TestUtils.create_warmer(:options_warmer, 50, fn _ ->
+    TestUtils.create_warmer(:options_warmer, fn _ ->
       {:ok, [{1, 1}], [expire: 60000]}
     end)
 
@@ -32,7 +32,7 @@ defmodule Cachex.WarmerTest do
 
   test "warmers which don't set values" do
     # create a test warmer to pass to the cache
-    TestUtils.create_warmer(:ignore_warmer, 50, fn _ ->
+    TestUtils.create_warmer(:ignore_warmer, fn _ ->
       :ignore
     end)
 
@@ -45,7 +45,7 @@ defmodule Cachex.WarmerTest do
 
   test "warmers which aren't blocking" do
     # create a test warmer to pass to the cache
-    TestUtils.create_warmer(:optional_warmer, 50, fn _ ->
+    TestUtils.create_warmer(:optional_warmer, fn _ ->
       :timer.sleep(3000)
       {:ok, [{1, 1}]}
     end)
@@ -60,7 +60,7 @@ defmodule Cachex.WarmerTest do
 
   test "providing warmers with states" do
     # create a test warmer to pass to the cache
-    TestUtils.create_warmer(:state_warmer, 50, fn state ->
+    TestUtils.create_warmer(:state_warmer, fn state ->
       {:ok, [{"state", state}]}
     end)
 
@@ -79,12 +79,12 @@ defmodule Cachex.WarmerTest do
 
   test "triggering cache hooks from within warmers" do
     # create a test warmer to pass to the cache
-    TestUtils.create_warmer(:hook_warmer_optional, 15000, fn _ ->
+    TestUtils.create_warmer(:hook_warmer_optional, fn _ ->
       {:ok, [{1, 1}]}
     end)
 
     # create a test warmer to pass to the cache
-    TestUtils.create_warmer(:hook_warmer_required, 15000, fn _ ->
+    TestUtils.create_warmer(:hook_warmer_required, fn _ ->
       {:ok, [{2, 2}]}
     end)
 
@@ -95,8 +95,8 @@ defmodule Cachex.WarmerTest do
     TestUtils.create_cache(
       hooks: [hook],
       warmers: [
-        warmer(module: :hook_warmer_optional, required: false),
-        warmer(module: :hook_warmer_required, required: true)
+        warmer(module: :hook_warmer_optional, interval: 15000, required: false),
+        warmer(module: :hook_warmer_required, interval: 15000, required: true)
       ]
     )