1use std::{
14 cmp::Reverse,
15 collections::BinaryHeap,
16 fs::{self, File},
17 io::{BufReader, BufWriter, Read, Write},
18 marker::PhantomData,
19 path::{Path, PathBuf},
20 sync::{
21 Arc,
22 atomic::{AtomicU64, Ordering},
23 },
24};
25
26use anyhow::{Context, Result, bail};
27use rayon::prelude::*;
28
29use crate::{
30 memory::{MemoryBudget, Reservation},
31 util::codec::{get_u64, put_u64},
32};
33
34const IO_BUFFER_BYTES: usize = 1 << 20;
35const MAX_MERGE_FAN_IN: usize = 64;
38const MIN_RUN_FRACTION: usize = 16;
41const MIN_CAPACITY: usize = 1_024;
43const MIN_MERGE_BUFFER_BYTES: usize = 64 << 10;
45
46pub trait Spill: Sized {
48 fn encode(&self, out: &mut Vec<u8>);
49 fn decode(input: &mut &[u8]) -> Result<Self>;
50 fn resident_bytes(&self) -> usize {
53 std::mem::size_of::<Self>()
54 }
55}
56
57#[derive(Debug)]
59pub struct Scratch {
60 path: PathBuf,
61 next_file: AtomicU64,
62 spilled_bytes: AtomicU64,
63}
64
65impl Scratch {
66 pub fn create(path: impl Into<PathBuf>) -> Result<Arc<Self>> {
67 let path = path.into();
68 fs::create_dir_all(&path)
69 .with_context(|| format!("failed to create scratch directory {}", path.display()))?;
70 Ok(Arc::new(Self {
71 path,
72 next_file: AtomicU64::new(0),
73 spilled_bytes: AtomicU64::new(0),
74 }))
75 }
76
77 pub fn path(&self) -> &Path {
78 &self.path
79 }
80
81 pub fn spilled_bytes(&self) -> u64 {
83 self.spilled_bytes.load(Ordering::Relaxed)
84 }
85
86 fn new_file(&self, label: &str) -> PathBuf {
87 let index = self.next_file.fetch_add(1, Ordering::Relaxed);
88 self.path.join(format!("{label}-{index:06}.bin"))
89 }
90}
91
92impl Drop for Scratch {
93 fn drop(&mut self) {
94 let _ = fs::remove_dir_all(&self.path);
95 }
96}
97
98#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
100pub struct SortStats {
101 pub items: u64,
102 pub runs: u64,
103}
104
105pub struct ExternalSorter<T> {
106 scratch: Arc<Scratch>,
107 budget: Arc<MemoryBudget>,
108 label: &'static str,
109 share_bytes: usize,
111 reservation: Reservation,
113 buffer: Vec<T>,
114 runs: Vec<PathBuf>,
115 stats: SortStats,
116}
117
118impl<T: Spill + Ord + Send> ExternalSorter<T> {
119 pub fn new(
120 scratch: &Arc<Scratch>,
121 label: &'static str,
122 budget: &Arc<MemoryBudget>,
123 share_bytes: usize,
124 ) -> Self {
125 Self {
126 scratch: Arc::clone(scratch),
127 budget: Arc::clone(budget),
128 label,
129 share_bytes: share_bytes.max(1),
130 reservation: Reservation::empty(budget),
131 buffer: Vec::new(),
132 runs: Vec::new(),
133 stats: SortStats::default(),
134 }
135 }
136
137 pub fn push(&mut self, item: T) -> Result<()> {
138 let heap = item
139 .resident_bytes()
140 .saturating_sub(std::mem::size_of::<T>());
141 let (mut growth, mut needed) = self.needed(heap);
142 let over_share = self.reservation.bytes() + needed > self.share_bytes;
143 if over_share || !self.reservation.try_grow(needed) {
144 if over_share || self.reservation.bytes() >= self.share_bytes / MIN_RUN_FRACTION {
147 self.spill()?;
148 (growth, needed) = self.needed(heap);
149 }
150 if !self.reservation.try_grow(needed) {
151 self.reservation.force_grow(needed);
152 }
153 }
154 self.buffer.reserve_exact(growth);
155 self.buffer.push(item);
156 self.stats.items += 1;
157 Ok(())
158 }
159
160 fn needed(&self, heap: usize) -> (usize, usize) {
163 let growth = if self.buffer.len() == self.buffer.capacity() {
164 let share_items = (self.share_bytes / std::mem::size_of::<T>().max(1)).max(1);
166 self.buffer
167 .capacity()
168 .max(MIN_CAPACITY)
169 .min(share_items.saturating_sub(self.buffer.capacity()))
170 .max(1)
171 } else {
172 0
173 };
174 (growth, heap + growth * std::mem::size_of::<T>())
175 }
176
177 pub fn stats(&self) -> SortStats {
178 self.stats
179 }
180
181 pub fn finish(mut self) -> Result<Sorted<T>> {
183 if self.runs.is_empty() {
184 self.buffer.par_sort();
186 let reservation =
187 std::mem::replace(&mut self.reservation, Reservation::empty(&self.budget));
188 return Ok(Sorted::Memory(
189 std::mem::take(&mut self.buffer).into_iter(),
190 reservation,
191 ));
192 }
193 self.spill()?;
196 while self.runs.len() > MAX_MERGE_FAN_IN {
197 let batch = self.runs.drain(..MAX_MERGE_FAN_IN).collect::<Vec<_>>();
198 let merged = self.scratch.new_file(self.label);
199 let mut writer = SpoolWriter::<T>::create(&self.scratch, &merged)?;
200 for item in Merge::<T>::open(&batch, &self.budget, self.share_bytes)? {
201 writer.write(&item?)?;
202 }
203 writer.finish()?;
204 self.runs.insert(0, merged);
205 }
206 Ok(Sorted::Merge(Merge::open(
207 &self.runs,
208 &self.budget,
209 self.share_bytes,
210 )?))
211 }
212
213 fn spill(&mut self) -> Result<()> {
214 if self.buffer.is_empty() {
215 return Ok(());
216 }
217 self.buffer.par_sort();
218 let path = self.scratch.new_file(self.label);
219 let mut writer = SpoolWriter::create(&self.scratch, &path)?;
220 for item in self.buffer.drain(..) {
221 writer.write(&item)?;
222 }
223 writer.finish()?;
224 self.buffer = Vec::new();
226 self.reservation.release_all();
227 self.runs.push(path);
228 self.stats.runs += 1;
229 Ok(())
230 }
231}
232
233pub enum Sorted<T> {
235 Memory(std::vec::IntoIter<T>, Reservation),
236 Merge(Merge<T>),
237}
238
239impl<T: Spill + Ord> Iterator for Sorted<T> {
240 type Item = Result<T>;
241
242 fn next(&mut self) -> Option<Self::Item> {
243 match self {
244 Self::Memory(items, _) => items.next().map(Ok),
245 Self::Merge(merge) => merge.next(),
246 }
247 }
248}
249
250pub struct Merge<T> {
252 sources: Vec<SpoolReader<T>>,
253 heap: BinaryHeap<Reverse<HeapEntry<T>>>,
254 failed: bool,
255 _buffers: Reservation,
257}
258
259struct HeapEntry<T> {
260 item: T,
261 source: usize,
262}
263
264impl<T: Ord> PartialEq for HeapEntry<T> {
265 fn eq(&self, other: &Self) -> bool {
266 self.cmp(other) == std::cmp::Ordering::Equal
267 }
268}
269
270impl<T: Ord> Eq for HeapEntry<T> {}
271
272impl<T: Ord> PartialOrd for HeapEntry<T> {
273 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
274 Some(self.cmp(other))
275 }
276}
277
278impl<T: Ord> Ord for HeapEntry<T> {
279 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
280 self.item
281 .cmp(&other.item)
282 .then_with(|| self.source.cmp(&other.source))
283 }
284}
285
286impl<T: Spill + Ord> Merge<T> {
287 fn open(runs: &[PathBuf], budget: &Arc<MemoryBudget>, share_bytes: usize) -> Result<Self> {
293 let buffer_bytes =
294 (share_bytes / runs.len().max(1)).clamp(MIN_MERGE_BUFFER_BYTES, IO_BUFFER_BYTES);
295 let mut buffers = Reservation::empty(budget);
296 if !buffers.try_grow(runs.len() * buffer_bytes) {
297 buffers.force_grow(runs.len() * buffer_bytes);
298 }
299 let mut sources = runs
300 .iter()
301 .map(|path| {
302 let mut reader = SpoolReader::open(path, buffer_bytes)?;
303 reader.remove_on_drop = true;
304 Ok(reader)
305 })
306 .collect::<Result<Vec<_>>>()?;
307 let mut heap = BinaryHeap::with_capacity(sources.len());
308 for (source, input) in sources.iter_mut().enumerate() {
309 if let Some(item) = input.next() {
310 heap.push(Reverse(HeapEntry {
311 item: item?,
312 source,
313 }));
314 }
315 }
316 Ok(Self {
317 sources,
318 heap,
319 failed: false,
320 _buffers: buffers,
321 })
322 }
323}
324
325impl<T: Spill + Ord> Iterator for Merge<T> {
326 type Item = Result<T>;
327
328 fn next(&mut self) -> Option<Self::Item> {
329 if self.failed {
330 return None;
331 }
332 let Reverse(entry) = self.heap.pop()?;
333 match self.sources[entry.source].next() {
334 Some(Ok(item)) => self.heap.push(Reverse(HeapEntry {
335 item,
336 source: entry.source,
337 })),
338 Some(Err(error)) => {
339 self.failed = true;
340 return Some(Err(error));
341 }
342 None => {}
343 }
344 Some(Ok(entry.item))
345 }
346}
347
348pub struct Spool<T> {
350 writer: SpoolWriter<T>,
351 path: PathBuf,
352}
353
354impl<T: Spill> Spool<T> {
355 pub fn create(scratch: &Arc<Scratch>, label: &str) -> Result<Self> {
356 let path = scratch.new_file(label);
357 Ok(Self {
358 writer: SpoolWriter::create(scratch, &path)?,
359 path,
360 })
361 }
362
363 pub fn write(&mut self, item: &T) -> Result<()> {
364 self.writer.write(item)
365 }
366
367 pub fn written(&self) -> u64 {
369 self.writer.items
370 }
371
372 pub fn into_reader(self) -> Result<SpoolReader<T>> {
375 self.writer.finish()?;
376 let mut reader = SpoolReader::open(&self.path, IO_BUFFER_BYTES)?;
377 reader.remove_on_drop = true;
378 Ok(reader)
379 }
380}
381
382struct SpoolWriter<T> {
383 file: BufWriter<File>,
384 scratch: Arc<Scratch>,
385 buffer: Vec<u8>,
386 items: u64,
387 _marker: PhantomData<T>,
388}
389
390impl<T: Spill> SpoolWriter<T> {
391 fn create(scratch: &Arc<Scratch>, path: &Path) -> Result<Self> {
392 let file = File::create(path)
393 .with_context(|| format!("failed to create scratch file {}", path.display()))?;
394 Ok(Self {
395 file: BufWriter::with_capacity(IO_BUFFER_BYTES, file),
396 scratch: Arc::clone(scratch),
397 buffer: Vec::new(),
398 items: 0,
399 _marker: PhantomData,
400 })
401 }
402
403 fn write(&mut self, item: &T) -> Result<()> {
404 self.buffer.clear();
405 item.encode(&mut self.buffer);
406 let mut prefix = Vec::with_capacity(10);
407 put_u64(&mut prefix, self.buffer.len() as u64);
408 self.file.write_all(&prefix)?;
409 self.file.write_all(&self.buffer)?;
410 self.scratch
411 .spilled_bytes
412 .fetch_add((prefix.len() + self.buffer.len()) as u64, Ordering::Relaxed);
413 self.items += 1;
414 Ok(())
415 }
416
417 fn finish(mut self) -> Result<()> {
418 self.file.flush().context("failed to flush scratch file")
419 }
420}
421
422pub struct SpoolReader<T> {
423 file: BufReader<File>,
424 path: PathBuf,
425 buffer: Vec<u8>,
426 remove_on_drop: bool,
427 _marker: PhantomData<T>,
428}
429
430impl<T: Spill> SpoolReader<T> {
431 fn open(path: &Path, buffer_bytes: usize) -> Result<Self> {
432 let file = File::open(path)
433 .with_context(|| format!("failed to open scratch file {}", path.display()))?;
434 Ok(Self {
435 file: BufReader::with_capacity(buffer_bytes, file),
436 path: path.to_path_buf(),
437 buffer: Vec::new(),
438 remove_on_drop: false,
439 _marker: PhantomData,
440 })
441 }
442
443 fn read_item(&mut self) -> Result<Option<T>> {
444 let Some(len) = self.read_len()? else {
445 return Ok(None);
446 };
447 self.buffer.resize(len, 0);
448 self.file
449 .read_exact(&mut self.buffer)
450 .with_context(|| format!("truncated scratch file {}", self.path.display()))?;
451 let mut input = self.buffer.as_slice();
452 let item = T::decode(&mut input)?;
453 if !input.is_empty() {
454 bail!("scratch item in {} has trailing bytes", self.path.display());
455 }
456 Ok(Some(item))
457 }
458
459 fn read_len(&mut self) -> Result<Option<usize>> {
461 let mut prefix = [0u8; 10];
462 let mut used = 0;
463 loop {
464 let mut byte = [0u8; 1];
465 if self.file.read(&mut byte)? == 0 {
466 if used == 0 {
467 return Ok(None);
468 }
469 bail!("truncated length prefix in {}", self.path.display());
470 }
471 prefix[used] = byte[0];
472 used += 1;
473 if byte[0] & 0x80 == 0 {
474 break;
475 }
476 if used == prefix.len() {
477 bail!("invalid length prefix in {}", self.path.display());
478 }
479 }
480 let len = get_u64(&mut &prefix[..used])?;
481 Ok(Some(
482 usize::try_from(len).context("scratch item too large")?,
483 ))
484 }
485}
486
487impl<T: Spill> Iterator for SpoolReader<T> {
488 type Item = Result<T>;
489
490 fn next(&mut self) -> Option<Self::Item> {
491 self.read_item().transpose()
492 }
493}
494
495impl<T> Drop for SpoolReader<T> {
496 fn drop(&mut self) {
497 if self.remove_on_drop {
498 let _ = fs::remove_file(&self.path);
499 }
500 }
501}
502
503macro_rules! spill_tuple_of_unsigned {
504 ($($name:ident: $ty:ty),+) => {
505 impl Spill for ($($ty,)+) {
506 fn encode(&self, out: &mut Vec<u8>) {
507 let ($($name,)+) = *self;
508 $(put_u64(out, u64::from($name));)+
509 }
510
511 fn decode(input: &mut &[u8]) -> Result<Self> {
512 Ok(($(<$ty>::try_from(get_u64(input)?).context("scratch value out of range")?,)+))
513 }
514 }
515 };
516}
517
518spill_tuple_of_unsigned!(a: u64, b: u64);
519spill_tuple_of_unsigned!(a: u64, b: u8, c: u64);
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524
525 fn scratch(name: &str) -> Arc<Scratch> {
526 Scratch::create(std::env::temp_dir().join(format!(
527 "open-geocode-extsort-{name}-{}",
528 uuid::Uuid::new_v4()
529 )))
530 .expect("scratch")
531 }
532
533 fn pseudo_random(count: u64) -> Vec<(u64, u64)> {
534 let mut state = 0x9e37_79b9_7f4a_7c15u64;
535 (0..count)
536 .map(|index| {
537 state ^= state << 13;
538 state ^= state >> 7;
539 state ^= state << 17;
540 (state % 1_000, index)
541 })
542 .collect()
543 }
544
545 #[test]
546 fn in_memory_sort_writes_no_runs() {
547 let scratch = scratch("memory");
548 let mut sorter =
549 ExternalSorter::new(&scratch, "pairs", &MemoryBudget::unlimited(), usize::MAX);
550 let input = pseudo_random(10_000);
551 for item in &input {
552 sorter.push(*item).expect("push");
553 }
554 assert_eq!(sorter.stats().runs, 0);
555 let sorted = sorter
556 .finish()
557 .expect("finish")
558 .collect::<Result<Vec<_>>>()
559 .expect("items");
560 let mut expected = input;
561 expected.sort();
562 assert_eq!(sorted, expected);
563 assert_eq!(scratch.spilled_bytes(), 0);
564 }
565
566 #[test]
567 fn tiny_budget_spills_many_runs_and_merges_in_rounds() {
568 let scratch = scratch("spill");
569 let mut sorter =
572 ExternalSorter::new(&scratch, "pairs", &MemoryBudget::unlimited(), 50 * 16);
573 let input = pseudo_random(20_000);
574 for item in &input {
575 sorter.push(*item).expect("push");
576 }
577 assert!(sorter.stats().runs as usize > MAX_MERGE_FAN_IN);
578 let sorted = sorter
579 .finish()
580 .expect("finish")
581 .collect::<Result<Vec<_>>>()
582 .expect("items");
583 let mut expected = input;
584 expected.sort();
585 assert_eq!(sorted, expected);
586 assert!(scratch.spilled_bytes() > 0);
587 }
588
589 #[test]
590 fn sorters_sharing_a_budget_stay_within_it() {
591 let scratch = scratch("shared");
592 let item = std::mem::size_of::<(u64, u64)>();
593 let budget = MemoryBudget::new(64 * 1024 * item);
595 let mut first = ExternalSorter::new(&scratch, "first", &budget, 48 * 1024 * item);
596 let mut second = ExternalSorter::new(&scratch, "second", &budget, 48 * 1024 * item);
597 let input = pseudo_random(400_000);
598 for pair in input.chunks(2) {
599 first.push(pair[0]).expect("push");
600 second.push(pair[1]).expect("push");
601 }
602 assert!(
603 budget.peak() <= budget.limit(),
604 "peak {} over {}",
605 budget.peak(),
606 budget.limit()
607 );
608 assert!(first.stats().runs > 0 && second.stats().runs > 0);
609 let first = first.finish().expect("finish");
610 assert_eq!(
612 budget.used(),
613 first_runs_buffers(&first) + second.reservation.bytes()
614 );
615 let mut merged = first.collect::<Result<Vec<_>>>().expect("first");
616 merged.extend(
617 second
618 .finish()
619 .expect("finish")
620 .collect::<Result<Vec<_>>>()
621 .expect("second"),
622 );
623 merged.sort();
624 let mut expected = input;
625 expected.sort();
626 assert_eq!(merged, expected);
627 assert_eq!(budget.used(), 0);
628 }
629
630 fn first_runs_buffers<T>(sorted: &Sorted<T>) -> usize {
631 match sorted {
632 Sorted::Memory(..) => 0,
633 Sorted::Merge(merge) => merge._buffers.bytes(),
634 }
635 }
636
637 #[test]
638 fn run_files_are_removed_once_merged() {
639 let scratch = scratch("cleanup");
640 let mut sorter =
641 ExternalSorter::new(&scratch, "pairs", &MemoryBudget::unlimited(), 10 * 16);
642 for item in pseudo_random(1_000) {
643 sorter.push(item).expect("push");
644 }
645 let files = || fs::read_dir(scratch.path()).expect("scratch").count();
646 assert!(files() > 0);
647 let sorted = sorter.finish().expect("finish");
648 assert_eq!(sorted.count(), 1_000);
649 assert_eq!(files(), 0);
650 }
651
652 #[test]
653 fn equal_keys_keep_insertion_order_across_runs() {
654 #[derive(Debug, Clone, PartialEq, Eq)]
655 struct Keyed {
656 key: u64,
657 payload: u64,
658 }
659 impl PartialOrd for Keyed {
660 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
661 Some(self.cmp(other))
662 }
663 }
664 impl Ord for Keyed {
665 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
666 self.key.cmp(&other.key)
667 }
668 }
669 impl Spill for Keyed {
670 fn encode(&self, out: &mut Vec<u8>) {
671 put_u64(out, self.key);
672 put_u64(out, self.payload);
673 }
674 fn decode(input: &mut &[u8]) -> Result<Self> {
675 Ok(Self {
676 key: get_u64(input)?,
677 payload: get_u64(input)?,
678 })
679 }
680 }
681
682 let scratch = scratch("stable");
683 let mut sorter = ExternalSorter::new(
684 &scratch,
685 "keyed",
686 &MemoryBudget::unlimited(),
687 7 * std::mem::size_of::<Keyed>(),
688 );
689 for payload in 0..1_000 {
690 sorter
691 .push(Keyed {
692 key: payload % 3,
693 payload,
694 })
695 .expect("push");
696 }
697 let sorted = sorter
698 .finish()
699 .expect("finish")
700 .collect::<Result<Vec<_>>>()
701 .expect("items");
702 for pair in sorted.windows(2) {
703 if pair[0].key == pair[1].key {
704 assert!(pair[0].payload < pair[1].payload);
705 }
706 }
707 }
708
709 #[test]
710 fn spool_replays_in_write_order_and_cleans_up() {
711 let scratch = scratch("spool");
712 let mut spool = Spool::create(&scratch, "items").expect("spool");
713 for item in [(3u64, 1u64), (1, 2), (2, 3)] {
714 spool.write(&item).expect("write");
715 }
716 assert_eq!(spool.written(), 3);
717 let reader = spool.into_reader().expect("reader");
718 let path = reader.path.clone();
719 let items = reader.collect::<Result<Vec<_>>>().expect("items");
720 assert_eq!(items, vec![(3, 1), (1, 2), (2, 3)]);
721 assert!(!path.exists());
722 }
723
724 #[test]
725 fn scratch_directory_is_removed_on_drop() {
726 let scratch = scratch("drop");
727 let path = scratch.path().to_path_buf();
728 let mut spool = Spool::create(&scratch, "items").expect("spool");
729 spool.write(&(1u64, 1u64)).expect("write");
730 drop(spool);
731 drop(scratch);
732 assert!(!path.exists());
733 }
734}