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