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