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 file_descriptors: Option<hawdb_storage::file_descriptors::FileOpenContext>,
65}
66
67#[derive(Debug)]
68struct QueryMemoryLedgerInner {
69 budget_bytes: usize,
70 state: Mutex<QueryMemoryLedgerState>,
71}
72
73#[derive(Debug, Default)]
74struct QueryMemoryLedgerState {
75 used_bytes: usize,
76 peak_bytes: usize,
77 next_account_id: u64,
78 accounts: BTreeMap<u64, QueryMemoryAccountState>,
79 classes: BTreeMap<QueryMemoryClass, QueryMemoryClassState>,
80}
81
82#[derive(Debug)]
83struct QueryMemoryAccountState {
84 class: QueryMemoryClass,
85 owner: Arc<str>,
86 budget_bytes: usize,
87 used_bytes: usize,
88 peak_bytes: usize,
89}
90
91#[derive(Debug, Default)]
92struct QueryMemoryClassState {
93 used_bytes: usize,
94 peak_bytes: usize,
95}
96
97impl QueryMemoryLedger {
98 pub fn new(budget_bytes: NonZeroUsize) -> Self {
99 Self {
100 inner: Arc::new(QueryMemoryLedgerInner {
101 budget_bytes: budget_bytes.get(),
102 state: Mutex::new(QueryMemoryLedgerState::default()),
103 }),
104 file_descriptors: None,
105 }
106 }
107
108 #[doc(hidden)]
110 pub fn with_file_descriptors(
111 mut self,
112 context: Option<hawdb_storage::file_descriptors::FileOpenContext>,
113 ) -> Self {
114 self.file_descriptors = context.or(self.file_descriptors);
115 self
116 }
117
118 pub(crate) fn file_descriptor_context(
119 &self,
120 ) -> Option<&hawdb_storage::file_descriptors::FileOpenContext> {
121 self.file_descriptors.as_ref()
122 }
123
124 pub fn account(
125 &self,
126 class: QueryMemoryClass,
127 owner: impl Into<Arc<str>>,
128 budget_bytes: NonZeroUsize,
129 ) -> QueryMemoryAccount {
130 let owner = owner.into();
132 let mut state = lock_recover(&self.inner.state);
133 let account_id = state.next_account_id;
134 state.next_account_id = state.next_account_id.saturating_add(1);
135 state.accounts.insert(
136 account_id,
137 QueryMemoryAccountState {
138 class,
139 owner,
140 budget_bytes: budget_bytes.get(),
141 used_bytes: 0,
142 peak_bytes: 0,
143 },
144 );
145 QueryMemoryAccount {
146 ledger: self.clone(),
147 account_id,
148 backing: None,
149 }
150 }
151
152 pub fn snapshot(&self) -> QueryMemoryLedgerSnapshot {
153 let state = lock_recover(&self.inner.state);
154 QueryMemoryLedgerSnapshot {
155 budget_bytes: self.inner.budget_bytes,
156 used_bytes: state.used_bytes,
157 peak_bytes: state.peak_bytes,
158 account_count: state.accounts.len(),
159 classes: state
160 .classes
161 .iter()
162 .map(|(class, state)| QueryMemoryClassSnapshot {
163 class: *class,
164 used_bytes: state.used_bytes,
165 peak_bytes: state.peak_bytes,
166 })
167 .collect(),
168 }
169 }
170
171 fn reserve(&self, account_id: u64, bytes: usize) -> Result<()> {
172 if bytes == 0 {
173 return Ok(());
174 }
175 let mut state = lock_recover(&self.inner.state);
176 let (class, owner, account_budget, account_next) = {
177 let account = state.accounts.get(&account_id).ok_or_else(|| {
178 HawDBError::Execution("query memory account is no longer registered".to_string())
179 })?;
180 let account_next = account.used_bytes.checked_add(bytes).ok_or_else(|| {
181 HawDBError::Execution(format!(
182 "query memory account {} ({}) byte accounting overflow",
183 account.owner,
184 account.class.as_str()
185 ))
186 })?;
187 (
188 account.class,
189 Arc::clone(&account.owner),
190 account.budget_bytes,
191 account_next,
192 )
193 };
194 if account_next > account_budget {
195 return Err(HawDBError::Execution(format!(
196 "query memory account {owner} ({}) would use {account_next} bytes, exceeding its {account_budget}-byte budget",
197 class.as_str()
198 )));
199 }
200 let root_next = state.used_bytes.checked_add(bytes).ok_or_else(|| {
201 HawDBError::Execution(format!(
202 "query memory ledger byte accounting overflow while charging {owner} ({})",
203 class.as_str()
204 ))
205 })?;
206 if root_next > self.inner.budget_bytes {
207 return Err(HawDBError::Execution(format!(
208 "query memory ledger would use {root_next} bytes while charging {owner} ({}), exceeding query_memory_bytes {}",
209 class.as_str(),
210 self.inner.budget_bytes
211 )));
212 }
213
214 state.classes.entry(class).or_default();
216 let account = state
217 .accounts
218 .get_mut(&account_id)
219 .expect("validated query memory account remains registered");
220 account.used_bytes = account_next;
221 account.peak_bytes = account.peak_bytes.max(account_next);
222 state.used_bytes = root_next;
223 state.peak_bytes = state.peak_bytes.max(root_next);
224 let class_state = state
225 .classes
226 .get_mut(&class)
227 .expect("prepared query memory class remains registered");
228 class_state.used_bytes = class_state.used_bytes.saturating_add(bytes);
229 class_state.peak_bytes = class_state.peak_bytes.max(class_state.used_bytes);
230 Ok(())
231 }
232
233 fn can_reserve(&self, account_id: u64, bytes: usize) -> bool {
234 let state = lock_recover(&self.inner.state);
235 let Some(account) = state.accounts.get(&account_id) else {
236 return false;
237 };
238 let Some(account_next) = account.used_bytes.checked_add(bytes) else {
239 return false;
240 };
241 let Some(root_next) = state.used_bytes.checked_add(bytes) else {
242 return false;
243 };
244 account_next <= account.budget_bytes && root_next <= self.inner.budget_bytes
245 }
246
247 fn release(&self, account_id: u64, bytes: usize) {
248 if bytes == 0 {
249 return;
250 }
251 let mut state = lock_recover(&self.inner.state);
252 let Some(account) = state.accounts.get_mut(&account_id) else {
253 return;
254 };
255 let released = bytes.min(account.used_bytes);
256 let class = account.class;
257 account.used_bytes -= released;
258 state.used_bytes = state.used_bytes.saturating_sub(released);
259 if let Some(class_state) = state.classes.get_mut(&class) {
260 class_state.used_bytes = class_state.used_bytes.saturating_sub(released);
261 }
262 }
263
264 fn transfer(
265 &self,
266 source_account_id: u64,
267 source_bytes: usize,
268 target_account_id: u64,
269 target_bytes: usize,
270 ) -> Result<()> {
271 if source_account_id == target_account_id {
272 return Err(HawDBError::Execution(
273 "query memory transfer requires distinct accounts".to_string(),
274 ));
275 }
276 let mut state = lock_recover(&self.inner.state);
277 let (source_class, source_used) = state
278 .accounts
279 .get(&source_account_id)
280 .map(|account| (account.class, account.used_bytes))
281 .ok_or_else(|| {
282 HawDBError::Execution(
283 "source query memory account is no longer registered".to_string(),
284 )
285 })?;
286 if source_bytes > source_used {
287 return Err(HawDBError::Execution(format!(
288 "query memory transfer tried to release {source_bytes} bytes from a source account using {source_used} bytes"
289 )));
290 }
291 let (target_class, target_owner, target_budget, target_used) = state
292 .accounts
293 .get(&target_account_id)
294 .map(|account| {
295 (
296 account.class,
297 Arc::clone(&account.owner),
298 account.budget_bytes,
299 account.used_bytes,
300 )
301 })
302 .ok_or_else(|| {
303 HawDBError::Execution(
304 "target query memory account is no longer registered".to_string(),
305 )
306 })?;
307 let target_next = target_used.checked_add(target_bytes).ok_or_else(|| {
308 HawDBError::Execution(format!(
309 "query memory account {target_owner} ({}) byte accounting overflow",
310 target_class.as_str()
311 ))
312 })?;
313 if target_next > target_budget {
314 return Err(HawDBError::Execution(format!(
315 "query memory account {target_owner} ({}) would use {target_next} bytes, exceeding its {target_budget}-byte budget",
316 target_class.as_str()
317 )));
318 }
319 let root_after_release = state.used_bytes.checked_sub(source_bytes).ok_or_else(|| {
320 HawDBError::Execution(
321 "query memory ledger underflow during ownership transfer".to_string(),
322 )
323 })?;
324 let root_next = root_after_release.checked_add(target_bytes).ok_or_else(|| {
325 HawDBError::Execution(format!(
326 "query memory ledger byte accounting overflow while transferring ownership to {target_owner} ({})",
327 target_class.as_str()
328 ))
329 })?;
330 if root_next > self.inner.budget_bytes {
331 return Err(HawDBError::Execution(format!(
332 "query memory ledger would use {root_next} bytes while transferring ownership to {target_owner} ({}), exceeding query_memory_bytes {}",
333 target_class.as_str(),
334 self.inner.budget_bytes
335 )));
336 }
337
338 state.classes.entry(target_class).or_default();
339 let source = state
340 .accounts
341 .get_mut(&source_account_id)
342 .expect("validated source query memory account remains registered");
343 source.used_bytes -= source_bytes;
344 let target = state
345 .accounts
346 .get_mut(&target_account_id)
347 .expect("validated target query memory account remains registered");
348 target.used_bytes = target_next;
349 target.peak_bytes = target.peak_bytes.max(target_next);
350 state.used_bytes = root_next;
351 state.peak_bytes = state.peak_bytes.max(root_next);
352 if let Some(class_state) = state.classes.get_mut(&source_class) {
353 class_state.used_bytes = class_state.used_bytes.saturating_sub(source_bytes);
354 }
355 let class_state = state
356 .classes
357 .get_mut(&target_class)
358 .expect("prepared target query memory class remains registered");
359 class_state.used_bytes = class_state.used_bytes.saturating_add(target_bytes);
360 class_state.peak_bytes = class_state.peak_bytes.max(class_state.used_bytes);
361 Ok(())
362 }
363}
364
365#[derive(Debug, Clone)]
366pub struct QueryMemoryAccount {
367 ledger: QueryMemoryLedger,
368 account_id: u64,
369 backing: Option<Arc<QueryMemoryLease>>,
372}
373
374impl QueryMemoryAccount {
375 pub(crate) fn sub_account(&self, budget: NonZeroUsize) -> Result<Self> {
378 let backing = Arc::new(self.reserve(budget.get())?);
379 let (class, owner) = {
380 let state = lock_recover(&self.ledger.inner.state);
381 let account = &state.accounts[&self.account_id];
382 (account.class, Arc::clone(&account.owner))
383 };
384 let mut child = QueryMemoryLedger::new(budget).account(class, owner, budget);
385 child.backing = Some(backing);
386 Ok(child)
387 }
388
389 pub(crate) fn budget_bytes(&self) -> NonZeroUsize {
390 NonZeroUsize::new(
391 lock_recover(&self.ledger.inner.state).accounts[&self.account_id].budget_bytes,
392 )
393 .unwrap()
394 }
395
396 pub(crate) fn available_bytes(&self) -> usize {
397 let state = lock_recover(&self.ledger.inner.state);
398 let account = &state.accounts[&self.account_id];
399 account.budget_bytes.saturating_sub(account.used_bytes).min(
400 self.ledger
401 .inner
402 .budget_bytes
403 .saturating_sub(state.used_bytes),
404 )
405 }
406
407 pub(crate) fn peak_bytes(&self) -> usize {
408 lock_recover(&self.ledger.inner.state).accounts[&self.account_id].peak_bytes
409 }
410
411 pub(crate) fn sibling(
412 &self,
413 class: QueryMemoryClass,
414 owner: impl Into<Arc<str>>,
415 budget_bytes: NonZeroUsize,
416 ) -> Self {
417 let mut account = self.ledger.account(class, owner, budget_bytes);
418 account.backing = self.backing.clone();
419 account
420 }
421
422 pub(crate) fn can_reserve(&self, bytes: usize) -> bool {
423 self.ledger.can_reserve(self.account_id, bytes)
424 }
425
426 pub fn reserve(&self, bytes: usize) -> Result<QueryMemoryLease> {
427 self.ledger.reserve(self.account_id, bytes)?;
428 Ok(QueryMemoryLease {
429 account: self.clone(),
430 bytes,
431 })
432 }
433}
434
435#[derive(Debug)]
436pub struct QueryMemoryLease {
437 account: QueryMemoryAccount,
438 bytes: usize,
439}
440
441impl QueryMemoryLease {
442 pub fn bytes(&self) -> usize {
443 self.bytes
444 }
445
446 pub fn grow(&mut self, bytes: usize) -> Result<()> {
447 self.account
448 .ledger
449 .reserve(self.account.account_id, bytes)?;
450 self.bytes = self.bytes.saturating_add(bytes);
451 Ok(())
452 }
453
454 pub fn shrink(&mut self, bytes: usize) {
455 let released = bytes.min(self.bytes);
456 self.account
457 .ledger
458 .release(self.account.account_id, released);
459 self.bytes -= released;
460 }
461
462 pub fn reset(&mut self) {
463 self.shrink(self.bytes);
464 }
465
466 pub(crate) fn transfer_to(
467 &mut self,
468 source_bytes: usize,
469 target: &mut Self,
470 target_bytes: usize,
471 ) -> Result<()> {
472 if !Arc::ptr_eq(&self.account.ledger.inner, &target.account.ledger.inner) {
473 return Err(HawDBError::Execution(
474 "query memory transfer requires accounts from the same ledger".to_string(),
475 ));
476 }
477 if source_bytes > self.bytes {
478 return Err(HawDBError::Execution(format!(
479 "query memory transfer tried to release {source_bytes} bytes from a {}-byte lease",
480 self.bytes,
481 )));
482 }
483 self.account.ledger.transfer(
484 self.account.account_id,
485 source_bytes,
486 target.account.account_id,
487 target_bytes,
488 )?;
489 self.bytes -= source_bytes;
490 target.bytes = target.bytes.saturating_add(target_bytes);
491 Ok(())
492 }
493}
494
495impl Drop for QueryMemoryLease {
496 fn drop(&mut self) {
497 self.account
498 .ledger
499 .release(self.account.account_id, self.bytes);
500 self.bytes = 0;
501 }
502}
503
504fn lock_recover<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
505 mutex
509 .lock()
510 .unwrap_or_else(|poisoned| poisoned.into_inner())
511}
512
513#[cfg(test)]
514#[path = "memory_ledger_hardening_tests.rs"]
515mod hardening_tests;
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520
521 #[test]
522 fn sub_accounts_reserve_parent_capacity_without_double_charging() {
523 let budget = NonZeroUsize::new(100).unwrap();
524 let ledger = QueryMemoryLedger::new(budget);
525 let parent = ledger.account(QueryMemoryClass::BlockingState, "merge", budget);
526 let retained = parent.reserve(10).unwrap();
527 let left = parent.sub_account(NonZeroUsize::new(60).unwrap()).unwrap();
528 let right = parent.sub_account(NonZeroUsize::new(30).unwrap()).unwrap();
529 assert_eq!(parent.available_bytes(), 0);
530 assert!(parent.sub_account(NonZeroUsize::MIN).is_err());
531 let mut left_lease = left.reserve(60).unwrap();
532 let right_lease = right.reserve(30).unwrap();
533 assert!(left_lease.grow(1).is_err());
534 assert_eq!(ledger.snapshot().used_bytes, 100);
535 drop((left, right, retained));
536 assert_eq!(ledger.snapshot().used_bytes, 90);
537 drop(left_lease);
538 assert_eq!(ledger.snapshot().used_bytes, 30);
539 drop(right_lease);
540 assert_eq!(ledger.snapshot().used_bytes, 0);
541 assert_eq!(ledger.snapshot().peak_bytes, 100);
542 }
543
544 #[test]
545 fn nested_accounts_and_siblings_keep_backing_until_last_owner_drops() {
546 let budget = NonZeroUsize::new(8).unwrap();
547 let ledger = QueryMemoryLedger::new(budget);
548 let parent = ledger.account(QueryMemoryClass::BlockingState, "merge", budget);
549 let child = parent.sub_account(budget).unwrap();
550 let sibling = child.sibling(QueryMemoryClass::SpillStaging, "staging", budget);
551 let nested = child.sub_account(NonZeroUsize::new(4).unwrap()).unwrap();
552 assert!(sibling.reserve(5).is_err());
553 let lease = nested.reserve(4).unwrap();
554 drop((parent, child, nested));
555 assert_eq!(ledger.snapshot().used_bytes, 8);
556 drop(lease);
557 assert_eq!(ledger.snapshot().used_bytes, 8);
558 let sibling_lease = sibling.reserve(8).unwrap();
559 drop(sibling);
560 assert_eq!(ledger.snapshot().used_bytes, 8);
561 drop(sibling_lease);
562 assert_eq!(ledger.snapshot().used_bytes, 0);
563 }
564
565 #[test]
566 fn failed_sub_account_admission_does_not_leak_root_capacity() {
567 let budget = NonZeroUsize::new(100).unwrap();
568 let ledger = QueryMemoryLedger::new(budget);
569 let parent = ledger.account(QueryMemoryClass::BlockingState, "merge", budget);
570 let other = ledger.account(QueryMemoryClass::PipelineBatch, "input", budget);
571 let retained = other.reserve(70).unwrap();
572 assert!(parent.sub_account(NonZeroUsize::new(31).unwrap()).is_err());
573 assert_eq!(ledger.snapshot().used_bytes, 70);
574 let child = parent.sub_account(NonZeroUsize::new(30).unwrap()).unwrap();
575 assert!(child.reserve(31).is_err());
576 drop((child, retained));
577 assert_eq!(ledger.snapshot().used_bytes, 0);
578 }
579
580 #[test]
581 fn sibling_accounts_share_one_root_budget() {
582 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(10).unwrap());
583 let left = ledger.account(
584 QueryMemoryClass::BlockingState,
585 "left",
586 NonZeroUsize::new(10).unwrap(),
587 );
588 let right = ledger.account(
589 QueryMemoryClass::PipelineBatch,
590 "right",
591 NonZeroUsize::new(10).unwrap(),
592 );
593 let left_lease = left.reserve(6).unwrap();
594 let error = right.reserve(5).unwrap_err();
595
596 assert!(error.to_string().contains("query_memory_bytes 10"));
597 assert_eq!(ledger.snapshot().used_bytes, 6);
598 drop(left_lease);
599 assert_eq!(ledger.snapshot().used_bytes, 0);
600 }
601
602 #[test]
603 fn capacity_preflight_checks_account_and_root_without_charging() {
604 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(10).unwrap());
605 let left = ledger.account(
606 QueryMemoryClass::BlockingState,
607 "left",
608 NonZeroUsize::new(8).unwrap(),
609 );
610 let right = ledger.account(
611 QueryMemoryClass::PipelineBatch,
612 "right",
613 NonZeroUsize::new(10).unwrap(),
614 );
615
616 assert!(left.can_reserve(8));
617 assert_eq!(ledger.snapshot().used_bytes, 0);
618 let left_lease = left.reserve(6).unwrap();
619 assert!(!left.can_reserve(3));
620 assert!(right.can_reserve(4));
621 assert!(!right.can_reserve(5));
622 assert_eq!(ledger.snapshot().used_bytes, 6);
623
624 drop(left_lease);
625 assert_eq!(ledger.snapshot().used_bytes, 0);
626 }
627
628 #[test]
629 fn derived_sibling_account_keeps_the_original_query_root() {
630 let budget = NonZeroUsize::new(8).unwrap();
631 let ledger = QueryMemoryLedger::new(budget);
632 let state = ledger.account(QueryMemoryClass::BlockingState, "merge", budget);
633 let output = state.sibling(QueryMemoryClass::PipelineBatch, "output", budget);
634 let state_lease = state.reserve(6).unwrap();
635 assert!(output
636 .reserve(3)
637 .unwrap_err()
638 .to_string()
639 .contains("query_memory_bytes 8"));
640 let output_lease = output.reserve(2).unwrap();
641 assert_eq!(ledger.snapshot().used_bytes, 8);
642 drop((state_lease, output_lease));
643 assert_eq!(ledger.snapshot().used_bytes, 0);
644 }
645
646 #[test]
647 fn ownership_transfer_does_not_require_double_root_capacity() {
648 let budget = NonZeroUsize::new(8).unwrap();
649 let ledger = QueryMemoryLedger::new(budget);
650 let source = ledger.account(QueryMemoryClass::BlockingState, "source", budget);
651 let target = ledger.account(QueryMemoryClass::PipelineBatch, "target", budget);
652 let mut source_lease = source.reserve(8).unwrap();
653 let mut target_lease = target.reserve(0).unwrap();
654
655 source_lease.transfer_to(8, &mut target_lease, 8).unwrap();
656
657 assert_eq!(source_lease.bytes(), 0);
658 assert_eq!(target_lease.bytes(), 8);
659 let snapshot = ledger.snapshot();
660 assert_eq!(snapshot.used_bytes, 8);
661 assert_eq!(snapshot.peak_bytes, 8);
662 drop((source_lease, target_lease));
663 assert_eq!(ledger.snapshot().used_bytes, 0);
664 }
665
666 #[test]
667 fn failed_ownership_transfer_preserves_source_charge() {
668 let budget = NonZeroUsize::new(8).unwrap();
669 let ledger = QueryMemoryLedger::new(budget);
670 let source = ledger.account(QueryMemoryClass::BlockingState, "source", budget);
671 let target = ledger.account(QueryMemoryClass::PipelineBatch, "target", budget);
672 let mut source_lease = source.reserve(8).unwrap();
673 let mut target_lease = target.reserve(0).unwrap();
674
675 let error = source_lease
676 .transfer_to(8, &mut target_lease, 9)
677 .unwrap_err();
678
679 assert!(error.to_string().contains("9 bytes"));
680 assert_eq!(source_lease.bytes(), 8);
681 assert_eq!(target_lease.bytes(), 0);
682 assert_eq!(ledger.snapshot().used_bytes, 8);
683 }
684
685 #[test]
686 fn account_budget_is_enforced_before_root_budget() {
687 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(100).unwrap());
688 let account = ledger.account(
689 QueryMemoryClass::BlockingState,
690 "sort",
691 NonZeroUsize::new(4).unwrap(),
692 );
693
694 let error = account.reserve(5).unwrap_err();
695
696 assert!(error.to_string().contains("sort"));
697 assert!(error.to_string().contains("4-byte budget"));
698 assert_eq!(ledger.snapshot().used_bytes, 0);
699 }
700
701 #[test]
702 fn lease_growth_shrink_and_drop_update_hierarchy() {
703 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(100).unwrap());
704 let account = ledger.account(
705 QueryMemoryClass::MorselOutput,
706 "reorder",
707 NonZeroUsize::new(80).unwrap(),
708 );
709 let mut lease = account.reserve(20).unwrap();
710 lease.grow(30).unwrap();
711 lease.shrink(10);
712 assert_eq!(lease.bytes(), 40);
713 assert_eq!(ledger.snapshot().used_bytes, 40);
714
715 drop(lease);
716 let snapshot = ledger.snapshot();
717 assert_eq!(snapshot.used_bytes, 0);
718 assert_eq!(snapshot.peak_bytes, 50);
719 assert_eq!(snapshot.classes[0].peak_bytes, 50);
720 }
721
722 #[test]
723 fn unwind_releases_query_memory_lease() {
724 let ledger = QueryMemoryLedger::new(NonZeroUsize::new(100).unwrap());
725 let account = ledger.account(
726 QueryMemoryClass::SpillStaging,
727 "spill",
728 NonZeroUsize::new(100).unwrap(),
729 );
730 let result = std::panic::catch_unwind(|| {
731 let _lease = account.reserve(80).unwrap();
732 panic!("injected panic");
733 });
734
735 assert!(result.is_err());
736 assert_eq!(ledger.snapshot().used_bytes, 0);
737 }
738}