1use std::{
4 sync::{Arc, Condvar, Mutex, PoisonError},
5 time::{Duration, Instant},
6};
7
8use query_planning::error::{QueryError, QueryErrorCode, Result};
9
10use super::ExecutionCancellation;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub struct LakehouseCacheFillConfig {
15 pub budget_bytes: Option<u64>,
19 pub acquire_timeout: Duration,
21 pub max_concurrent_ops: usize,
23}
24
25impl LakehouseCacheFillConfig {
26 pub(crate) const DEFAULT_BUDGET_BYTES: u64 = 1024 * 1024 * 1024;
27 pub(crate) const DEFAULT_BUDGET_CAP_BYTES: u64 = 16 * 1024 * 1024 * 1024;
28 pub const DEFAULT_ACQUIRE_TIMEOUT: Duration = Duration::from_secs(30);
30 pub(crate) const DEFAULT_MAX_CONCURRENT_OPS: usize = 4;
31
32 pub fn validate(&self) -> Result<()> {
36 if let Some(budget_bytes) = self.budget_bytes {
37 validate_budget_bytes(budget_bytes)?;
38 }
39 if self.acquire_timeout.is_zero() {
40 return Err(QueryError::new(
41 QueryErrorCode::InvalidConfiguration,
42 "query_engine.lakehouse_cache.fill_config.validate",
43 "lakehouse cache fill acquire timeout must be greater than zero",
44 )
45 .with_fact("field", "lakehouse_cache_fill_acquire_timeout")
46 .with_fact("reason", "zero"));
47 }
48 if self.max_concurrent_ops == 0 {
49 return Err(QueryError::new(
50 QueryErrorCode::InvalidConfiguration,
51 "query_engine.lakehouse_cache.fill_config.validate",
52 "lakehouse cache fill max concurrent operations must be greater than zero",
53 )
54 .with_fact("field", "lakehouse_cache_fill_max_concurrent_ops")
55 .with_fact("reason", "zero"));
56 }
57 Ok(())
58 }
59}
60
61impl Default for LakehouseCacheFillConfig {
62 fn default() -> Self {
63 Self {
64 budget_bytes: None,
65 acquire_timeout: Self::DEFAULT_ACQUIRE_TIMEOUT,
66 max_concurrent_ops: Self::DEFAULT_MAX_CONCURRENT_OPS,
67 }
68 }
69}
70
71pub(crate) fn default_lakehouse_cache_fill_budget_bytes() -> u64 {
72 default_lakehouse_cache_fill_budget_bytes_from_physical_memory(detect_physical_memory_bytes())
73}
74
75pub(super) fn default_lakehouse_cache_fill_budget_bytes_from_physical_memory(
76 physical_memory_bytes: Option<u64>,
77) -> u64 {
78 physical_memory_bytes
79 .and_then(|bytes| bytes.checked_div(4))
80 .map(|bytes| bytes.min(LakehouseCacheFillConfig::DEFAULT_BUDGET_CAP_BYTES))
81 .filter(|bytes| *bytes > 0)
82 .unwrap_or(LakehouseCacheFillConfig::DEFAULT_BUDGET_BYTES)
83}
84
85fn detect_physical_memory_bytes() -> Option<u64> {
86 let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
87 let line = meminfo.lines().find(|line| line.starts_with("MemTotal:"))?;
88 let kib = line.split_whitespace().nth(1)?.parse::<u64>().ok()?;
89 kib.checked_mul(1024)
90}
91
92#[derive(Debug)]
93struct CacheFillByteBudgetInner {
94 budget_bytes: u64,
95 state: Mutex<CacheFillByteBudgetState>,
96 available: Condvar,
97}
98
99#[derive(Debug, Default)]
100struct CacheFillByteBudgetState {
101 in_flight_bytes: u64,
102}
103
104#[derive(Debug, Clone)]
106pub struct CacheFillByteBudget {
107 inner: Arc<CacheFillByteBudgetInner>,
108}
109
110impl CacheFillByteBudget {
111 pub fn new(budget_bytes: u64) -> Result<Self> {
112 validate_budget_bytes(budget_bytes)?;
113 Ok(Self {
114 inner: Arc::new(CacheFillByteBudgetInner {
115 budget_bytes,
116 state: Mutex::new(CacheFillByteBudgetState::default()),
117 available: Condvar::new(),
118 }),
119 })
120 }
121
122 pub fn budget_bytes(&self) -> u64 {
123 self.inner.budget_bytes
124 }
125
126 #[cfg(test)]
127 pub(crate) fn try_acquire(&self, bytes: u64) -> Result<Option<CacheFillBytePermit>> {
128 validate_request_bytes(bytes, self.budget_bytes())?;
129 let mut state = self.lock_state("query_engine.lakehouse_cache.fill_budget.try_acquire")?;
130 if !bytes_fit(state.in_flight_bytes, bytes, self.budget_bytes()) {
131 return Ok(None);
132 }
133 state.in_flight_bytes = state.in_flight_bytes.saturating_add(bytes);
134 Ok(Some(CacheFillBytePermit { budget: self.clone(), bytes }))
135 }
136
137 pub fn acquire_with_timeout(
138 &self,
139 cancellation: &ExecutionCancellation,
140 bytes: u64,
141 timeout: Duration,
142 operation: &'static str,
143 ) -> Result<Option<CacheFillBytePermit>> {
144 validate_request_bytes(bytes, self.budget_bytes())?;
145 let start = Instant::now();
146 let mut state = self.lock_state(operation)?;
147 loop {
148 cancellation.check(operation)?;
149 if bytes_fit(state.in_flight_bytes, bytes, self.budget_bytes()) {
150 state.in_flight_bytes = state.in_flight_bytes.saturating_add(bytes);
151 return Ok(Some(CacheFillBytePermit { budget: self.clone(), bytes }));
152 }
153 let Some(remaining) = timeout.checked_sub(start.elapsed()) else {
154 return Ok(None);
155 };
156 let wait = remaining.min(Duration::from_millis(100));
157 let (next_state, _) = self.inner.available.wait_timeout(state, wait).map_err(|_| {
158 QueryError::new(
159 QueryErrorCode::Internal,
160 operation,
161 "lakehouse cache fill byte-budget mutex was poisoned while waiting",
162 )
163 })?;
164 state = next_state;
165 }
166 }
167
168 fn try_grow_permit(&self, permit: &mut CacheFillBytePermit, new_bytes: u64) -> Result<bool> {
169 if new_bytes <= permit.bytes {
170 return Ok(true);
171 }
172 validate_request_bytes(new_bytes, self.budget_bytes())?;
173 let delta = new_bytes - permit.bytes;
174 let mut state = self.lock_state("query_engine.lakehouse_cache.fill_budget.grow")?;
175 if !bytes_fit(state.in_flight_bytes, delta, self.budget_bytes()) {
180 return Ok(false);
181 }
182 state.in_flight_bytes = state.in_flight_bytes.saturating_add(delta);
183 permit.bytes = new_bytes;
184 Ok(true)
185 }
186
187 fn release(&self, bytes: u64) {
188 let mut state = self.inner.state.lock().unwrap_or_else(PoisonError::into_inner);
190 state.in_flight_bytes = state.in_flight_bytes.saturating_sub(bytes);
191 self.inner.available.notify_all();
192 }
193
194 fn lock_state(&self, operation: &'static str) -> Result<std::sync::MutexGuard<'_, CacheFillByteBudgetState>> {
195 self.inner.state.lock().map_err(|_| {
196 QueryError::new(QueryErrorCode::Internal, operation, "lakehouse cache fill byte-budget mutex was poisoned")
197 })
198 }
199}
200
201impl Default for CacheFillByteBudget {
202 fn default() -> Self {
203 Self::new(default_lakehouse_cache_fill_budget_bytes()).expect("default cache fill byte budget is non-zero")
204 }
205}
206
207#[derive(Debug)]
209pub struct CacheFillBytePermit {
210 budget: CacheFillByteBudget,
211 bytes: u64,
212}
213
214impl CacheFillBytePermit {
215 pub fn bytes(&self) -> u64 {
216 self.bytes
217 }
218
219 pub fn try_grow(&mut self, new_bytes: u64) -> Result<bool> {
223 let budget = self.budget.clone();
224 budget.try_grow_permit(self, new_bytes)
225 }
226}
227
228impl Drop for CacheFillBytePermit {
229 fn drop(&mut self) {
230 self.budget.release(self.bytes);
231 }
232}
233
234#[derive(Debug)]
235struct CacheFillConcurrencyGateInner {
236 max_concurrent_ops: usize,
237 state: Mutex<CacheFillConcurrencyGateState>,
238 available: Condvar,
239}
240
241#[derive(Debug, Default)]
242struct CacheFillConcurrencyGateState {
243 in_flight_ops: usize,
244}
245
246#[derive(Debug, Clone)]
247pub(super) struct CacheFillConcurrencyGate {
248 inner: Arc<CacheFillConcurrencyGateInner>,
249}
250
251impl CacheFillConcurrencyGate {
252 pub(super) fn new(max_concurrent_ops: usize) -> Result<Self> {
253 let config = LakehouseCacheFillConfig { max_concurrent_ops, ..Default::default() };
254 config.validate()?;
255 Ok(Self {
256 inner: Arc::new(CacheFillConcurrencyGateInner {
257 max_concurrent_ops,
258 state: Mutex::new(CacheFillConcurrencyGateState::default()),
259 available: Condvar::new(),
260 }),
261 })
262 }
263
264 pub(super) fn max_concurrent_ops(&self) -> usize {
265 self.inner.max_concurrent_ops
266 }
267
268 pub(super) fn acquire_with_timeout(
269 &self,
270 cancellation: &ExecutionCancellation,
271 timeout: Duration,
272 operation: &'static str,
273 ) -> Result<Option<CacheFillOperationPermit>> {
274 let start = Instant::now();
275 let mut state = self.lock_state(operation)?;
276 loop {
277 cancellation.check(operation)?;
278 if state.in_flight_ops < self.max_concurrent_ops() {
279 state.in_flight_ops = state.in_flight_ops.saturating_add(1);
280 return Ok(Some(CacheFillOperationPermit { gate: self.clone() }));
281 }
282 let Some(remaining) = timeout.checked_sub(start.elapsed()) else {
283 return Ok(None);
284 };
285 let wait = remaining.min(Duration::from_millis(100));
286 let (next_state, _) = self.inner.available.wait_timeout(state, wait).map_err(|_| {
287 QueryError::new(
288 QueryErrorCode::Internal,
289 operation,
290 "lakehouse cache fill concurrency mutex was poisoned while waiting",
291 )
292 })?;
293 state = next_state;
294 }
295 }
296
297 fn release(&self) {
298 let mut state = self.inner.state.lock().unwrap_or_else(PoisonError::into_inner);
299 state.in_flight_ops = state.in_flight_ops.saturating_sub(1);
300 self.inner.available.notify_one();
301 }
302
303 fn lock_state(&self, operation: &'static str) -> Result<std::sync::MutexGuard<'_, CacheFillConcurrencyGateState>> {
304 self.inner.state.lock().map_err(|_| {
305 QueryError::new(QueryErrorCode::Internal, operation, "lakehouse cache fill concurrency mutex was poisoned")
306 })
307 }
308}
309
310pub struct CacheFillOperationPermit {
311 gate: CacheFillConcurrencyGate,
312}
313
314impl Drop for CacheFillOperationPermit {
315 fn drop(&mut self) {
316 self.gate.release();
317 }
318}
319
320fn bytes_fit(in_flight_bytes: u64, requested_bytes: u64, budget_bytes: u64) -> bool {
321 in_flight_bytes.saturating_add(requested_bytes) <= budget_bytes
322}
323
324fn validate_budget_bytes(budget_bytes: u64) -> Result<()> {
325 if budget_bytes == 0 {
326 return Err(QueryError::new(
327 QueryErrorCode::InvalidConfiguration,
328 "query_engine.lakehouse_cache.fill_budget.validate",
329 "lakehouse cache fill budget bytes must be greater than zero",
330 )
331 .with_fact("field", "lakehouse_cache_fill_budget_bytes")
332 .with_fact("reason", "zero"));
333 }
334 Ok(())
335}
336
337fn validate_request_bytes(requested_bytes: u64, budget_bytes: u64) -> Result<()> {
338 if requested_bytes == 0 {
339 return Err(QueryError::new(
340 QueryErrorCode::InvalidPlan,
341 "query_engine.lakehouse_cache.fill_budget.request",
342 "lakehouse cache fill request bytes must be greater than zero",
343 )
344 .with_fact("field", "lakehouse_cache_fill_request_bytes")
345 .with_fact("reason", "zero"));
346 }
347 if requested_bytes > budget_bytes {
348 return Err(QueryError::new(
349 QueryErrorCode::MemoryReservationFailed,
350 "query_engine.lakehouse_cache.fill_budget.request",
351 "lakehouse cache fill exceeds its configured byte budget",
352 )
353 .with_fact("requested_bytes", requested_bytes)
354 .with_fact("budget_bytes", budget_bytes));
355 }
356 Ok(())
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn cache_fill_byte_budget_try_acquire_does_not_wait_when_capacity_is_held() {
365 let budget = CacheFillByteBudget::new(10).unwrap();
366 let held = budget.try_acquire(10).unwrap().unwrap();
367 assert!(budget.try_acquire(1).unwrap().is_none());
368 drop(held);
369 assert!(budget.try_acquire(1).unwrap().is_some());
370 }
371
372 #[test]
373 fn permits_return_their_share_after_the_state_mutex_is_poisoned() {
374 fn poison<T: Send>(mutex: &Mutex<T>) {
375 std::thread::scope(|scope| {
376 scope
377 .spawn(|| {
378 let _state = mutex.lock().unwrap();
379 panic!("poison the state mutex");
380 })
381 .join()
382 .expect_err("the poisoning thread panics while holding the lock");
383 });
384 }
385
386 let budget = CacheFillByteBudget::new(10).unwrap();
387 let gate = CacheFillConcurrencyGate::new(1).unwrap();
388 let bytes = budget.try_acquire(10).unwrap().unwrap();
389 let operation =
390 gate.acquire_with_timeout(&ExecutionCancellation::new(), Duration::from_secs(1), "test").unwrap().unwrap();
391 poison(&budget.inner.state);
392 poison(&gate.inner.state);
393
394 drop(bytes);
395 drop(operation);
396
397 let in_flight_bytes = budget.inner.state.lock().unwrap_or_else(PoisonError::into_inner).in_flight_bytes;
398 let in_flight_ops = gate.inner.state.lock().unwrap_or_else(PoisonError::into_inner).in_flight_ops;
399 assert_eq!(in_flight_bytes, 0, "the byte budget is returned");
400 assert_eq!(in_flight_ops, 0, "the concurrency slot is returned");
401 }
402}