Skip to content

Instantly share code, notes, and snippets.

@potatosalad
Last active February 2, 2019 15:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save potatosalad/82adefefc61cc8463c57ae6c26571e54 to your computer and use it in GitHub Desktop.
Save potatosalad/82adefefc61cc8463c57ae6c26571e54 to your computer and use it in GitHub Desktop.
defmodule MyCache do
@behaviour :gen_statem
@table :my_cache
@expire_after :timer.seconds(45)
@vacuum_idle_timeout :timer.minutes(5)
@vacuum_dead_timeout :timer.seconds(60)
# Public API functions
def start_link() do
:gen_statem.start_link({:local, __MODULE__}, __MODULE__, [], [])
end
def fetch(key, current_monotonic_time \\ :erlang.monotonic_time(:millisecond)) do
case :ets.lookup(@table, key) do
[{^key, expires_monotonic_time, value}] when expires_monotonic_time > current_monotonic_time ->
# instrumentation should record a cache hit
{:ok, value}
[{^key, expires_monotonic_time, _value}] when expires_monotonic_time <= current_monotonic_time ->
# instrumentation should record a cache miss
:ok = :gen_statem.cast(__MODULE__, :vacuum)
:error
[] ->
# instrumentation should record a cache miss
:error
end
end
def store(key, value, current_monotonic_time \\ :erlang.monotonic_time(:millisecond)) do
expires_monotonic_time = current_monotonic_time + @expire_after
true = :ets.insert(@table, {key, expires_monotonic_time, value})
:ok
end
def store_list(list, current_monotonic_time \\ :erlang.monotonic_time(:millisecond)) when is_list(list) do
expires_monotonic_time = current_monotonic_time + @expire_after
kvlist =
for {key, value} <- list, into: [] do
{key, expires_monotonic_time, value}
end
true = :ets.insert(@table, kvlist)
:ok
end
# gen_statem callbacks
defmodule Data do
@type t() :: %__MODULE__{worker: nil | {pid(), reference()}}
defstruct worker: nil
end
@impl :gen_statem
def callback_mode() do
[:handle_event_function, :state_enter]
end
@impl :gen_statem
def init([]) do
@table = :ets.new(@table, [:set, :public, :named_table, {:read_concurrency, true}])
data = %Data{}
{:ok, :idle, data}
end
@impl :gen_statem
def handle_event(:enter, :idle, :idle, _data) do
actions = [{:state_timeout, @vacuum_idle_timeout, :vacuum}]
{:keep_state_and_data, actions}
end
def handle_event(:enter, :idle, :vacuum, data = %Data{worker: nil}) do
{pid, ref} = :erlang.spawn_monitor(__MODULE__, :vacuum_init, [self()])
_ = :erlang.send(pid, {self(), ref, :erlang.monotonic_time(:millisecond)})
data = %Data{data | worker: {pid, ref}}
actions = [{:state_timeout, @vacuum_dead_timeout, :dead}]
{:keep_state, data, actions}
end
def handle_event(:enter, :vacuum, :idle, _data = %Data{worker: nil}) do
actions = [{:state_timeout, @vacuum_idle_timeout, :vacuum}]
{:keep_state_and_data, actions}
end
def handle_event(:state_timeout, :vacuum, :idle, data) do
{:next_state, :vacuum, data}
end
def handle_event(:state_timeout, :dead, :vacuum, _data = %Data{worker: {pid, _ref}}) do
# vacuum process should not take this long
# something is seriously wrong with the system and
# instrumentation/warnings should be recorded when
# this happens
# handle the :DOWN message below
true = :erlang.exit(pid, :timeout)
:keep_state_and_data
end
def handle_event(:cast, :vacuum, :idle, data) do
{:next_state, :vacuum, data}
end
def handle_event(:cast, :vacuum, :vacuum, _data) do
:keep_state_and_data
end
def handle_event(:info, {ref, _count}, :vacuum, data = %Data{worker: {pid, ref}}) do
# count should be recorded for instrumentation purposes
:ok =
receive do
{:DOWN, ^ref, :process, ^pid, :normal} ->
:ok
after
100 ->
_ = :erlang.demonitor(ref, [:flush])
true = :erlang.exit(pid, :kill)
:ok
end
data = %Data{data | worker: nil}
{:next_state, :idle, data}
end
def handle_event(:info, {:DOWN, ref, :process, pid, _reason}, :vacuum, data = %Data{worker: {pid, ref}}) do
# this is an error and should be logged
data = %Data{data | worker: nil}
{:next_state, :idle, data}
end
# Vacuum API (private)
@doc false
def vacuum_init(parent) do
receive do
{^parent, tag, current_monotonic_time} ->
vacuum_loop(parent, tag, current_monotonic_time)
end
end
@doc false
def vacuum_loop(parent, tag, current_monotonic_time) do
match_spec = [{{:_, :"$1", :_}, [{:"=<", :"$1", current_monotonic_time}], [true]}, {:_, [], [false]}]
count = :ets.select_delete(@table, match_spec)
_ = :erlang.send(parent, {tag, count})
exit(:normal)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment