Skip to content

Commit

Permalink
Add documentation to cover v4.x migration
Browse files Browse the repository at this point in the history
  • Loading branch information
whitfin committed Sep 24, 2024
1 parent 4bdc76a commit c716330
Show file tree
Hide file tree
Showing 37 changed files with 289 additions and 206 deletions.
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ def deps do
end
```

Depending on what you're trying to do, there are a couple of different ways you might want to go about starting a cache.

If you're testing out Cachex inside `iex`, you can call `Cachex.start_link/2` manually:
Depending on what you're trying to do, there are a couple of different ways you might want to go about starting a cache. If you're testing out Cachex inside `iex`, you can call `Cachex.start_link/2` manually:

```elixir
Cachex.start_link(:my_cache) # with default options
Expand All @@ -48,7 +46,7 @@ children = [

Both of these approaches work the same way; your options are parsed and your cache is started as a child under the appropriate parent process. The latter is recommended for production applications, as it will ensure your cache is managed correctly inside your application.

## Basic Operations
## Basic Examples

Working with a cache is pretty straightforward, and basically everything is provided by the core `Cachex` module. You can make calls to a cache using the name you registered it under at startup time.

Expand Down Expand Up @@ -91,13 +89,13 @@ nil = Cachex.get!(:my_cache, "key")

# but calling with `!` raises the error
Cachex.get!(:missing_cache, "key")
** (Cachex.ExecutionError) Specified cache not running
** (Cachex.Error) Specified cache not running
(cachex) lib/cachex.ex:249: Cachex.get!/3
```

The `!` version of functions exists for convenience, in particular to make chaining and assertions easier in unit testing. For production use cases it's recommended to avoid `!` wrappers, and instead explicitly handle the different response types.

## Advanced Operations
## Advanced Examples

Beyond the typical get/set semantics of a cache, Cachex offers many additional features to help with typical use cases and access patterns a developer may meeting during their day-to-day.

Expand Down
1 change: 0 additions & 1 deletion docs/extensions/custom-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,3 @@ Let's look at some examples of calling the new `:last` and `:lpop` commands we d
We can see how both commands are doing their job and we're left with an empty list at the end of this snippet. At the time of writing there are no options recognised by `Cachex.invoke/4` even though there _is_ an optional fourth parameter for options, it's simply future proofing.

This example does highlight one shortcoming that custom commands do have currently; it's not possible to remove an entry from the table inside a custom command yet. This may be supported in future but there's currently no real demand, and adding it would complicate the interface so it's on pause for now.

2 changes: 1 addition & 1 deletion docs/management/expiring-records.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Cachex implements several different ways to work with key expiration, with each

## Janitor Services

The Cachex Janitor (`Cachex.Services.Janitor`) is a background process used to purge the internal cache tables periodically. The Janitor operates using a full table sweep of records to ensure consistency and correctness. As such, a Janitor sweep will run somewhat less frequently - by default only once every few seconds. This frequency can be controlled by the developer, and can be controlled on a per-cache basis.
The Cachex Janitor is a background process used to purge the internal cache tables periodically. The Janitor operates using a full table sweep of records to ensure consistency and correctness. As such, a Janitor sweep will run somewhat less frequently - by default only once every few seconds. This frequency can be controlled by the developer, and can be controlled on a per-cache basis.

In the current version of Cachex, the Janitor is pretty well optimized as most of the work happens in the ETS layer. As a rough benchmark, it can check and purge 500,000 expired records in around a second (where the removal is a majority of the work). Keep in mind that the frequency of the Janitor execution has an impact on the memory being held by the expired keyset in your cache. For most use cases the default frequency should be just fine. If you need to, you can customize the frequency on which the Janitor runs:

Expand Down
115 changes: 115 additions & 0 deletions docs/migrations/migrating-to-v4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Migrating to v4.x

The release of Cachex v4.x includes a lot of internal cleanup and restructuring. As such, there are quite a few breaking changes to be aware of.

Some of them are simple (like changing names) and others require more involved migration. This page will go through everything and hopefully make it easy for you to upgrade!

## Cache Options

There are a number of changes to the options provided at cache startup in `Cachex.start_link/2`.

The `:fallback` option has been removed. This was introduced in earlier versions of Cachex before `Cachex.fetch/4` existed, and it doesn't serve nearly as much purpose anymore. Removing this cleaned up a lot of the internals and removes a lot of hidden magic, so it was time to go. To align with this change, the function parameter of `Cachex.fetch/4` has been changed to be required rather than optional.

Both the `:stats` and `:limit` options have been removed, in favour of explicitly providing the hooks that back them. These flags were sugar in the past but caused noise and confusion, and it's now much better to have people getting used to using `:hooks`:

```elixir
# behaviour in Cachex v3
Cachex.start_link(:my_cache, [
stats: true,
limit: limit(
size: 500,
policy: Cachex.Policy.LRW,
reclaim: 0.5,
options: []
)
])

# behaviour in Cachex v4
Cachex.start_link(:my_cache, [
hooks: [
hook(module: Cachex.Stats),
hook(module: Cachex.Limit.Scheduled, args: {
500, # setting cache max size
[], # options for `Cachex.prune/3`
[] # options for `Cachex.Limit.Scheduled`
})
]
])
```

Both of these features have had additional documentation written, so you can double check the relevant documentation in [Gathering Stats](../management/stats-gathering.md) and [Limiting Caches](../management/limiting-caches.md) as necessary. Limits in particular have had quite a shakeup in Cachex v4, so it's definitely worth a visit to the documentation if you're using those!

The `:nodes` option has also been removed, in favour of the new approach to routing in a distributed cache. It's possible to keep the same semantics as Cachex v3 using the `Cachex.Router.Jump` module implementation:

```elixir
# behaviour in Cachex v3
Cachex.start_link(:my_cache, [
nodes: [
:node1,
:node2,
:node3
]
])

# behaviour in Cachex v4
Cachex.start_link(:my_cache, [
router: router(module: Cachex.Router.Jump, options: [
nodes: [
:node1,
:node2,
:node3
]
])
])
```

This is covered in much more detail in the corresponding documentation of [Cache Routers](../routing/cache-routers.md) and [Distributed Caches](../routing/distributed-caches.md); it's heavily recommended you take a look through those pages if you were using `:nodes` in the past.

Last but not least, the `:transactional` flag has been renamed to `:transactions`. Ironically this used to be the name in the Cachex v2 days, but it turned out that it was a mistake to change it in Cachex v3!

## Warming Changes

There are some minor changes to cache warmers in Cachex v4, which require only a couple of minutes to update.

The `:async` field inside a warmer record has been replaced with the new `:required` field. This is basically equivalent to the inverse of whatever you would have set `:async` to in the past. As cache warmers can now be fired as either async or sync on the fly, this option didn't make much sense anymore. Instead the new `:required` field dictates that a warmer is _required_ to have run before a cache is considered fully started.

The other change affecting cache warmers is the removal of `interval/0` function from the `Cachex.Warmer` behaviour. The interval is something you might want to change dynamically, and so it didn't make sense to be defined in the code itself. It has been moved to the `:interval` field in the Cachex warmer record, and behaves exactly as before.

## Function Parameters

There are several naming changes to options passed to functions across the Cachex API. There are no functional differences, so these should be quick cosmetic things to change as needed.

First of all the `:ttl` option has been renamed to `:expire` in all places it was supported (mainly `Cachex.put/4` and various wrappers). It was strange to refer to expiration as "expiration" all over and have the setting be `:ttl`, so this just makes things more consistent.

The `:initial` option for `Cachex.incr/4` and `Cachex.decr/4` has been renamed to `:default`. This makes way more sense and is much more intuitive; it was probably just a misnaming all those years ago that stuck. Time to make it better!

For all of the functions which support `:batch_size`, namely `Cachex.stream/3` and functions which use it, this has now been renamed to `:buffer`. The previous name was too close to the underlying implementation, whereas the new name is much more generic (and shorter to type!).

## Removed & Renamed APIs

There are several changes to the main Cachex API, including removal of some functions and naming changes of others.

The `count/2` function has been removed in favour of `Cachex.size/2`. These two functions did almost the same thing, the only difference was that `Cachex.size/2` would return the size of the cache including unpurged expired records, while `count/2` would filter them out. Instead of two functions for this, you can now opt into this via `Cachex.size/2`:

```elixir
# total cache entry count
Cachex.size(:my_cache)
Cachex.size(:my_cache, expired: true)

# ignores expired but unremoved entries
Cachex.size(:my_cache, expired: false)
```

This should hopefully feel more intuitive, while allowing us to trim a bunch of the Cachex internals. The underlying implementations are identical, so it should be easy to migrate if you need to.

Both functions `dump/3` and `load/3` have been renamed in Cachex v4. These names were terrible to begin with, so it's about time they're changed! Instead we now have `Cachex.save/3` and `Cachex.restore/3`, which behave in exactly the same way (aside from being a bit cleaner in implementation!).

Finally the two deprecated functions `set/4` and `set_many/3` have finally been removed. If you were using these, please use `Cachex.put/4` and `Cachex.put_many/3` instead from now on.

## Other Miscellaneous Changes

There are a few other small changes which don't really need much explanation, but do need to be noted for reference.

The minimum supported Elixir version has been raised from Elixir 1.5 to Elixir 1.7. In reality there are probably very few people out there still using Elixir 1.7 and it could be raised further, but there's also nothing really compelling enough to make this happen at this time.

A lot of the record types in Cachex v4 had their orders changed, so if anyone was matching directly (instead of using record syntax) they should adapt to using `entry(entry, :field)` instead.
4 changes: 1 addition & 3 deletions docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ def deps do
end
```

Depending on what you're trying to do, there are a couple of different ways you might want to go about starting a cache.

If you're testing out Cachex inside `iex`, you can call `Cachex.start_link/2` manually:
Depending on what you're trying to do, there are a couple of different ways you might want to go about starting a cache. If you're testing out Cachex inside `iex`, you can call `Cachex.start_link/2` manually:

```elixir
Cachex.start_link(:my_cache) # with default options
Expand Down
2 changes: 2 additions & 0 deletions docs/routing/distributed-caches.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ Cachex.start(:my_cache, [

This option will listen to `:nodeup` and `:nodedown` events and redistribute keys around your cluster automatically; you will generally always want this enabled if you're planning to dynamically add and remove nodes from your cluster.

You can also visit the [Cache Routers](./cache-routers.md) documentation for further information on this topic.

## Distribution Rules

There are a number of behavioural changes when a cache is in distributed state, and it's important to be aware of them. I realise this is a lot of information, but it's good to have it all documented.
Expand Down
2 changes: 1 addition & 1 deletion docs/warming/reactive-warming.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ This allows you to easily lower the pressure on backing systems with very little

## 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.
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 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):

Expand Down
10 changes: 4 additions & 6 deletions lib/cachex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,14 @@ defmodule Cachex do
use Supervisor

# add all imports
import Cachex.Errors
import Cachex.Error
import Cachex.Spec

# allow unsafe generation
use Unsafe.Generator,
handler: :unwrap_unsafe

# add some aliases
alias Cachex.Errors
alias Cachex.ExecutionError
alias Cachex.Options
alias Cachex.Query, as: Q
alias Cachex.Router
Expand Down Expand Up @@ -1433,12 +1431,12 @@ defmodule Cachex do
# remove the binding Tuples in order to allow for easy piping of
# results from cache calls.
defp unwrap_unsafe({:error, value}) when is_atom(value),
do: raise(ExecutionError, message: Errors.long_form(value))
do: raise(Cachex.Error, message: Cachex.Error.long_form(value))

defp unwrap_unsafe({:error, value}) when is_binary(value),
do: raise(ExecutionError, message: value)
do: raise(Cachex.Error, message: value)

defp unwrap_unsafe({:error, %ExecutionError{stack: stack} = e}),
defp unwrap_unsafe({:error, %Cachex.Error{stack: stack} = e}),
do: reraise(e, stack)

defp unwrap_unsafe({_state, value}),
Expand Down
2 changes: 1 addition & 1 deletion lib/cachex/actions/incr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ defmodule Cachex.Actions.Incr do
alias Cachex.Services.Locksmith

# we need some imports
import Cachex.Errors
import Cachex.Spec
import Cachex.Error

##############
# Public API #
Expand Down
2 changes: 1 addition & 1 deletion lib/cachex/actions/inspect.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ defmodule Cachex.Actions.Inspect do
alias Services.Overseer

# we need macros
import Cachex.Errors
import Cachex.Error
import Cachex.Spec

# define our accepted options for the inspection calls
Expand Down
2 changes: 1 addition & 1 deletion lib/cachex/actions/invoke.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ defmodule Cachex.Actions.Invoke do
alias Cachex.Services.Locksmith

# add our imports
import Cachex.Errors
import Cachex.Spec
import Cachex.Error

##############
# Public API #
Expand Down
2 changes: 1 addition & 1 deletion lib/cachex/actions/put_many.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ defmodule Cachex.Actions.PutMany do
alias Cachex.Services.Locksmith

# add our macros
import Cachex.Errors
import Cachex.Spec
import Cachex.Error

##############
# Public API #
Expand Down
5 changes: 2 additions & 3 deletions lib/cachex/actions/restore.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ defmodule Cachex.Actions.Restore do
#
# Loading a cache from disk requires that it was previously saved using the
# `Cachex.save/3` command (it does not support loading from DETS).
alias Cachex.Actions.Import
alias Cachex.Options

# we need our imports
import Cachex.Errors
import Cachex.Error
import Cachex.Spec

##############
Expand Down Expand Up @@ -42,7 +41,7 @@ defmodule Cachex.Actions.Restore do
&File.close/1
)

Import.execute(cache, stream, [])
Cachex.import(cache, stream, const(:local) ++ const(:notify_false))
rescue
File.Error -> error(:unreachable_file)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/cachex/actions/save.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defmodule Cachex.Actions.Save do
alias Cachex.Router.Local

# import our macros
import Cachex.Errors
import Cachex.Error
import Cachex.Spec

##############
Expand Down
2 changes: 1 addition & 1 deletion lib/cachex/actions/stream.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule Cachex.Actions.Stream do
alias Cachex.Options

# need our imports
import Cachex.Errors
import Cachex.Error
import Cachex.Spec

# our test record for testing matches when a user provides a spec
Expand Down
11 changes: 5 additions & 6 deletions lib/cachex/application.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
defmodule Cachex.Application do
@moduledoc """
Application callback to start any global services.
This will start all needed services for Cachex using the `Cachex.Services`
module, rather than hardcoding any logic into this binding module.
"""
@moduledoc false
# Application callback to start any global services.
#
# This will start all needed services for Cachex using the `Cachex.Services`
# module, rather than hardcoding any logic into this binding module.
use Application

@doc """
Expand Down
19 changes: 13 additions & 6 deletions lib/cachex/errors.ex → lib/cachex/error.ex
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
defmodule Cachex.Errors do
defmodule Cachex.Error do
@moduledoc """
Module containing all error definitions used in the codebase.
This module allows users to catch Cachex errors in a separate block
to other errors/exceptions rather than using stdlib errors:
iex> try do
...> Cachex.put!(:cache, "key", "value")
...> rescue
...> e in Cachex.Error -> e
...> end
All error messages (both shorthand and long form) can be found in this module,
including the ability to convert from the short form to the long form using the
`long_form/1` function.
This module is provided to allow functions to return short errors, using the
easy syntax of `error(:short_name)` to generate a tuple of `{ :error, :short_name }`
but also to allow them to be converted to a readable form as needed, rather
than bloating blocks with potentially large error messages.
"""
defexception message: "Error during cache action", stack: nil

# all shorthands
@known_errors [
:cross_slot,
:invalid_command,
Expand Down
18 changes: 0 additions & 18 deletions lib/cachex/execution_error.ex

This file was deleted.

17 changes: 8 additions & 9 deletions lib/cachex/options.ex
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
defmodule Cachex.Options do
@moduledoc """
Binding module to parse options into a cache record.
This interim module is required to normalize the options passed to a
cache at startup into a well formed record instance, allowing the rest
of the codebase to make assumptions about what types of data are being
dealt with.
"""
@moduledoc false
# Binding module to parse options into a cache record.
#
# This interim module is required to normalize the options passed to a
# cache at startup into a well formed record instance, allowing the rest
# of the codebase to make assumptions about what types of data are being
# dealt with.
import Cachex.Spec
import Cachex.Errors
import Cachex.Error

# add some aliases
alias Cachex.Spec
Expand Down
Loading

0 comments on commit c716330

Please sign in to comment.