Packages

In-memory key/value store with expiring keys

Current section

Files

Jump to
sider lib sider.ex
Raw

lib/sider.ex

defmodule Sider do
@type key :: any()
@type value :: any()
@type args ::
%{
reap_interval: pos_integer(),
capacity: pos_integer(),
name: atom,
}
| %{
name: atom,
capacity: pos_integer()
}
| %{
capacity: pos_integer()
}
@moduledoc """
Sider is an in-memory key-value store with the following characteristics:
1. keys & values may be of any type
2. Key-value pairs expire - once set in the store, they are only valid for a given time
3. Sider has O(keys) + O(values) memory characteristics
4. The cache maintains consistent access times - It will not degrade when reaping expired values
The usage of Sider is as follows. Usually, it will be started under a supervisor with a given name
```
children = [
{Sider, %{capacity: 100, name: :my_cache}}
]
Supervisor.start_link(children, strategy: :one_for_one)
```
You can then call the cache via its given name, similar to this
```
Sider.get(:my_cache, :a)
```
"""
@doc """
Create a sider cache process. The behavior of the sider cache can be controlled by the following args
reap_interval: The number of milliseconds to wait before removing keys that have expired
capacity: The number of keys allowed in the store. This includes expired keys that have not been reaped.
## Examples
iex> {:ok, _pid} = Sider.start_link(%{reap_interval: 60_000, capacity: 1_000_000, name: :my_cache})
iex> :ok
:ok
"""
@spec start_link(args) :: GenServer.on_start()
def start_link(args) do
opts = case Map.fetch(args, :name) do
{:ok, name} -> [name: name]
:error -> []
end
args = %{
capacity: args.capacity,
reap_interval: Map.get(args, :reap_interval, 60_000)
}
GenServer.start_link(Sider.Impl, args, opts)
end
@spec child_spec(args) :: Supervisor.child_spec()
def child_spec(args) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [args]},
type: :worker,
restart: :permanent,
shutdown: 500
}
end
@doc """
Returns an existing key, if it has not expired.
## Examples
iex> {:ok, pid} = Sider.start_link(%{reap_interval: 1, capacity: 100})
iex> Sider.set(pid, :a, :foo)
iex> {:ok, :foo} = Sider.get(pid, :a)
iex> {:error, :missing_key} = Sider.get(pid, :b)
iex> :ok
:ok
"""
@spec get(GenServer.server(), key) :: {:ok, value} | {:error, :missing_key}
def get(pid, key) do
GenServer.call(pid, {:get, key})
end
@doc """
Store a new key-value pair, with an optional timeout for when the pair should expire
Returns :ok if successful, or {:error, :max_capacity} if the cache is full
If you call set() on a key that already exists in the store, that key-pair will be overwritten
## Examples
iex> {:ok, pid} = Sider.start_link(%{reap_interval: 1000, capacity: 1})
iex> :ok = Sider.set(pid, :a, :foo, 1000) # Set a key with a value of :foo that expires after 1000ms
iex> :ok = Sider.set(pid, :a, {1, 2}, 1000) # Overwrite the key
iex> Sider.set(pid, :b, :bar) # The capacity is 1, so the key cannot be written
{:error, :max_capacity}
"""
@spec set(GenServer.server(), key, value, pos_integer() | nil) :: :ok | {:error, :max_capacity}
def set(pid, key, value, timeout \\ nil) do
GenServer.call(pid, {:set, key, value, timeout})
end
@doc """
Removes a key-value pair from the cache, if it exists.
This function no-ops if the key is non-existant
If you pass in the `only: :expired` option, the value will only be removed if the entry has expired
(See the timeout value in `Sider.set/4`)
## Examples
iex> {:ok, pid} = Sider.start_link(%{reap_interval: 1000, capacity: 100})
iex> Sider.set(pid, :a, :foo)
iex> Sider.remove(pid, :a)
nil
"""
@spec remove(GenServer.server(), key, [] | [{:only, :expired}]) :: nil
def remove(pid, key, opts \\ []) do
GenServer.call(pid, {:remove, key, opts})
end
end