morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
morph::journal::FileActionLog Class Reference

Append-only, newline-delimited-JSON IActionLog backed by a local file. More...

#include <file_action_log.hpp>

Inheritance diagram for morph::journal::FileActionLog:
morph::journal::IActionLog

Public Member Functions

 FileActionLog (::morph::exec::IExecutor &owner, std::filesystem::path path, ::morph::core::FileIoOps ioOps={})
 Opens (creating if necessary) path for appending, owned by owner.
 FileActionLog (const FileActionLog &)=delete
FileActionLog & operator= (const FileActionLog &)=delete
 FileActionLog (FileActionLog &&)=delete
FileActionLog & operator= (FileActionLog &&)=delete
void append (LogEntry entry) override
 Appends entry as one JSON line, on the owner. Buffered until flush().
void flush () override
 Flushes stdio's buffer, then fsyncs the file descriptor. On the owner.
std::vector< LogEntry > entries (std::string_view entityKey={}) const override
 Re-reads the file from disk and decodes every line. On the owner.
::morph::exec::IExecutor * owner () const noexcept override
 The executor given at construction.
void rotate (const std::filesystem::path &sealedPath)
 Seals the active file and reopens a fresh, empty one at the same path. On the owner.
::morph::async::Completion< std::vector< LogEntry > > entries (::morph::exec::IExecutor &replyExec, std::string entityKey={}) const
 Returns recorded entries in append order, read on the log's owner and delivered on replyExec.
::morph::async::Completion< bool > flush (::morph::exec::IExecutor &replyExec)
 Pushes buffered entries to the durable backend on the log's owner; the answer is delivered on replyExec.
Public Member Functions inherited from morph::journal::IActionLog
::morph::async::Completion< std::vector< LogEntry > > entries (::morph::exec::IExecutor &replyExec, std::string entityKey={}) const
 Returns recorded entries in append order, read on the log's owner and delivered on replyExec.
::morph::async::Completion< bool > flush (::morph::exec::IExecutor &replyExec)
 Pushes buffered entries to the durable backend on the log's owner; the answer is delivered on replyExec.

Protected Member Functions

::morph::async::Completion< std::vector< LogEntry > > askEntries (::morph::exec::IExecutor &replyExec, std::string entityKey) const override
 Reads the file on the owner, delivered on replyExec.
::morph::async::Completion< bool > askFlush (::morph::exec::IExecutor &replyExec) override
 Flushes on the owner, answered on replyExec.

Detailed Description

Append-only, newline-delimited-JSON IActionLog backed by a local file.

Each entry is written as one journal::toJson-encoded line. flush() flushes the C stdio buffer and then issues a real fsync (POSIX fsync / Windows _commit), so a crash immediately after flush() returns cannot lose data — this is the "local file" sink from the original request, and the natural target for SessionLog::checkpoint() at a "Save" action. Dedups append() on a non-empty LogEntry::idempotencyKey, rebuilding the seen-key set from disk at open time — see IActionLog's class docs and the constructor's docs.

Process-local seq
Like InMemoryActionLog, seq is assigned fresh per process instance — it does not resume from the highest seq already on disk if the file is reopened. Entries remain correctly ordered on disk regardless (append-only), but seq alone is not a cross-restart unique key; use entries()' natural file order for that.
One owner
The file handle and the dedup state belong to the executor given at construction, which must run one task at a time. append() runs there — at once when called on it, posted from anywhere else (a model's strand) — so appends from many threads reach the file one at a time, in the order the owner runs them. flush(), entries() and rotate() run only there; a caller elsewhere uses flush(replyExec) and entries(replyExec, key). Not safe for multiple processes to append to the same path concurrently. See docs/spec/journal/journal.md, "One owner".
Rotation
rotate() seals the active file (rename to a host-chosen path) and reopens a fresh, empty one at the original path — the seam a host uses to implement its own retention policy. See rotate()'s own docs.

Definition at line 65 of file file_action_log.hpp.

Constructor & Destructor Documentation

◆ FileActionLog()

morph::journal::FileActionLog::FileActionLog ( ::morph::exec::IExecutor & owner,
std::filesystem::path path,
::morph::core::FileIoOps ioOps = {} )
inline

Opens (creating if necessary) path for appending, owned by owner.

Also rebuilds the idempotencyKey dedup set (see append()) from whatever is already on disk at path — an O(n) scan of the existing file's contents, paid once here, not on every append(). Runs on the constructing thread, before anything else can reach the log.

Parameters
ownerThe executor every access runs on; must run one task at a time. Borrowed: it must outlive this log and run what it posts.
pathFile to append entries to.
ioOpsInjectable file-I/O primitives; defaults to the real syscalls. Test-only seam — see morph::core::FileIoOps's own docs — for forcing the failure branches that need a real OS-level I/O error to reach.
Exceptions
std::runtime_errorif the file cannot be opened.
SerializationErrorif an existing file at path has a malformed interior line (a malformed trailing line is tolerated — see entries()).

Definition at line 87 of file file_action_log.hpp.

Member Function Documentation

◆ append()

void morph::journal::FileActionLog::append ( LogEntry entry)
inlineoverridevirtual

Appends entry as one JSON line, on the owner. Buffered until flush().

The entry's idempotencyKey is only remembered as seen once flush() confirms it reached the disk (see flush()), so a write that fails is still retryable. Recording the key up front — before the bytes were durable — silently deduplicated the retry of a row that was never written, turning a transient I/O error into permanent data loss.

Called off the owner, the write is posted there and this returns at once. A posted write that fails is logged, and the next flush() throws for it: the entry did not reach the backend.

Parameters
entryEntry to append; seq is overwritten regardless of the input value.
Exceptions
std::runtime_error,onthe owner, if the write fails, or if the log has no open file (only reachable after a rotate() that failed to reopen).

Implements morph::journal::IActionLog.

Definition at line 114 of file file_action_log.hpp.

◆ flush() [1/2]

void morph::journal::FileActionLog::flush ( )
inlineoverridevirtual

Flushes stdio's buffer, then fsyncs the file descriptor. On the owner.

Throws rather than returning a status because the interface is void-returning and the failure is not one a caller may ignore: OutboxRelay::relay() calls markRelayed() immediately after this, and on a silent failure would record rows as relayed in the model's own store while nothing reached the durable sink — the rows are then gone from the outbox and absent from the log. Throwing aborts relay() before markRelayed, leaving the batch to be retried.

On failure the keys buffered since the last successful flush are forgotten, so a retry writes them again instead of being deduplicated away. A duplicated audit row is recoverable; a dropped one is not.

A write posted from another thread that failed since the last flush is reported here too, after the rest is made durable: that entry is not on disk.

Exceptions
std::runtime_errorif the buffer flush or the fsync fails, if a posted append failed since the last flush, or if the log has no open file.

Implements morph::journal::IActionLog.

Definition at line 150 of file file_action_log.hpp.

◆ entries() [1/2]

std::vector< LogEntry > morph::journal::FileActionLog::entries ( std::string_view entityKey = {}) const
inlinenodiscardoverridevirtual

Re-reads the file from disk and decodes every line. On the owner.

Reads whatever is currently on disk, including anything written but not yet flush()ed if the platform's stdio buffering has already handed it to the OS — callers that need a guaranteed-durable view should flush() first.

Parameters
entityKeyIf non-empty, restricts the result to that entity's entries.
Returns
Matching entries, in on-disk (append) order.

Implements morph::journal::IActionLog.

Definition at line 160 of file file_action_log.hpp.

◆ owner()

::morph::exec::IExecutor * morph::journal::FileActionLog::owner ( ) const
inlinenodiscardoverridevirtualnoexcept

The executor given at construction.

Returns
The owner.

Reimplemented from morph::journal::IActionLog.

Definition at line 166 of file file_action_log.hpp.

◆ rotate()

void morph::journal::FileActionLog::rotate ( const std::filesystem::path & sealedPath)
inline

Seals the active file and reopens a fresh, empty one at the same path. On the owner.

Flushes (fflush + fsync/_commit) and closes the current active file, renames it to sealedPath, then reopens a fresh, empty active file at the original path. It runs on the owner, as every append() does, so no append is ever split across the sealed and the new active file. entries() keeps reading only the (post-rotation, empty) active file; composing full history across a rotation is a host-side recipe (concatenate each sealed segment's entries() oldest-to-newest, then the active file's), not a new API.

Crash safety
The rename is a single atomic filesystem operation. A crash before it completes leaves the pre-rotation active file untouched, as if rotate() was never called; a crash after leaves the sealed file plus a freshly recreated, empty active file. Either way no line is ever torn across the two files, so the existing torn-line rule keeps applying independently per file.
Parameters
sealedPathDestination path for the sealed segment. Platform rename semantics decide what happens if a filesystem entry already exists there (POSIX silently replaces it) — pass a path that does not collide with an existing segment.
Exceptions
std::runtime_errorif the pre-rotation flush/fsync fails (raised before anything is closed or renamed, so no segment is ever sealed around entries that never reached the disk); if renaming to sealedPath fails — in that case the original active file is reopened in place (still holding every entry recorded before the call, so no data is lost; the rotation simply did not happen) — or if reopening the active path afterward fails outright (only possible if the rename itself succeeded and the original path's directory then became unwritable). After that last case the log has no open file: append(), flush(), and a further rotate() all throw with that diagnosis rather than dereferencing a null handle, and destruction is still safe.

Definition at line 204 of file file_action_log.hpp.

◆ askEntries()

::morph::async::Completion< std::vector< LogEntry > > morph::journal::FileActionLog::askEntries ( ::morph::exec::IExecutor & replyExec,
std::string entityKey ) const
inlinenodiscardoverrideprotectedvirtual

Reads the file on the owner, delivered on replyExec.

Parameters
replyExecWhere the answer is delivered.
entityKeyIf non-empty, restricts the result to that entity's entries.
Returns
The matching entries, or the read's error.

Reimplemented from morph::journal::IActionLog.

Definition at line 211 of file file_action_log.hpp.

◆ askFlush()

::morph::async::Completion< bool > morph::journal::FileActionLog::askFlush ( ::morph::exec::IExecutor & replyExec)
inlinenodiscardoverrideprotectedvirtual

Flushes on the owner, answered on replyExec.

Parameters
replyExecWhere the answer is delivered.
Returns
true once durable, or the flush's error.

Reimplemented from morph::journal::IActionLog.

Definition at line 221 of file file_action_log.hpp.

◆ entries() [2/2]

::morph::async::Completion< std::vector< LogEntry > > morph::journal::IActionLog::entries ( ::morph::exec::IExecutor & replyExec,
std::string entityKey = {} ) const
inlinenodiscard

Returns recorded entries in append order, read on the log's owner and delivered on replyExec.

The form for a caller that is not on the owner: a model's strand, an application thread the log does not belong to. The read is ordered after every append() posted to the owner before it.

Parameters
replyExecWhere the answer is delivered, and where the caller attaches to it. Borrowed: it must outlive the completion.
entityKeyIf non-empty, restricts the result to that entity's entries.
Returns
The matching entries, in append order.

Definition at line 231 of file action_log.hpp.

◆ flush() [2/2]

Pushes buffered entries to the durable backend on the log's owner; the answer is delivered on replyExec.

The form of flush() for a caller that is not on the owner. It is ordered after every append() posted to the owner before it.

Parameters
replyExecWhere the answer is delivered. Borrowed: it must outlive the completion.
Returns
true once the entries are durable; rejected with the error flush() would have thrown.

Definition at line 245 of file action_log.hpp.


The documentation for this class was generated from the following file: