1use std::collections::HashSet;
28use std::sync::Arc;
29
30use arc_swap::ArcSwapOption;
31use tokio::sync::Mutex as AsyncMutex;
32use tokio::task::JoinHandle;
33
34use crate::directories::DirectoryWriter;
35use crate::error::{Error, Result};
36use crate::index::IndexMetadata;
37use crate::segment::{SegmentId, SegmentSnapshot, SegmentTracker, TrainedVectorStructures};
38#[cfg(feature = "native")]
39use crate::segment::{SegmentMerger, SegmentReader};
40
41use super::{MergePolicy, SegmentInfo};
42
43struct MergeInventory {
54 inner: parking_lot::Mutex<HashSet<String>>,
55}
56
57impl MergeInventory {
58 fn new() -> Self {
59 Self {
60 inner: parking_lot::Mutex::new(HashSet::new()),
61 }
62 }
63
64 fn try_register(self: &Arc<Self>, segment_ids: Vec<String>) -> Option<MergeGuard> {
67 let mut inner = self.inner.lock();
68 for id in &segment_ids {
70 if inner.contains(id) {
71 log::debug!(
72 "[merge_inventory] rejected: {} overlaps with active merge ({} active IDs)",
73 id,
74 inner.len()
75 );
76 return None;
77 }
78 }
79 log::debug!(
80 "[merge_inventory] registered {} IDs (total active: {})",
81 segment_ids.len(),
82 inner.len() + segment_ids.len()
83 );
84 for id in &segment_ids {
85 inner.insert(id.clone());
86 }
87 Some(MergeGuard {
88 inventory: Arc::clone(self),
89 segment_ids,
90 })
91 }
92
93 fn snapshot(&self) -> HashSet<String> {
95 self.inner.lock().clone()
96 }
97
98 fn contains(&self, segment_id: &str) -> bool {
100 self.inner.lock().contains(segment_id)
101 }
102}
103
104struct MergeGuard {
108 inventory: Arc<MergeInventory>,
109 segment_ids: Vec<String>,
110}
111
112impl Drop for MergeGuard {
113 fn drop(&mut self) {
114 let mut inner = self.inventory.inner.lock();
115 for id in &self.segment_ids {
116 inner.remove(id);
117 }
118 }
119}
120
121struct ManagerState {
123 metadata: IndexMetadata,
124 merge_policy: Box<dyn MergePolicy>,
125}
126
127pub struct SegmentManager<D: DirectoryWriter + 'static> {
131 state: AsyncMutex<ManagerState>,
133
134 merge_inventory: Arc<MergeInventory>,
137
138 merge_handles: AsyncMutex<Vec<JoinHandle<()>>>,
140
141 trained: ArcSwapOption<TrainedVectorStructures>,
143
144 tracker: Arc<SegmentTracker>,
146
147 delete_fn: Arc<dyn Fn(Vec<SegmentId>) + Send + Sync>,
149
150 directory: Arc<D>,
152 schema: Arc<crate::dsl::Schema>,
154 term_cache_blocks: usize,
156 max_concurrent_merges: usize,
158 reorder_on_merge: bool,
163 bg_cpu_pool: std::sync::OnceLock<Arc<rayon::ThreadPool>>,
167}
168
169impl<D: DirectoryWriter + 'static> SegmentManager<D> {
170 pub fn new(
172 directory: Arc<D>,
173 schema: Arc<crate::dsl::Schema>,
174 metadata: IndexMetadata,
175 merge_policy: Box<dyn MergePolicy>,
176 term_cache_blocks: usize,
177 max_concurrent_merges: usize,
178 ) -> Self {
179 let reorder_on_merge = schema.reorder_on_merge();
182 if reorder_on_merge {
183 log::info!("[merge] reorder-on-merge enabled by index schema");
184 }
185
186 let tracker = Arc::new(SegmentTracker::new());
187 for seg_id in metadata.segment_metas.keys() {
188 tracker.register(seg_id);
189 }
190
191 let delete_fn: Arc<dyn Fn(Vec<SegmentId>) + Send + Sync> = {
192 let dir = Arc::clone(&directory);
193 Arc::new(move |segment_ids| {
194 let Ok(handle) = tokio::runtime::Handle::try_current() else {
197 return;
198 };
199 let dir = Arc::clone(&dir);
200 handle.spawn(async move {
201 for segment_id in segment_ids {
202 log::info!(
203 "[segment_cleanup] deleting deferred segment {}",
204 segment_id.0
205 );
206 let _ = crate::segment::delete_segment(dir.as_ref(), segment_id).await;
207 }
208 });
209 })
210 };
211
212 Self {
213 state: AsyncMutex::new(ManagerState {
214 metadata,
215 merge_policy,
216 }),
217 merge_inventory: Arc::new(MergeInventory::new()),
218 merge_handles: AsyncMutex::new(Vec::new()),
219 trained: ArcSwapOption::new(None),
220 tracker,
221 delete_fn,
222 directory,
223 schema,
224 term_cache_blocks,
225 max_concurrent_merges: max_concurrent_merges.max(1),
226 reorder_on_merge,
227 bg_cpu_pool: std::sync::OnceLock::new(),
228 }
229 }
230
231 pub fn background_cpu_pool(&self) -> Arc<rayon::ThreadPool> {
235 Arc::clone(self.bg_cpu_pool.get_or_init(|| {
236 let threads = (num_cpus::get() / 4).max(1);
237 log::info!("[merge] background CPU pool: {} thread(s)", threads);
238 Arc::new(
239 rayon::ThreadPoolBuilder::new()
240 .num_threads(threads)
241 .thread_name(|i| format!("hermes-bg-cpu-{}", i))
242 .build()
243 .expect("failed to build background CPU pool"),
244 )
245 }))
246 }
247
248 pub async fn get_segment_ids(&self) -> Vec<String> {
254 self.state.lock().await.metadata.segment_ids()
255 }
256
257 pub fn trained(&self) -> Option<Arc<TrainedVectorStructures>> {
259 self.trained.load_full()
260 }
261
262 pub async fn load_and_publish_trained(&self) {
265 let vector_fields = {
267 let st = self.state.lock().await;
268 st.metadata.vector_fields.clone()
269 };
270 let trained =
272 IndexMetadata::load_trained_from_fields(&vector_fields, self.directory.as_ref()).await;
273 if let Some(t) = trained {
274 self.trained.store(Some(Arc::new(t)));
275 }
276 }
277
278 pub(crate) fn clear_trained(&self) {
280 self.trained.store(None);
281 }
282
283 pub(crate) async fn read_metadata<F, R>(&self, f: F) -> R
285 where
286 F: FnOnce(&IndexMetadata) -> R,
287 {
288 let st = self.state.lock().await;
289 f(&st.metadata)
290 }
291
292 pub(crate) async fn update_metadata<F>(&self, f: F) -> Result<()>
294 where
295 F: FnOnce(&mut IndexMetadata),
296 {
297 let mut st = self.state.lock().await;
298 f(&mut st.metadata);
299 st.metadata.save(self.directory.as_ref()).await
300 }
301
302 pub async fn acquire_snapshot(&self) -> SegmentSnapshot {
305 let acquired = {
306 let st = self.state.lock().await;
307 let segment_ids = st.metadata.segment_ids();
308 self.tracker.acquire(&segment_ids)
309 };
310
311 SegmentSnapshot::with_delete_fn(
312 Arc::clone(&self.tracker),
313 acquired,
314 Arc::clone(&self.delete_fn),
315 )
316 }
317
318 pub fn tracker(&self) -> Arc<SegmentTracker> {
320 Arc::clone(&self.tracker)
321 }
322
323 pub fn directory(&self) -> Arc<D> {
325 Arc::clone(&self.directory)
326 }
327}
328
329#[cfg(feature = "native")]
334impl<D: DirectoryWriter + 'static> SegmentManager<D> {
335 pub async fn commit(&self, new_segments: Vec<(String, u32)>) -> Result<()> {
337 let mut st = self.state.lock().await;
338 for (segment_id, num_docs) in new_segments {
339 if !st.metadata.has_segment(&segment_id) {
340 st.metadata.add_segment(segment_id.clone(), num_docs);
341 self.tracker.register(&segment_id);
342 }
343 }
344 st.metadata.save(self.directory.as_ref()).await
345 }
346
347 pub async fn maybe_merge(self: &Arc<Self>) {
359 let slots_available = {
361 let mut handles = self.merge_handles.lock().await;
362 handles.retain(|h| !h.is_finished());
363 self.max_concurrent_merges.saturating_sub(handles.len())
364 };
365
366 if slots_available == 0 {
367 log::debug!("[maybe_merge] at max concurrent merges, skipping");
368 return;
369 }
370
371 let new_handles = {
375 let st = self.state.lock().await;
376
377 let segments: Vec<SegmentInfo> = st
379 .metadata
380 .segment_metas
381 .iter()
382 .filter(|(id, _)| {
383 !self.tracker.is_pending_deletion(id) && !self.merge_inventory.contains(id)
384 })
385 .map(|(id, info)| SegmentInfo {
386 id: id.clone(),
387 num_docs: info.num_docs,
388 })
389 .collect();
390
391 log::debug!("[maybe_merge] {} eligible segments", segments.len());
392
393 let candidates = st.merge_policy.find_merges(&segments);
394
395 if candidates.is_empty() {
396 return;
397 }
398
399 log::debug!(
400 "[maybe_merge] {} merge candidates, {} slots available",
401 candidates.len(),
402 slots_available
403 );
404
405 let mut handles = Vec::new();
406 for c in candidates {
407 if handles.len() >= slots_available {
408 break;
409 }
410 if let Some(h) = self.spawn_merge(c.segment_ids) {
411 handles.push(h);
412 }
413 }
414 handles
415 };
417
418 if !new_handles.is_empty() {
419 self.merge_handles.lock().await.extend(new_handles);
420 }
421 }
422
423 fn spawn_merge(self: &Arc<Self>, segment_ids_to_merge: Vec<String>) -> Option<JoinHandle<()>> {
432 let output_id = SegmentId::new();
433 let output_hex = output_id.to_hex();
434
435 let mut all_ids = segment_ids_to_merge.clone();
436 all_ids.push(output_hex);
437
438 let guard = match self.merge_inventory.try_register(all_ids) {
439 Some(g) => g,
440 None => {
441 log::debug!("[spawn_merge] skipped: segments overlap with active merge");
442 return None;
443 }
444 };
445
446 let sm = Arc::clone(self);
447 let ids = segment_ids_to_merge;
448
449 Some(tokio::spawn(async move {
450 let _guard = guard;
451
452 let trained_snap = sm.trained();
453 let granularity = sm.merge_granularity(&ids).await;
454 let result = Self::do_merge(
455 sm.directory.as_ref(),
456 &sm.schema,
457 &ids,
458 output_id,
459 sm.term_cache_blocks,
460 trained_snap.as_deref(),
461 sm.reorder_on_merge,
462 granularity,
463 Some(sm.background_cpu_pool()),
464 )
465 .await;
466
467 match result {
468 Ok((new_id, doc_count)) => {
469 if let Err(e) = sm
470 .replace_segments(&ids, new_id, doc_count, sm.reorder_on_merge, true)
471 .await
472 {
473 log::error!("[merge] Failed to replace segments after merge: {:?}", e);
474 }
475 }
476 Err(e) => {
477 log::error!(
478 "[merge] Background merge failed for segments {:?}: {:?}",
479 ids,
480 e
481 );
482 }
483 }
484 sm.maybe_merge().await;
489 }))
490 }
491
492 async fn replace_segments(
496 &self,
497 old_ids: &[String],
498 new_id: String,
499 doc_count: u32,
500 reordered: bool,
501 bp_converged: bool,
502 ) -> Result<()> {
503 self.tracker.register(&new_id);
504
505 {
506 let mut st = self.state.lock().await;
507 let parent_gen = old_ids
509 .iter()
510 .filter_map(|id| st.metadata.segment_metas.get(id))
511 .map(|info| info.generation)
512 .max()
513 .unwrap_or(0);
514 let ancestors: Vec<String> = old_ids.to_vec();
515
516 for id in old_ids {
517 st.metadata.remove_segment(id);
518 }
519 st.metadata.add_merged_segment(
520 new_id,
521 doc_count,
522 ancestors,
523 parent_gen + 1,
524 reordered,
525 bp_converged,
526 );
527 st.metadata.save(self.directory.as_ref()).await?;
529 }
530
531 let ready_to_delete = self.tracker.mark_for_deletion(old_ids);
532 for segment_id in ready_to_delete {
533 let _ = crate::segment::delete_segment(self.directory.as_ref(), segment_id).await;
534 }
535 Ok(())
536 }
537
538 #[allow(clippy::too_many_arguments)]
542 #[allow(clippy::too_many_arguments)]
543 pub(crate) async fn do_merge(
544 directory: &D,
545 schema: &Arc<crate::dsl::Schema>,
546 segment_ids_to_merge: &[String],
547 output_segment_id: SegmentId,
548 term_cache_blocks: usize,
549 trained: Option<&TrainedVectorStructures>,
550 reorder_bmp: bool,
551 granularity: crate::segment::reorder::BpGranularity,
552 bg_cpu_pool: Option<Arc<rayon::ThreadPool>>,
553 ) -> Result<(String, u32)> {
554 let output_hex = output_segment_id.to_hex();
555 let load_start = std::time::Instant::now();
556
557 let segment_ids: Vec<SegmentId> = segment_ids_to_merge
558 .iter()
559 .map(|id_str| {
560 SegmentId::from_hex(id_str)
561 .ok_or_else(|| Error::Corruption(format!("Invalid segment ID: {}", id_str)))
562 })
563 .collect::<Result<Vec<_>>>()?;
564
565 let schema_arc = Arc::clone(schema);
566 let futures: Vec<_> = segment_ids
567 .iter()
568 .map(|&sid| {
569 let sch = Arc::clone(&schema_arc);
570 async move { SegmentReader::open(directory, sid, sch, term_cache_blocks).await }
571 })
572 .collect();
573
574 let results = futures::future::join_all(futures).await;
575 let mut readers = Vec::with_capacity(results.len());
576 let mut total_docs = 0u64;
577 for (i, result) in results.into_iter().enumerate() {
578 match result {
579 Ok(r) => {
580 total_docs += r.meta().num_docs as u64;
581 readers.push(r);
582 }
583 Err(e) => {
584 log::error!(
585 "[merge] Failed to open segment {}: {:?}",
586 segment_ids_to_merge[i],
587 e
588 );
589 return Err(e);
590 }
591 }
592 }
593
594 for (i, reader) in readers.iter().enumerate() {
598 let meta_docs = reader.meta().num_docs;
599 let store_docs = reader.store().num_docs();
600 if store_docs != meta_docs {
601 return Err(Error::Corruption(format!(
602 "pre-merge validation: segment {} store has {} docs but meta says {}",
603 segment_ids_to_merge[i], store_docs, meta_docs
604 )));
605 }
606 }
607
608 log::info!(
609 "[merge] loaded {} segment readers in {:.1}s",
610 readers.len(),
611 load_start.elapsed().as_secs_f64()
612 );
613
614 let merger = SegmentMerger::new(Arc::clone(schema))
615 .with_bmp_reorder(reorder_bmp)
616 .with_granularity(granularity)
617 .with_background_pool(bg_cpu_pool);
618
619 log::info!(
620 "[merge] {} segments -> {} (trained={})",
621 segment_ids_to_merge.len(),
622 output_hex,
623 trained.map_or(0, |t| t.centroids.len()),
624 );
625
626 merger
627 .merge(directory, &readers, output_segment_id, trained)
628 .await?;
629
630 log::info!(
631 "[merge] total wall-clock: {:.1}s ({} segments, {} docs)",
632 load_start.elapsed().as_secs_f64(),
633 readers.len(),
634 total_docs,
635 );
636
637 if total_docs > u32::MAX as u64 {
638 return Err(Error::Internal(format!(
639 "Merged segment doc count ({}) exceeds u32::MAX",
640 total_docs
641 )));
642 }
643 Ok((output_hex, total_docs as u32))
644 }
645
646 pub async fn abort_merges(&self) {
649 let handles: Vec<JoinHandle<()>> =
650 { std::mem::take(&mut *self.merge_handles.lock().await) };
651 for h in handles {
652 h.abort();
653 }
654 }
655
656 pub async fn wait_for_merging_thread(self: &Arc<Self>) {
658 let handles: Vec<JoinHandle<()>> =
659 { std::mem::take(&mut *self.merge_handles.lock().await) };
660 for h in handles {
661 let _ = h.await;
662 }
663 }
664
665 pub async fn wait_for_all_merges(self: &Arc<Self>) {
671 loop {
672 let handles: Vec<JoinHandle<()>> =
673 { std::mem::take(&mut *self.merge_handles.lock().await) };
674 if handles.is_empty() {
675 break;
676 }
677 for h in handles {
678 let _ = h.await;
679 }
680 }
681 }
682
683 pub async fn force_merge(self: &Arc<Self>) -> Result<()> {
692 const FORCE_MERGE_BATCH: usize = 64;
693
694 let max_segment_docs = {
695 let st = self.state.lock().await;
696 st.merge_policy.max_segment_docs()
697 };
698
699 self.wait_for_all_merges().await;
702
703 loop {
704 let mut segments: Vec<(String, u32)> = {
706 let st = self.state.lock().await;
707 st.metadata
708 .segment_metas
709 .iter()
710 .map(|(id, info)| (id.clone(), info.num_docs))
711 .collect()
712 };
713
714 if segments.len() < 2 {
715 return Ok(());
716 }
717
718 segments.sort_by_key(|(_, docs)| *docs);
719
720 let max_docs = max_segment_docs.map(|m| m as u64).unwrap_or(u64::MAX);
722 let mut batch = Vec::new();
723 let mut batch_docs = 0u64;
724
725 for (id, docs) in &segments {
726 if batch.len() >= FORCE_MERGE_BATCH {
727 break;
728 }
729 let next_total = batch_docs + *docs as u64;
730 if next_total > max_docs && !batch.is_empty() {
731 break;
732 }
733 batch.push(id.clone());
734 batch_docs += *docs as u64;
735 }
736
737 if batch.len() < 2 {
738 return Ok(());
739 }
740
741 log::info!(
742 "[force_merge] merging batch of {} segments ({} docs)",
743 batch.len(),
744 batch_docs
745 );
746
747 let output_id = SegmentId::new();
748 let output_hex = output_id.to_hex();
749
750 let mut all_ids = batch.clone();
752 all_ids.push(output_hex);
753 let _guard = match self.merge_inventory.try_register(all_ids) {
754 Some(g) => g,
755 None => {
756 self.wait_for_merging_thread().await;
758 continue;
759 }
760 };
761
762 let trained_snap = self.trained();
763 let granularity = self.merge_granularity(&batch).await;
764 let (new_segment_id, total_docs) = Self::do_merge(
765 self.directory.as_ref(),
766 &self.schema,
767 &batch,
768 output_id,
769 self.term_cache_blocks,
770 trained_snap.as_deref(),
771 self.reorder_on_merge,
772 granularity,
773 Some(self.background_cpu_pool()),
774 )
775 .await?;
776
777 self.replace_segments(
778 &batch,
779 new_segment_id,
780 total_docs,
781 self.reorder_on_merge,
782 true,
783 )
784 .await?;
785
786 }
788 }
789
790 pub async fn reorder_segments(self: &Arc<Self>) -> Result<()> {
797 self.wait_for_all_merges().await;
798 let segment_ids = self.get_segment_ids().await;
799
800 if segment_ids.is_empty() {
801 log::info!("[reorder] no segments to reorder");
802 return Ok(());
803 }
804
805 log::info!("[reorder] reordering {} segments", segment_ids.len());
806
807 for seg_id in segment_ids {
808 match self
809 .reorder_single_segment(&seg_id, None, crate::segment::BpBudget::full())
810 .await
811 {
812 Ok(true) => {}
813 Ok(false) => log::warn!("[reorder] segment {} skipped (in merge)", seg_id),
814 Err(e) => return Err(e),
815 }
816 }
817
818 log::info!("[reorder] all segments reordered");
819 Ok(())
820 }
821
822 pub async fn unreordered_segment_ids(&self) -> Vec<String> {
827 let st = self.state.lock().await;
828 let in_merge = self.merge_inventory.snapshot();
829 st.metadata
830 .segment_metas
831 .iter()
832 .filter(|(id, info)| !info.reordered && !in_merge.contains(*id))
833 .map(|(id, _)| id.clone())
834 .collect()
835 }
836
837 pub async fn unreordered_segments(&self) -> Vec<(String, u32)> {
840 let st = self.state.lock().await;
841 let in_merge = self.merge_inventory.snapshot();
842 st.metadata
843 .segment_metas
844 .iter()
845 .filter(|(id, info)| !info.reordered && !in_merge.contains(*id))
846 .map(|(id, info)| (id.clone(), info.num_docs))
847 .collect()
848 }
849
850 pub async fn unconverged_segments(&self) -> Vec<(String, u32)> {
854 let st = self.state.lock().await;
855 let in_merge = self.merge_inventory.snapshot();
856 st.metadata
857 .segment_metas
858 .iter()
859 .filter(|(id, info)| info.reordered && !info.bp_converged && !in_merge.contains(*id))
860 .map(|(id, info)| (id.clone(), info.num_docs))
861 .collect()
862 }
863
864 async fn merge_granularity(&self, ids: &[String]) -> crate::segment::reorder::BpGranularity {
874 let st = self.state.lock().await;
875 let deepening = ids.iter().any(|id| {
876 st.metadata
877 .segment_metas
878 .get(id)
879 .is_some_and(|info| info.reordered && !info.bp_converged)
880 });
881 drop(st);
882 if deepening {
883 log::info!(
884 "[reorder] source segment(s) unconverged — forcing record-level BP (deepening pass)",
885 );
886 crate::segment::reorder::BpGranularity::Records
887 } else {
888 crate::segment::reorder::BpGranularity::Auto
889 }
890 }
891
892 pub async fn reorder_single_segment(
897 self: &Arc<Self>,
898 seg_id: &str,
899 rayon_pool: Option<Arc<rayon::ThreadPool>>,
900 bp_budget: crate::segment::BpBudget,
901 ) -> Result<bool> {
902 let source_id = SegmentId::from_hex(seg_id)
903 .ok_or_else(|| Error::Corruption(format!("Invalid segment ID: {}", seg_id)))?;
904 let output_id = SegmentId::new();
905 let output_hex = output_id.to_hex();
906 let source_ids = [seg_id.to_string()];
907 let granularity = self.merge_granularity(&source_ids).await;
908
909 let all_ids = vec![seg_id.to_string(), output_hex];
910 let _guard = match self.merge_inventory.try_register(all_ids) {
911 Some(g) => g,
912 None => {
913 log::debug!("[optimizer] segment {} in active merge, skipping", seg_id);
914 return Ok(false);
915 }
916 };
917
918 let (new_id, total_docs, bp_converged) = crate::segment::reorder::reorder_segment(
919 self.directory.as_ref(),
920 &self.schema,
921 source_id,
922 output_id,
923 self.term_cache_blocks,
924 crate::segment::reorder::DEFAULT_MEMORY_BUDGET,
925 bp_budget,
926 granularity,
927 rayon_pool,
928 )
929 .await?;
930
931 self.replace_segments(
932 &[seg_id.to_string()],
933 new_id,
934 total_docs,
935 true,
936 bp_converged,
937 )
938 .await?;
939
940 Ok(true)
941 }
942
943 pub async fn cleanup_orphan_segments(&self) -> Result<usize> {
948 let (registered_set, in_merge_set) = {
952 let st = self.state.lock().await;
953 let registered = st
954 .metadata
955 .segment_metas
956 .keys()
957 .cloned()
958 .collect::<HashSet<String>>();
959 let in_merge = self.merge_inventory.snapshot();
960 (registered, in_merge)
961 };
962
963 let mut orphan_ids: HashSet<String> = HashSet::new();
964
965 if let Ok(entries) = self.directory.list_files(std::path::Path::new("")).await {
966 for entry in entries {
967 let filename = entry.to_string_lossy();
968 if filename.starts_with("seg_") && filename.len() > 37 {
969 let hex_part = &filename[4..36];
970 if !registered_set.contains(hex_part) && !in_merge_set.contains(hex_part) {
971 orphan_ids.insert(hex_part.to_string());
972 }
973 }
974 }
975 }
976
977 let mut deleted = 0;
978 for hex_id in &orphan_ids {
979 if let Some(segment_id) = SegmentId::from_hex(hex_id)
980 && crate::segment::delete_segment(self.directory.as_ref(), segment_id)
981 .await
982 .is_ok()
983 {
984 deleted += 1;
985 }
986 }
987
988 Ok(deleted)
989 }
990}
991
992#[cfg(test)]
993mod tests {
994 use super::*;
995
996 #[test]
997 fn test_inventory_guard_drop_unregisters() {
998 let inv = Arc::new(MergeInventory::new());
999 {
1000 let _guard = inv.try_register(vec!["a".into(), "b".into()]).unwrap();
1001 let snap = inv.snapshot();
1002 assert!(snap.contains("a"));
1003 assert!(snap.contains("b"));
1004 }
1005 assert!(inv.snapshot().is_empty());
1007 }
1008
1009 #[test]
1010 fn test_inventory_concurrent_non_overlapping_merges() {
1011 let inv = Arc::new(MergeInventory::new());
1012 let _g1 = inv.try_register(vec!["a".into(), "b".into()]).unwrap();
1013 let _g2 = inv.try_register(vec!["c".into(), "d".into()]).unwrap();
1015 let snap = inv.snapshot();
1016 assert_eq!(snap.len(), 4);
1017
1018 drop(_g1);
1020 let snap = inv.snapshot();
1021 assert_eq!(snap.len(), 2);
1022 assert!(snap.contains("c"));
1023 assert!(snap.contains("d"));
1024 }
1025
1026 #[test]
1027 fn test_inventory_overlapping_merge_rejected() {
1028 let inv = Arc::new(MergeInventory::new());
1029 let _g1 = inv.try_register(vec!["a".into(), "b".into()]).unwrap();
1030 assert!(inv.try_register(vec!["b".into(), "c".into()]).is_none());
1032 drop(_g1);
1034 assert!(inv.try_register(vec!["b".into(), "c".into()]).is_some());
1035 }
1036
1037 #[test]
1038 fn test_inventory_snapshot() {
1039 let inv = Arc::new(MergeInventory::new());
1040 let _g = inv.try_register(vec!["x".into(), "y".into()]).unwrap();
1041 let snap = inv.snapshot();
1042 assert!(snap.contains("x"));
1043 assert!(snap.contains("y"));
1044 assert!(!snap.contains("z"));
1045 }
1046}