diff --git a/src/state.rs b/src/state.rs index d50656b..8bda04e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -80,6 +80,21 @@ pub(crate) struct Args { /// Durable daemon state persisted to the state file. /// /// Runtime-only state (like `TrustEngine`) lives in `daemon::run()`. +/// +/// # Notify-before-persist contract +/// +/// Mutating paths notify subscribers via `notify_change()` *before* persisting +/// to disk, so subscribers (e.g. the manager, gossip loop) can begin work +/// without waiting on the disk write, and a crash before `save()` only leaves +/// slightly stale state that converges on the next gossip round. +/// `set_adhoc_membership` follows this: it notifies inline and relies on its +/// caller (`persist_membership`) to `save()`. +/// +/// `update_messages` is the documented exception: it saves *before* notifying, +/// because the manager always follows it with `trust_engine.update()` as a +/// separate step. Notifying first would let the woken gossip loop run during +/// the `save().await` yield and read stale derivation. See `update_messages` +/// for details. pub(crate) struct State { pub(crate) keypair: ImidKeypair, pub(crate) imid: Imid, @@ -258,7 +273,8 @@ impl State { self.mutable.lock().await.adhoc_membership.clone() } - /// Sets the adhoc membership. Call `save()` to persist. + /// Sets the adhoc membership. Notifies subscribers first; caller should + /// `save()` to persist. See the notify-before-persist contract on `State`. pub(crate) async fn set_adhoc_membership(&self, membership: Option) { { let mut mutable = self.mutable.lock().await; @@ -284,6 +300,8 @@ impl State { } /// Apply removals and additions as one observed message set update. + /// Saves before notifying — the documented exception to the + /// notify-before-persist contract on `State` (see inline comment for why). pub(crate) async fn update_messages(&self, update: MessageUpdate) -> Result<()> { let mut mutable = self.mutable.lock().await; let generation = mutable.messages.generation(); @@ -294,6 +312,16 @@ impl State { } assert!(new_generation > generation); drop(mutable); + // Persist before notifying. Unlike `set_adhoc_membership` (which + // notifies first and lets its caller save), `update_messages` is always + // followed by `trust_engine.update()` in the manager as a separate + // step. If we notified first, the `save().await` below would yield to + // the already-woken gossip loop before the manager could update the + // derivation — gossip would read stale derivation and contact the wrong + // peer set. Saving first keeps gossip asleep until after the yield + // point, so by the time it wakes the manager can promptly update the + // trust engine. See the notify-before-persist contract on `State` for + // why this path diverges. self.save().await?; self.notify_change(); Ok(())