1use hawdb_core::{
18 HawDBError, ResourceBudgetError, ResourceBudgetKind, ResourceBudgetScope, Result,
19};
20use std::collections::BTreeMap;
21use std::num::NonZeroUsize;
22use std::sync::{Arc, Mutex, MutexGuard};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
25pub enum QueryMemoryClass {
26 PipelineBatch,
27 BlockingState,
28 ExternalRead,
29 SpillStaging,
30 MorselOutput,
31 ResultMaterialization,
32}
33
34impl QueryMemoryClass {
35 pub const fn as_str(self) -> &'static str {
36 match self {
37 Self::PipelineBatch => "pipeline_batch",
38 Self::BlockingState => "blocking_state",
39 Self::ExternalRead => "external_read",
40 Self::SpillStaging => "spill_staging",
41 Self::MorselOutput => "morsel_output",
42 Self::ResultMaterialization => "result_materialization",
43 }
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct QueryMemoryClassSnapshot {
49 pub class: QueryMemoryClass,
50 pub used_bytes: usize,
51 pub peak_bytes: usize,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct QueryMemoryLedgerSnapshot {
56 pub budget_bytes: usize,
57 pub used_bytes: usize,
58 pub peak_bytes: usize,
59 pub account_count: usize,
60 pub classes: Vec<QueryMemoryClassSnapshot>,
61}
62
63#[derive(Debug, Clone)]
64pub struct QueryMemoryLedger {
65 inner: Arc<QueryMemoryLedgerInner>,
66 file_descriptors: Option<hawdb_storage::file_descriptors::FileOpenContext>,
67}
68
69#[derive(Debug)]
70struct QueryMemoryLedgerInner {
71 budget_bytes: usize,
72 budget_option: &'static str,
73 state: Mutex<QueryMemoryLedgerState>,
74}
75
76#[derive(Debug, Default)]
77struct QueryMemoryLedgerState {
78 used_bytes: usize,
79 peak_bytes: usize,
80 next_account_id: u64,
81 accounts: BTreeMap<u64, QueryMemoryAccountState>,
82 classes: BTreeMap<QueryMemoryClass, QueryMemoryClassState>,
83}
84
85#[derive(Debug)]
86struct QueryMemoryAccountState {
87 class: QueryMemoryClass,
88 owner: Arc<str>,
89 budget_bytes: usize,
90 budget_option: &'static str,
91 used_bytes: usize,
92 peak_bytes: usize,
93}
94
95#[derive(Debug, Default)]
96struct QueryMemoryClassState {
97 used_bytes: usize,
98 peak_bytes: usize,
99}
100
101impl QueryMemoryAccountState {
102 fn budget_error(
103 &self,
104 scope: ResourceBudgetScope,
105 requested: usize,
106 used: usize,
107 budget: usize,
108 budget_option: &'static str,
109 message: String,
110 ) -> HawDBError {
111 HawDBError::from(ResourceBudgetError {
112 resource: ResourceBudgetKind::MemoryBytes,
113 scope,
114 owner: self.owner.to_string(),
115 memory_class: Some(self.class.as_str()),
116 requested: requested as u64,
117 used: used as u64,
118 budget: budget as u64,
119 budget_option,
120 message,
121 })
122 }
123
124 fn account_budget_error(&self, requested: usize, message: String) -> HawDBError {
125 self.budget_error(
126 ResourceBudgetScope::Account,
127 requested,
128 self.used_bytes,
129 self.budget_bytes,
130 self.budget_option,
131 message,
132 )
133 }
134}
135
136impl QueryMemoryLedger {
137 #[doc(hidden)]
139 pub fn for_query(
140 memory: &crate::ExecutionMemoryConfig,
141 task_context: Option<&hawdb_core::RuntimeTaskContext>,
142 ) -> Result<Self> {
143 Ok(Self::new_with_budget_option(
144 crate::memory::enforced_query_memory_budget(memory, task_context)?,
145 if task_context
146 .and_then(hawdb_core::RuntimeTaskContext::memory_reservation)
147 .is_some()
148 {
149 "RuntimeTaskContext.memory_reservation.memory_bytes"
150 } else {
151 "ExecutionMemoryConfig.query_memory_bytes"
152 },
153 ))
154 }
155
156 pub(crate) fn budget_bytes(&self) -> NonZeroUsize {
157 NonZeroUsize::new(self.inner.budget_bytes).unwrap()
158 }
159
160 pub(crate) fn source_account(
161 &self,
162 owner: &'static str,
163 source_budget: NonZeroUsize,
164 retained_budget: NonZeroUsize,
165 ) -> QueryMemoryAccount {
166 self.source_account_retaining(
167 owner,
168 source_budget,
169 self.account(QueryMemoryClass::BlockingState, owner, retained_budget),
170 )
171 }
172
173 pub(crate) fn source_account_retaining(
174 &self,
175 owner: &'static str,
176 source_budget: NonZeroUsize,
177 retained: QueryMemoryAccount,
178 ) -> QueryMemoryAccount {
179 self.account(QueryMemoryClass::ExternalRead, owner, source_budget)
180 .with_retained_state(retained)
181 }
182
183 pub fn new(budget_bytes: NonZeroUsize) -> Self {
184 Self::new_with_budget_option(budget_bytes, "QueryMemoryLedger::new(budget_bytes)")
185 }
186
187 pub fn new_with_budget_option(budget_bytes: NonZeroUsize, budget_option: &'static str) -> Self {
189 Self {
190 inner: Arc::new(QueryMemoryLedgerInner {
191 budget_bytes: budget_bytes.get(),
192 budget_option,
193 state: Mutex::new(QueryMemoryLedgerState::default()),
194 }),
195 file_descriptors: None,
196 }
197 }
198
199 #[doc(hidden)]
201 pub fn with_file_descriptors(
202 mut self,
203 context: Option<hawdb_storage::file_descriptors::FileOpenContext>,
204 ) -> Self {
205 self.file_descriptors = context.or(self.file_descriptors);
206 self
207 }
208
209 pub(crate) fn file_descriptor_context(
210 &self,
211 ) -> Option<&hawdb_storage::file_descriptors::FileOpenContext> {
212 self.file_descriptors.as_ref()
213 }
214
215 pub fn account(
216 &self,
217 class: QueryMemoryClass,
218 owner: impl Into<Arc<str>>,
219 budget_bytes: NonZeroUsize,
220 ) -> QueryMemoryAccount {
221 self.account_with_budget_option(
222 class,
223 owner,
224 budget_bytes,
225 "QueryMemoryLedger::account(budget_bytes)",
226 )
227 }
228
229 pub fn account_with_budget_option(
231 &self,
232 class: QueryMemoryClass,
233 owner: impl Into<Arc<str>>,
234 budget_bytes: NonZeroUsize,
235 budget_option: &'static str,
236 ) -> QueryMemoryAccount {
237 let owner = owner.into();
239 let mut state = lock_recover(&self.inner.state);
240 let account_id = state.next_account_id;
241 state.next_account_id = state.next_account_id.saturating_add(1);
242 state.accounts.insert(
243 account_id,
244 QueryMemoryAccountState {
245 class,
246 owner,
247 budget_bytes: budget_bytes.get(),
248 budget_option,
249 used_bytes: 0,
250 peak_bytes: 0,
251 },
252 );
253 QueryMemoryAccount {
254 ledger: self.clone(),
255 account_id,
256 backing: None,
257 retained_state: None,
258 }
259 }
260
261 pub fn snapshot(&self) -> QueryMemoryLedgerSnapshot {
262 let state = lock_recover(&self.inner.state);
263 QueryMemoryLedgerSnapshot {
264 budget_bytes: self.inner.budget_bytes,
265 used_bytes: state.used_bytes,
266 peak_bytes: state.peak_bytes,
267 account_count: state.accounts.len(),
268 classes: state
269 .classes
270 .iter()
271 .map(|(class, state)| QueryMemoryClassSnapshot {
272 class: *class,
273 used_bytes: state.used_bytes,
274 peak_bytes: state.peak_bytes,
275 })
276 .collect(),
277 }
278 }
279
280 #[cfg(test)]
282 pub(crate) fn test_owner_used_bytes(&self, class: QueryMemoryClass, owner: &str) -> usize {
283 lock_recover(&self.inner.state)
284 .accounts
285 .values()
286 .filter(|account| account.class == class && account.owner.as_ref() == owner)
287 .map(|account| account.used_bytes)
288 .sum()
289 }
290
291 fn reserve(&self, account_id: u64, bytes: usize) -> Result<()> {
292 if bytes == 0 {
293 return Ok(());
294 }
295 let mut state = lock_recover(&self.inner.state);
296 let account = state.accounts.get(&account_id).ok_or_else(|| {
297 HawDBError::Execution("query memory account is no longer registered".to_string())
298 })?;
299 let class = account.class;
300 let owner = &account.owner;
301 let account_budget = account.budget_bytes;
302 let account_next = account.used_bytes.checked_add(bytes).ok_or_else(|| {
303 account.account_budget_error(
304 bytes,
305 format!(
306 "query memory account {owner} ({}) byte accounting overflow",
307 class.as_str()
308 ),
309 )
310 })?;
311 if account_next > account_budget {
312 return Err(account.account_budget_error(bytes, format!(
313 "query memory account {owner} ({}) would use {account_next} bytes, exceeding its {account_budget}-byte budget",
314 class.as_str()
315 )));
316 }
317 let query_error = |message| {
318 account.budget_error(
319 ResourceBudgetScope::Query,
320 bytes,
321 state.used_bytes,
322 self.inner.budget_bytes,
323 self.inner.budget_option,
324 message,
325 )
326 };
327 let root_next = state.used_bytes.checked_add(bytes).ok_or_else(|| {
328 query_error(format!(
329 "query memory ledger byte accounting overflow while charging {owner} ({})",
330 class.as_str()
331 ))
332 })?;
333 if root_next > self.inner.budget_bytes {
334 return Err(query_error(format!(
335 "query memory ledger would use {root_next} bytes while charging {owner} ({}), exceeding query_memory_bytes {}",
336 class.as_str(),
337 self.inner.budget_bytes
338 )));
339 }
340
341 state.classes.entry(class).or_default();
343 let account = state
344 .accounts
345 .get_mut(&account_id)
346 .expect("validated query memory account remains registered");
347 account.used_bytes = account_next;
348 account.peak_bytes = account.peak_bytes.max(account_next);
349 state.used_bytes = root_next;
350 state.peak_bytes = state.peak_bytes.max(root_next);
351 let class_state = state
352 .classes
353 .get_mut(&class)
354 .expect("prepared query memory class remains registered");
355 class_state.used_bytes = class_state.used_bytes.saturating_add(bytes);
356 class_state.peak_bytes = class_state.peak_bytes.max(class_state.used_bytes);
357 Ok(())
358 }
359
360 fn can_reserve(&self, account_id: u64, bytes: usize) -> bool {
361 let state = lock_recover(&self.inner.state);
362 let Some(account) = state.accounts.get(&account_id) else {
363 return false;
364 };
365 let Some(account_next) = account.used_bytes.checked_add(bytes) else {
366 return false;
367 };
368 let Some(root_next) = state.used_bytes.checked_add(bytes) else {
369 return false;
370 };
371 account_next <= account.budget_bytes && root_next <= self.inner.budget_bytes
372 }
373
374 fn release(&self, account_id: u64, bytes: usize) {
375 if bytes == 0 {
376 return;
377 }
378 let mut state = lock_recover(&self.inner.state);
379 let Some(account) = state.accounts.get_mut(&account_id) else {
380 return;
381 };
382 let released = bytes.min(account.used_bytes);
383 let class = account.class;
384 account.used_bytes -= released;
385 state.used_bytes = state.used_bytes.saturating_sub(released);
386 if let Some(class_state) = state.classes.get_mut(&class) {
387 class_state.used_bytes = class_state.used_bytes.saturating_sub(released);
388 }
389 }
390
391 fn transfer(
392 &self,
393 source_account_id: u64,
394 source_bytes: usize,
395 target_account_id: u64,
396 target_bytes: usize,
397 ) -> Result<()> {
398 if source_account_id == target_account_id {
399 return Err(HawDBError::Execution(
400 "query memory transfer requires distinct accounts".to_string(),
401 ));
402 }
403 let mut state = lock_recover(&self.inner.state);
404 let (source_class, source_used) = state
405 .accounts
406 .get(&source_account_id)
407 .map(|account| (account.class, account.used_bytes))
408 .ok_or_else(|| {
409 HawDBError::Execution(
410 "source query memory account is no longer registered".to_string(),
411 )
412 })?;
413 if source_bytes > source_used {
414 return Err(HawDBError::Execution(format!(
415 "query memory transfer tried to release {source_bytes} bytes from a source account using {source_used} bytes"
416 )));
417 }
418 let target_account = state.accounts.get(&target_account_id).ok_or_else(|| {
419 HawDBError::Execution("target query memory account is no longer registered".to_string())
420 })?;
421 let target_class = target_account.class;
422 let target_owner = &target_account.owner;
423 let target_budget = target_account.budget_bytes;
424 let target_used = target_account.used_bytes;
425 let target_next = target_used.checked_add(target_bytes).ok_or_else(|| {
426 target_account.account_budget_error(
427 target_bytes,
428 format!(
429 "query memory account {target_owner} ({}) byte accounting overflow",
430 target_class.as_str()
431 ),
432 )
433 })?;
434 if target_next > target_budget {
435 return Err(target_account.account_budget_error(target_bytes, format!(
436 "query memory account {target_owner} ({}) would use {target_next} bytes, exceeding its {target_budget}-byte budget",
437 target_class.as_str()
438 )));
439 }
440 let root_after_release = state.used_bytes.checked_sub(source_bytes).ok_or_else(|| {
441 HawDBError::Execution(
442 "query memory ledger underflow during ownership transfer".to_string(),
443 )
444 })?;
445 let query_error = |message| {
446 target_account.budget_error(
447 ResourceBudgetScope::Query,
448 target_bytes.saturating_sub(source_bytes),
449 state.used_bytes,
450 self.inner.budget_bytes,
451 self.inner.budget_option,
452 message,
453 )
454 };
455 let root_next = root_after_release.checked_add(target_bytes).ok_or_else(|| {
456 query_error(format!(
457 "query memory ledger byte accounting overflow while transferring ownership to {target_owner} ({})",
458 target_class.as_str()
459 ))
460 })?;
461 if root_next > self.inner.budget_bytes {
462 return Err(query_error(format!(
463 "query memory ledger would use {root_next} bytes while transferring ownership to {target_owner} ({}), exceeding query_memory_bytes {}",
464 target_class.as_str(),
465 self.inner.budget_bytes
466 )));
467 }
468
469 state.classes.entry(target_class).or_default();
470 let source = state
471 .accounts
472 .get_mut(&source_account_id)
473 .expect("validated source query memory account remains registered");
474 source.used_bytes -= source_bytes;
475 let target = state
476 .accounts
477 .get_mut(&target_account_id)
478 .expect("validated target query memory account remains registered");
479 target.used_bytes = target_next;
480 target.peak_bytes = target.peak_bytes.max(target_next);
481 state.used_bytes = root_next;
482 state.peak_bytes = state.peak_bytes.max(root_next);
483 if let Some(class_state) = state.classes.get_mut(&source_class) {
484 class_state.used_bytes = class_state.used_bytes.saturating_sub(source_bytes);
485 }
486 let class_state = state
487 .classes
488 .get_mut(&target_class)
489 .expect("prepared target query memory class remains registered");
490 class_state.used_bytes = class_state.used_bytes.saturating_add(target_bytes);
491 class_state.peak_bytes = class_state.peak_bytes.max(class_state.used_bytes);
492 Ok(())
493 }
494}
495
496#[derive(Debug, Clone)]
497pub struct QueryMemoryAccount {
498 ledger: QueryMemoryLedger,
499 account_id: u64,
500 backing: Option<Arc<QueryMemoryLease>>,
503 retained_state: Option<Arc<QueryMemoryAccount>>,
504}
505
506impl QueryMemoryAccount {
507 pub(crate) fn query_budget_bytes(&self) -> NonZeroUsize {
508 self.ledger.budget_bytes()
509 }
510
511 pub(crate) fn with_retained_state(mut self, account: Self) -> Self {
512 self.retained_state = Some(Arc::new(account));
513 self
514 }
515
516 pub(crate) fn retained_state(&self) -> Option<&Self> {
517 self.retained_state.as_deref()
518 }
519
520 pub(crate) fn sub_account(&self, budget: NonZeroUsize) -> Result<Self> {
523 let backing = Arc::new(self.reserve(budget.get())?);
524 let (class, owner) = {
525 let state = lock_recover(&self.ledger.inner.state);
526 let account = &state.accounts[&self.account_id];
527 (account.class, Arc::clone(&account.owner))
528 };
529 let origin = "QueryMemoryAccount::sub_account(budget)";
530 let mut child = QueryMemoryLedger::new_with_budget_option(budget, origin)
531 .account_with_budget_option(class, owner, budget, origin);
532 child.backing = Some(backing);
533 Ok(child)
534 }
535
536 pub(crate) fn budget_bytes(&self) -> NonZeroUsize {
537 NonZeroUsize::new(
538 lock_recover(&self.ledger.inner.state).accounts[&self.account_id].budget_bytes,
539 )
540 .unwrap()
541 }
542
543 #[doc(hidden)]
547 pub fn available_bytes(&self) -> usize {
548 let state = lock_recover(&self.ledger.inner.state);
549 let account = &state.accounts[&self.account_id];
550 account.budget_bytes.saturating_sub(account.used_bytes).min(
551 self.ledger
552 .inner
553 .budget_bytes
554 .saturating_sub(state.used_bytes),
555 )
556 }
557
558 pub(crate) fn peak_bytes(&self) -> usize {
559 lock_recover(&self.ledger.inner.state).accounts[&self.account_id].peak_bytes
560 }
561
562 pub(crate) fn query_available_bytes(&self) -> usize {
563 let state = lock_recover(&self.ledger.inner.state);
564 self.ledger
565 .inner
566 .budget_bytes
567 .saturating_sub(state.used_bytes)
568 }
569
570 pub(crate) fn sibling(
571 &self,
572 class: QueryMemoryClass,
573 owner: impl Into<Arc<str>>,
574 budget_bytes: NonZeroUsize,
575 ) -> Self {
576 self.sibling_with_budget_option(
577 class,
578 owner,
579 budget_bytes,
580 "QueryMemoryAccount::sibling(budget_bytes)",
581 )
582 }
583
584 pub(crate) fn sibling_with_budget_option(
585 &self,
586 class: QueryMemoryClass,
587 owner: impl Into<Arc<str>>,
588 budget_bytes: NonZeroUsize,
589 budget_option: &'static str,
590 ) -> Self {
591 let mut account =
592 self.ledger
593 .account_with_budget_option(class, owner, budget_bytes, budget_option);
594 account.backing = self.backing.clone();
595 account
596 }
597
598 pub(crate) fn can_reserve(&self, bytes: usize) -> bool {
599 self.ledger.can_reserve(self.account_id, bytes)
600 }
601
602 pub fn reserve(&self, bytes: usize) -> Result<QueryMemoryLease> {
603 self.ledger.reserve(self.account_id, bytes)?;
604 Ok(QueryMemoryLease {
605 account: self.clone(),
606 bytes,
607 })
608 }
609}
610
611#[derive(Debug)]
612pub struct QueryMemoryLease {
613 account: QueryMemoryAccount,
614 bytes: usize,
615}
616
617impl QueryMemoryLease {
618 pub(crate) fn operator_budget_error(
619 &self,
620 requested: usize,
621 used: usize,
622 budget: usize,
623 budget_option: &'static str,
624 message: String,
625 ) -> HawDBError {
626 let state = lock_recover(&self.account.ledger.inner.state);
627 state.accounts[&self.account.account_id].budget_error(
628 ResourceBudgetScope::Operator,
629 requested,
630 used,
631 budget,
632 budget_option,
633 message,
634 )
635 }
636
637 pub(crate) fn belongs_to(&self, account: &QueryMemoryAccount) -> bool {
638 self.account.account_id == account.account_id
639 && Arc::ptr_eq(&self.account.ledger.inner, &account.ledger.inner)
640 }
641
642 pub fn bytes(&self) -> usize {
643 self.bytes
644 }
645
646 pub(crate) fn split_off(&mut self, bytes: usize) -> Result<Self> {
649 if bytes > self.bytes {
650 return Err(HawDBError::Execution(
651 "query memory lease split exceeds its admitted charge".into(),
652 ));
653 }
654 self.bytes -= bytes;
655 Ok(Self {
656 account: self.account.clone(),
657 bytes,
658 })
659 }
660
661 pub(crate) fn absorb(&mut self, mut other: Self) -> std::result::Result<(), Self> {
664 if self.account.account_id != other.account.account_id
665 || !Arc::ptr_eq(&self.account.ledger.inner, &other.account.ledger.inner)
666 {
667 return Err(other);
668 }
669 self.bytes = self.bytes.saturating_add(other.bytes);
670 other.bytes = 0;
671 Ok(())
672 }
673
674 pub fn grow(&mut self, bytes: usize) -> Result<()> {
675 self.account
676 .ledger
677 .reserve(self.account.account_id, bytes)?;
678 self.bytes = self.bytes.saturating_add(bytes);
679 Ok(())
680 }
681
682 pub fn shrink(&mut self, bytes: usize) {
683 let released = bytes.min(self.bytes);
684 self.account
685 .ledger
686 .release(self.account.account_id, released);
687 self.bytes -= released;
688 }
689
690 pub fn reset(&mut self) {
691 self.shrink(self.bytes);
692 }
693
694 pub(crate) fn transfer_to(
695 &mut self,
696 source_bytes: usize,
697 target: &mut Self,
698 target_bytes: usize,
699 ) -> Result<()> {
700 if !Arc::ptr_eq(&self.account.ledger.inner, &target.account.ledger.inner) {
701 return Err(HawDBError::Execution(
702 "query memory transfer requires accounts from the same ledger".to_string(),
703 ));
704 }
705 if source_bytes > self.bytes {
706 return Err(HawDBError::Execution(format!(
707 "query memory transfer tried to release {source_bytes} bytes from a {}-byte lease",
708 self.bytes,
709 )));
710 }
711 self.account.ledger.transfer(
712 self.account.account_id,
713 source_bytes,
714 target.account.account_id,
715 target_bytes,
716 )?;
717 self.bytes -= source_bytes;
718 target.bytes = target.bytes.saturating_add(target_bytes);
719 Ok(())
720 }
721}
722
723impl Drop for QueryMemoryLease {
724 fn drop(&mut self) {
725 self.account
726 .ledger
727 .release(self.account.account_id, self.bytes);
728 self.bytes = 0;
729 }
730}
731
732fn lock_recover<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
733 mutex
737 .lock()
738 .unwrap_or_else(|poisoned| poisoned.into_inner())
739}
740
741#[cfg(test)]
742#[path = "memory_ledger_hardening_tests.rs"]
743mod hardening_tests;
744
745#[cfg(test)]
746#[path = "memory_ledger_budget_tests.rs"]
747mod budget_tests;
748
749#[cfg(test)]
750mod tests {
751 use super::*;
752
753 #[test]
754 fn splitting_a_full_lease_preserves_charge_and_independent_drop() {
755 let budget = NonZeroUsize::new(8).unwrap();
756 let ledger = QueryMemoryLedger::new(budget);
757 let account = ledger.account(QueryMemoryClass::ResultMaterialization, "retained", budget);
758 let mut owner = account.reserve(8).unwrap();
759 assert!(owner.split_off(9).is_err());
760 assert_eq!(owner.bytes(), 8);
761 let view = owner.split_off(3).unwrap();
762 assert_eq!(owner.bytes(), 5);
763 assert_eq!(view.bytes(), 3);
764 assert_eq!(ledger.snapshot().used_bytes, 8);
765 assert_eq!(ledger.snapshot().peak_bytes, 8);
766 drop(owner);
767 assert_eq!(ledger.snapshot().used_bytes, 3);
768 drop(view);
769 assert_eq!(ledger.snapshot().used_bytes, 0);
770 }
771
772 #[test]
773 fn source_account_reuses_retained_cap_without_registering_an_unused_account() {
774 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(1024).unwrap());
775 let retained = ledger.account(
776 QueryMemoryClass::BlockingState,
777 "shared state",
778 NonZeroUsize::new(512).unwrap(),
779 );
780 let source =
781 ledger.source_account_retaining("source", ledger.budget_bytes(), retained.clone());
782 assert_eq!(ledger.snapshot().account_count, 2);
783 let mut grant = crate::store::admit_graph_read(&source, None, 512).unwrap();
784 grant.retain_state().unwrap();
785 assert!(retained.reserve(1).is_err());
786 assert_eq!(ledger.snapshot().used_bytes, 512);
787 drop(grant);
788 let reused = retained.reserve(512).unwrap();
789 assert_eq!(ledger.snapshot().account_count, 2);
790 drop(reused);
791 assert_eq!(ledger.snapshot().used_bytes, 0);
792 }
793
794 #[test]
795 fn sub_accounts_reserve_parent_capacity_without_double_charging() {
796 let budget = NonZeroUsize::new(100).unwrap();
797 let ledger = QueryMemoryLedger::new(budget);
798 let parent = ledger.account(QueryMemoryClass::BlockingState, "merge", budget);
799 let retained = parent.reserve(10).unwrap();
800 let left = parent.sub_account(NonZeroUsize::new(60).unwrap()).unwrap();
801 let right = parent.sub_account(NonZeroUsize::new(30).unwrap()).unwrap();
802 assert_eq!(parent.available_bytes(), 0);
803 assert!(parent.sub_account(NonZeroUsize::MIN).is_err());
804 let mut left_lease = left.reserve(60).unwrap();
805 let right_lease = right.reserve(30).unwrap();
806 assert!(left_lease.grow(1).is_err());
807 assert_eq!(ledger.snapshot().used_bytes, 100);
808 drop((left, right, retained));
809 assert_eq!(ledger.snapshot().used_bytes, 90);
810 drop(left_lease);
811 assert_eq!(ledger.snapshot().used_bytes, 30);
812 drop(right_lease);
813 assert_eq!(ledger.snapshot().used_bytes, 0);
814 assert_eq!(ledger.snapshot().peak_bytes, 100);
815 }
816
817 #[test]
818 fn nested_accounts_and_siblings_keep_backing_until_last_owner_drops() {
819 let budget = NonZeroUsize::new(8).unwrap();
820 let ledger = QueryMemoryLedger::new(budget);
821 let parent = ledger.account(QueryMemoryClass::BlockingState, "merge", budget);
822 let child = parent.sub_account(budget).unwrap();
823 let sibling = child.sibling(QueryMemoryClass::SpillStaging, "staging", budget);
824 let nested = child.sub_account(NonZeroUsize::new(4).unwrap()).unwrap();
825 assert!(sibling.reserve(5).is_err());
826 let lease = nested.reserve(4).unwrap();
827 drop((parent, child, nested));
828 assert_eq!(ledger.snapshot().used_bytes, 8);
829 drop(lease);
830 assert_eq!(ledger.snapshot().used_bytes, 8);
831 let sibling_lease = sibling.reserve(8).unwrap();
832 drop(sibling);
833 assert_eq!(ledger.snapshot().used_bytes, 8);
834 drop(sibling_lease);
835 assert_eq!(ledger.snapshot().used_bytes, 0);
836 }
837
838 #[test]
839 fn failed_sub_account_admission_does_not_leak_root_capacity() {
840 let budget = NonZeroUsize::new(100).unwrap();
841 let ledger = QueryMemoryLedger::new(budget);
842 let parent = ledger.account(QueryMemoryClass::BlockingState, "merge", budget);
843 let other = ledger.account(QueryMemoryClass::PipelineBatch, "input", budget);
844 let retained = other.reserve(70).unwrap();
845 assert!(parent.sub_account(NonZeroUsize::new(31).unwrap()).is_err());
846 assert_eq!(ledger.snapshot().used_bytes, 70);
847 let child = parent.sub_account(NonZeroUsize::new(30).unwrap()).unwrap();
848 assert!(child.reserve(31).is_err());
849 drop((child, retained));
850 assert_eq!(ledger.snapshot().used_bytes, 0);
851 }
852
853 #[test]
854 fn sibling_accounts_share_one_root_budget() {
855 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(10).unwrap());
856 let left = ledger.account(
857 QueryMemoryClass::BlockingState,
858 "left",
859 NonZeroUsize::new(10).unwrap(),
860 );
861 let right = ledger.account(
862 QueryMemoryClass::PipelineBatch,
863 "right",
864 NonZeroUsize::new(10).unwrap(),
865 );
866 let left_lease = left.reserve(6).unwrap();
867 let error = right.reserve(5).unwrap_err();
868
869 assert!(error.to_string().contains("query_memory_bytes 10"));
870 assert_eq!(ledger.snapshot().used_bytes, 6);
871 drop(left_lease);
872 assert_eq!(ledger.snapshot().used_bytes, 0);
873 }
874
875 #[test]
876 fn capacity_preflight_checks_account_and_root_without_charging() {
877 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(10).unwrap());
878 let left = ledger.account(
879 QueryMemoryClass::BlockingState,
880 "left",
881 NonZeroUsize::new(8).unwrap(),
882 );
883 let right = ledger.account(
884 QueryMemoryClass::PipelineBatch,
885 "right",
886 NonZeroUsize::new(10).unwrap(),
887 );
888
889 assert!(left.can_reserve(8));
890 assert_eq!(ledger.snapshot().used_bytes, 0);
891 let left_lease = left.reserve(6).unwrap();
892 assert!(!left.can_reserve(3));
893 assert!(right.can_reserve(4));
894 assert!(!right.can_reserve(5));
895 assert_eq!(ledger.snapshot().used_bytes, 6);
896
897 drop(left_lease);
898 assert_eq!(ledger.snapshot().used_bytes, 0);
899 }
900
901 #[test]
902 fn derived_sibling_account_keeps_the_original_query_root() {
903 let budget = NonZeroUsize::new(8).unwrap();
904 let ledger = QueryMemoryLedger::new(budget);
905 let state = ledger.account(QueryMemoryClass::BlockingState, "merge", budget);
906 let output = state.sibling(QueryMemoryClass::PipelineBatch, "output", budget);
907 let state_lease = state.reserve(6).unwrap();
908 assert!(output
909 .reserve(3)
910 .unwrap_err()
911 .to_string()
912 .contains("query_memory_bytes 8"));
913 let output_lease = output.reserve(2).unwrap();
914 assert_eq!(ledger.snapshot().used_bytes, 8);
915 drop((state_lease, output_lease));
916 assert_eq!(ledger.snapshot().used_bytes, 0);
917 }
918
919 #[test]
920 fn ownership_transfer_does_not_require_double_root_capacity() {
921 let budget = NonZeroUsize::new(8).unwrap();
922 let ledger = QueryMemoryLedger::new(budget);
923 let source = ledger.account(QueryMemoryClass::BlockingState, "source", budget);
924 let target = ledger.account(QueryMemoryClass::PipelineBatch, "target", budget);
925 let mut source_lease = source.reserve(8).unwrap();
926 let mut target_lease = target.reserve(0).unwrap();
927
928 source_lease.transfer_to(8, &mut target_lease, 8).unwrap();
929
930 assert_eq!(source_lease.bytes(), 0);
931 assert_eq!(target_lease.bytes(), 8);
932 let snapshot = ledger.snapshot();
933 assert_eq!(snapshot.used_bytes, 8);
934 assert_eq!(snapshot.peak_bytes, 8);
935 drop((source_lease, target_lease));
936 assert_eq!(ledger.snapshot().used_bytes, 0);
937 }
938
939 #[test]
940 fn failed_ownership_transfer_preserves_source_charge() {
941 let budget = NonZeroUsize::new(8).unwrap();
942 let ledger = QueryMemoryLedger::new(budget);
943 let source = ledger.account(QueryMemoryClass::BlockingState, "source", budget);
944 let target = ledger.account(QueryMemoryClass::PipelineBatch, "target", budget);
945 let mut source_lease = source.reserve(8).unwrap();
946 let mut target_lease = target.reserve(0).unwrap();
947
948 let error = source_lease
949 .transfer_to(8, &mut target_lease, 9)
950 .unwrap_err();
951
952 assert!(error.to_string().contains("9 bytes"));
953 assert_eq!(source_lease.bytes(), 8);
954 assert_eq!(target_lease.bytes(), 0);
955 assert_eq!(ledger.snapshot().used_bytes, 8);
956 }
957
958 #[test]
959 fn account_budget_is_enforced_before_root_budget() {
960 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(100).unwrap());
961 let account = ledger.account(
962 QueryMemoryClass::BlockingState,
963 "sort",
964 NonZeroUsize::new(4).unwrap(),
965 );
966
967 let error = account.reserve(5).unwrap_err();
968
969 assert!(error.to_string().contains("sort"));
970 assert!(error.to_string().contains("4-byte budget"));
971 assert_eq!(ledger.snapshot().used_bytes, 0);
972 }
973
974 #[test]
975 fn lease_growth_shrink_and_drop_update_hierarchy() {
976 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(100).unwrap());
977 let account = ledger.account(
978 QueryMemoryClass::MorselOutput,
979 "reorder",
980 NonZeroUsize::new(80).unwrap(),
981 );
982 let mut lease = account.reserve(20).unwrap();
983 lease.grow(30).unwrap();
984 lease.shrink(10);
985 assert_eq!(lease.bytes(), 40);
986 assert_eq!(ledger.snapshot().used_bytes, 40);
987
988 drop(lease);
989 let snapshot = ledger.snapshot();
990 assert_eq!(snapshot.used_bytes, 0);
991 assert_eq!(snapshot.peak_bytes, 50);
992 assert_eq!(snapshot.classes[0].peak_bytes, 50);
993 }
994
995 #[test]
996 fn unwind_releases_query_memory_lease() {
997 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(100).unwrap());
998 let account = ledger.account(
999 QueryMemoryClass::SpillStaging,
1000 "spill",
1001 NonZeroUsize::new(100).unwrap(),
1002 );
1003 let result = std::panic::catch_unwind(|| {
1004 let _lease = account.reserve(80).unwrap();
1005 panic!("injected panic");
1006 });
1007
1008 assert!(result.is_err());
1009 assert_eq!(ledger.snapshot().used_bytes, 0);
1010 }
1011}