Cache behaviour (elixir_cache v0.3.9)
View SourceElixirCache
The goal of this project is to unify Cache APIs and make Strategies easy to implement and sharable across all storage types/adapters
The second goal is to make sure testing of all cache related funciions is easy, meaning caches should be isolated per test and not leak their state to outside tests
Installation
The package can be installed by adding elixir_cache
to your list of dependencies in mix.exs
:
def deps do
[
{:elixir_cache, "~> 0.1.0"}
]
end
The docs can be found at https://hexdocs.pm/elixir_cache.
Usage
defmodule MyModule do
use Cache,
adapter: Cache.Redis,
name: :my_name,
sandbox?: Mix.env() === :test,
opts: [...opts]
end
In our application.ex
children = [
{Cache, [MyModule]}
]
Now we can use MyModule
to call various Cache apis
MyModule.get("key") #> {:ok, nil}
MyModule.put("key", "value") #> :ok
MyModule.get("key") #> {:ok, "value"}
Adapters
Cache.Agent
- Simple agent based cachingCache.DETS
- Disk persisted caching with detsCache.ETS
- Super quick in-memory cache withets
Cache.Redis
- Caching adapter using Redix & Poolboy, supports Redis JSON an Redis HashesCache.ConCache
- Wrapper around ConCache library
Adapter Specific Functions
Some adapters have specific functions such as redis which has hash functions and pipeline functions to make calls easier.
These adapter when used will add extra commands to your cache module.
Sandboxing
Our cache config accepts a sandbox?: boolean
. In sandbox mode, the Cache.Sandbox
adapter will be used, which is just a simple Agent cache unique to the root process. The Cache.SandboxRegistry
is responsible for registering test processes to a
unique instance of the Sandbox adapter cache. This makes it safe in test mode to run all your tests asynchronously!
For test isolation via the Cache.SandboxRegistry
to work, you must start the registry in your setup, or your test_helper.exs:
# test/test_helper.exs
+ Cache.SandboxRegistry.start_link()
ExUnit.start()
Then inside our setup
for a test we can do:
Cache.SandboxRegistry.start([MyCache, CacheItem])
Creating Adapters
Adapters are very easy to create in this model and are basically just a module that implement the @behaviour Cache
This behaviour adds the following callbacks
put(cache_name, key, ttl, value, opts \\ [])
get(cache_name, key, opts \\ [])
delete(cache_name, key, opts \\ [])
opts_definition() # NimbleOptions definition map
child_spec({cache_name, cache_opts})
Cache.ETS
is probably the easiest adapter to follow as a guide as it's a simple Task
Runtime Configuration
Adapter configuration can also be specified at runtime. These options are first passed to the adapter child_spec when starting the adapter and then passed to all runtime function calls.
For example:
# Configure with Module Function
defmodule Cache.Example do
use Cache,
adapter: Cache.Redis,
name: :test_cache_redis,
opts: {Cache.Example, :opts, []}
def opts, do: [host: "localhost", port: 6379]
end
# Configure with callback function
defmodule Cache.Example do
use Cache,
adapter: Cache.Redis,
name: :test_cache_redis,
opts: &Cache.Example.opts/0
def opts, do: [host: "localhost", port: 6379]
end
# Fetch from application config
# config :elixir_cache, Cache.Example, []
defmodule Cache.Example do
use Cache,
adapter: Cache.Redis,
name: :test_cache_redis,
opts: :elixir_cache
end
# Fetch from application config
# config :elixir_cache, :cache_opts, []
defmodule Cache.Example do
use Cache,
adapter: Cache.Redis,
name: :test_cache_redis,
opts: {:elixir_cache, :cache_opts}
end
Runtime options can be configured in one of the following formats:
{module, function, args}
- Module, function, args{application_name, key}
- Application name. This is called asApplication.fetch_env!(application_name, key)
.application_name
- Application name as an atom. This is called asApplication.fetch_env!(application_name, cache_module})
.function
- Zero arity callback function. For eg.&YourModule.options/0
[key: value_type]
- Keyword list of options.
Summary
Functions
Returns a specification to start this module under a supervisor.
Retrieves a value from the cache if it exists, or executes a function to create and store it.
Callback implementation for Supervisor.init/1
.
Callbacks
@callback child_spec({cache_name :: atom(), cache_opts :: Keyword.t()}) :: Supervisor.child_spec() | :supervisor.child_spec()
@callback delete(cache_name :: atom(), key :: atom() | String.t()) :: :ok | ErrorMessage.t()
@callback delete(cache_name :: atom(), key :: atom() | String.t(), opts :: Keyword.t()) :: :ok | ErrorMessage.t()
@callback get(cache_name :: atom(), key :: atom() | String.t()) :: ErrorMessage.t_res(any())
@callback opts_definition() :: Keyword.t()
@callback put( cache_name :: atom(), key :: atom() | String.t(), ttl :: pos_integer(), value :: any() ) :: :ok | ErrorMessage.t()
@callback put( cache_name :: atom(), key :: atom() | String.t(), ttl :: pos_integer(), value :: any(), Keyword.t() ) :: :ok | ErrorMessage.t()
Functions
Returns a specification to start this module under a supervisor.
See Supervisor
.
@spec get_or_create(module(), atom() | String.t(), (-> {:ok, any()} | {:error, any()})) :: {:ok, any()} | {:error, any()}
Retrieves a value from the cache if it exists, or executes a function to create and store it.
This is a convenience function implementing the common "get or create" pattern for caches. It attempts to fetch a value from the cache first, and only if the value doesn't exist, it will execute the provided function to generate the value and store it in the cache.
Parameters
cache
- The cache module to use (must implement the Cache behaviour)key
- The key to look up or createfnc
- A function that returns{:ok, value}
or{:error, reason}
Returns
{:ok, value}
- The value from cache or newly created value{:error, reason}
- If an error occurred during retrieval or creation
Examples
Cache.get_or_create(MyApp.Cache, "user:123", fn ->
case UserRepo.get(123) do
nil -> {:error, ErrorMessage.not_found("User not found")}
user -> {:ok, user}
end
end)
Callback implementation for Supervisor.init/1
.