metrale_cache/
kv_spill.rs1use anyhow::Result;
15use std::fs;
16use std::io::{BufReader, BufWriter};
17use std::path::PathBuf;
18
19pub struct KvSpillManager {
21 spill_dir: PathBuf,
22 next_id: u64,
23 max_bytes: u64,
24 used_bytes: u64,
25}
26
27impl KvSpillManager {
28 pub fn new(spill_dir: PathBuf, max_bytes: u64) -> Result<Self> {
32 if spill_dir.exists() {
33 for entry in fs::read_dir(&spill_dir)? {
34 let entry = entry?;
35 if entry.file_name().to_string_lossy().starts_with("swap_") {
36 let _ = fs::remove_file(entry.path());
37 }
38 }
39 } else {
40 fs::create_dir_all(&spill_dir)?;
41 }
42 Ok(Self {
43 spill_dir,
44 next_id: 0,
45 max_bytes,
46 used_bytes: 0,
47 })
48 }
49
50 pub fn create_file(&mut self) -> Result<(u64, BufWriter<fs::File>)> {
53 let id = self.next_id;
54 self.next_id += 1;
55 let path = self.file_path(id);
56 let file = fs::File::create(&path)?;
57 Ok((id, BufWriter::new(file)))
58 }
59
60 pub fn open_file(&self, id: u64) -> Result<BufReader<fs::File>> {
62 let path = self.file_path(id);
63 let file = fs::File::open(&path)?;
64 Ok(BufReader::new(file))
65 }
66
67 pub fn remove_file(&mut self, id: u64) -> Result<()> {
70 let path = self.file_path(id);
71 let size = fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
72 fs::remove_file(&path)?;
73 self.used_bytes = self.used_bytes.saturating_sub(size);
74 Ok(())
75 }
76
77 pub fn record_usage(&mut self, id: u64) {
80 let path = self.file_path(id);
81 if let Ok(meta) = fs::metadata(&path) {
82 self.used_bytes += meta.len();
83 }
84 }
85
86 pub fn has_space(&self, estimated_bytes: u64) -> bool {
88 self.used_bytes + estimated_bytes <= self.max_bytes
89 }
90
91 pub fn used_bytes(&self) -> u64 {
93 self.used_bytes
94 }
95
96 fn file_path(&self, id: u64) -> PathBuf {
97 self.spill_dir.join(format!("swap_{id}.bin"))
98 }
99}
100
101impl Drop for KvSpillManager {
102 fn drop(&mut self) {
103 if let Ok(entries) = fs::read_dir(&self.spill_dir) {
106 for entry in entries.flatten() {
107 if entry.file_name().to_string_lossy().starts_with("swap_") {
108 let _ = fs::remove_file(entry.path());
109 }
110 }
111 }
112 let _ = fs::remove_dir(&self.spill_dir);
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use std::io::Write;
124
125 fn temp_dir() -> PathBuf {
126 std::env::temp_dir().join(format!(
127 "metrale_spill_test_{}",
128 std::time::SystemTime::now()
129 .duration_since(std::time::UNIX_EPOCH)
130 .unwrap()
131 .as_nanos()
132 ))
133 }
134
135 #[test]
136 fn test_create_open_remove_lifecycle() {
137 let dir = temp_dir();
138 let mut mgr = KvSpillManager::new(dir.clone(), 1024 * 1024).unwrap();
139
140 let (id, mut writer) = mgr.create_file().unwrap();
141 writer.write_all(&[1u8; 256]).unwrap();
142 writer.flush().unwrap();
143 drop(writer);
144 mgr.record_usage(id);
145 assert_eq!(mgr.used_bytes(), 256);
146
147 let mut reader = mgr.open_file(id).unwrap();
148 let mut buf = Vec::new();
149 std::io::Read::read_to_end(&mut reader, &mut buf).unwrap();
150 assert_eq!(buf.len(), 256);
151 assert!(buf.iter().all(|&b| b == 1));
152
153 mgr.remove_file(id).unwrap();
154 assert_eq!(mgr.used_bytes(), 0);
155 assert!(!dir.join("swap_0.bin").exists());
156
157 let _ = fs::remove_dir_all(&dir);
158 }
159
160 #[test]
161 fn test_has_space() {
162 let dir = temp_dir();
163 let mut mgr = KvSpillManager::new(dir.clone(), 512).unwrap();
164
165 assert!(mgr.has_space(512));
166 assert!(!mgr.has_space(513));
167
168 let (id, mut writer) = mgr.create_file().unwrap();
169 writer.write_all(&[0u8; 256]).unwrap();
170 writer.flush().unwrap();
171 drop(writer);
172 mgr.record_usage(id);
173
174 assert!(mgr.has_space(256));
175 assert!(!mgr.has_space(257));
176
177 let _ = fs::remove_dir_all(&dir);
178 }
179
180 #[test]
181 fn test_stale_cleanup_on_new() {
182 let dir = temp_dir();
183 fs::create_dir_all(&dir).unwrap();
184
185 fs::write(dir.join("swap_99.bin"), [0u8; 64]).unwrap();
186 assert!(dir.join("swap_99.bin").exists());
187
188 let _mgr = KvSpillManager::new(dir.clone(), 1024).unwrap();
189 assert!(!dir.join("swap_99.bin").exists());
190
191 let _ = fs::remove_dir_all(&dir);
192 }
193
194 #[test]
195 fn test_drop_cleanup() {
196 let dir = temp_dir();
197 {
198 let mut mgr = KvSpillManager::new(dir.clone(), 1024).unwrap();
199 let (_, mut writer) = mgr.create_file().unwrap();
200 writer.write_all(&[0u8; 32]).unwrap();
201 writer.flush().unwrap();
202 drop(writer);
203 }
204 assert!(!dir.join("swap_0.bin").exists());
205 let _ = fs::remove_dir_all(&dir);
206 }
207
208 #[test]
209 fn test_sequential_ids() {
210 let dir = temp_dir();
211 let mut mgr = KvSpillManager::new(dir.clone(), 1024 * 1024).unwrap();
212
213 let (id0, w0) = mgr.create_file().unwrap();
214 drop(w0);
215 let (id1, w1) = mgr.create_file().unwrap();
216 drop(w1);
217
218 assert_eq!(id0, 0);
219 assert_eq!(id1, 1);
220
221 let _ = fs::remove_dir_all(&dir);
222 }
223}
224
225#[cfg(test)]
226mod dir_cleanup_tests {
227 use super::*;
228
229 #[test]
230 fn dropping_the_manager_leaves_no_directory_behind() {
231 let dir = std::env::temp_dir().join(format!(
233 "metrale_spill_cleanup_{}_{:?}",
234 std::process::id(),
235 std::thread::current().id()
236 ));
237 {
238 let mgr = KvSpillManager::new(dir.clone(), 1024).expect("constructs");
239 assert!(dir.exists(), "the manager creates its directory");
240 drop(mgr);
241 }
242 assert!(!dir.exists(), "and removes it again");
243 }
244
245 #[test]
246 fn a_directory_holding_someone_elses_file_is_left_alone() {
247 let dir = std::env::temp_dir().join(format!(
250 "metrale_spill_shared_{}_{:?}",
251 std::process::id(),
252 std::thread::current().id()
253 ));
254 std::fs::create_dir_all(&dir).expect("mkdir");
255 std::fs::write(dir.join("not-ours.txt"), b"keep me").expect("write");
256 {
257 let _mgr = KvSpillManager::new(dir.clone(), 1024).expect("constructs");
258 }
259 assert!(dir.exists(), "a directory with a foreign file survives");
260 assert!(dir.join("not-ours.txt").exists(), "and so does the file");
261 let _ = std::fs::remove_dir_all(&dir);
262 }
263}