Packages

A library to cache Ecto query results.

Current section

Files

Jump to
ecto_cache lib ecto_cache.ex
Raw

lib/ecto_cache.ex

defmodule EctoCache do
@moduledoc """
Implements an in-memory process to cache the results of database queries.
"""
use GenServer
@doc false
@opaque t :: %__MODULE__{value: term(), updated_at: Time.t()}
defstruct [:value, :updated_at]
## Client implementation
@doc """
Starts the `EctoCache` process.
You should not use this function directly. Instead add `EctoCache` to your
supervision tree as explained in the module documentation.
"""
def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
@doc """
Gets the value of `key` from the cache, if available. If the value has not
been cached yet or it has expired, executes the `fun` function and cache
their result.
You can set `lifetime` to the number of seconds you want the value to be
available or to `infinity` so that it never expires. Every time the `key`
value is read, the availability time is reset again to `lifetime`.
## Example
iex> EctoCache.cache(:posts, &Repo.all(Post), 600)
[%Post{}, ...]
"""
@spec cache(atom(), (-> term()), {non_neg_integer() | :infinity}) :: term()
def cache(key, fun, lifetime \\ :infinity)
when is_atom(key) and is_function(fun) and
(lifetime == :infinity or (is_integer(lifetime) and lifetime >= 0)) do
case GenServer.call(__MODULE__, {:get, key, lifetime}) do
{:ok, value} ->
value
:error ->
value = fun.()
GenServer.cast(__MODULE__, {:put, key, value})
value
end
end
@doc """
Deletes the cached `key` value.
Use this function when the cached values have changed and you want to disable
the cache to avoid reading outdated data.
This function always returns `:ok` regardless of whether or not a cached value
exists for the `key`.
## Examples
iex> EctoCache.delete(:posts)
:ok
"""
@spec delete(atom()) :: :ok
def delete(key) when is_atom(key), do: GenServer.call(__MODULE__, {:delete, key})
@doc """
Same as `EctoCache.delete/1` but designed to be used in a pipeline as a
result of a `Ecto.Repo` write operation.
This function integrates with `Ecto.Repo.insert/2`, `Ecto.Repo.update/2` and
`Ecto.Repo.delete/2`, deleting the cache _only_ if the operation was carried
out and returning the result of the database operation.
## Examples
iex> Repo.insert(post) |> EctoCache.delete(:posts)
{:ok, %Post{}}
iex> Repo.update(post) |> EctoCache.delete(:posts)
{:ok, %Post{}}
iex> Repo.delete(post) |> EctoCache.delete(:posts)
{:ok, %Post{}}
"""
@spec delete({:ok, Ecto.Schema.t()}, atom()) :: {:ok, Ecto.Schema.t()}
def delete({:ok, schema}, key) when is_atom(key) do
GenServer.call(__MODULE__, {:delete, key})
{:ok, schema}
end
@spec delete({:error, Ecto.Changeset.t()}, atom()) :: {:error, Ecto.Changeset.t()}
def delete({:error, changeset}, _key), do: {:error, changeset}
## GenServer implementation
@impl true
def init(_opts), do: {:ok, %{}}
@impl true
def handle_cast({:put, key, value}, state) do
cached = struct(__MODULE__, value: value, updated_at: Time.utc_now())
{:noreply, Map.put(state, key, cached)}
end
@impl true
def handle_call({:get, key, :infinity}, _from, state) do
case Map.fetch(state, key) do
{:ok, cached} -> {:reply, {:ok, cached.value}, state}
:error -> {:reply, :error, state}
end
end
@impl true
def handle_call({:get, key, lifetime}, _from, state) do
with {:ok, cached} <- Map.fetch(state, key),
now <- Time.utc_now(),
true <- Time.diff(now, cached.updated_at) < lifetime do
cached = struct(cached, updated_at: now)
{:reply, {:ok, cached.value}, Map.put(state, key, cached)}
else
_cached_expired_or_key_not_found ->
{:reply, :error, state}
end
end
@impl true
def handle_call({:delete, key}, _from, state) do
{:reply, :ok, Map.delete(state, key)}
end
end