aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-09 22:25:53 +0000
committertv <tv@krebsco.de>2026-11-11 23:28:59 +0100
commit2a4316f4a256012120337064df150944880b6453 (patch)
tree00d607060c3947b37e01a60ca752d7f1cda0b60b
parent925f566990fdd4043df691b3277a2a50b937ec1b (diff)
document file: undo the clean stop's mark before deciding the actor
At start, a node on a document file appends an Opened record before it writes anything, so that a Closed record before it no longer says the file holds every change of the actor it names. It does so now before deciding which actor to write as, and DocumentFile::open says whether it could: where the append fails (the disk full, permissions), the file still ends with Closed, and later appends likely fail too. A crash, or a clean stop whose last append failed (close then sees the file behind and writes no mark), would leave that Closed standing for a run the file never got. So with --keep-actor, the node writes on as the actor of before only where the file was marked as opened; else it writes as a fresh actor and warns.
-rw-r--r--docs/lifecycle.md5
-rw-r--r--src/document/file.rs20
-rw-r--r--src/main.rs32
3 files changed, 34 insertions, 23 deletions
diff --git a/docs/lifecycle.md b/docs/lifecycle.md
index e09ef01..d703fe3 100644
--- a/docs/lifecycle.md
+++ b/docs/lifecycle.md
@@ -93,8 +93,9 @@ document file holding a document is an error with no override.
by a crash is dropped when reading. A node starting from it is ready at once; sync
merges what it missed, or it rejoins a newer epoch. A node writes as a fresh actor
each start, unless `--keep-actor` and it stopped cleanly (the file marks a clean
- stop, and the mark is undone as the node starts on it): then it writes on as the actor
- of before, as nothing it wrote is missing. Fewer actors mean fewer signatures in every
+ stop, and the mark is undone as the node starts on it, before it decides its actor):
+ then it writes on as the actor of before, as nothing it wrote is missing. Where the
+ mark can't be undone (the disk full, say), it writes as a fresh one. Fewer actors mean fewer signatures in every
transfer of the epoch's history (a writer each); a crash, a drop, or a switch or a
rejoin still starts a fresh one.
* **A backup** (`GET /admin/document`) is the whole document with its history, naming
diff --git a/src/document/file.rs b/src/document/file.rs
index 56114f5..1a33918 100644
--- a/src/document/file.rs
+++ b/src/document/file.rs
@@ -399,16 +399,22 @@ impl DocumentFile {
/// Marks the file as opened (`Record::Opened`), at start, before the node writes:
/// a `Closed` before it no longer says the file holds everything (a crash from here
- /// on leaves it without one). Nothing where there is no file yet.
- pub(crate) fn open(&self) {
+ /// on leaves it without one). Nothing where there is no file yet. Whether the file
+ /// now ends without a `Closed`: where the mark couldn't be appended, it still says
+ /// the run of before stopped cleanly, and the node must not write on as its actor.
+ pub(crate) fn open(&self) -> bool {
if !self.path.exists() {
- return;
+ return true;
}
- if let Err(error) =
- self.files
- .append(FileKind::Document, &self.path, &record(Record::Opened, &[]))
+ match self
+ .files
+ .append(FileKind::Document, &self.path, &record(Record::Opened, &[]))
{
- error!(path = %self.path.display(), error = %error, "Couldn't mark the document file as opened");
+ Ok(()) => true,
+ Err(error) => {
+ error!(path = %self.path.display(), error = %error, "Couldn't mark the document file as opened");
+ false
+ }
}
}
diff --git a/src/main.rs b/src/main.rs
index 7c819e5..d8704a0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -184,13 +184,30 @@ async fn main() -> Result<()> {
None => (Document::new(), None, None, Vec::new(), Vec::new(), None),
};
let mut initial_doc = initial_doc;
+ let document_file = args.document_file.clone().map(|path| {
+ Arc::new(document::file::DocumentFile::new(
+ path,
+ Arc::clone(&files),
+ args.document_append,
+ args.document_rewrite,
+ ))
+ });
+ // A clean stop's mark undone before anything is written, and before deciding the
+ // actor: a crash from here on leaves a file that doesn't say so. A file that still
+ // says so (the mark not undone) gets a fresh actor.
+ let opened = document_file.as_ref().is_none_or(|file| file.open());
// Writing on as the actor of before, where this node stopped cleanly: everything it
// wrote as that one is in the document, so no change of it comes twice.
- match resumable.filter(|_| args.keep_actor) {
+ match resumable.filter(|_| args.keep_actor && opened) {
Some(actor) => {
info!(actor = %actor, "Writing on as the actor of before (--keep-actor)");
initial_doc.set_actor(actor);
}
+ None if args.keep_actor && !opened => {
+ warn!(
+ "Writing as a fresh actor: the document file couldn't be marked as opened (--keep-actor)"
+ );
+ }
None if args.keep_actor && starting_epoch.is_some() => {
info!("Writing as a fresh actor: this node didn't stop cleanly (--keep-actor)");
}
@@ -239,19 +256,6 @@ async fn main() -> Result<()> {
let mut transports = transports.into_iter();
let (scheme, transport) = transports.next().context("No transports")?;
- let document_file = args.document_file.clone().map(|path| {
- Arc::new(document::file::DocumentFile::new(
- path,
- Arc::clone(&files),
- args.document_append,
- args.document_rewrite,
- ))
- });
- // A clean stop's mark undone before anything is written: a crash from here on
- // leaves a file that doesn't say so.
- if let Some(file) = &document_file {
- file.open();
- }
let node = node::Node::new(node::NodeConfig {
transport,
identity: Some(transport::identity::IdentityConfig {