126 lines
3 KiB
Elixir
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
|