1use hawdb_core::{HawDBError, Result};
18use std::collections::BTreeMap;
19use std::num::NonZeroUsize;
20use std::sync::{Arc, Mutex, MutexGuard};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub enum QueryMemoryClass {
24 PipelineBatch,
25 BlockingState,
26 ExternalRead,
27 SpillStaging,
28 MorselOutput,
29 ResultMaterialization,
30}
31
32impl QueryMemoryClass {
33 pub const fn as_str(self) -> &'static str {
34 match self {
35 Self::PipelineBatch => "pipeline_batch",
36 Self::BlockingState => "blocking_state",
37 Self::ExternalRead => "external_read",
38 Self::SpillStaging => "spill_staging",
39 Self::MorselOutput => "morsel_output",
40 Self::ResultMaterialization => "result_materialization",
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct QueryMemoryClassSnapshot {
47 pub class: QueryMemoryClass,
48 pub used_bytes: usize,
49 pub peak_bytes: usize,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct QueryMemoryLedgerSnapshot {
54 pub budget_bytes: usize,
55 pub used_bytes: usize,
56 pub peak_bytes: usize,
57 pub account_count: usize,
58 pub classes: Vec<QueryMemoryClassSnapshot>,
59}
60
61#[derive(Debug, Clone)]
62pub struct QueryMemoryLedger {
63 inner: Arc<QueryMemoryLedgerInner>,
64}
65
66#[derive(Debug)]
67struct QueryMemoryLedgerInner {
68 budget_bytes: usize,
69 state: Mutex<QueryMemoryLedgerState>,
70}
71
72#[derive(Debug, Default)]
73struct QueryMemoryLedgerState {
74 used_bytes: usize,
75 peak_bytes: usize,
76 next_account_id: u64,
77 accounts: BTreeMap<u64, QueryMemoryAccountState>,
78 classes: BTreeMap<QueryMemoryClass, QueryMemoryClassState>,
79}
80
81#[derive(Debug)]
82struct QueryMemoryAccountState {
83 class: QueryMemoryClass,
84 owner: Arc<str>,
85 budget_bytes: usize,
86 used_bytes: usize,
87 peak_bytes: usize,
88}
89
90#[derive(Debug, Default)]
91struct QueryMemoryClassState {
92 used_bytes: usize,
93 peak_bytes: usize,
94}
95
96impl QueryMemoryLedger {
97 pub fn new(budget_bytes: NonZeroUsize) -> Self {
98 Self {
99 inner: Arc::new(QueryMemoryLedgerInner {
100 budget_bytes: budget_bytes.get(),
101 state: Mutex::new(QueryMemoryLedgerState::default()),
102 }),
103 }
104 }
105
106 pub fn account(
107 &self,
108 class: QueryMemoryClass,
109 owner: impl Into<Arc<str>>,
110 budget_bytes: NonZeroUsize,
111 ) -> QueryMemoryAccount {
112 let owner = owner.into();
114 let mut state = lock_recover(&self.inner.state);
115 let account_id = state.next_account_id;
116 state.next_account_id = state.next_account_id.saturating_add(1);
117 state.accounts.insert(
118 account_id,
119 QueryMemoryAccountState {
120 class,
121 owner,
122 budget_bytes: budget_bytes.get(),
123 used_bytes: 0,
124 peak_bytes: 0,
125 },
126 );
127 QueryMemoryAccount {
128 ledger: self.clone(),
129 account_id,
130 }
131 }
132
133 pub fn snapshot(&self) -> QueryMemoryLedgerSnapshot {
134 let state = lock_recover(&self.inner.state);
135 QueryMemoryLedgerSnapshot {
136 budget_bytes: self.inner.budget_bytes,
137 used_bytes: state.used_bytes,
138 peak_bytes: state.peak_bytes,
139 account_count: state.accounts.len(),
140 classes: state
141 .classes
142 .iter()
143 .map(|(class, state)| QueryMemoryClassSnapshot {
144 class: *class,
145 used_bytes: state.used_bytes,
146 peak_bytes: state.peak_bytes,
147 })
148 .collect(),
149 }
150 }
151
152 fn reserve(&self, account_id: u64, bytes: usize) -> Result<()> {
153 if bytes == 0 {
154 return Ok(());
155 }
156 let mut state = lock_recover(&self.inner.state);
157 let (class, owner, account_budget, account_next) = {
158 let account = state.accounts.get(&account_id).ok_or_else(|| {
159 HawDBError::Execution("query memory account is no longer registered".to_string())
160 })?;
161 let account_next = account.used_bytes.checked_add(bytes).ok_or_else(|| {
162 HawDBError::Execution(format!(
163 "query memory account {} ({}) byte accounting overflow",
164 account.owner,
165 account.class.as_str()
166 ))
167 })?;
168 (
169 account.class,
170 Arc::clone(&account.owner),
171 account.budget_bytes,
172 account_next,
173 )
174 };
175 if account_next > account_budget {
176 return Err(HawDBError::Execution(format!(
177 "query memory account {owner} ({}) would use {account_next} bytes, exceeding its {account_budget}-byte budget",
178 class.as_str()
179 )));
180 }
181 let root_next = state.used_bytes.checked_add(bytes).ok_or_else(|| {
182 HawDBError::Execution(format!(
183 "query memory ledger byte accounting overflow while charging {owner} ({})",
184 class.as_str()
185 ))
186 })?;
187 if root_next > self.inner.budget_bytes {
188 return Err(HawDBError::Execution(format!(
189 "query memory ledger would use {root_next} bytes while charging {owner} ({}), exceeding query_memory_bytes {}",
190 class.as_str(),
191 self.inner.budget_bytes
192 )));
193 }
194
195 state.classes.entry(class).or_default();
197 let account = state
198 .accounts
199 .get_mut(&account_id)
200 .expect("validated query memory account remains registered");
201 account.used_bytes = account_next;
202 account.peak_bytes = account.peak_bytes.max(account_next);
203 state.used_bytes = root_next;
204 state.peak_bytes = state.peak_bytes.max(root_next);
205 let class_state = state
206 .classes
207 .get_mut(&class)
208 .expect("prepared query memory class remains registered");
209 class_state.used_bytes = class_state.used_bytes.saturating_add(bytes);
210 class_state.peak_bytes = class_state.peak_bytes.max(class_state.used_bytes);
211 Ok(())
212 }
213
214 fn can_reserve(&self, account_id: u64, bytes: usize) -> bool {
215 let state = lock_recover(&self.inner.state);
216 let Some(account) = state.accounts.get(&account_id) else {
217 return false;
218 };
219 let Some(account_next) = account.used_bytes.checked_add(bytes) else {
220 return false;
221 };
222 let Some(root_next) = state.used_bytes.checked_add(bytes) else {
223 return false;
224 };
225 account_next <= account.budget_bytes && root_next <= self.inner.budget_bytes
226 }
227
228 fn release(&self, account_id: u64, bytes: usize) {
229 if bytes == 0 {
230 return;
231 }
232 let mut state = lock_recover(&self.inner.state);
233 let Some(account) = state.accounts.get_mut(&account_id) else {
234 return;
235 };
236 let released = bytes.min(account.used_bytes);
237 let class = account.class;
238 account.used_bytes -= released;
239 state.used_bytes = state.used_bytes.saturating_sub(released);
240 if let Some(class_state) = state.classes.get_mut(&class) {
241 class_state.used_bytes = class_state.used_bytes.saturating_sub(released);
242 }
243 }
244
245 fn transfer(
246 &self,
247 source_account_id: u64,
248 source_bytes: usize,
249 target_account_id: u64,
250 target_bytes: usize,
251 ) -> Result<()> {
252 if source_account_id == target_account_id {
253 return Err(HawDBError::Execution(
254 "query memory transfer requires distinct accounts".to_string(),
255 ));
256 }
257 let mut state = lock_recover(&self.inner.state);
258 let (source_class, source_used) = state
259 .accounts
260 .get(&source_account_id)
261 .map(|account| (account.class, account.used_bytes))
262 .ok_or_else(|| {
263 HawDBError::Execution(
264 "source query memory account is no longer registered".to_string(),
265 )
266 })?;
267 if source_bytes > source_used {
268 return Err(HawDBError::Execution(format!(
269 "query memory transfer tried to release {source_bytes} bytes from a source account using {source_used} bytes"
270 )));
271 }
272 let (target_class, target_owner, target_budget, target_used) = state
273 .accounts
274 .get(&target_account_id)
275 .map(|account| {
276 (
277 account.class,
278 Arc::clone(&account.owner),
279 account.budget_bytes,
280 account.used_bytes,
281 )
282 })
283 .ok_or_else(|| {
284 HawDBError::Execution(
285 "target query memory account is no longer registered".to_string(),
286 )
287 })?;
288 let target_next = target_used.checked_add(target_bytes).ok_or_else(|| {
289 HawDBError::Execution(format!(
290 "query memory account {target_owner} ({}) byte accounting overflow",
291 target_class.as_str()
292 ))
293 })?;
294 if target_next > target_budget {
295 return Err(HawDBError::Execution(format!(
296 "query memory account {target_owner} ({}) would use {target_next} bytes, exceeding its {target_budget}-byte budget",
297 target_class.as_str()
298 )));
299 }
300 let root_after_release = state.used_bytes.checked_sub(source_bytes).ok_or_else(|| {
301 HawDBError::Execution(
302 "query memory ledger underflow during ownership transfer".to_string(),
303 )
304 })?;
305 let root_next = root_after_release.checked_add(target_bytes).ok_or_else(|| {
306 HawDBError::Execution(format!(
307 "query memory ledger byte accounting overflow while transferring ownership to {target_owner} ({})",
308 target_class.as_str()
309 ))
310 })?;
311 if root_next > self.inner.budget_bytes {
312 return Err(HawDBError::Execution(format!(
313 "query memory ledger would use {root_next} bytes while transferring ownership to {target_owner} ({}), exceeding query_memory_bytes {}",
314 target_class.as_str(),
315 self.inner.budget_bytes
316 )));
317 }
318
319 state.classes.entry(target_class).or_default();
320 let source = state
321 .accounts
322 .get_mut(&source_account_id)
323 .expect("validated source query memory account remains registered");
324 source.used_bytes -= source_bytes;
325 let target = state
326 .accounts
327 .get_mut(&target_account_id)
328 .expect("validated target query memory account remains registered");
329 target.used_bytes = target_next;
330 target.peak_bytes = target.peak_bytes.max(target_next);
331 state.used_bytes = root_next;
332 state.peak_bytes = state.peak_bytes.max(root_next);
333 if let Some(class_state) = state.classes.get_mut(&source_class) {
334 class_state.used_bytes = class_state.used_bytes.saturating_sub(source_bytes);
335 }
336 let class_state = state
337 .classes
338 .get_mut(&target_class)
339 .expect("prepared target query memory class remains registered");
340 class_state.used_bytes = class_state.used_bytes.saturating_add(target_bytes);
341 class_state.peak_bytes = class_state.peak_bytes.max(class_state.used_bytes);
342 Ok(())
343 }
344}
345
346#[derive(Debug, Clone)]
347pub struct QueryMemoryAccount {
348 ledger: QueryMemoryLedger,
349 account_id: u64,
350}
351
352impl QueryMemoryAccount {
353 pub(crate) fn peak_bytes(&self) -> usize {
354 lock_recover(&self.ledger.inner.state).accounts[&self.account_id].peak_bytes
355 }
356
357 pub(crate) fn sibling(
358 &self,
359 class: QueryMemoryClass,
360 owner: impl Into<Arc<str>>,
361 budget_bytes: NonZeroUsize,
362 ) -> Self {
363 self.ledger.account(class, owner, budget_bytes)
364 }
365
366 pub(crate) fn can_reserve(&self, bytes: usize) -> bool {
367 self.ledger.can_reserve(self.account_id, bytes)
368 }
369
370 pub fn reserve(&self, bytes: usize) -> Result<QueryMemoryLease> {
371 self.ledger.reserve(self.account_id, bytes)?;
372 Ok(QueryMemoryLease {
373 account: self.clone(),
374 bytes,
375 })
376 }
377}
378
379#[derive(Debug)]
380pub struct QueryMemoryLease {
381 account: QueryMemoryAccount,
382 bytes: usize,
383}
384
385impl QueryMemoryLease {
386 pub fn bytes(&self) -> usize {
387 self.bytes
388 }
389
390 pub fn grow(&mut self, bytes: usize) -> Result<()> {
391 self.account
392 .ledger
393 .reserve(self.account.account_id, bytes)?;
394 self.bytes = self.bytes.saturating_add(bytes);
395 Ok(())
396 }
397
398 pub fn shrink(&mut self, bytes: usize) {
399 let released = bytes.min(self.bytes);
400 self.account
401 .ledger
402 .release(self.account.account_id, released);
403 self.bytes -= released;
404 }
405
406 pub fn reset(&mut self) {
407 self.shrink(self.bytes);
408 }
409
410 pub(crate) fn transfer_to(
411 &mut self,
412 source_bytes: usize,
413 target: &mut Self,
414 target_bytes: usize,
415 ) -> Result<()> {
416 if !Arc::ptr_eq(&self.account.ledger.inner, &target.account.ledger.inner) {
417 return Err(HawDBError::Execution(
418 "query memory transfer requires accounts from the same ledger".to_string(),
419 ));
420 }
421 if source_bytes > self.bytes {
422 return Err(HawDBError::Execution(format!(
423 "query memory transfer tried to release {source_bytes} bytes from a {}-byte lease",
424 self.bytes,
425 )));
426 }
427 self.account.ledger.transfer(
428 self.account.account_id,
429 source_bytes,
430 target.account.account_id,
431 target_bytes,
432 )?;
433 self.bytes -= source_bytes;
434 target.bytes = target.bytes.saturating_add(target_bytes);
435 Ok(())
436 }
437}
438
439impl Drop for QueryMemoryLease {
440 fn drop(&mut self) {
441 self.account
442 .ledger
443 .release(self.account.account_id, self.bytes);
444 self.bytes = 0;
445 }
446}
447
448fn lock_recover<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
449 mutex
453 .lock()
454 .unwrap_or_else(|poisoned| poisoned.into_inner())
455}
456
457#[cfg(test)]
458#[path = "memory_ledger_hardening_tests.rs"]
459mod hardening_tests;
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn sibling_accounts_share_one_root_budget() {
467 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(10).unwrap());
468 let left = ledger.account(
469 QueryMemoryClass::BlockingState,
470 "left",
471 NonZeroUsize::new(10).unwrap(),
472 );
473 let right = ledger.account(
474 QueryMemoryClass::PipelineBatch,
475 "right",
476 NonZeroUsize::new(10).unwrap(),
477 );
478 let left_lease = left.reserve(6).unwrap();
479 let error = right.reserve(5).unwrap_err();
480
481 assert!(error.to_string().contains("query_memory_bytes 10"));
482 assert_eq!(ledger.snapshot().used_bytes, 6);
483 drop(left_lease);
484 assert_eq!(ledger.snapshot().used_bytes, 0);
485 }
486
487 #[test]
488 fn capacity_preflight_checks_account_and_root_without_charging() {
489 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(10).unwrap());
490 let left = ledger.account(
491 QueryMemoryClass::BlockingState,
492 "left",
493 NonZeroUsize::new(8).unwrap(),
494 );
495 let right = ledger.account(
496 QueryMemoryClass::PipelineBatch,
497 "right",
498 NonZeroUsize::new(10).unwrap(),
499 );
500
501 assert!(left.can_reserve(8));
502 assert_eq!(ledger.snapshot().used_bytes, 0);
503 let left_lease = left.reserve(6).unwrap();
504 assert!(!left.can_reserve(3));
505 assert!(right.can_reserve(4));
506 assert!(!right.can_reserve(5));
507 assert_eq!(ledger.snapshot().used_bytes, 6);
508
509 drop(left_lease);
510 assert_eq!(ledger.snapshot().used_bytes, 0);
511 }
512
513 #[test]
514 fn derived_sibling_account_keeps_the_original_query_root() {
515 let budget = NonZeroUsize::new(8).unwrap();
516 let ledger = QueryMemoryLedger::new(budget);
517 let state = ledger.account(QueryMemoryClass::BlockingState, "merge", budget);
518 let output = state.sibling(QueryMemoryClass::PipelineBatch, "output", budget);
519 let state_lease = state.reserve(6).unwrap();
520 assert!(output
521 .reserve(3)
522 .unwrap_err()
523 .to_string()
524 .contains("query_memory_bytes 8"));
525 let output_lease = output.reserve(2).unwrap();
526 assert_eq!(ledger.snapshot().used_bytes, 8);
527 drop((state_lease, output_lease));
528 assert_eq!(ledger.snapshot().used_bytes, 0);
529 }
530
531 #[test]
532 fn ownership_transfer_does_not_require_double_root_capacity() {
533 let budget = NonZeroUsize::new(8).unwrap();
534 let ledger = QueryMemoryLedger::new(budget);
535 let source = ledger.account(QueryMemoryClass::BlockingState, "source", budget);
536 let target = ledger.account(QueryMemoryClass::PipelineBatch, "target", budget);
537 let mut source_lease = source.reserve(8).unwrap();
538 let mut target_lease = target.reserve(0).unwrap();
539
540 source_lease.transfer_to(8, &mut target_lease, 8).unwrap();
541
542 assert_eq!(source_lease.bytes(), 0);
543 assert_eq!(target_lease.bytes(), 8);
544 let snapshot = ledger.snapshot();
545 assert_eq!(snapshot.used_bytes, 8);
546 assert_eq!(snapshot.peak_bytes, 8);
547 drop((source_lease, target_lease));
548 assert_eq!(ledger.snapshot().used_bytes, 0);
549 }
550
551 #[test]
552 fn failed_ownership_transfer_preserves_source_charge() {
553 let budget = NonZeroUsize::new(8).unwrap();
554 let ledger = QueryMemoryLedger::new(budget);
555 let source = ledger.account(QueryMemoryClass::BlockingState, "source", budget);
556 let target = ledger.account(QueryMemoryClass::PipelineBatch, "target", budget);
557 let mut source_lease = source.reserve(8).unwrap();
558 let mut target_lease = target.reserve(0).unwrap();
559
560 let error = source_lease
561 .transfer_to(8, &mut target_lease, 9)
562 .unwrap_err();
563
564 assert!(error.to_string().contains("9 bytes"));
565 assert_eq!(source_lease.bytes(), 8);
566 assert_eq!(target_lease.bytes(), 0);
567 assert_eq!(ledger.snapshot().used_bytes, 8);
568 }
569
570 #[test]
571 fn account_budget_is_enforced_before_root_budget() {
572 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(100).unwrap());
573 let account = ledger.account(
574 QueryMemoryClass::BlockingState,
575 "sort",
576 NonZeroUsize::new(4).unwrap(),
577 );
578
579 let error = account.reserve(5).unwrap_err();
580
581 assert!(error.to_string().contains("sort"));
582 assert!(error.to_string().contains("4-byte budget"));
583 assert_eq!(ledger.snapshot().used_bytes, 0);
584 }
585
586 #[test]
587 fn lease_growth_shrink_and_drop_update_hierarchy() {
588 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(100).unwrap());
589 let account = ledger.account(
590 QueryMemoryClass::MorselOutput,
591 "reorder",
592 NonZeroUsize::new(80).unwrap(),
593 );
594 let mut lease = account.reserve(20).unwrap();
595 lease.grow(30).unwrap();
596 lease.shrink(10);
597 assert_eq!(lease.bytes(), 40);
598 assert_eq!(ledger.snapshot().used_bytes, 40);
599
600 drop(lease);
601 let snapshot = ledger.snapshot();
602 assert_eq!(snapshot.used_bytes, 0);
603 assert_eq!(snapshot.peak_bytes, 50);
604 assert_eq!(snapshot.classes[0].peak_bytes, 50);
605 }
606
607 #[test]
608 fn unwind_releases_query_memory_lease() {
609 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(100).unwrap());
610 let account = ledger.account(
611 QueryMemoryClass::SpillStaging,
612 "spill",
613 NonZeroUsize::new(100).unwrap(),
614 );
615 let result = std::panic::catch_unwind(|| {
616 let _lease = account.reserve(80).unwrap();
617 panic!("injected panic");
618 });
619
620 assert!(result.is_err());
621 assert_eq!(ledger.snapshot().used_bytes, 0);
622 }
623}