View Source Custom Commands
Cachex allows for custom commands to be attached to a cache, in order to simplify common logic without having to channel all of your cache calls through a specific block of code or a specific module. Cache commands are the solution for extending Cachex with operations or verbiage specific to your application and/or domain without bloating Cachex itself.
Commands operate in such a way that they're marginally quicker than hand-writing your own wrapper functions, but only very slightly. As a rule of thumb you should aim to set only very general actions as commands on a cache, and keep very specific actions outside of the caching layer. It's possible that in future Cachex may ship with some additional built-in commands for very common functionality (perhaps as a separate library).
Defining a Command
Commands are defined on a per-cache basis via the :commands
flag inside the Cachex.start_link/2
options.
There are two types of command, either :read
or :write
. As you might guess the former will return a modified value from within a cache, while the latter will modify the value inside the cache before returning it.
Let's consider some basic List
operations, and assume that we're storing some List
types in a cache. In this case we might wish to have some typical List
operations attached to our cache, rather than defining them externally.
Two perfect examples for us to look at are retrieving the last item in a list (List.last/1
), and also popping the first item from a list (List.pop_at/3
with index 0). As the former does not need to modify the List
, it would be classed as a :read
command. In contrast the latter does need to modify the List
, and so it would be classed as a :write
command.
Let's look at how we can define simple versions of these commands and attach them to a cache at startup:
# need the records
import Cachex.Spec
# define some custom commands
last = &List.last/1
lpop = fn
([ head | tail ]) ->
{ head, tail }
([ ] = list) ->
{ nil, list }
end
# attach them to the cache
Cachex.start_link(:my_cache, [
commands: [
last: command(type: :read, execute: last),
lpop: command(type: :write, execute: lpop)
]
])
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
.
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.
Invoking Commands
The entry point to command invocation is via the Cachex.invoke/4
interface function. This function accepts a command name and the key it should be called on. All value retrieval is handled automatically, and errors like invalid command names will result in an error as expected.
Let's look at some examples of calling the new :last
and :lpop
commands we defined above, after populating an example list in our cache.
# place a new list into our cache of 3 elements
{ :ok, true } = Cachex.put(:my_cache, "my_list", [ 1, 2, 3 ])
# check the last value in the list stored under "my_list"
{ :ok, 3 } = Cachex.invoke(:my_cache, :last, "my_list")
# pop all values from the list stored under "my_list"
{ :ok, 1 } = Cachex.invoke(:my_cache, :lpop, "my_list")
{ :ok, 2 } = Cachex.invoke(:my_cache, :lpop, "my_list")
{ :ok, 3 } = Cachex.invoke(:my_cache, :lpop, "my_list")
{ :ok, nil } = Cachex.invoke(:my_cache, :lpop, "my_list")
# check the last value in the list stored under "my_list"
{ :ok, nil } = Cachex.invoke(:my_cache, :last, "my_list")
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.