1use std::io::{Cursor, Read, Seek, Write};
7use std::path::PathBuf;
8
9use super::{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 fn hard_limit(&self) -> u64 {
46 match self {
47 SpillPolicy::Off => 0,
48 SpillPolicy::Memory { cap } => *cap,
49 SpillPolicy::Temp { max, .. } => *max,
50 }
51 }
52}
53
54#[derive(Debug)]
55enum Backing {
56 Mem(Cursor<Vec<u8>>),
57 File(std::fs::File),
58}
59
60#[derive(Debug)]
62pub struct SpillSource {
63 backing: Backing,
64 len: u64,
65 on_disk: bool,
66}
67
68impl SpillSource {
69 pub fn materialize(mut src: Box<dyn Source>, policy: &SpillPolicy) -> Result<Self> {
75 let limit = policy.hard_limit();
76 if !policy.is_enabled() {
77 return Err(Error::SpillLimitExceeded { limit: 0 });
78 }
79
80 let mem_cap = match policy {
81 SpillPolicy::Off => 0,
82 SpillPolicy::Memory { cap } => *cap,
83 SpillPolicy::Temp { mem_cap, .. } => *mem_cap,
84 };
85
86 let mut mem: Vec<u8> = Vec::new();
87 let mut buf = vec![0u8; 64 * 1024];
88 let mut total: u64 = 0;
89
90 loop {
92 let n = src.read(&mut buf)?;
93 if n == 0 {
94 return Ok(Self {
95 len: total,
96 backing: Backing::Mem(Cursor::new(mem)),
97 on_disk: false,
98 });
99 }
100 total += n as u64;
101 if total > limit {
102 return Err(Error::SpillLimitExceeded { limit });
103 }
104 if total > mem_cap {
105 mem.extend_from_slice(&buf[..n]);
106 break;
107 }
108 mem.extend_from_slice(&buf[..n]);
109 }
110
111 let SpillPolicy::Temp { dir, .. } = policy else {
113 return Err(Error::SpillLimitExceeded { limit });
117 };
118
119 let mut file = match dir {
120 Some(d) => tempfile::tempfile_in(d)?,
121 None => tempfile::tempfile()?,
122 };
123 file.write_all(&mem)?;
124 drop(mem);
125
126 loop {
127 let n = src.read(&mut buf)?;
128 if n == 0 {
129 break;
130 }
131 total += n as u64;
132 if total > limit {
133 return Err(Error::SpillLimitExceeded { limit });
134 }
135 file.write_all(&buf[..n])?;
136 }
137
138 file.flush()?;
139 file.seek(std::io::SeekFrom::Start(0))?;
140 Ok(Self {
141 backing: Backing::File(file),
142 len: total,
143 on_disk: true,
144 })
145 }
146
147 pub fn spilled_to_disk(&self) -> bool {
149 self.on_disk
150 }
151}
152
153impl Read for SpillSource {
154 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
155 match &mut self.backing {
156 Backing::Mem(c) => c.read(buf),
157 Backing::File(f) => f.read(buf),
158 }
159 }
160}
161
162impl Source for SpillSource {
163 fn caps(&self) -> SourceCaps {
164 SourceCaps {
165 seekable: true,
166 len: Some(self.len),
167 }
168 }
169
170 fn as_seek(&mut self) -> Option<&mut dyn SeekRead> {
171 match &mut self.backing {
172 Backing::Mem(c) => Some(c),
173 Backing::File(f) => Some(f),
174 }
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use crate::source::ReaderSource;
181 use std::io::{Read, SeekFrom};
182
183 use super::*;
184
185 fn pipe(bytes: Vec<u8>) -> Box<dyn crate::source::Source> {
186 Box::new(ReaderSource::new(std::io::Cursor::new(bytes)))
187 }
188
189 #[test]
190 fn small_input_stays_in_memory_and_becomes_seekable() {
191 let mut s = SpillSource::materialize(
192 pipe(b"0123456789".to_vec()),
193 &SpillPolicy::Temp {
194 dir: None,
195 mem_cap: 1024,
196 max: 1 << 20,
197 },
198 )
199 .unwrap();
200
201 assert!(!s.spilled_to_disk());
202 assert!(s.caps().seekable);
203 assert_eq!(s.caps().len, Some(10));
204
205 let seek = s.as_seek().unwrap();
206 seek.seek(SeekFrom::Start(7)).unwrap();
207 let mut buf = [0u8; 3];
208 seek.read_exact(&mut buf).unwrap();
209 assert_eq!(&buf, b"789");
210 }
211
212 #[test]
213 fn input_over_the_memory_cap_escalates_to_a_temp_file() {
214 let data: Vec<u8> = (0..4096u32).map(|i| (i % 251) as u8).collect();
215 let mut s = SpillSource::materialize(
216 pipe(data.clone()),
217 &SpillPolicy::Temp {
218 dir: None,
219 mem_cap: 64,
220 max: 1 << 20,
221 },
222 )
223 .unwrap();
224
225 assert!(s.spilled_to_disk());
226 assert_eq!(s.caps().len, Some(4096));
227
228 let mut out = Vec::new();
230 s.read_to_end(&mut out).unwrap();
231 assert_eq!(out, data);
232 }
233
234 #[test]
235 fn memory_only_policy_refuses_to_exceed_its_cap() {
236 let err = SpillSource::materialize(pipe(vec![7u8; 500]), &SpillPolicy::Memory { cap: 100 })
237 .unwrap_err();
238
239 assert!(matches!(
241 err,
242 crate::Error::SpillLimitExceeded { limit: 100 }
243 ));
244 assert_eq!(err.exit_code(), 6);
245 }
246
247 #[test]
248 fn max_spill_is_enforced_for_the_temp_policy_too() {
249 let err = SpillSource::materialize(
250 pipe(vec![7u8; 5000]),
251 &SpillPolicy::Temp {
252 dir: None,
253 mem_cap: 16,
254 max: 1000,
255 },
256 )
257 .unwrap_err();
258 assert!(matches!(
259 err,
260 crate::Error::SpillLimitExceeded { limit: 1000 }
261 ));
262 }
263
264 #[test]
265 fn off_policy_is_not_enabled_and_cannot_materialize() {
266 assert!(!SpillPolicy::Off.is_enabled());
267 assert!(SpillPolicy::default().is_enabled());
268 let err = SpillSource::materialize(pipe(b"x".to_vec()), &SpillPolicy::Off).unwrap_err();
269 assert!(matches!(err, crate::Error::SpillLimitExceeded { limit: 0 }));
270 }
271
272 #[test]
273 fn defaults_match_the_spec() {
274 match SpillPolicy::default() {
275 SpillPolicy::Temp { mem_cap, max, dir } => {
276 assert_eq!(mem_cap, 64 * 1024 * 1024);
277 assert_eq!(max, 2 * 1024 * 1024 * 1024);
278 assert!(dir.is_none());
279 }
280 other => panic!("unexpected default: {other:?}"),
281 }
282 }
283
284 #[test]
285 fn empty_input_materializes_to_an_empty_seekable_source() {
286 let mut s = SpillSource::materialize(pipe(Vec::new()), &SpillPolicy::default()).unwrap();
287 assert_eq!(s.caps().len, Some(0));
288 let mut out = Vec::new();
289 s.read_to_end(&mut out).unwrap();
290 assert!(out.is_empty());
291 }
292
293 #[test]
294 fn escalation_preserves_order_across_many_sub_cap_chunks() {
295 let data: Vec<u8> = (0..4096u32).map(|i| (i % 251) as u8).collect();
300 let src: Box<dyn Source> = Box::new(crate::source::ReaderSource::new(
301 crate::testing::ChunkedReader::new(data.clone(), 100),
302 ));
303 let mut s = SpillSource::materialize(
304 src,
305 &SpillPolicy::Temp {
306 dir: None,
307 mem_cap: 1000,
308 max: 1 << 20,
309 },
310 )
311 .unwrap();
312
313 assert!(s.spilled_to_disk());
314 let mut out = Vec::new();
315 s.read_to_end(&mut out).unwrap();
316 assert_eq!(out, data, "bytes must survive escalation in order");
317 }
318}