diff --git a/src/cortex-storage/src/sessions/storage.rs b/src/cortex-storage/src/sessions/storage.rs index dd750b08..7f3d38cb 100644 --- a/src/cortex-storage/src/sessions/storage.rs +++ b/src/cortex-storage/src/sessions/storage.rs @@ -124,20 +124,67 @@ impl SessionStorage { } /// Save a session to disk. + /// + /// This function ensures data durability by calling sync_all() (fsync) + /// after writing to prevent data loss on crash or forceful termination. pub async fn save_session(&self, session: &StoredSession) -> Result<()> { let path = self.paths.session_path(&session.id); let content = serde_json::to_string_pretty(session)?; - fs::write(&path, content).await?; + + // Write content to file + let file = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&path) + .await?; + + use tokio::io::AsyncWriteExt; + let mut file = file; + file.write_all(content.as_bytes()).await?; + file.flush().await?; + + // Ensure data is durably written to disk (fsync) to prevent data loss on crash + file.sync_all().await?; + + // Sync parent directory on Unix for crash safety (ensures directory entry is persisted) + #[cfg(unix)] + { + if let Some(parent) = path.parent() { + if let Ok(dir) = fs::File::open(parent).await { + let _ = dir.sync_all().await; + } + } + } + debug!(session_id = %session.id, "Session saved"); Ok(()) } /// Save a session synchronously. + /// + /// This function ensures data durability by calling sync_all() (fsync) + /// after writing to prevent data loss on crash or forceful termination. pub fn save_session_sync(&self, session: &StoredSession) -> Result<()> { let path = self.paths.session_path(&session.id); let file = std::fs::File::create(&path)?; - let writer = BufWriter::new(file); - serde_json::to_writer_pretty(writer, session)?; + let mut writer = BufWriter::new(file); + serde_json::to_writer_pretty(&mut writer, session)?; + writer.flush()?; + + // Ensure data is durably written to disk (fsync) to prevent data loss on crash + writer.get_ref().sync_all()?; + + // Sync parent directory on Unix for crash safety (ensures directory entry is persisted) + #[cfg(unix)] + { + if let Some(parent) = path.parent() { + if let Ok(dir) = std::fs::File::open(parent) { + let _ = dir.sync_all(); + } + } + } + debug!(session_id = %session.id, "Session saved"); Ok(()) } diff --git a/src/cortex-tui/src/session/storage.rs b/src/cortex-tui/src/session/storage.rs index 7e1621ee..39479687 100644 --- a/src/cortex-tui/src/session/storage.rs +++ b/src/cortex-tui/src/session/storage.rs @@ -87,6 +87,9 @@ impl SessionStorage { // ======================================================================== /// Saves session metadata. + /// + /// Uses atomic write (temp file + rename) with fsync for durability. + /// This prevents data loss on crash or forceful termination. pub fn save_meta(&self, meta: &SessionMeta) -> Result<()> { self.ensure_session_dir(&meta.id)?; @@ -94,13 +97,37 @@ impl SessionStorage { let content = serde_json::to_string_pretty(meta).context("Failed to serialize session metadata")?; - // Atomic write: write to temp file then rename + // Atomic write: write to temp file, fsync, then rename let temp_path = path.with_extension("json.tmp"); - fs::write(&temp_path, &content) + + // Write and sync temp file + let file = File::create(&temp_path) + .with_context(|| format!("Failed to create temp metadata file: {:?}", temp_path))?; + let mut writer = BufWriter::new(file); + writer + .write_all(content.as_bytes()) .with_context(|| format!("Failed to write temp metadata file: {:?}", temp_path))?; + writer.flush()?; + + // Ensure data is durably written to disk (fsync) before rename + writer.get_ref().sync_all().with_context(|| { + format!("Failed to sync temp metadata file to disk: {:?}", temp_path) + })?; + + // Rename temp file to final path fs::rename(&temp_path, &path) .with_context(|| format!("Failed to rename metadata file: {:?}", path))?; + // Sync parent directory on Unix for crash safety (ensures directory entry is persisted) + #[cfg(unix)] + { + if let Some(parent) = path.parent() { + if let Ok(dir) = File::open(parent) { + let _ = dir.sync_all(); + } + } + } + Ok(()) } @@ -212,6 +239,16 @@ impl SessionStorage { fs::rename(&temp_path, &path) .with_context(|| format!("Failed to rename history file: {:?}", path))?; + // Sync parent directory on Unix for crash safety (ensures directory entry is persisted) + #[cfg(unix)] + { + if let Some(parent) = path.parent() { + if let Ok(dir) = File::open(parent) { + let _ = dir.sync_all(); + } + } + } + Ok(()) }