pub struct Journal { /* private fields */ }Expand description
Where a change set’s write-ahead journal lives — and, because they must agree about it, both halves of the protocol that depends on the answer.
The name is a single transient dotfile, by default in the root: it exists
only between a change set’s commit point and its completion, so in steady
state the tree carries no journal at all, and no dotfolder is spawned to
hold one. It survives a crash solely so Journal::recover can find it,
and is removed the moment recovery (or a clean apply) finishes.
§Why this is a type and not a parameter
apply and recover have to name the
same file. If they disagree, nothing fails loudly — recovery simply looks
where no journal is and reports Recovered::Nothing, leaving an
interrupted change half-applied forever. A caller that holds one Journal
and uses it for both cannot make that mistake, which is why the two
operations live on the value rather than taking the name separately.
ChangeSet::apply and the free
recover are shorthands for Journal::default(); reach for a named one
when the default would collide with something the tree already means, or
when an existing deployment already writes a journal under its own name.
let journal = Journal::named(".myapp-journal")?;
assert_eq!(journal.name(), ".myapp-journal");
// Not a single path component — refused rather than escaping the root.
assert!(Journal::named("../elsewhere").is_err());§A journal outside the tree
By default the journal lands in the root it applies to, which is right for
a tree only this machine writes. It is wrong for a tree something syncs —
an iCloud or Dropbox folder — because the journal is this process’s crash
state, and a sync service cannot tell it from content: the file travels to
machines that never crashed, where a recovery would replay another
machine’s intent against a tree that may have moved on, and an apply
would refuse a “stale” journal no local change left behind.
kept_in is the fix: the journal lives in a directory
the caller owns and nothing syncs (an application-support or cache
directory), and the tree itself never holds a journal at all, transiently
or otherwise.
Two obligations come with a homed journal, both the caller’s. The home
must be absolute — a relative one would resolve against whatever the
process’s current directory happens to be, and a journal written from one
directory and sought from another is exactly the stranding this type
exists to prevent; kept_in refuses anything else. And the pairing of
home and root is not recorded anywhere: the journal does not know which
tree it belongs to, so recovering it against a different root replays
intent against the wrong tree. One home directory, one root, one name —
a caller with several roots keeps several names.
Implementations§
Source§impl Journal
impl Journal
Sourcepub async fn apply<FS: Storage>(
&self,
changes: &ChangeSet,
fs: &FS,
root: &Path,
) -> Result<()>
pub async fn apply<FS: Storage>( &self, changes: &ChangeSet, fs: &FS, root: &Path, ) -> Result<()>
Execute every op changes staged against fs, rooted at root, as one
unit — crash-atomically, behind this write-ahead journal.
The set’s intent is journaled and flushed before any document is
touched (see crate::journal); that flush is the commit point. From
there the ops run in order, each recording how to undo itself:
- On success, everything the set dirtied is flushed durable —
barriers capped by one drain — and only then is the journal removed:
Okmeans the change survives a power cut, not merely that it happened. - On an error (a full disk, a permission fault), every op already applied is unwound in reverse, the restored state is flushed, and the journal is durably cleared — the mutation aborts as if it never began, and a power cut cannot contradict the abort by resurrecting the journal for recovery to roll forward.
- On a crash (a
kill -9, a power cut) there is no error to catch and no chance to unwind, so the journal simply survives; the nextcrate::journal::recoverrolls the set forward to its fully-applied state. An interrupted change set is therefore always resolved to a consistent tree — fully before it on a caught error, fully after it on a crash.
A set of one op skips the journal entirely: a single op is already
indivisible on a backend claiming atomic_replace, so there is no
multi-file window for a journal to close, and a crash leaves the op either
wholly done or wholly not — the same two states a recovered set lands on.
It is the ordinary shape of a save, and it costs one file operation rather
than four. What it does not skip is durability: a lone rename or remove
still flushes the entries it edited before Ok, on the same promise a
journaled set keeps.
The rare exception is a rollback that itself fails (Error::Torn):
the pre-change state could not be restored, so — rather than leave an
unknown one — the journal is kept, and recovery will later roll the set
forward to the consistent applied state. Either way the tree lands on
a state this crate can name.
Takes fs/root rather than a higher-level object so a bootstrap
that must write two files before the tree exists can still land them
together.
Whatever recovers an interruption of this call must name the same
journal — see Journal for why the two operations live together.
Source§impl Journal
impl Journal
Sourcepub const DEFAULT_NAME: &'static str = ".fstx-journal"
pub const DEFAULT_NAME: &'static str = ".fstx-journal"
The name used when none is given: a dotted, crate-namespaced file unlikely to collide with anything the tree itself means.
Sourcepub fn named(name: impl Into<Cow<'static, str>>) -> Result<Self>
pub fn named(name: impl Into<Cow<'static, str>>) -> Result<Self>
A journal under name, which must be a single path component — not
empty, not . or .., and containing no separator.
The check is what keeps the name from being an escape hatch: it is
joined onto a caller-supplied root, and a name like ../../elsewhere
would write outside the very tree
ChangeSet::apply clamps every staged op
into.
Sourcepub fn kept_in(self, home: impl Into<PathBuf>) -> Result<Self>
pub fn kept_in(self, home: impl Into<PathBuf>) -> Result<Self>
This journal, kept in home instead of in the root it applies to.
For trees something syncs — see the type docs for why the journal must not live where a sync service can carry it to another machine, and for the two obligations (an absolute home, and a stable home–root pairing) that come with taking this.
home is a directory; the journal keeps its name
inside it. A relative home is refused
(Error::InvalidJournalHome): it would resolve against the process’s
current directory, which apply and a later recovery have no reason to
share.
let journal = Journal::named(".myapp-journal")?
.kept_in("/var/lib/myapp/journals")?;
assert!(Journal::default().kept_in("not/absolute").is_err());Sourcepub fn path_in(&self, root: &Path) -> PathBuf
pub fn path_in(&self, root: &Path) -> PathBuf
Where this journal lives when applying to root: its
home if it has one, otherwise the root itself.
Sourcepub fn owns_path(&self, path: &Path) -> bool
pub fn owns_path(&self, path: &Path) -> bool
Whether path names this journal, or the staging sibling
Storage::write_atomic publishes it through.
A containment test on the file name rather than an equality one, so
that both the journal and its transient write_atomic temporary are
recognized. A homed journal additionally requires
the path to sit in its home: the homed design’s whole point is that no
file in the root is this journal, so a same-named file there — synced
in, or simply a coincidence — must not be claimed. Used by a
fault-injecting backend to leave the journal’s own writes alone and
fail only the file writes it means to.
Source§impl Journal
impl Journal
Sourcepub async fn recover<FS: Storage>(
&self,
fs: &FS,
root: &Path,
) -> Result<Recovered>
pub async fn recover<FS: Storage>( &self, fs: &FS, root: &Path, ) -> Result<Recovered>
Finish any change set a crash left journaled at root, rolling the tree
forward to the fully-applied state, then remove the journal.
The recovery entry point: run it before anything reads the tree, so an interrupted change heals first. A no-op when no journal is present, so it is cheap to call unconditionally. Replay is idempotent — a write already landed is simply rewritten, a rename already done is recognized and skipped — so recovering the same journal twice (a crash during recovery) is safe.
Must name the same journal the interrupted apply
wrote. A different one finds nothing and reports
Recovered::Nothing, which is why both live on this value.