Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions src/filetail/supervisor/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -121,7 +125,7 @@ pub(crate) async fn path_identity_changed(
async fn reopen_from_start(source: &FileTailSource) -> Result<OpenedTailFile> {
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,
Expand All @@ -131,8 +135,11 @@ async fn reopen_from_start(source: &FileTailSource) -> Result<OpenedTailFile> {
})
}

async fn file_prefix_fingerprint(file: &mut tokio::fs::File) -> std::io::Result<Vec<u8>> {
let mut buf = vec![0; FILE_TAIL_FINGERPRINT_BYTES];
async fn file_prefix_fingerprint(
file: &mut tokio::fs::File,
limit: usize,
) -> std::io::Result<Vec<u8>> {
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);
Expand Down
52 changes: 52 additions & 0 deletions src/filetail/supervisor_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading