aboutsummaryrefslogtreecommitdiffstats
path: root/src/swarm
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-09 20:29:32 +0000
committertv <tv@krebsco.de>2026-11-09 22:20:00 +0100
commit152890f321075beede075314d37ff6451604c359 (patch)
tree352bbdeabea55735d9bafe78cba24b58a1182c2f /src/swarm
parent5f7e29cec4333715346b4bf3195633db93449d9d (diff)
document file: a clean stop marked, and `--keep-actor` writes on as the same actor
A node writes as a fresh Automerge actor at each start, so no change number of an actor comes twice: one sent before a crash but not yet appended to the document file would otherwise be made again, differently. An epoch thus gathers a writer per start, and every transfer of its history carries a signature per writer. The document file now says when it holds everything the node wrote: * at a clean stop, after the last append, an empty `Closed` record, only where the file is up to the document's heads (ready, not rejoining); * as a node starts on the file, before anything is written, an empty `Opened` record, so a crash from then on leaves a file without the mark; * the header names the actor the node writes with (`current`). With `--keep-actor` (off by default; needs `--document-file`), a node starting on a file that ends with `Closed` writes on as that actor. A crash, a drop, a switch or a rejoin still starts a fresh one, so an epoch's writers come to one per node plus one per such event. lifecycle.md says so.
Diffstat (limited to 'src/swarm')
-rw-r--r--src/swarm/bootstrap.rs10
-rw-r--r--src/swarm/document_file.rs14
2 files changed, 22 insertions, 2 deletions
diff --git a/src/swarm/bootstrap.rs b/src/swarm/bootstrap.rs
index d2a593b..5c3b5ef 100644
--- a/src/swarm/bootstrap.rs
+++ b/src/swarm/bootstrap.rs
@@ -266,6 +266,9 @@ pub(crate) struct StartingDocument {
pub(crate) earlier_actors: Vec<ActorId>,
/// The signatures of its changes (from the document file).
pub(crate) signatures: Vec<(crate::document::ChangeHash, [u8; 64])>,
+ /// The actor this node wrote with, where it stopped cleanly (the document file's
+ /// `resumable`): it may write on as that one (`--keep-actor`).
+ pub(crate) resumable: Option<ActorId>,
}
/// What `--recover-from` and `--document-file` say about where to start.
@@ -314,7 +317,7 @@ pub(crate) fn starting_document(
Some(path) => Some(load_backup(path, files)?),
};
- let (doc, origin, earlier_actors, signatures) = match (backup, from_file) {
+ let (doc, origin, earlier_actors, signatures, resumable) = match (backup, from_file) {
(Some(_), Some(_)) if !force.allows("replaceDocumentFile") => {
bail!(
"recover-from would replace the document in the document file; add --force replaceDocumentFile to recover the backup anyway"
@@ -326,13 +329,14 @@ pub(crate) fn starting_document(
"Recovering the backup instead of the document file's document (--force replaceDocumentFile)"
);
}
- (backup, Origin::Backup, Vec::new(), Vec::new())
+ (backup, Origin::Backup, Vec::new(), Vec::new(), None)
}
(None, Some(loaded)) => (
loaded.doc,
Origin::DocumentFile,
loaded.actors,
loaded.signatures,
+ loaded.resumable,
),
(None, None) => return Ok(None),
};
@@ -343,6 +347,7 @@ pub(crate) fn starting_document(
}
let mut starting = identify(doc, origin, earlier_actors)?;
starting.signatures = signatures;
+ starting.resumable = resumable;
if let Some(remembered) = options.remembered.filter(|id| *id != starting.swarm_id) {
let what = origin.describe();
@@ -379,6 +384,7 @@ fn identify(
origin,
earlier_actors,
signatures: Vec::new(),
+ resumable: None,
})
}
diff --git a/src/swarm/document_file.rs b/src/swarm/document_file.rs
index e066e27..4c7843d 100644
--- a/src/swarm/document_file.rs
+++ b/src/swarm/document_file.rs
@@ -25,6 +25,20 @@ pub(crate) async fn run(state: AppState) {
}
}
+/// At a clean stop, after the last `flush`: the file marked as holding everything this
+/// node wrote (`DocumentFile::close`), where it does (ready, not rejoining, up to the
+/// document's heads), so a restart may write on as the same actor (`--keep-actor`).
+pub(crate) async fn close(state: &AppState) {
+ let Some(file) = state.document_file.clone() else {
+ return;
+ };
+ if state.remake.is_rejoining() || !state.is_ready().await {
+ return;
+ }
+ let heads = state.doc.read().await.doc.get_heads();
+ file.close(&heads);
+}
+
/// Writes what is due now (`DocumentFile::due`). Also called at shutdown.
pub(crate) async fn flush(state: &AppState) {
let Some(file) = state.document_file.clone() else {