1use std::io::{Cursor, Read, Seek, Write};
7use std::path::PathBuf;
8
9use super::{GuardedSeek, SeekRead, Source, SourceCaps};
10use crate::error::{Error, Result};
11
12pub const DEFAULT_MEM_CAP: u64 = 64 * 1024 * 1024;
13pub const DEFAULT_MAX_SPILL: u64 = 2 * 1024 * 1024 * 1024;
14
15#[derive(Clone, PartialEq, Eq, Debug)]
16pub enum SpillPolicy {
17 Off,
19 Memory { cap: u64 },
21 Temp {
24 dir: Option<PathBuf>,
25 mem_cap: u64,
26 max: u64,
27 },
28}
29
30impl Default for SpillPolicy {
31 fn default() -> Self {
32 SpillPolicy::Temp {
33 dir: None,
34 mem_cap: DEFAULT_MEM_CAP,
35 max: DEFAULT_MAX_SPILL,
36 }
37 }
38}
39
40impl SpillPolicy {
41 pub fn is_enabled(&self) -> bool {
42 !matches!(self, SpillPolicy::Off)
43 }
44
45 pub fn memory_cap(&self) -> u64 {
48 match self {
49 SpillPolicy::Off => 0,
50 SpillPolicy::Memory { cap } => *cap,
51 SpillPolicy::Temp { mem_cap, .. } => *mem_cap,
52 }
53 }
54
55 fn hard_limit(&self) -> u64 {
56 match self {
57 SpillPolicy::Off => 0,
58 SpillPolicy::Memory { cap } => *cap,
59 SpillPolicy::Temp { max, .. } => *max,
60 }
61 }
62}
63
64#[derive(Debug)]
65enum Backing {
66 Mem(GuardedSeek<Cursor<Vec<u8>>>),
67 File(GuardedSeek<std::fs::File>),
68}
69
70#[derive(Debug)]
72pub struct SpillSource {
73 backing: Backing,
74 len: u64,
75 on_disk: bool,
76}
77
78impl SpillSource {
79 pub fn materialize(mut src: Box<dyn Source>, policy: &SpillPolicy) -> Result<Self> {
85 Self::materialize_from(&mut *src, policy)
86 }
87
88 pub fn materialize_from<R: Read + ?Sized>(src: &mut R, policy: &SpillPolicy) -> Result<Self> {
94 let mut spool = SpillWriter::new(policy)?;
95 let mut buf = vec![0u8; 64 * 1024];
96 loop {
97 let n = src.read(&mut buf)?;
98 if n == 0 {
99 return spool.finish();
100 }
101 spool.push(&buf[..n])?;
102 }
103 }
104
105 pub fn spilled_to_disk(&self) -> bool {
107 self.on_disk
108 }
109}
110
111#[derive(Debug)]
121pub struct SpillWriter {
122 limit: u64,
123 mem_cap: u64,
124 dir: Option<Option<PathBuf>>,
125 mem: Vec<u8>,
126 file: Option<std::fs::File>,
127 total: u64,
128}
129
130impl SpillWriter {
131 pub fn new(policy: &SpillPolicy) -> Result<Self> {
134 if !policy.is_enabled() {
135 return Err(Error::SpillLimitExceeded { limit: 0 });
136 }
137 Ok(Self {
138 limit: policy.hard_limit(),
139 mem_cap: policy.memory_cap(),
140 dir: match policy {
141 SpillPolicy::Temp { dir, .. } => Some(dir.clone()),
142 SpillPolicy::Off | SpillPolicy::Memory { .. } => None,
143 },
144 mem: Vec::new(),
145 file: None,
146 total: 0,
147 })
148 }
149
150 pub fn len(&self) -> u64 {
152 self.total
153 }
154
155 pub fn is_empty(&self) -> bool {
157 self.total == 0
158 }
159
160 pub fn reserve(&mut self, additional: u64) {
165 if self.file.is_some() {
166 return;
167 }
168 let room = self.mem_cap.saturating_sub(self.total);
169 let n = usize::try_from(additional.min(room)).unwrap_or(0);
170 self.mem.reserve_exact(n);
171 }
172
173 pub fn push(&mut self, bytes: &[u8]) -> Result<()> {
176 self.total += bytes.len() as u64;
177 if self.total > self.limit {
178 return Err(Error::SpillLimitExceeded { limit: self.limit });
179 }
180 if let Some(file) = &mut self.file {
181 file.write_all(bytes)?;
182 return Ok(());
183 }
184 if self.total <= self.mem_cap {
185 self.mem.extend_from_slice(bytes);
186 return Ok(());
187 }
188 let Some(dir) = &self.dir else {
192 return Err(Error::SpillLimitExceeded { limit: self.limit });
193 };
194 let mut file = match dir {
195 Some(d) => tempfile::tempfile_in(d)?,
196 None => tempfile::tempfile()?,
197 };
198 file.write_all(&self.mem)?;
199 file.write_all(bytes)?;
200 self.mem = Vec::new();
201 self.file = Some(file);
202 Ok(())
203 }
204
205 pub fn finish(mut self) -> Result<SpillSource> {
207 match self.file.take() {
208 None => {
209 self.mem.shrink_to_fit();
211 Ok(SpillSource {
212 backing: Backing::Mem(GuardedSeek::new(Cursor::new(self.mem))),
213 len: self.total,
214 on_disk: false,
215 })
216 }
217 Some(mut file) => {
218 file.flush()?;
219 file.seek(std::io::SeekFrom::Start(0))?;
220 Ok(SpillSource {
221 backing: Backing::File(GuardedSeek::new(file)),
222 len: self.total,
223 on_disk: true,
224 })
225 }
226 }
227 }
228}
229
230impl Read for SpillSource {
231 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
232 match &mut self.backing {
233 Backing::Mem(c) => c.read(buf),
234 Backing::File(f) => f.read(buf),
235 }
236 }
237}
238
239impl Source for SpillSource {
240 fn caps(&self) -> SourceCaps {
241 SourceCaps {
242 seekable: true,
243 len: Some(self.len),
244 }
245 }
246
247 fn as_seek(&mut self) -> Option<&mut dyn SeekRead> {
248 match &mut self.backing {
249 Backing::Mem(c) => Some(c),
250 Backing::File(f) => Some(f),
251 }
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use crate::source::ReaderSource;
258 use std::io::{Read, SeekFrom};
259
260 use super::*;
261
262 fn pipe(bytes: Vec<u8>) -> Box<dyn crate::source::Source> {
263 Box::new(ReaderSource::new(std::io::Cursor::new(bytes)))
264 }
265
266 #[test]
267 fn small_input_stays_in_memory_and_becomes_seekable() {
268 let mut s = SpillSource::materialize(
269 pipe(b"0123456789".to_vec()),
270 &SpillPolicy::Temp {
271 dir: None,
272 mem_cap: 1024,
273 max: 1 << 20,
274 },
275 )
276 .unwrap();
277
278 assert!(!s.spilled_to_disk());
279 assert!(s.caps().seekable);
280 assert_eq!(s.caps().len, Some(10));
281
282 let seek = s.as_seek().unwrap();
283 seek.seek(SeekFrom::Start(7)).unwrap();
284 let mut buf = [0u8; 3];
285 seek.read_exact(&mut buf).unwrap();
286 assert_eq!(&buf, b"789");
287 }
288
289 #[test]
290 fn input_over_the_memory_cap_escalates_to_a_temp_file() {
291 let data: Vec<u8> = (0..4096u32).map(|i| (i % 251) as u8).collect();
292 let mut s = SpillSource::materialize(
293 pipe(data.clone()),
294 &SpillPolicy::Temp {
295 dir: None,
296 mem_cap: 64,
297 max: 1 << 20,
298 },
299 )
300 .unwrap();
301
302 assert!(s.spilled_to_disk());
303 assert_eq!(s.caps().len, Some(4096));
304
305 let mut out = Vec::new();
307 s.read_to_end(&mut out).unwrap();
308 assert_eq!(out, data);
309 }
310
311 #[test]
312 fn memory_only_policy_refuses_to_exceed_its_cap() {
313 let err = SpillSource::materialize(pipe(vec![7u8; 500]), &SpillPolicy::Memory { cap: 100 })
314 .unwrap_err();
315
316 assert!(matches!(
318 err,
319 crate::Error::SpillLimitExceeded { limit: 100 }
320 ));
321 assert_eq!(err.exit_code(), 6);
322 }
323
324 #[test]
325 fn max_spill_is_enforced_for_the_temp_policy_too() {
326 let err = SpillSource::materialize(
327 pipe(vec![7u8; 5000]),
328 &SpillPolicy::Temp {
329 dir: None,
330 mem_cap: 16,
331 max: 1000,
332 },
333 )
334 .unwrap_err();
335 assert!(matches!(
336 err,
337 crate::Error::SpillLimitExceeded { limit: 1000 }
338 ));
339 }
340
341 #[test]
342 fn off_policy_is_not_enabled_and_cannot_materialize() {
343 assert!(!SpillPolicy::Off.is_enabled());
344 assert!(SpillPolicy::default().is_enabled());
345 let err = SpillSource::materialize(pipe(b"x".to_vec()), &SpillPolicy::Off).unwrap_err();
346 assert!(matches!(err, crate::Error::SpillLimitExceeded { limit: 0 }));
347 }
348
349 #[test]
352 fn a_spill_writer_escalates_and_refuses_like_materialize() {
353 let policy = SpillPolicy::Temp {
354 dir: None,
355 mem_cap: 10,
356 max: 30,
357 };
358 let mut w = SpillWriter::new(&policy).unwrap();
359 w.push(b"0123456789").unwrap();
360 w.push(b"abcdef").unwrap();
361 assert_eq!(w.len(), 16);
362 let mut s = w.finish().unwrap();
363 assert!(s.spilled_to_disk());
364 let mut out = Vec::new();
365 s.read_to_end(&mut out).unwrap();
366 assert_eq!(out, b"0123456789abcdef");
367
368 let mut w = SpillWriter::new(&policy).unwrap();
369 let err = w.push(&[0u8; 31]).unwrap_err();
370 assert!(matches!(
371 err,
372 crate::Error::SpillLimitExceeded { limit: 30 }
373 ));
374 assert!(matches!(
375 SpillWriter::new(&SpillPolicy::Off).unwrap_err(),
376 crate::Error::SpillLimitExceeded { limit: 0 }
377 ));
378 assert_eq!(policy.memory_cap(), 10);
379 }
380
381 #[test]
382 fn defaults_match_the_spec() {
383 match SpillPolicy::default() {
384 SpillPolicy::Temp { mem_cap, max, dir } => {
385 assert_eq!(mem_cap, 64 * 1024 * 1024);
386 assert_eq!(max, 2 * 1024 * 1024 * 1024);
387 assert!(dir.is_none());
388 }
389 other => panic!("unexpected default: {other:?}"),
390 }
391 }
392
393 #[test]
394 fn empty_input_materializes_to_an_empty_seekable_source() {
395 let mut s = SpillSource::materialize(pipe(Vec::new()), &SpillPolicy::default()).unwrap();
396 assert_eq!(s.caps().len, Some(0));
397 let mut out = Vec::new();
398 s.read_to_end(&mut out).unwrap();
399 assert!(out.is_empty());
400 }
401
402 #[test]
403 fn escalation_preserves_order_across_many_sub_cap_chunks() {
404 let data: Vec<u8> = (0..4096u32).map(|i| (i % 251) as u8).collect();
409 let src: Box<dyn Source> = Box::new(crate::source::ReaderSource::new(
410 crate::testing::ChunkedReader::new(data.clone(), 100),
411 ));
412 let mut s = SpillSource::materialize(
413 src,
414 &SpillPolicy::Temp {
415 dir: None,
416 mem_cap: 1000,
417 max: 1 << 20,
418 },
419 )
420 .unwrap();
421
422 assert!(s.spilled_to_disk());
423 let mut out = Vec::new();
424 s.read_to_end(&mut out).unwrap();
425 assert_eq!(out, data, "bytes must survive escalation in order");
426 }
427}