fix(persistence): FlushAndAck is a durability barrier that reports lost writes
The persistence actor acked FlushAndAck unconditionally after writing its pending merged notification: nothing was fsynced and every storage failure was only warn-logged, so persist_ack, the reconnect flush and history repair treated lost writes as persisted. The barrier now writes the pending notification, reports the first storage failure latched since the last barrier (every actor arm latches), and fsyncs the whole session directory tree (files, directories, deletions). Session directories are created durably (init and fork copy), and the shared summary writer syncs its staged bytes before publishing and the parent afterwards, so a concurrent /rename cannot publish unsynced bytes over a synced summary.json. FlushComplete and the persist_ack, history repair and end-of-turn callers propagate the io::Result; CopyFile fails on a sync error but keeps the latch for the barrier. Ported from upstream 1.0.1x (grok-build persistence.rs pending_write_error + durable storage helpers), reshaped without the dirty-file set. Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
This commit is contained in:
1 parent
5f7416f3ef
commit
63d26d4a3e
11 files changed
+413
-106
No files matched your search
@@ -548,17 +548,10 @@ impl SessionActor {
|
||||
|
||||
if report.changed() && !dry_run {
|
||||
// Flush barrier: success must mean the rewrite is on disk.
|
||||
let (flush_tx, flush_rx) = tokio::sync::oneshot::channel();
|
||||
if self
|
||||
.notifications
|
||||
.persistence_tx
|
||||
.send(PersistenceMsg::FlushAndAck {
|
||||
respond_to: flush_tx,
|
||||
})
|
||||
.is_err()
|
||||
|| flush_rx.await.is_err()
|
||||
{
|
||||
anyhow::bail!("history repaired in memory but the persistence flush failed");
|
||||
if let Err(error) = self.persistence_barrier().await {
|
||||
anyhow::bail!(
|
||||
"history repaired in memory but the persistence flush failed: {error}"
|
||||
);
|
||||
}
|
||||
tracing::warn!(
|
||||
session_id = %self.session_info.id.0,
|
||||
|
||||
@@ -681,22 +681,14 @@ impl SessionActor {
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
let (flush_tx, flush_rx) = oneshot::channel();
|
||||
if self
|
||||
.notifications
|
||||
.persistence_tx
|
||||
.send(PersistenceMsg::FlushAndAck {
|
||||
respond_to: flush_tx,
|
||||
})
|
||||
.is_ok()
|
||||
&& flush_rx.await.is_ok()
|
||||
{
|
||||
let _ = ack.send(());
|
||||
} else {
|
||||
tracing::error!(
|
||||
session_id = % self.session_info.id.0, prompt_id = %
|
||||
prompt_id, "persist_ack flush barrier failed"
|
||||
);
|
||||
match self.persistence_barrier().await {
|
||||
Ok(()) => {
|
||||
let _ = ack.send(());
|
||||
}
|
||||
Err(error) => tracing::error!(
|
||||
session_id = % self.session_info.id.0, prompt_id = % prompt_id,
|
||||
%error, "persist_ack flush barrier failed"
|
||||
),
|
||||
}
|
||||
} else {
|
||||
tracing::error!(
|
||||
|
||||
@@ -333,15 +333,24 @@ impl SessionActor {
|
||||
if let Err(e) = crate::session::replay_events::flush_replay_actor(&self.event_tx).await {
|
||||
tracing::warn!(?e, "flush_replay_actor failed");
|
||||
}
|
||||
if let Err(error) = self.persistence_barrier().await {
|
||||
tracing::error!(%error, "persistence flush failed");
|
||||
}
|
||||
}
|
||||
/// The `FlushAndAck` durability barrier: `Ok` only once every prior
|
||||
/// persistence write is on stable media.
|
||||
pub(super) async fn persistence_barrier(&self) -> std::io::Result<()> {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
if self
|
||||
.notifications
|
||||
.persistence_tx
|
||||
.send(PersistenceMsg::FlushAndAck { respond_to: tx })
|
||||
.is_ok()
|
||||
.is_err()
|
||||
{
|
||||
let _ = rx.await;
|
||||
return Err(std::io::Error::other("persistence actor gone"));
|
||||
}
|
||||
rx.await
|
||||
.unwrap_or_else(|_| Err(std::io::Error::other("persistence actor gone")))
|
||||
}
|
||||
/// Extracts the update type name and relevant parameters for logging
|
||||
fn extract_update_info(
|
||||
|
||||
Reference in new issue
Block a user