diff --git a/src/daemon.rs b/src/daemon.rs index 0e701b6..33e373a 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -361,6 +361,22 @@ fn is_missing_working_dir_error(error: &AutterError) -> bool { } } +/// Returns true when the error comes from a full disk on the host (ENOSPC on +/// Unix, ERROR_DISK_FULL on Windows). That is an environment condition on the +/// user's machine, not a daemon fault, so side-effect failures with this cause +/// log at warn level and do not become error-tracking exceptions. +fn is_storage_full_error(error: &AutterError) -> bool { + matches!(error, AutterError::IoError(io_error) if io_error.kind() == std::io::ErrorKind::StorageFull) +} + +fn log_side_effect_error(error: &AutterError, family: &str, seq: u64, message: &str) { + if is_storage_full_error(error) { + tracing::warn!(%error, %family, seq, "{message}: host disk is full"); + } else { + tracing::error!(%error, %family, seq, "{message}"); + } +} + /// Returns true when a side effect failed because its repository was deleted /// mid-operation. git reports this race in more shapes than /// `is_missing_working_dir_error` knows (for example `git diff` exits 1 with @@ -6100,11 +6116,11 @@ impl ActorDaemonCoordinator { Ok(Ok((applied, side_effect_result))) => { if let Err(error) = &side_effect_result { let _ = self.record_side_effect_error(family, order, error); - tracing::error!( - %error, - %family, - seq = applied.seq, - "command side effect failed" + log_side_effect_error( + error, + family, + applied.seq, + "command side effect failed", ); } if let Err(error) = self.append_command_completion_log( @@ -7864,11 +7880,11 @@ impl ActorDaemonCoordinator { let _ = self.end_family_effect(&family); if let Err(error) = result { let _ = self.record_side_effect_error(&family, applied.seq, &error); - tracing::error!( - %error, - %family, - seq = applied.seq, - "async side-effect error" + log_side_effect_error( + &error, + &family, + applied.seq, + "async side-effect error", ); } else if let Err(error) = self.append_command_completion_log(&family, &applied, &Ok(()), applied.seq) @@ -9448,6 +9464,29 @@ mod tests { ))); } + #[test] + fn storage_full_error_detects_disk_full_io_errors() { + let disk_full = AutterError::IoError(std::io::Error::new( + std::io::ErrorKind::StorageFull, + "No space left on device (os error 28)", + )); + assert!(is_storage_full_error(&disk_full)); + + #[cfg(unix)] + assert!(is_storage_full_error(&AutterError::IoError( + std::io::Error::from_raw_os_error(28) + ))); + + let permission_denied = AutterError::IoError(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "access denied", + )); + assert!(!is_storage_full_error(&permission_denied)); + assert!(!is_storage_full_error(&AutterError::Generic( + "No space left on device".to_string() + ))); + } + fn init_fake_repo(worktree: &Path) { fs::create_dir_all(worktree.join(".git").join("objects")).unwrap(); fs::write(worktree.join(".git").join("HEAD"), "ref: refs/heads/main\n").unwrap();