Skip to content

Instantly share code, notes, and snippets.

@raorao
Created May 12, 2017 04:30
Show Gist options
  • Save raorao/ac99362e4c437a3115273f4389903835 to your computer and use it in GitHub Desktop.
Save raorao/ac99362e4c437a3115273f4389903835 to your computer and use it in GitHub Desktop.
Simple Process cache in Elixir.
defmodule RequestCache do
@moduledoc """
Simple GenServer-based cache.
"""
use GenServer
@type t :: %{cache: %{optional(cache_key) => cache_value}, interval: integer}
@typep cache_key :: any
@typep cache_value :: any
@doc """
Starts a RequestCache process linked to the current process. See
`GenServer.start_link/2` for details.
By default, the cache clears every 6 hours.
"""
@spec start_link(integer) :: GenServer.on_start
def start_link(interval_seconds \\ 21_600) do
GenServer.start_link(__MODULE__, interval_seconds, name: __MODULE__)
end
@doc """
Asynchronous call to cache a value at the provided key. Any key that can
be used with `Map.t` can be used, and will be evaluated using `==`.
"""
@spec cache(cache_key, cache_value) :: :ok
def cache(key, val) do
GenServer.cast(__MODULE__, {:cache, key, val})
end
@doc """
Asynchronous clears all values in the cache.
"""
@spec clear() :: :ok
def clear do
GenServer.cast(__MODULE__, :clear)
end
@doc """
Sychronously reads the cache for the provided key. If no value is found,
returns :not_found .
"""
@spec read(cache_key) :: cache_value | :not_found
def read(key) do
GenServer.call(__MODULE__, {:read, key})
end
@doc """
Sychronously reads the cache for the provided key. If no value is found,
invokes default_fn and caches the result. Note: in order to prevent congestion
of the RequestCache process, default_fn is invoked in the context of the caller
process.
"""
@spec read_or_cache_default(cache_key, (() -> cache_value)) :: cache_value
def read_or_cache_default(key, default_fn) do
case read(key) do
:not_found ->
value = default_fn.()
cache key, value
value
value ->
value
end
end
# GenServer Callbacks
@spec init(integer) :: {:ok, t}
def init(interval_seconds) do
initial_state = %{cache: %{}, interval: interval_seconds * 1000}
schedule_clear initial_state
{:ok, initial_state}
end
@spec handle_cast({:cache, cache_key, cache_value}, t) :: {:noreply, t}
def handle_cast({:cache, key, val}, state = %{cache: cache}) do
{:noreply, %{state | cache: Map.put(cache, key, val)}}
end
@spec handle_cast(:clear, t) :: {:noreply, t}
def handle_cast(:clear, state) do
{:noreply, %{state | cache: %{}}}
end
@spec handle_info(:clear_and_schedule, t) :: {:noreply, t}
def handle_info(:clear_and_schedule, state) do
schedule_clear state
{:noreply, %{state | cache: %{}}}
end
@spec handle_call({:read, cache_key}, GenServer.from, t) :: {:reply, cache_value | :not_found, t}
def handle_call({:read, key}, _from, state = %{cache: cache}) do
{:reply, Map.get(cache, key, :not_found), state}
end
defp schedule_clear(%{interval: interval}) do
Process.send_after self(), :clear_and_schedule, interval
end
end
@fdoclaves
Copy link

cool

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment