From 7a5a7408179c5fe9943bd2ce60295e86f1afaa40 Mon Sep 17 00:00:00 2001 From: tv Date: Sun, 1 Nov 2026 13:19:04 +0100 Subject: document: reject writes during rejoin --- src/state/mod.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'src/state/mod.rs') diff --git a/src/state/mod.rs b/src/state/mod.rs index d8bdac5..9ee08b7 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -38,7 +38,7 @@ use tracing::{debug, error, warn}; use crate::document::lock::DocumentLock; use crate::document::replica::{Epoch, Stamp}; -use crate::document::store::ChangeSet; +use crate::document::store::{ChangeSet, StoreError}; use crate::document::{Document, Transaction}; use crate::locks::MutexExt; use crate::remake::{Host, RemakeState}; @@ -393,20 +393,26 @@ impl AppState { /// Mutates the document on the blocking pool as one change, then /// publishes it. Write and notification run in the same blocking task, so a - /// cancelled request can't leave a committed write unannounced. + /// cancelled request can't leave a committed write unannounced. Refused + /// (`StoreError::Rejoining`, `f` not called) while the document isn't writable. pub(crate) async fn mutate_doc( &self, f: impl FnOnce(&mut Transaction<'_>) -> Result + Send + 'static, ) -> Result where T: Send + 'static, - E: Send + 'static, + E: From + Send + 'static, { let state = self.clone(); tokio::task::spawn_blocking(move || { let result = { let mut replica = state.doc.blocking_write(); + // Checked here, under the lock: the caller's readiness check (if any) + // was before a rejoin could swap in a fresh document. + if !replica.writable { + return Err(E::from(StoreError::Rejoining)); + } // One API write = one change; on error, its partial writes (e.g. the old // value deleted, the new one not written) are dropped. let result = replica.transact(state.clock.now_millis(), f); -- cgit v1.3.1