alopex_core/sql/
stream.rs1use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use crate::{Error, Result};
7
8pub const DEFAULT_SPILL_THRESHOLD_BYTES: u64 = 256 * 1024 * 1024;
10
11#[derive(Clone, Debug)]
13pub enum SpillPolicy {
14 FailFast,
16 SpillToDisk {
18 directory: PathBuf,
20 },
21}
22
23pub trait SpillMetricsSink: Send + Sync {
25 fn record_spill(&self, bytes: u64, files: u64);
27}
28
29pub trait ByteSized {
31 fn estimated_bytes(&self) -> u64;
33}
34
35#[derive(Clone)]
37pub struct MemoryPolicy {
38 limit_bytes: Option<u64>,
39 spill_policy: SpillPolicy,
40 metrics: Option<Arc<dyn SpillMetricsSink>>,
41}
42
43impl MemoryPolicy {
44 pub fn new(limit_bytes: Option<u64>, spill_policy: SpillPolicy) -> Self {
46 Self {
47 limit_bytes,
48 spill_policy,
49 metrics: None,
50 }
51 }
52
53 pub fn limit_bytes(&self) -> Option<u64> {
55 self.limit_bytes
56 }
57
58 pub fn spill_policy(&self) -> &SpillPolicy {
60 &self.spill_policy
61 }
62
63 pub fn with_metrics(mut self, metrics: Arc<dyn SpillMetricsSink>) -> Self {
65 self.metrics = Some(metrics);
66 self
67 }
68
69 pub fn spill_directory(&self) -> Option<&Path> {
71 match &self.spill_policy {
72 SpillPolicy::SpillToDisk { directory } => Some(directory.as_path()),
73 SpillPolicy::FailFast => None,
74 }
75 }
76
77 pub fn record_spill(&self, bytes: u64, files: u64) {
79 if let Some(metrics) = &self.metrics {
80 metrics.record_spill(bytes, files);
81 }
82 }
83
84 pub fn over_limit(&self, used_bytes: u64) -> bool {
86 self.limit_bytes
87 .map(|limit| used_bytes > limit)
88 .unwrap_or(false)
89 }
90
91 pub fn enforce(&self, used_bytes: u64) -> Result<()> {
93 let Some(limit) = self.limit_bytes else {
94 return Ok(());
95 };
96 if used_bytes <= limit {
97 return Ok(());
98 }
99 match &self.spill_policy {
100 SpillPolicy::FailFast => Err(Error::MemoryLimitExceeded {
101 limit: saturating_usize(limit),
102 requested: saturating_usize(used_bytes),
103 }),
104 SpillPolicy::SpillToDisk { .. } => Ok(()),
105 }
106 }
107}
108
109impl std::fmt::Debug for MemoryPolicy {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 f.debug_struct("MemoryPolicy")
112 .field("limit_bytes", &self.limit_bytes)
113 .field("spill_policy", &self.spill_policy)
114 .finish()
115 }
116}
117
118#[derive(Clone, Debug)]
120pub struct MemoryTracker {
121 policy: MemoryPolicy,
122 used_bytes: u64,
123}
124
125impl MemoryTracker {
126 pub fn new(policy: MemoryPolicy) -> Self {
128 Self {
129 policy,
130 used_bytes: 0,
131 }
132 }
133
134 pub fn used_bytes(&self) -> u64 {
136 self.used_bytes
137 }
138
139 pub fn policy(&self) -> &MemoryPolicy {
141 &self.policy
142 }
143
144 pub fn over_limit(&self) -> bool {
146 self.policy.over_limit(self.used_bytes)
147 }
148
149 pub fn reset(&mut self) {
151 self.used_bytes = 0;
152 }
153
154 pub fn add_bytes(&mut self, bytes: u64) -> Result<()> {
156 self.used_bytes = self.used_bytes.saturating_add(bytes);
157 self.policy.enforce(self.used_bytes)
158 }
159
160 pub fn add_row<T: ByteSized>(&mut self, values: &[T]) -> Result<()> {
162 self.add_values(values)
163 }
164
165 pub fn add_values<T: ByteSized>(&mut self, values: &[T]) -> Result<()> {
167 let bytes: u64 = values.iter().map(ByteSized::estimated_bytes).sum();
168 self.add_bytes(bytes)
169 }
170
171 pub fn add_value<T: ByteSized>(&mut self, value: &T) -> Result<()> {
173 self.add_bytes(value.estimated_bytes())
174 }
175}
176
177fn saturating_usize(value: u64) -> usize {
178 usize::try_from(value).unwrap_or(usize::MAX)
179}
180
181#[cfg(all(test, not(target_arch = "wasm32")))]
182mod tests {
183 use super::{ByteSized, MemoryPolicy, MemoryTracker, SpillPolicy};
184 use crate::Error;
185
186 #[derive(Clone)]
187 struct TestValue(u64);
188
189 impl ByteSized for TestValue {
190 fn estimated_bytes(&self) -> u64 {
191 self.0
192 }
193 }
194
195 #[test]
196 fn memory_tracker_detects_fail_fast_limit_exceeded() {
197 let policy = MemoryPolicy::new(Some(10), SpillPolicy::FailFast);
198 let mut tracker = MemoryTracker::new(policy);
199
200 tracker.add_value(&TestValue(6)).unwrap();
201 let err = tracker.add_value(&TestValue(5)).unwrap_err();
202
203 assert!(tracker.over_limit());
204 assert_eq!(tracker.used_bytes(), 11);
205 assert!(matches!(
206 err,
207 Error::MemoryLimitExceeded {
208 limit: 10,
209 requested: 11
210 }
211 ));
212 }
213
214 #[test]
215 fn spill_to_disk_policy_allows_limit_exceeded_until_caller_spills() {
216 let dir = tempfile::tempdir().unwrap();
217 let policy = MemoryPolicy::new(
218 Some(4),
219 SpillPolicy::SpillToDisk {
220 directory: dir.path().to_path_buf(),
221 },
222 );
223 let mut tracker = MemoryTracker::new(policy);
224
225 tracker.add_values(&[TestValue(2), TestValue(3)]).unwrap();
226
227 assert!(tracker.over_limit());
228 assert_eq!(tracker.used_bytes(), 5);
229 tracker.reset();
230 assert_eq!(tracker.used_bytes(), 0);
231 }
232}