linkrs_query/executor/streaming/spill/
manager.rs1use std::io::{BufWriter, Write};
2use std::path::{Path, PathBuf};
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::Arc;
5
6use linkrs_core::error::QueryError;
7
8use super::config::{SpillConfig, COLLECTOR_SPILL_ROWS_DEFAULT};
9use super::hash_partition::HashPartitionSpiller;
10use super::quota::DiskQuota;
11use super::run_format::{SpilledRun, RUN_HEADER_SIZE};
12use super::run_io::RunWriter;
13
14#[derive(Debug)]
22pub struct SpillManager {
23 pub(crate) config: SpillConfig,
24 pub(crate) base_dir: PathBuf,
25 pub(crate) file_counter: AtomicU64,
26 pub(crate) spill_bytes: Arc<AtomicU64>,
27 pub(crate) disk_quota: DiskQuota,
28}
29
30impl SpillManager {
31 pub fn new(config: SpillConfig, query_id: u64) -> Result<Self, QueryError> {
32 Self::new_with_quota(config, query_id, DiskQuota::default_quota())
33 }
34
35 pub fn new_with_quota(
37 config: SpillConfig,
38 query_id: u64,
39 disk_quota: DiskQuota,
40 ) -> Result<Self, QueryError> {
41 let base = config
42 .temp_dir
43 .clone()
44 .unwrap_or_else(std::env::temp_dir)
45 .join(format!("linkrs_spill_{}", query_id));
46 std::fs::create_dir_all(&base)
47 .map_err(|e| QueryError::execution(format!("Failed to create spill dir: {}", e)))?;
48 Ok(Self {
49 config,
50 base_dir: base,
51 file_counter: AtomicU64::new(0),
52 spill_bytes: Arc::new(AtomicU64::new(0)),
53 disk_quota,
54 })
55 }
56
57 pub fn create_run_writer(&self, schema_fingerprint: u64) -> Result<RunWriter, QueryError> {
63 let id = self.file_counter.fetch_add(1, Ordering::Relaxed);
64 if id >= self.config.max_spill_files as u64 {
65 return Err(QueryError::execution(format!(
66 "spill run: too many spill files (limit {})",
67 self.config.max_spill_files,
68 )));
69 }
70 std::fs::create_dir_all(&self.base_dir)
71 .map_err(|e| QueryError::execution(format!("recreate spill dir: {}", e)))?;
72 let path = self.base_dir.join(format!("run_{:016x}.run", id));
73 let file = std::fs::File::create(&path)
74 .map_err(|e| QueryError::execution(format!("create run file: {}", e)))?;
75
76 let mut writer = BufWriter::new(file);
78 let header_placeholder = [0u8; RUN_HEADER_SIZE as usize];
79 writer
80 .write_all(&header_placeholder)
81 .map_err(|e| QueryError::execution(format!("write run header placeholder: {}", e)))?;
82
83 Ok(RunWriter::new(writer, path, schema_fingerprint))
84 }
85
86 pub fn base_dir(&self) -> &Path {
87 &self.base_dir
88 }
89
90 pub fn config(&self) -> &SpillConfig {
92 &self.config
93 }
94
95 pub fn spilled_bytes(&self) -> u64 {
97 self.spill_bytes.load(Ordering::Relaxed)
98 }
99
100 pub fn collector_spill_threshold(&self) -> u64 {
105 match self.config.collect_spill_rows {
106 None => COLLECTOR_SPILL_ROWS_DEFAULT,
107 Some(0) => u64::MAX,
108 Some(v) => v,
109 }
110 }
111
112 pub fn finalize_run(&self, writer: RunWriter) -> Result<SpilledRun, QueryError> {
120 let path = writer.path().to_path_buf();
121 let run = writer.finalize()?;
122 if let Err(e) = self.disk_quota.try_reserve(run.byte_size) {
123 let _ = std::fs::remove_file(&path);
124 return Err(e);
125 }
126 self.spill_bytes.fetch_add(run.byte_size, Ordering::Relaxed);
127 Ok(run)
128 }
129
130 pub fn disk_quota(&self) -> &DiskQuota {
132 &self.disk_quota
133 }
134
135 pub fn register_cleanup(
137 &self,
138 runtime: &crate::executor::streaming::runtime::ExecutionRuntime,
139 ) {
140 let base = self.base_dir.clone();
141 runtime.on_cleanup(move || {
142 let _ = std::fs::remove_dir_all(&base);
143 });
144 }
145}
146
147pub fn finalize_partitions_with_runtime(
156 mut spiller: HashPartitionSpiller,
157 runtime: Option<&Arc<crate::executor::streaming::runtime::ExecutionRuntime>>,
158) -> Result<Vec<Option<SpilledRun>>, QueryError> {
159 match runtime.and_then(|rt| rt.get_spill_manager()) {
160 Some(sm) => {
161 let runs = spiller.finalize_with_manager(&sm)?;
162 if let Some(rt) = runtime {
163 let stats = rt.columnar_stats();
164 for run in runs.iter().flatten() {
165 stats.record_spill(run.row_count, run.byte_size);
166 }
167 }
168 Ok(runs)
169 }
170 None => {
171 let mut runs = Vec::with_capacity(spiller.writers.len());
172 for writer in spiller.writers.drain(..) {
173 match writer {
174 Some(w) => runs.push(Some(w.finalize()?)),
175 None => runs.push(None),
176 }
177 }
178 Ok(runs)
179 }
180 }
181}
182
183impl Drop for SpillManager {
184 fn drop(&mut self) {
185 let _ = std::fs::remove_dir(&self.base_dir);
186 }
187}