floof/lib/floof/packet_spool.ex
2022-11-26 15:34:51 +01:00

126 lines
3 KiB
Elixir

defmodule Floof.PacketSpool do
@moduledoc """
disk-backed packet spooling management
saves packets to disk, and manages encoding/decoding
the user is encouraged to use a backing disk
with long commit times to improve performance and avoid disk churn
"""
use GenServer
require Logger
def start_link(initval \\ "/var/spool/floof") do
GenServer.start_link(__MODULE__, initval, name: __MODULE__)
end
### client interface
## packet is expected to be of type :XferBlob
def store(hash, packet) do
GenServer.call(__MODULE__, {:store, hash, packet, true})
end
def store_new(hash, packet) do
GenServer.call(__MODULE__, {:store, hash, packet, false})
end
def fetch(hash) do
GenServer.call(__MODULE__, {:fetch, hash})
end
def drop(hash) do
GenServer.cast(__MODULE__, {:drop, hash})
end
def keep_only(hashes) do
GenServer.cast(__MODULE__, {:keep_only, hashes})
end
### server interface
@impl true
def init(state) do
{:ok, state}
end
@impl true
def handle_call({:store, hash, packet, overwrite}, _, state) do
path = hash_to_path(hash, state)
echain(
state,
fn ->
if not overwrite and File.exists?(path) do
:ok
else
:FloofProtocol.encode(:XferBlob, packet)
end
end,
&File.write(path, &1)
)
end
@impl true
def handle_call({:fetch, hash}, _, state) do
echain(
state,
fn -> File.read(hash_to_path(hash, state)) end,
&:FloofProtocol.decode(:XferBlob, &1)
)
end
@impl true
def handle_cast({:drop, hash}, state) do
case File.rm(hash_to_path(hash, state)) do
:ok -> nil
{:error, e} -> Logger.error("spool: unable to remove #{inspect(hash)}: #{inspect(e)}")
end
{:noreply, state}
end
@impl true
def handle_cast({:keep_only, hashes}, state) do
case File.ls(state) do
{:error, e} ->
Logger.error("spool: unable to browse spool directory for garbage collection: #{inspect(e)}")
{:ok, items} ->
present = MapSet.new(Enum.flat_map(items, fn x ->
case Base.url_decode64(x) do
{:ok, y} -> [y]
{:error, e} ->
Logger.warn("spool: unable to decode entry name #{x}: #{inspect(e)}")
[]
end
end))
hashes = MapSet.new(hashes)
Logger.debug("spool: present #{inspect(present)} vs. to_keep #{inspect(hashes)}")
for hash <- MapSet.difference(present, hashes) do
case File.rm(hash_to_path(hash, state)) do
:ok -> nil
{:error, e} -> Logger.error("spool: unable to remove #{inspect(hash)}: #{inspect(e)}")
end
end
end
{:noreply, state}
end
# internal interface
defp hash_to_path(hash, state) do
state <> "/" <> Base.url_encode64(hash)
end
defp echain(state, handler1, handler2) do
retval = try do
case handler1.() do
{:ok, encd} -> handler2.(encd)
x -> x
end
catch
y, z -> {:error, y, z}
end
{:reply, retval, state}
end
end