1use std::ops::Deref;
4use std::sync::{Arc, Mutex};
5
6use super::budget::Budgets;
7use super::{Store, StoreError, StoreState};
8
9#[cfg(feature = "graph-cypher")]
10mod allocation_interval;
11#[cfg(feature = "graph-cypher")]
12pub use allocation_interval::{
13 AllocationInterval, AllocationIntervalError, AllocationIntervalSnapshot,
14};
15
16#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
19pub struct GraphWorkLedger {
20 pub storage_lookups: u64,
22 pub storage_scans: u64,
24 pub storage_adjacency_entries: u64,
26 pub storage_copied_bytes: u64,
28 pub storage_pages_decoded: u64,
30 pub storage_pages_copied: u64,
32 pub storage_property_values: u64,
34 pub storage_property_bytes: u64,
36 pub storage_adjacency_physical_entries: u64,
38 pub storage_adjacency_merged_visits: u64,
40 pub storage_adjacency_merge_runs: u64,
42
43 pub canonical_comparison_bytes: u64,
45 pub canonical_encoding_bytes: u64,
47 pub wal_codec_units: u64,
49 pub encoded_wal_bytes: u64,
51 pub artifact_bytes_written: u64,
53 pub artifact_writes: u64,
55 pub wal_bytes_appended: u64,
57 pub wal_appends: u64,
59 pub full_sync_attempts: u64,
61 pub full_sync_successes: u64,
63 pub directory_sync_attempts: u64,
65 pub directory_sync_successes: u64,
67}
68#[derive(Clone, Copy)]
69pub(crate) enum GraphWorkKind {
70 StorageLookups,
71 StorageScans,
72 StorageAdjacencyEntries,
73 StorageCopiedBytes,
74 StoragePagesDecoded,
75 StoragePagesCopied,
76 StoragePropertyValues,
77 StoragePropertyBytes,
78 StorageAdjacencyPhysicalEntries,
79 StorageAdjacencyMergedVisits,
80 StorageAdjacencyMergeRuns,
81
82 CanonicalComparisonBytes,
83 CanonicalEncodingBytes,
84 WalCodecUnits,
85 EncodedWalBytes,
86 ArtifactBytesWritten,
87 ArtifactWrites,
88 WalBytesAppended,
89 WalAppends,
90 FullSyncAttempts,
91 FullSyncSuccesses,
92 DirectorySyncAttempts,
93 DirectorySyncSuccesses,
94}
95impl GraphWorkLedger {
96 pub(crate) fn add(&mut self, kind: GraphWorkKind, units: u64) {
97 let slot = match kind {
98 GraphWorkKind::StorageLookups => &mut self.storage_lookups,
99 GraphWorkKind::StorageScans => &mut self.storage_scans,
100 GraphWorkKind::StorageAdjacencyEntries => &mut self.storage_adjacency_entries,
101 GraphWorkKind::StorageCopiedBytes => &mut self.storage_copied_bytes,
102 GraphWorkKind::StoragePagesDecoded => &mut self.storage_pages_decoded,
103 GraphWorkKind::StoragePagesCopied => &mut self.storage_pages_copied,
104 GraphWorkKind::StoragePropertyValues => &mut self.storage_property_values,
105 GraphWorkKind::StoragePropertyBytes => &mut self.storage_property_bytes,
106 GraphWorkKind::StorageAdjacencyPhysicalEntries => {
107 &mut self.storage_adjacency_physical_entries
108 }
109 GraphWorkKind::StorageAdjacencyMergedVisits => {
110 &mut self.storage_adjacency_merged_visits
111 }
112 GraphWorkKind::StorageAdjacencyMergeRuns => &mut self.storage_adjacency_merge_runs,
113
114 GraphWorkKind::CanonicalComparisonBytes => &mut self.canonical_comparison_bytes,
115 GraphWorkKind::CanonicalEncodingBytes => &mut self.canonical_encoding_bytes,
116 GraphWorkKind::WalCodecUnits => &mut self.wal_codec_units,
117 GraphWorkKind::EncodedWalBytes => &mut self.encoded_wal_bytes,
118 GraphWorkKind::ArtifactBytesWritten => &mut self.artifact_bytes_written,
119 GraphWorkKind::ArtifactWrites => &mut self.artifact_writes,
120 GraphWorkKind::WalBytesAppended => &mut self.wal_bytes_appended,
121 GraphWorkKind::WalAppends => &mut self.wal_appends,
122 GraphWorkKind::FullSyncAttempts => &mut self.full_sync_attempts,
123 GraphWorkKind::FullSyncSuccesses => &mut self.full_sync_successes,
124 GraphWorkKind::DirectorySyncAttempts => &mut self.directory_sync_attempts,
125 GraphWorkKind::DirectorySyncSuccesses => &mut self.directory_sync_successes,
126 };
127 *slot = slot.saturating_add(units);
128 }
129}
130
131impl GraphWorkLedger {
132 pub(crate) fn merge(&mut self, other: Self) {
133 self.storage_lookups = self.storage_lookups.saturating_add(other.storage_lookups);
134 self.storage_scans = self.storage_scans.saturating_add(other.storage_scans);
135 self.storage_adjacency_entries = self
136 .storage_adjacency_entries
137 .saturating_add(other.storage_adjacency_entries);
138 self.storage_copied_bytes = self
139 .storage_copied_bytes
140 .saturating_add(other.storage_copied_bytes);
141 self.storage_pages_decoded = self
142 .storage_pages_decoded
143 .saturating_add(other.storage_pages_decoded);
144 self.storage_pages_copied = self
145 .storage_pages_copied
146 .saturating_add(other.storage_pages_copied);
147 self.storage_property_values = self
148 .storage_property_values
149 .saturating_add(other.storage_property_values);
150 self.storage_property_bytes = self
151 .storage_property_bytes
152 .saturating_add(other.storage_property_bytes);
153 self.storage_adjacency_physical_entries = self
154 .storage_adjacency_physical_entries
155 .saturating_add(other.storage_adjacency_physical_entries);
156 self.storage_adjacency_merged_visits = self
157 .storage_adjacency_merged_visits
158 .saturating_add(other.storage_adjacency_merged_visits);
159 self.storage_adjacency_merge_runs = self
160 .storage_adjacency_merge_runs
161 .saturating_add(other.storage_adjacency_merge_runs);
162 self.canonical_comparison_bytes = self
163 .canonical_comparison_bytes
164 .saturating_add(other.canonical_comparison_bytes);
165 self.canonical_encoding_bytes = self
166 .canonical_encoding_bytes
167 .saturating_add(other.canonical_encoding_bytes);
168 self.wal_codec_units = self.wal_codec_units.saturating_add(other.wal_codec_units);
169 self.encoded_wal_bytes = self
170 .encoded_wal_bytes
171 .saturating_add(other.encoded_wal_bytes);
172 self.artifact_bytes_written = self
173 .artifact_bytes_written
174 .saturating_add(other.artifact_bytes_written);
175 self.artifact_writes = self.artifact_writes.saturating_add(other.artifact_writes);
176 self.wal_bytes_appended = self
177 .wal_bytes_appended
178 .saturating_add(other.wal_bytes_appended);
179 self.wal_appends = self.wal_appends.saturating_add(other.wal_appends);
180 self.full_sync_attempts = self
181 .full_sync_attempts
182 .saturating_add(other.full_sync_attempts);
183 self.full_sync_successes = self
184 .full_sync_successes
185 .saturating_add(other.full_sync_successes);
186 self.directory_sync_attempts = self
187 .directory_sync_attempts
188 .saturating_add(other.directory_sync_attempts);
189 self.directory_sync_successes = self
190 .directory_sync_successes
191 .saturating_add(other.directory_sync_successes);
192 }
193}
194#[allow(dead_code)]
195#[derive(Clone, Copy)]
196pub(crate) enum AllocationComponent {
197 Snapshot,
198 Active,
199 Wal,
200 Cache,
201 Temporary,
202 QueryPool,
203 NativeGraph,
204}
205
206impl AllocationComponent {
207 const fn name(self) -> &'static str {
208 match self {
209 Self::Snapshot => "snapshot",
210 Self::Active => "active segment",
211 Self::Wal => "wal",
212 Self::Cache => "cache",
213 Self::Temporary => "temporary",
214 Self::QueryPool => "query pool",
215 Self::NativeGraph => "native graph registry",
216 }
217 }
218
219 const fn is_temporary(self) -> bool {
220 matches!(self, Self::Temporary)
221 }
222}
223
224#[derive(Default)]
225struct AccountingState {
226 resident_owned_bytes: u64,
227 resident_peak_bytes: u64,
228 wal_bytes: u64,
229 cache_bytes: u64,
230 temporary_bytes: u64,
231 snapshot_bytes: u64,
232 active_bytes: u64,
233 query_pool_bytes: u64,
234 native_graph_bytes: u64,
235 mapped_bytes: u64,
236 scans_by_reason: [u64; crate::planner::SCAN_REASON_COUNTER_COUNT],
237 graph_segments_served: u64,
238 #[cfg(feature = "graph-cypher")]
239 allocation_interval_owner: u64,
240 #[cfg(feature = "graph-cypher")]
241 allocation_interval: Option<allocation_interval::AllocationIntervalState>,
242}
243
244pub(crate) struct Accounting {
245 budgets: Budgets,
246 state: Mutex<AccountingState>,
247 graph_work: Mutex<GraphWorkLedger>,
248 #[cfg(test)]
249 graph_work_merges: std::sync::atomic::AtomicU64,
250}
251
252impl Accounting {
253 pub(crate) const fn new(max_resident_bytes: u64, max_temp_bytes: u64) -> Self {
254 Self {
255 budgets: Budgets::new(max_resident_bytes, max_temp_bytes),
256 #[cfg(test)]
257 graph_work_merges: std::sync::atomic::AtomicU64::new(0),
258 graph_work: Mutex::new(GraphWorkLedger {
259 storage_lookups: 0,
260 storage_scans: 0,
261 storage_adjacency_entries: 0,
262 storage_copied_bytes: 0,
263 storage_pages_decoded: 0,
264 storage_pages_copied: 0,
265 storage_property_values: 0,
266 storage_property_bytes: 0,
267 storage_adjacency_physical_entries: 0,
268 storage_adjacency_merged_visits: 0,
269 storage_adjacency_merge_runs: 0,
270 canonical_comparison_bytes: 0,
271 canonical_encoding_bytes: 0,
272 wal_codec_units: 0,
273 encoded_wal_bytes: 0,
274 artifact_bytes_written: 0,
275 artifact_writes: 0,
276 wal_bytes_appended: 0,
277 wal_appends: 0,
278 full_sync_attempts: 0,
279 full_sync_successes: 0,
280 directory_sync_attempts: 0,
281 directory_sync_successes: 0,
282 }),
283 state: Mutex::new(AccountingState {
284 resident_owned_bytes: 0,
285 resident_peak_bytes: 0,
286 wal_bytes: 0,
287 cache_bytes: 0,
288 temporary_bytes: 0,
289 snapshot_bytes: 0,
290 active_bytes: 0,
291 query_pool_bytes: 0,
292 native_graph_bytes: 0,
293 mapped_bytes: 0,
294 scans_by_reason: [0; crate::planner::SCAN_REASON_COUNTER_COUNT],
295 graph_segments_served: 0,
296 #[cfg(feature = "graph-cypher")]
297 allocation_interval_owner: 0,
298 #[cfg(feature = "graph-cypher")]
299 allocation_interval: None,
300 }),
301 }
302 }
303
304 #[cfg(feature = "graph-cypher")]
305 pub(crate) const fn resident_limit(&self) -> u64 {
306 self.budgets.resident_limit()
307 }
308
309 #[cfg(feature = "graph-cypher")]
310 pub(crate) fn resident_peak_bytes(&self) -> Result<u64, StoreError> {
311 Ok(self
312 .state
313 .lock()
314 .map_err(|_| StoreError::Synchronization {
315 component: "memory accounting",
316 })?
317 .resident_peak_bytes)
318 }
319
320 fn reserve(
321 self: &Arc<Self>,
322 bytes: u64,
323 component: AllocationComponent,
324 ) -> Result<Reservation, StoreError> {
325 self.add(bytes, component)?;
326 Ok(Reservation {
327 accounting: Arc::clone(self),
328 bytes,
329 component,
330 })
331 }
332
333 fn add(&self, bytes: u64, component: AllocationComponent) -> Result<(), StoreError> {
334 let mut state = self.state.lock().map_err(|_| StoreError::Synchronization {
335 component: "memory accounting",
336 })?;
337 let (resident, temporary) = self.budgets.check(
338 state.resident_owned_bytes,
339 state.temporary_bytes,
340 bytes,
341 component.name(),
342 component.is_temporary(),
343 )?;
344 state.resident_owned_bytes = resident;
345 state.resident_peak_bytes = state.resident_peak_bytes.max(resident);
346 #[cfg(feature = "graph-cypher")]
347 if let Some(interval) = state.allocation_interval.as_mut() {
348 interval.peak_reserved_bytes = interval.peak_reserved_bytes.max(resident);
349 }
350 match component {
351 AllocationComponent::Snapshot => {
352 state.snapshot_bytes = state.snapshot_bytes.saturating_add(bytes);
353 }
354 AllocationComponent::Active => {
355 state.active_bytes = state.active_bytes.saturating_add(bytes);
356 }
357 AllocationComponent::Wal => state.wal_bytes = state.wal_bytes.saturating_add(bytes),
358 AllocationComponent::Cache => {
359 state.cache_bytes = state.cache_bytes.saturating_add(bytes);
360 }
361 AllocationComponent::Temporary => state.temporary_bytes = temporary,
362 AllocationComponent::QueryPool => {
363 state.query_pool_bytes = state.query_pool_bytes.saturating_add(bytes);
364 }
365 AllocationComponent::NativeGraph => {
366 state.native_graph_bytes = state.native_graph_bytes.saturating_add(bytes);
367 }
368 }
369 drop(state);
370 Ok(())
371 }
372
373 pub(crate) fn audit(&self) -> Result<AccountingAudit, StoreError> {
374 let state = self.state.lock().map_err(|_| StoreError::Synchronization {
375 component: "memory accounting",
376 })?;
377 Ok(AccountingAudit {
378 resident_owned_bytes: state.resident_owned_bytes,
379 wal_bytes: state.wal_bytes,
380 cache_bytes: state.cache_bytes,
381 temporary_bytes: state.temporary_bytes,
382 snapshot_bytes: state.snapshot_bytes,
383 active_bytes: state.active_bytes,
384 query_pool_bytes: state.query_pool_bytes,
385 native_graph_bytes: state.native_graph_bytes,
386 mapped_bytes: state.mapped_bytes,
387 scans_by_reason: state.scans_by_reason,
388 graph_segments_served: state.graph_segments_served,
389 })
390 }
391
392 #[cfg(feature = "graph-cypher")]
393 pub(crate) fn graph_resource_bytes(&self) -> Result<(u64, u64), StoreError> {
394 let state = self.state.lock().map_err(|_| StoreError::Synchronization {
395 component: "graph resources",
396 })?;
397 Ok((state.resident_owned_bytes, state.resident_peak_bytes))
398 }
399 #[cfg(any(test, feature = "test-seams"))]
400 pub(crate) fn graph_work(&self) -> GraphWorkLedger {
401 *self
402 .graph_work
403 .lock()
404 .unwrap_or_else(std::sync::PoisonError::into_inner)
405 }
406 pub(crate) fn merge_graph_work(&self, delta: GraphWorkLedger) {
407 #[cfg(test)]
408 self.graph_work_merges
409 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
410 self.graph_work
411 .lock()
412 .unwrap_or_else(std::sync::PoisonError::into_inner)
413 .merge(delta);
414 }
415 #[cfg(test)]
416 pub(crate) fn ze76_work_merges(&self) -> u64 {
417 self.graph_work_merges
418 .load(std::sync::atomic::Ordering::Relaxed)
419 }
420 #[cfg(test)]
421 #[allow(clippy::unwrap_used)]
422 pub(crate) fn ze76_overflow_work(&self) {
423 self.graph_work.lock().unwrap().wal_appends = u64::MAX;
424 }
425 #[cfg(test)]
426 #[allow(clippy::unwrap_used, clippy::panic)]
427 pub(crate) fn ze76_poison_work(&self) {
428 let _ = std::panic::catch_unwind(|| {
429 let _guard = self.graph_work.lock().unwrap();
430 panic!("test-only diagnostic lock poison");
431 });
432 }
433 pub(crate) fn record_plans(
434 &self,
435 plans: &[crate::planner::SegmentPlan],
436 ) -> Result<(), StoreError> {
437 let mut state = self.state.lock().map_err(|_| StoreError::Synchronization {
438 component: "query plan accounting",
439 })?;
440 let mut scans_by_reason = state.scans_by_reason;
441 let mut graph_segments_served = state.graph_segments_served;
442 for plan in plans {
443 if let Some(reason) = plan.scan_reason {
444 let slot = reason.counter().index();
445 let counter =
446 scans_by_reason
447 .get_mut(slot)
448 .ok_or_else(|| StoreError::Statistics {
449 component: "scan reason counter",
450 source: std::io::Error::other(
451 "scan reason slot is outside the registry",
452 ),
453 })?;
454 *counter = counter
455 .checked_add(1)
456 .ok_or_else(|| StoreError::Statistics {
457 component: "scan reason counter",
458 source: std::io::Error::other("scan reason counter overflow"),
459 })?;
460 }
461 if matches!(
462 plan.branch,
463 crate::planner::SegmentBranch::Graph | crate::planner::SegmentBranch::FilteredGraph
464 ) {
465 graph_segments_served =
466 graph_segments_served
467 .checked_add(1)
468 .ok_or_else(|| StoreError::Statistics {
469 component: "graph segment counter",
470 source: std::io::Error::other("graph segment counter overflow"),
471 })?;
472 }
473 }
474 state.scans_by_reason = scans_by_reason;
475 state.graph_segments_served = graph_segments_served;
476 drop(state);
477 Ok(())
478 }
479
480 pub(crate) fn track_mapping(
481 self: &Arc<Self>,
482 bytes: u64,
483 ) -> Result<MappingReservation, StoreError> {
484 let mut state = self.state.lock().map_err(|_| StoreError::Synchronization {
485 component: "memory accounting",
486 })?;
487 state.mapped_bytes =
488 state
489 .mapped_bytes
490 .checked_add(bytes)
491 .ok_or_else(|| StoreError::Statistics {
492 component: "mapped bytes",
493 source: std::io::Error::other("mapped byte count overflow"),
494 })?;
495 drop(state);
496 Ok(MappingReservation {
497 accounting: Arc::clone(self),
498 bytes,
499 })
500 }
501}
502
503#[derive(Clone, Copy)]
504pub(crate) struct AccountingAudit {
505 pub(crate) resident_owned_bytes: u64,
506 pub(crate) wal_bytes: u64,
507 pub(crate) cache_bytes: u64,
508 pub(crate) temporary_bytes: u64,
509 pub(crate) snapshot_bytes: u64,
510 pub(crate) active_bytes: u64,
511 pub(crate) query_pool_bytes: u64,
512 pub(crate) native_graph_bytes: u64,
513 pub(crate) mapped_bytes: u64,
514 pub(crate) scans_by_reason: [u64; crate::planner::SCAN_REASON_COUNTER_COUNT],
515 pub(crate) graph_segments_served: u64,
516}
517
518impl AccountingAudit {
519 pub(crate) const fn component_sum(self) -> u64 {
520 self.wal_bytes
521 .saturating_add(self.cache_bytes)
522 .saturating_add(self.temporary_bytes)
523 .saturating_add(self.snapshot_bytes)
524 .saturating_add(self.active_bytes)
525 .saturating_add(self.query_pool_bytes)
526 .saturating_add(self.native_graph_bytes)
527 }
528}
529
530pub(crate) struct MappingReservation {
531 accounting: Arc<Accounting>,
532 bytes: u64,
533}
534
535impl Drop for MappingReservation {
536 fn drop(&mut self) {
537 let mut state = match self.accounting.state.lock() {
538 Ok(state) => state,
539 Err(poisoned) => poisoned.into_inner(),
540 };
541 state.mapped_bytes = state.mapped_bytes.saturating_sub(self.bytes);
542 }
543}
544
545struct Reservation {
546 accounting: Arc<Accounting>,
547 bytes: u64,
548 component: AllocationComponent,
549}
550
551impl Reservation {
552 fn grow(&mut self, additional_bytes: u64) -> Result<(), StoreError> {
553 self.accounting.add(additional_bytes, self.component)?;
554 self.bytes =
555 self.bytes
556 .checked_add(additional_bytes)
557 .ok_or(StoreError::BudgetExceeded {
558 needed: u64::MAX,
559 budget: u64::MAX,
560 component: self.component.name(),
561 })?;
562 Ok(())
563 }
564
565 fn shrink(&mut self, released_bytes: u64) {
566 let released = released_bytes.min(self.bytes);
567 self.bytes = self.bytes.saturating_sub(released);
568 let mut state = match self.accounting.state.lock() {
569 Ok(state) => state,
570 Err(poisoned) => poisoned.into_inner(),
571 };
572 state.resident_owned_bytes = state.resident_owned_bytes.saturating_sub(released);
573 match self.component {
574 AllocationComponent::Snapshot => {
575 state.snapshot_bytes = state.snapshot_bytes.saturating_sub(released);
576 }
577 AllocationComponent::Active => {
578 state.active_bytes = state.active_bytes.saturating_sub(released);
579 }
580 AllocationComponent::Wal => {
581 state.wal_bytes = state.wal_bytes.saturating_sub(released);
582 }
583 AllocationComponent::Cache => {
584 state.cache_bytes = state.cache_bytes.saturating_sub(released);
585 }
586 AllocationComponent::Temporary => {
587 state.temporary_bytes = state.temporary_bytes.saturating_sub(released);
588 }
589 AllocationComponent::QueryPool => {
590 state.query_pool_bytes = state.query_pool_bytes.saturating_sub(released);
591 }
592 AllocationComponent::NativeGraph => {
593 state.native_graph_bytes = state.native_graph_bytes.saturating_sub(released);
594 }
595 }
596 }
597}
598
599impl Drop for Reservation {
600 fn drop(&mut self) {
601 let mut state = match self.accounting.state.lock() {
602 Ok(state) => state,
603 Err(poisoned) => poisoned.into_inner(),
604 };
605 state.resident_owned_bytes = state.resident_owned_bytes.saturating_sub(self.bytes);
606 match self.component {
607 AllocationComponent::Snapshot => {
608 state.snapshot_bytes = state.snapshot_bytes.saturating_sub(self.bytes);
609 }
610 AllocationComponent::Active => {
611 state.active_bytes = state.active_bytes.saturating_sub(self.bytes);
612 }
613 AllocationComponent::Wal => {
614 state.wal_bytes = state.wal_bytes.saturating_sub(self.bytes);
615 }
616 AllocationComponent::Cache => {
617 state.cache_bytes = state.cache_bytes.saturating_sub(self.bytes);
618 }
619 AllocationComponent::Temporary => {
620 state.temporary_bytes = state.temporary_bytes.saturating_sub(self.bytes);
621 }
622 AllocationComponent::QueryPool => {
623 state.query_pool_bytes = state.query_pool_bytes.saturating_sub(self.bytes);
624 }
625 AllocationComponent::NativeGraph => {
626 state.native_graph_bytes = state.native_graph_bytes.saturating_sub(self.bytes);
627 }
628 }
629 }
630}
631
632pub(crate) struct AccountedCounter {
635 reservation: Reservation,
636}
637
638impl AccountedCounter {
639 pub(crate) fn new(
640 accounting: &Arc<Accounting>,
641 component: AllocationComponent,
642 ) -> Result<Self, StoreError> {
643 Ok(Self {
644 reservation: accounting.reserve(0, component)?,
645 })
646 }
647
648 pub(crate) fn set(&mut self, bytes: usize) -> Result<(), StoreError> {
649 let bytes = u64::try_from(bytes).map_err(|_| StoreError::BudgetExceeded {
650 needed: u64::MAX,
651 budget: u64::MAX,
652 component: self.reservation.component.name(),
653 })?;
654 if bytes >= self.reservation.bytes {
655 return self
656 .reservation
657 .grow(bytes.saturating_sub(self.reservation.bytes));
658 }
659 self.reservation
660 .shrink(self.reservation.bytes.saturating_sub(bytes));
661 Ok(())
662 }
663
664 pub(crate) const fn bytes(&self) -> u64 {
665 self.reservation.bytes
666 }
667
668 #[cfg(feature = "graph-cypher")]
669 pub(crate) fn belongs_to(&self, accounting: &Arc<Accounting>) -> bool {
670 Arc::ptr_eq(&self.reservation.accounting, accounting)
671 }
672}
673
674pub(crate) struct Accounted<T> {
676 value: T,
677 _reservation: Option<Reservation>,
678 element_limit: Option<usize>,
679}
680
681impl<T> Deref for Accounted<T> {
682 type Target = T;
683
684 fn deref(&self) -> &Self::Target {
685 &self.value
686 }
687}
688
689impl<T> Accounted<Vec<T>> {
690 pub(crate) fn unaccounted_empty() -> Self {
691 Self {
692 value: Vec::new(),
693 _reservation: None,
694 element_limit: None,
695 }
696 }
697
698 pub(crate) fn try_with_capacity(
699 accounting: &Arc<Accounting>,
700 capacity: usize,
701 component: AllocationComponent,
702 ) -> Result<Self, StoreError> {
703 let requested =
704 capacity
705 .checked_mul(std::mem::size_of::<T>())
706 .ok_or(StoreError::BudgetExceeded {
707 needed: u64::MAX,
708 budget: u64::MAX,
709 component: component.name(),
710 })?;
711 let bytes = u64::try_from(requested).map_err(|_| StoreError::BudgetExceeded {
712 needed: u64::MAX,
713 budget: u64::MAX,
714 component: component.name(),
715 })?;
716 let reservation = accounting.reserve(bytes, component)?;
717 let mut value = Vec::new();
718 #[cfg(feature = "allocation-audit")]
719 let allocation = crate::allocation_audit::attributed(|| value.try_reserve_exact(capacity));
720 #[cfg(not(feature = "allocation-audit"))]
721 let allocation = value.try_reserve_exact(capacity);
722 allocation.map_err(|_| StoreError::AllocationFailed {
723 needed: bytes,
724 component: component.name(),
725 })?;
726 Ok(Self {
727 value,
728 _reservation: Some(reservation),
729 element_limit: Some(capacity),
730 })
731 }
732
733 pub(crate) fn try_from_vec(
734 accounting: &Arc<Accounting>,
735 value: Vec<T>,
736 component: AllocationComponent,
737 ) -> Result<Self, StoreError> {
738 let capacity = value.capacity();
739 let requested =
740 capacity
741 .checked_mul(std::mem::size_of::<T>())
742 .ok_or(StoreError::BudgetExceeded {
743 needed: u64::MAX,
744 budget: u64::MAX,
745 component: component.name(),
746 })?;
747 let bytes = u64::try_from(requested).map_err(|_| StoreError::BudgetExceeded {
748 needed: u64::MAX,
749 budget: u64::MAX,
750 component: component.name(),
751 })?;
752 let reservation = accounting.reserve(bytes, component)?;
753 Ok(Self {
754 value,
755 _reservation: Some(reservation),
756 element_limit: Some(capacity),
757 })
758 }
759
760 pub(crate) fn resident_bytes(&self) -> u64 {
761 self._reservation
762 .as_ref()
763 .map_or(0, |reservation| reservation.bytes)
764 }
765
766 pub(crate) fn try_reserve_total(
769 &mut self,
770 accounting: &Arc<Accounting>,
771 capacity: usize,
772 component: AllocationComponent,
773 ) -> Result<(), StoreError> {
774 if capacity <= self.element_limit.unwrap_or(0) {
775 return Ok(());
776 }
777 let mut replacement = Self::try_with_capacity(accounting, capacity, component)?;
778 if self.value.len() > capacity {
779 return Err(StoreError::AllocationFailed {
780 needed: self.value.len() as u64,
781 component: "accounted vector capacity",
782 });
783 }
784 replacement.value.append(&mut self.value);
787 *self = replacement;
788 Ok(())
789 }
790
791 pub(crate) fn push(&mut self, value: T) -> Result<(), StoreError> {
792 let accounted_elements = self.element_limit.unwrap_or(usize::MAX);
793 if self.value.len() >= accounted_elements || self.value.len() == self.value.capacity() {
794 return Err(StoreError::AllocationFailed {
795 needed: u64::try_from(self.value.len().saturating_add(1)).unwrap_or(u64::MAX),
796 component: "accounted vector capacity",
797 });
798 }
799 self.value.push(value);
800 Ok(())
801 }
802
803 pub(crate) fn extend_from_slice(&mut self, values: &[T]) -> Result<(), StoreError>
804 where
805 T: Clone,
806 {
807 let new_len =
808 self.value
809 .len()
810 .checked_add(values.len())
811 .ok_or(StoreError::AllocationFailed {
812 needed: u64::MAX,
813 component: "accounted vector capacity",
814 })?;
815 let accounted_elements = self.element_limit.unwrap_or(usize::MAX);
816 if new_len > accounted_elements || new_len > self.value.capacity() {
817 return Err(StoreError::AllocationFailed {
818 needed: u64::try_from(new_len).unwrap_or(u64::MAX),
819 component: "accounted vector capacity",
820 });
821 }
822 self.value.extend_from_slice(values);
823 Ok(())
824 }
825
826 pub(crate) fn replace_range(
827 &mut self,
828 range: std::ops::Range<usize>,
829 replacement: &[T],
830 ) -> Result<(), StoreError>
831 where
832 T: Copy + Default,
833 {
834 let old_len = self.value.len();
835 if range.start > range.end || range.end > old_len {
836 return Err(StoreError::ActiveRowOverflow);
837 }
838 let removed = range.end.saturating_sub(range.start);
839 let new_len = old_len
840 .checked_sub(removed)
841 .and_then(|length| length.checked_add(replacement.len()))
842 .ok_or(StoreError::ActiveRowOverflow)?;
843 let accounted_elements = self.element_limit.unwrap_or(usize::MAX);
844 if new_len > accounted_elements || new_len > self.value.capacity() {
845 return Err(StoreError::AllocationFailed {
846 needed: u64::try_from(new_len).unwrap_or(u64::MAX),
847 component: "accounted vector capacity",
848 });
849 }
850 if new_len > old_len {
851 self.value.resize(new_len, T::default());
852 }
853 let replacement_end = range
854 .start
855 .checked_add(replacement.len())
856 .ok_or(StoreError::ActiveRowOverflow)?;
857 self.value.copy_within(range.end..old_len, replacement_end);
858 self.value
859 .get_mut(range.start..replacement_end)
860 .ok_or(StoreError::ActiveRowOverflow)?
861 .copy_from_slice(replacement);
862 self.value.truncate(new_len);
863 Ok(())
864 }
865
866 pub(crate) fn as_mut_slice(&mut self) -> &mut [T] {
867 self.value.as_mut_slice()
868 }
869
870 pub(crate) fn clear(&mut self) {
872 self.value.clear();
873 }
874
875 pub(crate) fn dedup(&mut self)
876 where
877 T: PartialEq,
878 {
879 self.value.dedup();
880 }
881
882 pub(crate) fn reserve_additional_bytes(&mut self, additional: u64) -> Result<(), StoreError> {
883 match self._reservation.as_mut() {
884 Some(reservation) => reservation.grow(additional),
885 None if additional == 0 => Ok(()),
886 None => Err(StoreError::AllocationFailed {
887 needed: additional,
888 component: "unaccounted vector",
889 }),
890 }
891 }
892}
893
894#[cfg(test)]
895impl Accounted<Vec<u8>> {
896 fn try_zeroed(
897 accounting: &Arc<Accounting>,
898 bytes: u64,
899 component: AllocationComponent,
900 ) -> Result<Self, StoreError> {
901 let capacity = usize::try_from(bytes).map_err(|_| StoreError::BudgetExceeded {
902 needed: bytes,
903 budget: usize::MAX as u64,
904 component: component.name(),
905 })?;
906 let mut accounted = Self::try_with_capacity(accounting, capacity, component)?;
907 accounted.value.resize(capacity, 0);
908 Ok(accounted)
909 }
910}
911
912#[derive(Clone, Copy, Debug, Eq, PartialEq)]
914pub struct Stats {
915 pub resident_owned_bytes: u64,
917 pub snapshot_bytes: u64,
919 pub mapped_bytes: u64,
921 pub mapped_resident_bytes: u64,
923 pub segment_bytes: u64,
925 pub active_segment_bytes: u64,
928 pub retired_active_segment_bytes: u64,
931 pub active_row_count: u64,
933 pub tombstone_count: u64,
935 pub tombstone_bytes: u64,
937 pub wal_bytes: u64,
940 pub cache_bytes: u64,
942 pub temporary_bytes: u64,
944 pub query_pool_bytes: u64,
946 pub native_graph_bytes: u64,
950 pub open_files: u64,
952 pub active_queries: u64,
954 pub active_snapshot_leases: u64,
956 pub phys_footprint: Option<u64>,
959 pub scans_by_reason: [u64; crate::planner::SCAN_REASON_COUNTER_COUNT],
961 pub graph_segments_served: u64,
963}
964
965impl Store {
966 pub fn stats(&self) -> Result<Stats, StoreError> {
972 let state = self
973 .state
974 .lock()
975 .map_err(|_| StoreError::Synchronization { component: "state" })?;
976 match *state {
977 StoreState::Open => {}
978 StoreState::Closing => return Err(StoreError::Closing),
979 StoreState::Closed => return Err(StoreError::Closed),
980 }
981 let stats = self.stats_while_open();
982 drop(state);
983 stats
984 }
985
986 pub(crate) fn stats_while_open(&self) -> Result<Stats, StoreError> {
987 let writer_lock = self
988 .writer_lock
989 .lock()
990 .map_err(|_| StoreError::Synchronization {
991 component: "writer lock",
992 })?;
993 let wal_writer = self
994 .wal_writer
995 .lock()
996 .map_err(|_| StoreError::Synchronization {
997 component: "WAL writer",
998 })?;
999 let base_open_files =
1000 u64::from(writer_lock.is_some()).saturating_add(u64::from(wal_writer.is_some()));
1001 let active_guard = self
1002 .active
1003 .lock()
1004 .map_err(|_| StoreError::Synchronization {
1005 component: "active segment",
1006 })?;
1007 #[cfg(feature = "graph-cypher")]
1008 let native_only = active_guard.is_none() && self.native_graph.has_publication()?;
1009 #[cfg(not(feature = "graph-cypher"))]
1010 let native_only = false;
1011 let (active_segment_bytes, active_row_count, tombstone_count, tombstone_bytes) =
1012 if let Some(active_state) = active_guard.as_ref() {
1013 let rows = u64::try_from(active_state.segment.row_count()).map_err(|_| {
1014 StoreError::Statistics {
1015 component: "active row count",
1016 source: std::io::Error::other("active row count exceeds u64"),
1017 }
1018 })?;
1019 (
1020 active_state.segment.resident_bytes(),
1021 rows,
1022 active_state.segment.tombstone_count(),
1023 active_state.segment.tombstone_bytes(),
1024 )
1025 } else if native_only {
1026 (0, 0, 0, 0)
1027 } else {
1028 return Err(StoreError::Closed);
1029 };
1030 let active_queries = self
1031 .active_queries
1032 .load(std::sync::atomic::Ordering::Relaxed);
1033 let accounting = self.accounting.audit()?;
1034 if accounting.resident_owned_bytes != accounting.component_sum() {
1035 return Err(StoreError::Statistics {
1036 component: "resident accounting conservation",
1037 source: std::io::Error::other(
1038 "resident-owned total disagrees with component counters",
1039 ),
1040 });
1041 }
1042 let retired_active_segment_bytes = accounting
1043 .active_bytes
1044 .checked_sub(active_segment_bytes)
1045 .ok_or_else(|| StoreError::Statistics {
1046 component: "active segment accounting",
1047 source: std::io::Error::other(
1048 "current active segment capacity exceeds the accounting component",
1049 ),
1050 })?;
1051 let snapshot_guard = self
1052 .snapshot
1053 .read()
1054 .map_err(|_| StoreError::Synchronization {
1055 component: "published snapshot",
1056 })?;
1057 let snapshot = snapshot_guard.as_ref();
1058 if snapshot.is_none() != native_only {
1059 return Err(StoreError::Closed);
1060 }
1061 let active_snapshot_leases = if let Some(snapshot) = snapshot {
1062 u64::try_from(Arc::strong_count(snapshot).saturating_sub(1)).map_err(|_| {
1063 StoreError::Statistics {
1064 component: "active snapshot leases",
1065 source: std::io::Error::other("snapshot lease count exceeds u64"),
1066 }
1067 })?
1068 } else {
1069 0
1070 };
1071 let mut mapped_resident_bytes = 0_u64;
1072 let mut segment_bytes = 0_u64;
1073 for segment in snapshot
1074 .into_iter()
1075 .flat_map(|snapshot| snapshot.all_segments())
1076 {
1077 let bytes =
1078 u64::try_from(segment.mapped_bytes()).map_err(|_| StoreError::Statistics {
1079 component: "segment mapped bytes",
1080 source: std::io::Error::other("segment mapped byte count exceeds u64"),
1081 })?;
1082 segment_bytes =
1083 segment_bytes
1084 .checked_add(bytes)
1085 .ok_or_else(|| StoreError::Statistics {
1086 component: "segment mapped bytes",
1087 source: std::io::Error::other("segment mapped byte count overflow"),
1088 })?;
1089 let resident =
1090 segment
1091 .mapped_resident_bytes()
1092 .map_err(|source| StoreError::Statistics {
1093 component: "mapped resident bytes",
1094 source,
1095 })?;
1096 mapped_resident_bytes =
1097 mapped_resident_bytes.checked_add(resident).ok_or_else(|| {
1098 StoreError::Statistics {
1099 component: "mapped resident bytes",
1100 source: std::io::Error::other("mapped resident byte count overflow"),
1101 }
1102 })?;
1103 }
1104 #[cfg(feature = "graph-cypher")]
1105 let (native_mapping_count, native_resident_bytes) = self.native_graph.mapping_stats()?;
1106 #[cfg(not(feature = "graph-cypher"))]
1107 let (native_mapping_count, native_resident_bytes) = (0, 0);
1108 mapped_resident_bytes = mapped_resident_bytes
1109 .checked_add(native_resident_bytes)
1110 .ok_or_else(|| StoreError::Statistics {
1111 component: "mapped resident bytes",
1112 source: std::io::Error::other("mapped resident byte count overflow"),
1113 })?;
1114 let open_files = base_open_files
1115 .checked_add(native_mapping_count)
1116 .ok_or_else(|| StoreError::Statistics {
1117 component: "open file count",
1118 source: std::io::Error::other("open file count overflow"),
1119 })?;
1120 #[cfg(any(target_os = "macos", target_os = "ios"))]
1121 let phys_footprint =
1122 Some(
1123 crate::sys::darwin::phys_footprint().map_err(|error| StoreError::Statistics {
1124 component: "phys_footprint",
1125 source: std::io::Error::other(error),
1126 })?,
1127 );
1128 #[cfg(not(any(target_os = "macos", target_os = "ios")))]
1129 let phys_footprint = None;
1130 drop(snapshot_guard);
1131 drop(active_guard);
1132 drop(wal_writer);
1133 drop(writer_lock);
1134 Ok(Stats {
1135 resident_owned_bytes: accounting.resident_owned_bytes,
1136 snapshot_bytes: accounting.snapshot_bytes,
1137 mapped_bytes: accounting.mapped_bytes,
1138 mapped_resident_bytes,
1139 segment_bytes,
1140 active_segment_bytes,
1141 retired_active_segment_bytes,
1142 active_row_count,
1143 tombstone_count,
1144 tombstone_bytes,
1145 wal_bytes: accounting.wal_bytes,
1146 cache_bytes: accounting.cache_bytes,
1147 temporary_bytes: accounting.temporary_bytes,
1148 query_pool_bytes: accounting.query_pool_bytes,
1149 native_graph_bytes: accounting.native_graph_bytes,
1150 open_files,
1151 active_queries,
1152 active_snapshot_leases,
1153 phys_footprint,
1154 scans_by_reason: accounting.scans_by_reason,
1155 graph_segments_served: accounting.graph_segments_served,
1156 })
1157 }
1158
1159 #[cfg(test)]
1160 fn allocate_bytes(
1161 &self,
1162 bytes: u64,
1163 component: AllocationComponent,
1164 ) -> Result<Accounted<Vec<u8>>, StoreError> {
1165 let state = self
1166 .state
1167 .lock()
1168 .map_err(|_| StoreError::Synchronization { component: "state" })?;
1169 match *state {
1170 StoreState::Open => {}
1171 StoreState::Closing => return Err(StoreError::Closing),
1172 StoreState::Closed => return Err(StoreError::Closed),
1173 }
1174 let allocation = Accounted::try_zeroed(&self.accounting, bytes, component)?;
1175 drop(state);
1176 Ok(allocation)
1177 }
1178}
1179
1180#[cfg(test)]
1181#[allow(clippy::expect_used)]
1182mod tests {
1183 use tempfile::{TempDir, tempdir};
1184
1185 use super::super::{CancelToken, OpenOptions, QueryControl, Store, StoreError};
1186 use super::{Accounted, Accounting, AllocationComponent};
1187 use crate::lifecycle::durability::{CommitTier, DurabilityMode, DurabilityPolicy};
1188 use crate::manifest::Manifest;
1189 use crate::manifest::io::commit_manifest;
1190 use crate::meta::{AliveSet, ColumnStoreBuilder, Schema};
1191 use crate::quant::Bit4Factors;
1192 use crate::scan::{F32Rows, ScanOptions, ScanQuery, ScanRequest, ScanRows};
1193 use crate::segment::SegmentId;
1194 use crate::segment::layout::RegionEntry;
1195 use crate::segment::reader::SegmentReader;
1196 use crate::segment::writer::{SegmentBuild, SegmentFactors, write_segment};
1197 use crate::vfs::StdVfs;
1198
1199 #[cfg(feature = "graph-cypher")]
1200 #[test]
1201 fn ze204_native_only_stats_succeed_until_close() {
1202 let directory = tempdir().expect("directory");
1203 let store = Store::create_native_graph(
1204 directory.path().join("native"),
1205 OpenOptions::new()
1206 .with_durability(DurabilityMode::Durable, CommitTier::Durable)
1207 .with_max_resident_bytes(256 * 1024 * 1024),
1208 None,
1209 )
1210 .expect("native store");
1211 assert!(store.active.lock().expect("active").is_none());
1212 assert!(store.snapshot.read().expect("snapshot").is_none());
1213 let stats = store.stats().expect("open native stats");
1214 assert_eq!(stats.segment_bytes, 0);
1215 assert_eq!(stats.active_segment_bytes, 0);
1216 assert_eq!(stats.retired_active_segment_bytes, 0);
1217 assert_eq!(stats.active_row_count, 0);
1218 assert_eq!(stats.tombstone_count, 0);
1219 assert_eq!(stats.tombstone_bytes, 0);
1220 assert_eq!(stats.snapshot_bytes, 0);
1221 assert_eq!(stats.active_snapshot_leases, 0);
1222 assert!(stats.native_graph_bytes > 0);
1223 assert_eq!(
1224 stats.resident_owned_bytes,
1225 store.accounting.audit().expect("audit").component_sum()
1226 );
1227 store.close().expect("close");
1228 assert!(matches!(store.stats(), Err(StoreError::Closed)));
1229 }
1230
1231 #[test]
1232 fn stats_bytes_are_conserved() {
1233 let directory = tempdir().expect("store directory");
1234 let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
1235 let baseline = store.stats().expect("baseline stats");
1236 let wal = store
1237 .allocate_bytes(37, AllocationComponent::Wal)
1238 .expect("accounted WAL bytes");
1239 let cache = store
1240 .allocate_bytes(19, AllocationComponent::Cache)
1241 .expect("accounted cache bytes");
1242 let temporary = store
1243 .allocate_bytes(11, AllocationComponent::Temporary)
1244 .expect("accounted temporary bytes");
1245
1246 let stats = store.stats().expect("stats");
1247 let audit = store.accounting.audit().expect("accounting audit");
1248 assert_eq!(stats.wal_bytes, 37);
1249 assert_eq!(stats.cache_bytes, 19);
1250 assert_eq!(stats.temporary_bytes, 11);
1251 assert_eq!(stats.retired_active_segment_bytes, 0);
1252 assert_eq!(
1253 stats.resident_owned_bytes,
1254 baseline.resident_owned_bytes + 67
1255 );
1256 assert_eq!(stats.resident_owned_bytes, audit.component_sum());
1257 assert_eq!(stats.resident_owned_bytes, audit.resident_owned_bytes);
1258
1259 drop((wal, cache, temporary));
1260 assert_eq!(
1261 store.stats().expect("released stats").resident_owned_bytes,
1262 baseline.resident_owned_bytes
1263 );
1264
1265 let published = published_store();
1266 let mapped = Store::open(published.path(), OpenOptions::default()).expect("mapped open");
1267 let snapshot = mapped.snapshot().expect("mapped snapshot");
1268 let segment = snapshot.segments().first().expect("fixture segment");
1269 let expected_snapshot_bytes = std::mem::size_of::<SegmentReader>()
1270 + std::mem::size_of_val(segment.directory())
1271 + segment.retained_validation_bytes()
1272 + usize::try_from(baseline.snapshot_bytes).expect("baseline snapshot bytes fit usize")
1273 + usize::try_from(baseline.native_graph_bytes)
1276 .expect("baseline native graph bytes fit usize");
1277 drop(snapshot);
1278 let mapped_stats = mapped.stats().expect("mapped stats");
1279 assert_eq!(
1280 mapped_stats.resident_owned_bytes,
1281 expected_snapshot_bytes as u64
1282 );
1283 assert_eq!(
1284 mapped_stats.resident_owned_bytes,
1285 mapped
1286 .accounting
1287 .audit()
1288 .expect("mapped accounting audit")
1289 .component_sum()
1290 );
1291 assert_eq!(mapped_stats.tombstone_count, 0);
1292 assert_eq!(mapped_stats.tombstone_bytes, 0);
1293 assert_eq!(mapped_stats.wal_bytes, 0);
1294 assert_eq!(mapped_stats.cache_bytes, 0);
1295 assert_eq!(mapped_stats.temporary_bytes, 0);
1296 assert_eq!(mapped_stats.open_files, 2);
1297 #[cfg(any(target_os = "macos", target_os = "ios"))]
1298 assert!(mapped_stats.phys_footprint.is_some_and(|bytes| bytes > 0));
1299
1300 mapped.close().expect("close mapped store");
1301 assert_eq!(
1302 mapped
1303 .accounting
1304 .audit()
1305 .expect("closed mapping accounting")
1306 .mapped_bytes,
1307 0,
1308 "close must release the exact mapped-byte accounting"
1309 );
1310 assert!(matches!(mapped.stats(), Err(StoreError::Closed)));
1311 }
1312
1313 #[test]
1314 fn repeated_close_returns_exact_stats_counters_to_pre_open_baseline() {
1315 const ITERATIONS: usize = 20;
1316 const PRE_OPEN_MAPPED_BYTES: u64 = 0;
1317 const PRE_OPEN_RESIDENT_OWNED_BYTES: u64 = 0;
1318
1319 let published = published_store();
1320 for iteration in 0..ITERATIONS {
1321 let store = Store::open(published.path(), OpenOptions::default()).expect("mapped open");
1322 let query = [1.0_f32];
1323 let rows = F32Rows::new(vec![1.0_f32; 128]);
1324 store
1325 .top_k_with_options(
1326 ScanRequest {
1327 query: ScanQuery::F32(&query),
1328 rows: ScanRows::F32RowMajor(&rows),
1329 row_mask: None,
1330 },
1331 1,
1332 ScanOptions { thread_budget: 1 },
1333 QueryControl::Cancel(CancelToken::new()),
1334 )
1335 .expect("start the persistent query pool");
1336 let live = store.stats().expect("live stats");
1337 assert!(live.mapped_bytes > PRE_OPEN_MAPPED_BYTES);
1338 assert!(
1339 live.resident_owned_bytes > PRE_OPEN_RESIDENT_OWNED_BYTES,
1340 "iteration {iteration} must own mapped-snapshot bookkeeping"
1341 );
1342 assert!(
1343 live.query_pool_bytes > 0,
1344 "iteration {iteration} must own an accounted query pool"
1345 );
1346
1347 store.close().expect("close mapped store");
1348
1349 let closed = store
1350 .accounting
1351 .audit()
1352 .expect("post-close accounting source for Stats");
1353 assert_eq!(
1354 closed.mapped_bytes, PRE_OPEN_MAPPED_BYTES,
1355 "Stats::mapped_bytes source leaked at iteration {iteration}"
1356 );
1357 assert_eq!(
1358 closed.resident_owned_bytes, PRE_OPEN_RESIDENT_OWNED_BYTES,
1359 "Stats::resident_owned_bytes source leaked at iteration {iteration}"
1360 );
1361 assert_eq!(
1362 closed.query_pool_bytes, 0,
1363 "Stats::query_pool_bytes source leaked at iteration {iteration}"
1364 );
1365 }
1366 }
1367
1368 #[test]
1369 fn budget_exceeded_is_typed_and_pre_allocation() {
1370 let directory = tempdir().expect("store directory");
1371 let baseline_store =
1372 Store::open(directory.path(), OpenOptions::default()).expect("baseline open");
1373 let baseline = baseline_store
1376 .stats()
1377 .expect("baseline stats")
1378 .resident_owned_bytes;
1379 baseline_store.close().expect("close baseline");
1380 let options = OpenOptions::new()
1381 .with_max_resident_bytes(baseline + 64)
1382 .with_max_temp_bytes(32);
1383 let store = Store::open(directory.path(), options).expect("open");
1384
1385 let rejected = store.allocate_bytes(40, AllocationComponent::Temporary);
1386 assert!(matches!(
1387 rejected,
1388 Err(StoreError::BudgetExceeded {
1389 needed: 40,
1390 budget: 32,
1391 component: "temporary",
1392 })
1393 ));
1394 let after_rejection = store.stats().expect("stats after rejection");
1395 assert_eq!(after_rejection.resident_owned_bytes, baseline);
1396 assert_eq!(after_rejection.temporary_bytes, 0);
1397
1398 let accepted = store
1399 .allocate_bytes(24, AllocationComponent::Temporary)
1400 .expect("smaller allocation on same handle");
1401 assert_eq!(accepted.len(), 24);
1402 assert_eq!(store.stats().expect("usable stats").temporary_bytes, 24);
1403 assert!(matches!(
1404 store.allocate_bytes(50, AllocationComponent::Wal),
1405 Err(StoreError::BudgetExceeded {
1406 needed,
1407 budget,
1408 component: "wal",
1409 }) if needed == baseline + 74 && budget == baseline + 64
1410 ));
1411 assert_eq!(
1412 store.stats().expect("resident rejection stats").wal_bytes,
1413 0
1414 );
1415 store.snapshot().expect("same handle remains usable");
1416
1417 let published = published_store();
1418 let snapshot_bytes =
1419 std::mem::size_of::<SegmentReader>() + 6 * std::mem::size_of::<RegionEntry>();
1420 let budget =
1421 u64::try_from(snapshot_bytes.saturating_sub(1)).expect("snapshot budget fits u64");
1422 let needed = u64::try_from(snapshot_bytes).expect("snapshot bytes fit u64");
1423 let rejected_open = Store::open(
1424 published.path(),
1425 OpenOptions::new().with_max_resident_bytes(budget),
1426 );
1427 assert!(matches!(
1428 rejected_open,
1429 Err(StoreError::BudgetExceeded {
1430 needed: actual_needed,
1431 budget: actual_budget,
1432 component: "snapshot",
1433 }) if actual_needed == needed && actual_budget == budget
1434 ));
1435 }
1436
1437 #[test]
1438 fn accounted_vector_rejects_growth_without_a_reservation() {
1439 let mut unaccounted = Accounted::<Vec<u8>>::unaccounted_empty();
1440 unaccounted
1441 .reserve_additional_bytes(0)
1442 .expect("zero bytes require no reservation");
1443 assert!(matches!(
1444 unaccounted.reserve_additional_bytes(1),
1445 Err(StoreError::AllocationFailed {
1446 needed: 1,
1447 component: "unaccounted vector",
1448 })
1449 ));
1450
1451 let accounting = std::sync::Arc::new(Accounting::new(u64::MAX, u64::MAX));
1452 let mut fixed = Accounted::try_with_capacity(&accounting, 1, AllocationComponent::Wal)
1453 .expect("fixed-capacity vector");
1454 fixed.push(7_u8).expect("reserved element");
1455 assert!(matches!(
1456 fixed.push(8_u8),
1457 Err(StoreError::AllocationFailed {
1458 needed: 2,
1459 component: "accounted vector capacity",
1460 })
1461 ));
1462 }
1463
1464 #[test]
1465 fn accounted_allocation_after_close_is_typed_closed() {
1466 let directory = tempdir().expect("store directory");
1467 let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
1468 store.close().expect("close");
1469
1470 assert!(matches!(
1471 store.allocate_bytes(1, AllocationComponent::Wal),
1472 Err(StoreError::Closed)
1473 ));
1474 }
1475
1476 #[cfg(feature = "allocation-audit")]
1477 #[test]
1478 fn nothing_allocates_outside_accounting() {
1479 let directory = tempdir().expect("store directory");
1480 let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
1481
1482 let (unaccounted, negative_control) =
1483 crate::allocation_audit::audit_engine_path(|| Vec::<u8>::with_capacity(13));
1484 assert_eq!(negative_control.attributed_bytes, 0);
1485 assert!(
1486 negative_control.unattributed_bytes >= 13,
1487 "the audit wrapper failed to detect an unattributed engine-path allocation"
1488 );
1489 drop(unaccounted);
1490 store.stats().expect("warm accounting synchronization");
1491
1492 let (accounted, report) = crate::allocation_audit::audit_engine_path(|| {
1493 store.allocate_bytes(23, AllocationComponent::Wal)
1494 });
1495 let accounted = accounted.expect("accounted allocation");
1496 assert_eq!(report.unattributed_bytes, 0);
1497 assert_eq!(report.attributed_bytes, 23);
1498 assert_eq!(store.stats().expect("stats").wal_bytes, 23);
1499 drop(accounted);
1500 }
1501
1502 fn published_store() -> TempDir {
1503 let directory = tempdir().expect("published store directory");
1504 let schema = Schema::new(Vec::new()).expect("timestamp-only schema");
1505 let mut columns = ColumnStoreBuilder::new(schema.clone());
1506 columns.push_row(17, &[]).expect("fixture row");
1507 let columns = columns.finish().expect("fixture columns");
1508 let alive = AliveSet::new(1);
1509 let id = SegmentId::new(0x0102_0304_0506, [0x09; 10]);
1510 let codes = [0x88_u8];
1511 let factors = [Bit4Factors::from_persisted(1.0, 1.0, 1.0)];
1512 let rescore = [0.0_f32, 0.0_f32];
1513 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::Ordered)
1514 .expect("derived policy");
1515 let segment = write_segment(
1516 &StdVfs,
1517 directory.path(),
1518 SegmentBuild {
1519 id,
1520 scheme: 4,
1521 dims: 2,
1522 codes: &codes,
1523 factors: SegmentFactors::Bit4(&factors),
1524 rescore: &rescore,
1525 columns: &columns,
1526 alive: &alive,
1527 },
1528 policy,
1529 )
1530 .expect("fixture segment");
1531 commit_manifest(
1532 &StdVfs,
1533 directory.path(),
1534 &Manifest {
1535 generation: 1,
1536 log_seq: 0,
1537 segments: vec![segment],
1538 epochs: Vec::new(),
1539 epoch_alias: None,
1540 schema,
1541 },
1542 policy,
1543 )
1544 .expect("fixture manifest");
1545 directory
1546 }
1547}