Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions docs/extensions/custom-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,20 @@ Cachex.start_link(:my_cache, [
])
```

Each command receives a cache value to operate on and return. A command flagged as `:read` (such as `:last` above) will simply transforms the cache value before the final command return occurs, allowing the cache to mask complicated logic from the calling module. Commands flagged as `:write` are a little more complicated, but still fairly easy to grasp. These commands *must* return a 2-element tuple, with the return value in index `0` and the new cache value in index `1`.
Each command receives a cache value to operate on an return. A command flagged as `:read` will simply transform the cache value before it's returned the user, allowing a developer to mask complicated logic directly in the cache itself rather than the calling module. This is suitable for storing specific structures in your cache and allowing "direct" operations on them (i.e. lists, maps, etc.).

It is important to note that custom cache commands _will_ receive `nil` values in the cache of a missing cache key. If you're using a `:write` command and receive a misisng value, your returned modified value will only be written back to the cache if it's no longer `nil`. This allows the developer to implement logic such as lazy loading, but also escape the situation where you're cornered into writing to the cache.
Commands flagged as `:write` as a little more complicated, but still fairly easy to grasp. These commands *must* always resolve to a 2 element tuple, with the value to return from the call at index `0` and the new cache value in index `1`. You can either return a 2 element tuple as-is, or it can be contained in the `:commit` interfaces of Cachex:

```elixir
lpop = fn
([ head | tail ]) ->
{:commit, {head, tail}}
(_) ->
{:ignore, nil}
end
```

This provides uniform handling across other cache interfaces, and makes it possible to implement things like lazy loading while providing an escape for the developer in cases where writing should be skipped. This is not perfect, so behaviour here may change in future as new options become available.

## Invoking Commands

Expand Down
88 changes: 88 additions & 0 deletions docs/general/committing-changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Batching Actions

It's sometimes the case that you need to execute several cache actions in a row. Although you can do this in the normal, this is actually somewhat inefficient as each call has to do various management (such as looking up cache states). For this reason Cachex offers several mechanisms for making multiple calls in sequence.

## Submitting Batches

The simplest way to make several cache calls together is `Cachex.execute/3`. This API allows the caller to provide a function which will be provided with a pre-validated cache state which can be used (instead of the cache name) to execute cache actions. This will skip all of the cache management overhead you'd see typically:

```elixir
# standard way to execute several actions
r1 = Cachex.get!(:my_cache, "key1")
r2 = Cachex.get!(:my_cache, "key2")
r3 = Cachex.get!(:my_cache, "key3")

# using Cachex.execute/3 to optimize the batch of calls
{r1, r2, r3} =
Cachex.execute!(:my_cache, fn cache ->
# execute our batch of actions
r1 = Cachex.get!(cache, "key1")
r2 = Cachex.get!(cache, "key2")
r3 = Cachex.get!(cache, "key3")

# pass back all results as a tuple
{r1, r2, r3}
end)
```

Although this syntax might look a little more complicated at a glance, it should be fairly straightforward to get used to. The small change in approach here gives a fairly large boost to cache throughput. To compare the two examples above, we can use a tool like [Benchee](https://github.com/PragTob/benchee) for a rough comparison:

```
Name ips average deviation median 99th %
grouped 1.72 M 580.68 ns ±3649.68% 500 ns 750 ns
individually 1.31 M 764.02 ns ±2335.25% 625 ns 958 ns
```

We can clearly see the time saving when using the batched approach, even if there is a large deviation in the numbers above. Somewhat intuitively, the time saving scales to the number of actions you're executing in your batch, even if it is unlikely that anyone is doing more than a few calls at once.

It's important to note that even though you're executing a batch of actions, other processes can access and modify keys at any time during your `Cachex.execute/3` call. These calls still occur your calling process; they're not sent through any kind of arbitration process. To demonstrate this, here's a quick example:

```elixir
# start our execution block
Cachex.execute!(:my_cache, fn cache ->
# set a base value in the cache
Cachex.put!(cache, "key", "value")

# we're paused but other changes can happen
:timer.sleep(5000)

# this may have have been set elsewhere
Cachex.get!(cache, "key")
end)
```

As we wait 5 seconds before reading the value back, the value may have been modified or even removed by other processes using the cache (such as TTL cleanup or other places in your application). If you want to guarantee that nothing is modified between your interactions, you should consider a transactional block instead.

## Transactional Batches

A transactional block will guarantee that your actions against a cache key will happen with zero interaction from other processes. Transactions look almost exactly the same as `Cachex.execute/3`, except that they require a list of keys to lock for the duration of their execution.

The entry point to a Cachex transaction is (unsurprisingly) `Cachex.transaction/4`. If we take the example from the previous section, let's look at how we can guarantee consistency between our cache calls:

```elixir
# start our execution block
Cachex.transaction!(:my_cache, ["key"], fn cache ->
# set a base value in the cache
Cachex.put!(cache, "key", "value")

# we're paused but other changes will not happen
:timer.sleep(5000)

# this will be guaranteed to return "value"
Cachex.get!(cache, "key")
end)
```

It's critical to provide the keys you wish to lock when calling `Cachex.transaction/4`, as any keys not specified will still be available to be written by other processes during your function's execution. If you're making a simple cache call, the transactional flow will only be taken if there is a simultaneous transaction happening against the same key. This enables caches to stay lightweight whilst allowing for these batches when they really matter.

Another pattern which may prove useful is providing an empty list of keys, which will guarantee that your transaction runs at a time when no keys in the cache are currently locked. For example, the following code will guarantee that no keys are locked when purging expired records:

```elixir
Cachex.transaction!(:my_cache, [], fn cache ->
Cachex.purge!(cache)
end)
```

Transactional flows are only enabled the first time you call `Cachex.transaction/4`, so you shouldn't see any peformance penalty in the case you're not actively using transactions. This also has the benefit of not requiring transaction support to be configured inside the cache options, as was the case in earlier versions of Cachex.

The last major difference between `Cachex.execute/3` and `Cachex.transaction/4` is where they run; transactions are executed inside a secondary worker process, so each transaction will run only after the previous has completed. As such there is a minor performance overhead when working with transactions, so use them only when you need to.
20 changes: 11 additions & 9 deletions docs/management/limiting-caches.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Limiting Caches

Cache limits are restrictions on a cache to ensure that it stays within given bounds. The limits currently shipped inside Cachex are based around the number of entries inside a cache, but there are plans to add new policies in future (for example basing the limits on memory spaces). You even even write your own!
Cache limits are restrictions on a cache to ensure that it stays within given bounds. The limits currently shipped inside Cachex are based around the number of entries inside a cache, but there are plans to add new policies in future (for example basing the limits on memory spaces). You can even write your own!

## Manual Pruning

Expand All @@ -14,19 +14,21 @@ Cachex.start(:my_cache)

# insert 100 keys
for i <- 1..100 do
Cachex.put!(:my_cache, i, i)
Cachex.put(:my_cache, i, i)
end

# guarantee we have 100 keys in the cache
{ :ok, 100 } = Cachex.size(:my_cache)
100 = Cachex.size(:my_cache)

# trigger a pruning down to 50 keys only
{ :ok, true } = Cachex.prune(:my_cache, 50, reclaim: 0)
50 = Cachex.prune(:my_cache, 50, reclaim: 0)

# verify that we're down to 50 keys
{ :ok, 50 } = Cachex.size(:my_cache)
50 = Cachex.size(:my_cache)
```

As part of pruning, `Cachex.prune/3` will trigger a call to `Cachex.purge/2` to first remove expired entries before cutting potentially unnecessary entries. While the return value of `Cachex.prune/3` represents how many cache entries were *pruned*, it should be noted that the number of expired entries is not included in this value.

The `:reclaim` option can be used to reduce thrashing, by evicting an additional number of entries. In the case above the next write would cause the cache to once again need pruning, and then so on. The `:reclaim` option accepts a percentage (as a decimal) of extra keys to evict, which gives us a buffer between pruning of a cache.

To demonstrate this we can run the same example as above, except using a `:reclaim` of `0.1` (the default). This time we'll be left with 45 keys instead of 50, as we reclaimed an extra 10% of the table (`50 * 0.1 = 5`):
Expand All @@ -37,17 +39,17 @@ Cachex.start(:my_cache)

# insert 100 keys
for i <- 1..100 do
Cachex.put!(:my_cache, i, i)
Cachex.put(:my_cache, i, i)
end

# guarantee we have 100 keys in the cache
{ :ok, 100 } = Cachex.size(:my_cache)
100 = Cachex.size(:my_cache)

# trigger a pruning down to 50 keys, reclaiming 10%
{ :ok, true } = Cachex.prune(:my_cache, 50, reclaim: 0.1)
55 = Cachex.prune(:my_cache, 50, reclaim: 0.1)

# verify that we're down to 45 keys
{ :ok, 45 } = Cachex.size(:my_cache)
45 = Cachex.size(:my_cache)
```

It is almost never a good idea to set `reclaim: 0` unless you have very specific use cases, so if you don't it's recommended to leave `:reclaim` at the default value - it was only used above for example purposes.
Expand Down
Loading