Packages
baobab
0.19.1
0.35.1
0.35.0
0.34.0
0.31.0
0.30.0
0.21.2
0.21.1
0.21.0
0.20.0
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.17.1
0.17.0
0.16.8
0.16.7
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.2
0.15.1
0.15.0
0.14.1
0.14.0
0.13.1
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.0
Bamboo append-only logs
Current section
Files
Jump to
Current section
Files
lib/baobab/persistence.ex
defmodule Baobab.Persistence do
alias Baobab.{Entry, Identity}
@moduledoc """
Functions related to Baobab values persistence
"""
@doc """
Interact with a Baobab persistence mechanism
Actions closely mirror the underlying `dets` at present
"""
def action(which, clump_id, action, value \\ nil) do
store(which, clump_id, :open)
retval = perform_action(which, action, value)
case action in [:truncate, :delete, :put, :match_delete] do
true -> recompute_hash(clump_id, which)
false -> :ok
end
store(which, clump_id, :close)
retval
end
defp perform_action(which, :get, key) do
case :dets.lookup(which, key) do
[{^key, val} | _] -> val
[] -> nil
end
end
defp perform_action(which, :foldl, fun), do: :dets.foldl(fun, [], which)
defp perform_action(which, :truncate, _), do: :dets.delete_all_objects(which)
defp perform_action(which, :delete, key), do: :dets.delete(which, key)
defp perform_action(which, :put, kv), do: :dets.insert(which, kv)
defp perform_action(which, :match_delete, key_pattern),
do: :dets.match_delete(which, {key_pattern, :_})
defp perform_action(which, :match, key_pattern),
do: :dets.match(which, {key_pattern, :_})
@doc false
def store(which, clump_id, :open) do
{:ok, ^which} = :dets.open_file(which, file: proper_db_path(which, clump_id))
end
def store(which, _clump_id, :close), do: :dets.close(which)
@doc """
Retrieve the current hash of the `:content` or `:identity` store.
No information should be gleaned from any particular hash beyond whether
the contents have changed since a previous check.
"""
def current_hash(which, clump_id \\ "default")
def current_hash(which, clump_id) do
case action(:status, clump_id, :get, {clump_id, which}) do
{hash, _stuff} ->
hash
_ ->
recompute_hash(clump_id, which)
current_hash(which, clump_id)
end
end
@doc """
Retrieve the current stored info which is the max entry for each
stored log
"""
def current_stored_info(clump_id \\ "default")
def current_stored_info(clump_id) do
case action(:status, clump_id, :get, {clump_id, :stored_info}) do
{:ok, si} ->
si
_ ->
recompute_hash(clump_id, :content)
current_stored_info(clump_id)
end
end
@doc """
Retrieve the current value of the `:content` or `:identity` store.
No information should be gleaned from any particular hash beyond whether
the contents have changed since a previous check.
"""
def current_value(which, clump_id \\ "default")
def current_value(which, clump_id) do
case action(:status, clump_id, :get, {clump_id, which}) do
{_hash, stuff} ->
stuff
_ ->
recompute_hash(clump_id, which)
end
end
defp recompute_hash(clump_id, table)
defp recompute_hash(_, :status), do: "nahnah"
# This one should probably have this available at some point
defp recompute_hash(_, :metadata), do: "nahnah"
defp recompute_hash(clump_id, which) do
stuff =
case which do
:content ->
val = all_entries(clump_id)
recompute_si(val, clump_id)
val
:identity ->
Baobab.Identity.list()
end
hash =
stuff
|> :erlang.term_to_binary()
|> Blake2.hash2b(7)
|> BaseX.Base62.encode()
# Even though identities are the same in both
# I might be convinced otherwise later
action(:status, clump_id, :put, {{clump_id, which}, {hash, stuff}})
hash
end
defp all_entries(clump_id) do
:content
|> action(clump_id, :foldl, fn item, acc ->
case item do
{e, _} -> [e | acc]
_ -> acc
end
end)
end
defp recompute_si(all_entries, clump_id) do
all_entries
|> Enum.reduce(MapSet.new(), fn {a, l, _}, c ->
MapSet.put(c, {a, l})
end)
|> MapSet.to_list()
|> make_stored_info(clump_id, [])
|> then(fn si -> action(:status, clump_id, :put, {{clump_id, :stored_info}, {:ok, si}}) end)
end
defp make_stored_info([], _clump_id, acc), do: acc |> Enum.reverse()
defp make_stored_info([{a, l} | rest], clump_id, acc) do
a =
case Baobab.max_seqnum(a, log_id: l, clump_id: clump_id) do
0 -> acc
n -> [{a, l, n} | acc]
end
make_stored_info(rest, clump_id, a)
end
@doc """
Deal with the peristed bamboo content
"""
def content(subject, action, entry_id, clump_id, addlval \\ nil)
def content(subject, action, {author, log_id, seq}, clump_id, addlval) do
key = {author |> Identity.as_base62(), log_id, seq}
curr = action(:content, clump_id, :get, key)
case {action, curr} do
{:delete, nil} ->
:ok
{:delete, _} ->
action(:content, clump_id, :delete, key)
{:contents, nil} ->
case subject do
:both -> {:error, :error}
_ -> :error
end
{:contents, map} ->
case subject do
:both -> {Map.get(map, :entry, :error), Map.get(map, :payload, :error)}
key -> Map.get(map, key, :error)
end
{:hash, %{^subject => c}} ->
YAMFhash.create(c, 0)
{:write, prev} ->
case subject do
:both ->
{entry, payload} = addlval
action(:content, clump_id, :put, {key, %{:entry => entry, :payload => payload}})
map_key ->
map = if is_nil(prev), do: %{}, else: prev
action(:content, clump_id, :put, {key, Map.merge(map, %{map_key => addlval})})
end
{:exists, nil} ->
false
{:exists, _} ->
true
{_, _} ->
:error
end
end
@doc false
# Handle the simplest case first
def retrieve(author, seq, {:binary, log_id, false, clump_id}) do
entry_id = {author, log_id, seq}
case content(:both, :contents, entry_id, clump_id) do
{:error, _} -> :error
{_, :error} -> :error
{entry, payload} -> entry <> payload
end
end
# This handles the other three cases:
# :entry validated or unvalidated
# :binary validated
def retrieve(author, seq, {fmt, log_id, validate, clump_id}) do
entry_id = {author, log_id, seq}
binary = content(:entry, :contents, entry_id, clump_id)
res = Entry.from_binaries(binary, validate, clump_id) |> hd
case {res, fmt} do
{{:error, :missing}, _} ->
:error
{:error, _} ->
content(:entry, :delete, entry_id, clump_id)
:error
{entry, :entry} ->
entry
{_, :binary} ->
binary
end
end
defp proper_db_path(:identity, clump_id) when byte_size(clump_id) > 0,
do: proper_db_path(:identity, "")
defp proper_db_path(which, clump_id) when is_binary(clump_id) and is_atom(which) do
file = Atom.to_string(which) <> ".dets"
dir = Application.fetch_env!(:baobab, :spool_dir) |> Path.expand()
Path.join([dir, clump_id, file]) |> to_charlist
end
defp proper_db_path(_, _), do: raise("Improper clump_id")
end