feat(aof): add append-only file persistence

This commit is contained in:
dualfroz committed 2026-08-27 11:17:00 +02:00
1 parent d7235b4f99
commit 316413b90e
1 file changed
+83
+83
View File
@@ -0,0 +1,83 @@
defmodule Excache.Aof do
@moduledoc false
use GenServer
alias Excache.RESP.{Encoder, Parser}
def start_link(_arg), do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)
def path do
Application.get_env(:excache, :aof_path) ||
Path.join(to_string(:code.priv_dir(:excache)), "excache.aof")
end
def fsync_policy, do: Application.get_env(:excache, :aof_fsync, :everysec)
def append(command, args), do: GenServer.cast(__MODULE__, {:append, command, args})
def flush, do: GenServer.call(__MODULE__, :flush)
def restart do
Supervisor.terminate_child(Excache.Supervisor, __MODULE__)
Supervisor.restart_child(Excache.Supervisor, __MODULE__)
:ok
end
def replay(file_path) do
case File.read(file_path) do
{:ok, binary} -> replay_commands(binary)
{:error, _reason} -> :ok
end
end
@impl true
def init(_arg) do
file_path = path()
File.mkdir_p!(Path.dirname(file_path))
{:ok, device} = File.open(file_path, [:append, :binary])
schedule_fsync(fsync_policy())
{:ok, %{device: device}}
end
@impl true
def handle_cast({:append, command, args}, state) do
IO.binwrite(state.device, Encoder.encode({:array, bulks([command | args])}))
if fsync_policy() == :always, do: :file.sync(state.device)
{:noreply, state}
end
@impl true
def handle_call(:flush, _from, state) do
:file.sync(state.device)
{:reply, :ok, state}
end
@impl true
def handle_info(:fsync_tick, state) do
if fsync_policy() == :everysec, do: :file.sync(state.device)
schedule_fsync(fsync_policy())
{:noreply, state}
end
defp replay_commands(<<>>), do: :ok
defp replay_commands(binary) do
case Parser.parse_command(binary) do
{:ok, [], rest} ->
replay_commands(rest)
{:ok, [name | args], rest} ->
Excache.Command.replay_dispatch(String.upcase(name), args)
replay_commands(rest)
_incomplete_or_error ->
:ok
end
end
defp bulks(values), do: Enum.map(values, fn value -> {:bulk, value} end)
defp schedule_fsync(:everysec), do: Process.send_after(self(), :fsync_tick, 1000)
defp schedule_fsync(_policy), do: :ok
end