1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
//! File-level AOF utilities — the quarantine copy, the fresh rewrite
//! base, and the rewrite temp path. Split from `aof.rs` for the 500-LOC
//! house rule; behaviour unchanged.
use std::fs::File;
use std::io::{self, Seek, SeekFrom};
use std::path::{Path, PathBuf};
/// Copy `[from, EOF)` of `path` to `<path>.corrupt-quarantine.<unix_ts>`
/// (fsynced) and return the quarantine path. Called before the torn/corrupt
/// tail is truncated away, so the dropped bytes stay inspectable — replay
/// never re-applies them. Errors propagate: failing to preserve the bytes
/// (e.g. disk full) fails the open with the file intact, which beats
/// destroying the only copy; free space and reopen.
pub(crate) fn quarantine_dropped_tail(path: &Path, from: u64) -> io::Result<PathBuf> {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let qpath = PathBuf::from(format!("{}.corrupt-quarantine.{ts}", path.display()));
let mut src = File::open(path)?;
src.seek(SeekFrom::Start(from))?;
let mut dst = File::create(&qpath)?;
io::copy(&mut src, &mut dst)?;
dst.sync_data()?;
Ok(qpath)
}
/// `<aof>.rewrite` — same-directory temp path so `rename(2)` stays atomic.
pub(crate) fn rewrite_tmp_path(path: &Path) -> PathBuf {
let mut p = path.to_path_buf();
let new_name = match path.file_name() {
Some(n) => {
let mut s = n.to_os_string();
s.push(".rewrite");
s
}
None => std::ffi::OsString::from("aof.rewrite"),
};
p.set_file_name(new_name);
p
}
/// Cut the file back to where a replay settled, when it dropped nothing
/// (the rest is a mapped log's zero tail); otherwise repair the tail.
pub(crate) fn settle_tail(
path: &Path,
file: &mut File,
size: &mut u64,
settled: Option<u64>,
resync: bool,
) -> io::Result<Option<PathBuf>> {
match settled.filter(|&s| s <= *size) {
Some(s) => {
if s < *size {
file.set_len(s)?;
file.sync_data()?;
*size = s;
}
Ok(None)
}
None => repair_tail(path, file, size, resync),
}
}
/// The tail-repair half of [`crate::Aof::open_with_repair`]. A crash —
/// power loss, or a VM/process kill with un-fsynced `EverySec`
/// pages — can leave a partial frame, or a zero-filled region,
/// after the last complete one. `replay_aof` stops there
/// (prefix-only), but an O_APPEND write would land *after* the
/// garbage, orphaning it behind the torn frame on the next replay:
/// silent data loss. Truncate to the last complete frame so
/// appends stay contiguous with the replayable prefix (Redis
/// `aof-load-truncated`) — but FIRST copy the dropped region
/// aside: after a corrupt frame MID-file the region is mostly
/// well-formed frames (a real incident dropped 231 MB over a
/// few-KB bad frame), and the forensic copy is the only way to
/// ever get them back.
pub(crate) fn repair_tail(
path: &Path,
file: &mut File,
size: &mut u64,
resync: bool,
) -> io::Result<Option<PathBuf>> {
let (valid, zero_tail) = crate::replay::valid_prefix_len_of_file(path, resync)?;
if valid >= *size {
return Ok(None);
}
if valid + zero_tail == *size {
// only a mapped log's unused preallocation: not data, nothing to keep
file.set_len(valid)?;
file.sync_data()?;
*size = valid;
return Ok(None);
}
let q = crate::aof_util::quarantine_dropped_tail(path, valid)?;
file.set_len(valid)?;
file.sync_data()?;
eprintln!(
"kevy WARN: AOF {} dropped a non-replayable tail: {} bytes \
quarantined to {} then truncated at byte {valid}.",
path.display(),
*size - valid,
q.display(),
);
*size = valid;
Ok(Some(q))
}