diff --git a/src/filetail/supervisor/io.rs b/src/filetail/supervisor/io.rs index 3b481ae0..2bd4bb33 100644 --- a/src/filetail/supervisor/io.rs +++ b/src/filetail/supervisor/io.rs @@ -43,7 +43,7 @@ pub(crate) async fn open_tail_file( let mut file = open_validated_tail_file(&source.path).await?; let metadata = file.metadata().await?; let identity = FileIdentity::from_metadata(&metadata); - let fingerprint = file_prefix_fingerprint(&mut file).await?; + let fingerprint = file_prefix_fingerprint(&mut file, FILE_TAIL_FINGERPRINT_BYTES).await?; let checkpoint_matches = source.checkpoint_dev == Some(identity.dev) && source.checkpoint_ino == Some(identity.ino) && source @@ -89,8 +89,12 @@ pub(crate) async fn reopen_if_rotated_or_truncated( } if position > 0 { let mut file = open_validated_tail_file(&source.path).await?; - let current_fingerprint = file_prefix_fingerprint(&mut file).await?; - if current_fingerprint != fingerprint { + // Compare only the baseline bytes. Reading farther after a short file grows + // would misclassify an ordinary append as same-inode replacement, but keep + // the full current prefix so a real replacement gets a fresh baseline. + let current_fingerprint = + file_prefix_fingerprint(&mut file, FILE_TAIL_FINGERPRINT_BYTES).await?; + if !current_fingerprint.starts_with(fingerprint) { let metadata = file.metadata().await?; file.seek(std::io::SeekFrom::Start(0)).await?; return Ok(Some(OpenedTailFile { @@ -121,7 +125,7 @@ pub(crate) async fn path_identity_changed( async fn reopen_from_start(source: &FileTailSource) -> Result { let mut file = open_validated_tail_file(&source.path).await?; let metadata = file.metadata().await?; - let fingerprint = file_prefix_fingerprint(&mut file).await?; + let fingerprint = file_prefix_fingerprint(&mut file, FILE_TAIL_FINGERPRINT_BYTES).await?; file.seek(std::io::SeekFrom::Start(0)).await?; Ok(OpenedTailFile { file, @@ -131,8 +135,11 @@ async fn reopen_from_start(source: &FileTailSource) -> Result { }) } -async fn file_prefix_fingerprint(file: &mut tokio::fs::File) -> std::io::Result> { - let mut buf = vec![0; FILE_TAIL_FINGERPRINT_BYTES]; +async fn file_prefix_fingerprint( + file: &mut tokio::fs::File, + limit: usize, +) -> std::io::Result> { + let mut buf = vec![0; limit.min(FILE_TAIL_FINGERPRINT_BYTES)]; file.seek(std::io::SeekFrom::Start(0)).await?; let n = file.read(&mut buf).await?; buf.truncate(n); diff --git a/src/filetail/supervisor_tests.rs b/src/filetail/supervisor_tests.rs index 03b53cea..b89c2164 100644 --- a/src/filetail/supervisor_tests.rs +++ b/src/filetail/supervisor_tests.rs @@ -503,6 +503,58 @@ async fn reopen_if_rotated_or_truncated_detects_copytruncate() { assert_eq!(reopened.position, 0); } +#[tokio::test] +async fn reopen_if_rotated_or_truncated_ignores_append_beyond_original_fingerprint() { + let temp = tempfile::tempdir().unwrap(); + let file_path = temp.path().join("app.log"); + let old_contents = b"already here\n"; + tokio::fs::write(&file_path, old_contents).await.unwrap(); + let src = source("app", &file_path.to_string_lossy(), "app"); + let mut opened = open_tail_file(&src, false).await.unwrap(); + opened.position = old_contents.len() as u64; + + let mut writer = tokio::fs::OpenOptions::new() + .append(true) + .open(&file_path) + .await + .unwrap(); + writer.write_all(b"after reconcile\n").await.unwrap(); + writer.flush().await.unwrap(); + + let reopened = + reopen_if_rotated_or_truncated(&src, opened.identity, opened.position, &opened.fingerprint) + .await + .unwrap(); + assert!( + reopened.is_none(), + "append-only growth must not be mistaken for same-inode replacement" + ); +} + +#[tokio::test] +async fn reopen_if_rotated_or_truncated_refreshes_full_fingerprint_after_replacement() { + let temp = tempfile::tempdir().unwrap(); + let file_path = temp.path().join("app.log"); + let old_contents = b"old +"; + tokio::fs::write(&file_path, old_contents).await.unwrap(); + let src = source("app", &file_path.to_string_lossy(), "app"); + let mut opened = open_tail_file(&src, false).await.unwrap(); + opened.position = old_contents.len() as u64; + + let replacement = b"new replacement that is longer than the old prefix +"; + tokio::fs::write(&file_path, replacement).await.unwrap(); + + let reopened = + reopen_if_rotated_or_truncated(&src, opened.identity, opened.position, &opened.fingerprint) + .await + .unwrap() + .expect("replacement should reopen"); + assert_eq!(reopened.position, 0); + assert_eq!(reopened.fingerprint, replacement.to_vec()); +} + #[tokio::test] async fn reopen_if_rotated_or_truncated_detects_same_inode_copytruncate_regrow() { let temp = tempfile::tempdir().unwrap();