Packages

Simplest persistence layer for in-memory state storage

Current section

Files

Jump to
persist lib persist.ex
Raw

lib/persist.ex

defmodule Persist do
@moduledoc """
A simple KV for doma.
"""
import DynHacks
@generations 3
@doc """
Saves state into file system and returns the state it saved or crashes.
NB! No functions allowed.
"""
@spec save_state(any, atom, atom | nil) :: any
def save_state(state, module, host \\ nil) do
{_, new_tip_path, vs} = get_values_and_new_tip(module, host)
:ok = File.touch(new_tip_path)
:ok = File.write(new_tip_path, :erlang.term_to_binary(state))
Task.start(fn ->
discard_obsolete_snapshots(vs, module, host)
end)
state
end
defp discard_obsolete_snapshots(vs, module, host) do
if length(vs) > @generations do
vs
|> Enum.take(length(vs) - @generations)
|> Enum.map(&File.rm(snapshot_path(module, host, &1)))
end
end
@spec load_state(atom, atom | nil) :: any
def load_state(module, host \\ nil) do
{_, _, vs} = get_values_and_new_tip(module, host)
load_latest_state(
vs
|> Enum.reverse()
|> Enum.map(fn x -> [key_path(module, host), x] |> Path.join() end)
)
end
defp load_latest_state([]) do
nil
end
defp load_latest_state([h | t]) do
case File.read(h) do
{:ok, fc} ->
:erlang.binary_to_term(fc)
_ ->
File.rm(h)
load_latest_state(t)
end
end
defp mk_key(module, host) do
r_m(
key_path(module, host),
&File.mkdir_p/1
)
end
defp get_values_and_new_tip(module, host) do
key = mk_key(module, host)
vs =
Enum.sort(
File.ls(key) |> elem(1),
&(String.to_integer(&1) <= String.to_integer(&2))
)
{tip, vs} =
case vs do
[] ->
{"1", vs}
[h | []] ->
{bump_tip(h), vs}
[_ | t] ->
{t |> Enum.reverse() |> hd() |> bump_tip(), vs}
end
{tip, Path.join([key, tip]), vs}
end
defp bump_tip(tip_n) do
((tip_n |> String.to_integer()) + 1) |> Integer.to_string()
end
defp snapshot_path(module, host, snapshot) do
Path.join([key_path(module, host), snapshot])
end
defp key_path(module, host), do: [db_path(host), module |> Atom.to_string()] |> Path.join()
@spec get_key_path(atom, atom | nil) :: Uptight.Text.t()
def get_key_path(module, host \\ nil),
do: key_path(module, host) |> Uptight.Text.new!()
@spec get_db_path(atom | nil) :: Uptight.Text.t()
def get_db_path(host \\ nil), do: db_path(host) |> Uptight.Text.new!()
@spec db_path(atom | nil) :: Path.t()
defp db_path(host) do
n = (host || :erlang.node()) |> Atom.to_string()
Path.join(["db", n])
end
end