Keeps the turns of each conversation in one append-only file per session.
A file is a sequence of records framed by Mead.Session.Record, each the encoding of one event: {:append, turn} when a turn starts, {:replace_last, turn} when the newest turn is closed, and :interrupt when every turn still in progress became interrupted. Replaying the events in order rebuilds the list the history agent held.
A last record that is cut short or fails its payload checksum is what a crash during a write
leaves, so it is dropped with a warning. The length has a checksum of its own, so a flipped bit
in a header cannot pass for a cut write: damage anywhere else is :corrupt_history, because a
conversation with a hole in the middle is worse than no conversation. load/2 only reads;
recover/2 also cuts the dropped tail off the file, which a session must do before it writes
again, or its next record would land behind the garbage and make the whole file unreadable. A
write that fails is rolled back to the size the file had before it, for the same reason. The
directory is single-writer.
compact/3 rewrites a file as one {:append, turn} per turn, through a synced <id>.log.tmp
renamed over the log, so a crash leaves the old file or the new one and load/2 answers the
same turns before and after.
Payloads are decoded without :safe. A failed turn keeps its reason as output, and that reason
can hold an exception or an atom from a module a freshly booted node has not loaded yet, which
safe decoding refuses: a conversation with a failure in it could not be resumed. The files are
written by this node, are private to its owner, and are guarded by a checksum; decoding never
runs code, and whoever can write the directory can already do far worse. Do not point the
directory at data from a source you do not trust.
A session id becomes a file name, so it is accepted only in the shape Mead.UUID.generate/0
produces, and checked before any path is built.
Summary
Functions
Appends one event to the file of a session, creating the directory and file on first use.
Rewrites the file of a session as one {:append, turn} record per turn, given in
chronological order, and returns the size of the file before and after.
Returns the size in bytes a live session's file may reach before it is compacted.
Removes the file of a session, and the temporary file a crashed compaction may have left beside it; a missing file or an invalid id is not an error.
Returns the configured storage directory, or nil when persistence is off.
Returns true when a file exists for the session.
Returns the size in bytes the ledger journal may reach before it is rewritten as a checkpoint.
Lists the stored sessions, newest first, as maps with the :session_id and the :updated_at of
its file.
Returns the turns of a session in chronological order, without touching the file.
Returns how many of the newest turns a conversation keeps when its file is compacted, or nil
for all of them, which is the default.
Like load/2, and cuts a torn last record off the file so that later appends follow the last
complete record.
Returns the size in bytes of the file of a session.
The newest maximum of turns (chronological order), or all of them when it is nil.
Types
@type event() :: {:append, Mead.Session.Turn.t()} | {:replace_last, Mead.Session.Turn.t()} | :interrupt
Functions
Appends one event to the file of a session, creating the directory and file on first use.
@spec compact(String.t(), String.t(), [Mead.Session.Turn.t()]) :: {:ok, %{bytes_before: non_neg_integer(), bytes_after: non_neg_integer()}} | {:error, :not_found | {:persistence, term()}}
Rewrites the file of a session as one {:append, turn} record per turn, given in
chronological order, and returns the size of the file before and after.
The file is replaced only when the new form is smaller, so a file that is already compact is
left alone and both sizes are the current one. The replacement is written to <id>.log.tmp,
synced, and renamed over the log, so a crash leaves the old file or the new one. A failure
leaves the log as it was and removes the temporary file. A file that does not exist is
{:error, :not_found}: compaction never creates a conversation.
@spec compact_after_bytes() :: pos_integer()
Returns the size in bytes a live session's file may reach before it is compacted.
Defaults to one mebibyte; config :mead, :storage, compact_after_bytes: sets it. Anything but
a positive integer raises ArgumentError, rather than quietly never compacting.
Removes the file of a session, and the temporary file a crashed compaction may have left beside it; a missing file or an invalid id is not an error.
@spec directory() :: String.t() | nil
Returns the configured storage directory, or nil when persistence is off.
Returns true when a file exists for the session.
@spec ledger_journal_max_bytes() :: pos_integer()
Returns the size in bytes the ledger journal may reach before it is rewritten as a checkpoint.
Defaults to one mebibyte; config :mead, :storage, ledger_journal_max_bytes: sets it, and nil means the default. Anything but a positive integer raises ArgumentError.
@spec list(String.t()) :: {:ok, [%{session_id: String.t(), updated_at: DateTime.t()}]} | {:error, {:persistence, term()}}
Lists the stored sessions, newest first, as maps with the :session_id and the :updated_at of
its file.
Only names and file times are read, never the content, so the cost does not grow with the
length of the conversations. Anything in the directory that is not a session file is ignored,
and a damaged file is listed like any other: only recover/2 reads it. A directory that does
not exist yet holds no sessions.
@spec load(String.t(), String.t()) :: {:ok, [Mead.Session.Turn.t()]} | {:error, :not_found | :corrupt_history | {:persistence, term()}}
Returns the turns of a session in chronological order, without touching the file.
@spec max_turns() :: pos_integer() | nil
Returns how many of the newest turns a conversation keeps when its file is compacted, or nil
for all of them, which is the default.
config :mead, :storage, max_turns: sets it. It is the one setting that makes compaction drop
turns, so it is off unless asked for; anything but nil or a positive integer raises
ArgumentError.
@spec recover(String.t(), String.t()) :: {:ok, [Mead.Session.Turn.t()]} | {:error, :not_found | :corrupt_history | {:persistence, term()}}
Like load/2, and cuts a torn last record off the file so that later appends follow the last
complete record.
@spec size(String.t(), String.t()) :: {:ok, non_neg_integer()} | {:error, :not_found | {:persistence, term()}}
Returns the size in bytes of the file of a session.
@spec window([Mead.Session.Turn.t()], pos_integer() | nil) :: [Mead.Session.Turn.t()]
The newest maximum of turns (chronological order), or all of them when it is nil.