1use super::primary_key::load_pk_segment_data;
39use super::staged_row::{StagedRow, StagedSegment};
40
41struct QueuedDocument {
42 doc: Document,
43 row: Option<Arc<StagedRow>>,
44}
45use std::sync::Arc;
46use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
47
48use futures::{FutureExt, StreamExt, TryStreamExt};
49use rustc_hash::FxHashMap;
50
51use crate::directories::DirectoryWriter;
52use crate::dsl::{Document, Field, Schema};
53use crate::error::{Error, Result};
54use crate::segment::{
55 SegmentBuilder, SegmentBuilderConfig, SegmentId, validate_vector_value_counts,
56};
57use crate::tokenizer::BoxedTokenizer;
58
59use super::IndexConfig;
60
61const PIPELINE_MAX_SIZE_IN_DOCS: usize = 10_000;
63
64const SOFT_FLUSH_MIN_PERCENT: usize = 70;
70const SOFT_FLUSH_MAX_PERCENT: usize = 80;
71
72pub const WRITER_LOCK_FILENAME: &str = ".summa_writer.lock";
74
75fn soft_flush_threshold(memory_budget: usize, worker_id: usize, num_workers: usize) -> usize {
82 if num_workers <= 1 {
83 return hard_flush_threshold(memory_budget);
84 }
85
86 let worker_id = worker_id.min(num_workers - 1);
87 let span = SOFT_FLUSH_MAX_PERCENT - SOFT_FLUSH_MIN_PERCENT;
88 let denominator = 100u128 * (num_workers - 1) as u128;
89 let numerator = (SOFT_FLUSH_MIN_PERCENT * (num_workers - 1) + span * worker_id) as u128;
90 ((memory_budget as u128 * numerator) / denominator) as usize
91}
92
93fn hard_flush_threshold(memory_budget: usize) -> usize {
97 memory_budget.saturating_mul(SOFT_FLUSH_MAX_PERCENT) / 100
98}
99
100fn default_builder_config(index_config: &IndexConfig) -> SegmentBuilderConfig {
107 let bounds = index_config.effective_posting_bounds();
108 SegmentBuilderConfig {
109 num_compression_threads: index_config.num_compression_threads,
110 optimization: index_config.optimization,
111 posting_codec: index_config.effective_posting_codec(),
112 quantized_norms: index_config.quantized_norms,
113 compact_text: index_config.compact_text,
114 posting_ratio_bounds: bounds.ratio,
115 posting_impact_bounds: bounds.impact,
116 term_dict_block_size: index_config.term_dict_block_size,
117 ..SegmentBuilderConfig::default()
118 }
119}
120
121struct SegmentBuildLimiter {
129 live_max_active: usize,
130 flush_max_active: usize,
131 active: AtomicUsize,
132 flushing: AtomicBool,
133 wait_mutex: parking_lot::Mutex<()>,
134 available: parking_lot::Condvar,
135}
136
137impl SegmentBuildLimiter {
138 fn new(num_workers: usize) -> Self {
139 Self {
140 live_max_active: num_workers.saturating_sub(1).max(1),
143 flush_max_active: num_workers.max(1),
146 active: AtomicUsize::new(0),
147 flushing: AtomicBool::new(false),
148 wait_mutex: parking_lot::Mutex::new(()),
149 available: parking_lot::Condvar::new(),
150 }
151 }
152
153 fn try_acquire(&self) -> Option<SegmentBuildPermit<'_>> {
154 self.try_acquire_up_to(self.live_max_active)
155 }
156
157 fn try_acquire_up_to(&self, limit: usize) -> Option<SegmentBuildPermit<'_>> {
158 let mut active = self.active.load(Ordering::Acquire);
159 loop {
160 if active >= limit {
161 return None;
162 }
163 match self.active.compare_exchange_weak(
164 active,
165 active + 1,
166 Ordering::AcqRel,
167 Ordering::Acquire,
168 ) {
169 Ok(_) => return Some(SegmentBuildPermit { limiter: self }),
170 Err(observed) => active = observed,
171 }
172 }
173 }
174
175 fn acquire(&self) -> SegmentBuildPermit<'_> {
176 let mut wait = self.wait_mutex.lock();
177 loop {
178 let limit = if self.flushing.load(Ordering::Acquire) {
179 self.flush_max_active
180 } else {
181 self.live_max_active
182 };
183 if let Some(permit) = self.try_acquire_up_to(limit) {
184 return permit;
185 }
186 self.available.wait(&mut wait);
187 }
188 }
189
190 fn acquire_flush(&self) -> SegmentBuildPermit<'_> {
191 self.acquire_up_to(self.flush_max_active)
192 }
193
194 fn acquire_up_to(&self, limit: usize) -> SegmentBuildPermit<'_> {
195 let mut wait = self.wait_mutex.lock();
196 loop {
197 if let Some(permit) = self.try_acquire_up_to(limit) {
198 return permit;
199 }
200 self.available.wait(&mut wait);
201 }
202 }
203
204 fn begin_flush(&self) {
208 self.flushing.store(true, Ordering::Release);
209 let _wait = self.wait_mutex.lock();
210 self.available.notify_all();
211 }
212
213 fn end_flush(&self) {
214 self.flushing.store(false, Ordering::Release);
215 }
216
217 fn reserve_if_due(
223 &self,
224 builder_memory: usize,
225 soft_threshold: usize,
226 hard_budget: usize,
227 ) -> Option<SegmentBuildPermit<'_>> {
228 if builder_memory < soft_threshold {
229 return None;
230 }
231 if let Some(permit) = self.try_acquire() {
232 return Some(permit);
233 }
234 if builder_memory < hard_budget {
235 return None;
236 }
237 Some(self.acquire())
238 }
239}
240
241struct SegmentBuildPermit<'a> {
242 limiter: &'a SegmentBuildLimiter,
243}
244
245impl Drop for SegmentBuildPermit<'_> {
246 fn drop(&mut self) {
247 let previous = self.limiter.active.fetch_sub(1, Ordering::AcqRel);
248 debug_assert!(previous > 0);
249 let _wait = self.limiter.wait_mutex.lock();
252 self.limiter.available.notify_all();
255 }
256}
257
258#[cfg(test)]
259mod indexing_pipeline_tests {
260 use std::sync::Arc;
261 use std::time::Duration;
262
263 use super::{
264 SOFT_FLUSH_MAX_PERCENT, SOFT_FLUSH_MIN_PERCENT, SegmentBuildLimiter,
265 default_builder_config, hard_flush_threshold, soft_flush_threshold,
266 };
267
268 #[test]
269 fn standard_builder_config_honors_index_compression_width() {
270 let index_config = crate::index::IndexConfig {
271 num_compression_threads: 7,
272 ..Default::default()
273 };
274
275 let builder_config = default_builder_config(&index_config);
276
277 assert_eq!(builder_config.num_compression_threads, 7);
278 }
279
280 #[test]
281 fn flush_thresholds_are_staggered_without_increasing_memory_budget() {
282 const WORKERS: usize = 12;
283 const PER_WORKER_BUDGET: usize = 1024 * 1024 * 1024;
284
285 let thresholds: Vec<_> = (0..WORKERS)
286 .map(|worker| soft_flush_threshold(PER_WORKER_BUDGET, worker, WORKERS))
287 .collect();
288
289 assert_eq!(
290 thresholds[0],
291 PER_WORKER_BUDGET * SOFT_FLUSH_MIN_PERCENT / 100
292 );
293 assert_eq!(
294 thresholds[WORKERS - 1],
295 PER_WORKER_BUDGET * SOFT_FLUSH_MAX_PERCENT / 100
296 );
297 assert!(
298 thresholds.windows(2).all(|pair| pair[0] < pair[1]),
299 "production-width workers must not reach identical flush thresholds: {thresholds:?}"
300 );
301
302 let staggered_total: usize = thresholds.iter().sum();
303 let former_uniform_total = WORKERS * (PER_WORKER_BUDGET * SOFT_FLUSH_MAX_PERCENT / 100);
304 assert!(
305 staggered_total <= former_uniform_total,
306 "staggering must not increase aggregate builder memory"
307 );
308
309 assert_eq!(
310 soft_flush_threshold(PER_WORKER_BUDGET, 0, 1),
311 PER_WORKER_BUDGET * SOFT_FLUSH_MAX_PERCENT / 100,
312 "single-worker behavior retains the former 80% build headroom"
313 );
314
315 let hard_threshold = hard_flush_threshold(PER_WORKER_BUDGET);
316 let build_scratch = PER_WORKER_BUDGET - hard_threshold;
317 let steady_state_peak = (WORKERS - 1) * (hard_threshold + build_scratch) + hard_threshold;
318 assert!(
319 steady_state_peak <= WORKERS * PER_WORKER_BUDGET,
320 "rotated hard-threshold builds must retain aggregate scratch headroom"
321 );
322 }
323
324 #[test]
325 fn full_build_gate_leaves_soft_threshold_worker_draining() {
326 const WORKERS: usize = 12;
327 let limiter = SegmentBuildLimiter::new(WORKERS);
328 let mut active_builds: Vec<_> = (0..WORKERS - 1)
329 .map(|_| {
330 limiter
331 .try_acquire()
332 .expect("N - 1 builds should be admitted")
333 })
334 .collect();
335
336 assert!(
337 limiter.try_acquire().is_none(),
338 "the final worker must be reserved from concurrent segment builds"
339 );
340 assert!(
341 limiter.reserve_if_due(750, 700, 800).is_none(),
342 "a worker below its hard budget must keep draining when builds are saturated"
343 );
344
345 drop(active_builds.pop());
346 let replacement = limiter
347 .reserve_if_due(750, 700, 800)
348 .expect("a completed build must immediately rotate draining capacity");
349 assert!(limiter.try_acquire().is_none());
350 drop(replacement);
351 drop(active_builds);
352 }
353
354 #[test]
355 fn closed_queue_flush_uses_the_reserved_build_slot() {
356 let limiter = SegmentBuildLimiter::new(2);
357 let live_build = limiter
358 .try_acquire()
359 .expect("one live build should be admitted");
360 assert!(limiter.try_acquire().is_none());
361
362 let tail_build = limiter.acquire_flush();
363 assert!(
364 limiter
365 .try_acquire_up_to(limiter.flush_max_active)
366 .is_none(),
367 "closed-queue flushes must remain bounded by the worker count"
368 );
369
370 drop(tail_build);
371 drop(live_build);
372 }
373
374 #[test]
375 fn closing_input_promotes_an_existing_live_waiter() {
376 let limiter = Arc::new(SegmentBuildLimiter::new(2));
377 let live_build = limiter.try_acquire().unwrap();
378 let waiter_limiter = Arc::clone(&limiter);
379 let (started_tx, started_rx) = std::sync::mpsc::channel();
380 let (acquired_tx, acquired_rx) = std::sync::mpsc::channel();
381
382 let waiter = std::thread::spawn(move || {
383 started_tx.send(()).unwrap();
384 let _permit = waiter_limiter
385 .reserve_if_due(800, 700, 800)
386 .expect("closed input must promote a hard-boundary waiter");
387 acquired_tx.send(()).unwrap();
388 });
389
390 started_rx.recv_timeout(Duration::from_secs(1)).unwrap();
391 assert!(acquired_rx.recv_timeout(Duration::from_millis(50)).is_err());
392
393 limiter.begin_flush();
394 acquired_rx
395 .recv_timeout(Duration::from_secs(1))
396 .expect("live waiter did not adopt the closed-queue build limit");
397 waiter.join().unwrap();
398 limiter.end_flush();
399 drop(live_build);
400 }
401
402 #[test]
403 fn hard_budget_waiter_resumes_when_a_build_finishes() {
404 let limiter = Arc::new(SegmentBuildLimiter::new(3));
405 let first = limiter.try_acquire().unwrap();
406 let second = limiter.try_acquire().unwrap();
407 let waiter_limiter = Arc::clone(&limiter);
408 let (started_tx, started_rx) = std::sync::mpsc::channel();
409 let (acquired_tx, acquired_rx) = std::sync::mpsc::channel();
410
411 let waiter = std::thread::spawn(move || {
412 started_tx.send(()).unwrap();
413 let _permit = waiter_limiter
414 .reserve_if_due(800, 700, 800)
415 .expect("hard-budget worker must eventually acquire a build slot");
416 acquired_tx.send(()).unwrap();
417 });
418
419 started_rx.recv_timeout(Duration::from_secs(1)).unwrap();
420 assert!(
421 acquired_rx.recv_timeout(Duration::from_millis(50)).is_err(),
422 "hard-budget worker must not over-subscribe segment builds"
423 );
424 drop(first);
425 acquired_rx
426 .recv_timeout(Duration::from_secs(1))
427 .expect("hard-budget worker did not wake after a build completed");
428 waiter.join().unwrap();
429 drop(second);
430 }
431}
432
433enum WriterLock {
442 Held { _file: std::fs::File },
444 NotApplicable,
447 Unavailable { reason: String },
450}
451
452fn writer_lock_root<D: DirectoryWriter + 'static>(directory: &D) -> Option<std::path::PathBuf> {
455 let any: &dyn std::any::Any = directory;
456 if let Some(mmap) = any.downcast_ref::<crate::directories::MmapDirectory>() {
457 return Some(mmap.root().to_path_buf());
458 }
459 if any
463 .downcast_ref::<crate::directories::FsDirectory>()
464 .is_some()
465 {
466 log::warn!(
467 "[writer_lock] FsDirectory exposes no root path; single-writer locking \
468 is not enforced for this writer — do not open a second writer for the \
469 same index directory"
470 );
471 }
472 None
473}
474
475fn try_acquire_writer_lock<D: DirectoryWriter + 'static>(directory: &D) -> Result<WriterLock> {
480 let Some(root) = writer_lock_root(directory) else {
481 return Ok(WriterLock::NotApplicable);
482 };
483 std::fs::create_dir_all(&root)?;
484 let lock_path = root.join(WRITER_LOCK_FILENAME);
485 let file = std::fs::OpenOptions::new()
486 .create(true)
487 .truncate(false)
488 .write(true)
489 .open(&lock_path)?;
490 match file.try_lock() {
491 Ok(()) => Ok(WriterLock::Held { _file: file }),
492 Err(std::fs::TryLockError::WouldBlock) => Ok(WriterLock::Unavailable {
493 reason: format!(
494 "another IndexWriter already holds the single-writer lock for this \
495 index ({}); Summa supports one writer per index directory — stop \
496 the other writer (e.g. a running summa-server or summa-tool) \
497 before opening this one",
498 lock_path.display()
499 ),
500 }),
501 Err(std::fs::TryLockError::Error(error)) => Err(Error::Io(error)),
502 }
503}
504
505pub struct IndexWriter<D: DirectoryWriter + 'static> {
519 pub(super) directory: Arc<D>,
520 pub(super) schema: Arc<Schema>,
521 pub(super) config: IndexConfig,
522 doc_sender: Arc<parking_lot::RwLock<async_channel::Sender<QueuedDocument>>>,
525 workers: Vec<std::thread::JoinHandle<()>>,
527 worker_state: Arc<WorkerState<D>>,
529 pub(super) segment_manager: Arc<crate::merge::SegmentManager<D>>,
531 flushed_segments: Arc<parking_lot::Mutex<Vec<PreparedSegment<D>>>>,
534 primary_key_index: Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
536 primary_key_refresh_lock: Arc<tokio::sync::Mutex<()>>,
539 commit_finalization: Arc<CommitFinalizationState>,
543 pk_reservations_retained: Arc<AtomicBool>,
548 writer_lock: parking_lot::RwLock<WriterLock>,
553}
554
555#[derive(Default)]
556struct CommitFinalizationState {
557 in_progress: AtomicBool,
558 idle: tokio::sync::Notify,
559}
560
561impl CommitFinalizationState {
562 fn begin(&self) -> bool {
563 self.in_progress
564 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
565 .is_ok()
566 }
567
568 fn finish(&self) {
569 self.in_progress.store(false, Ordering::Release);
570 self.idle.notify_waiters();
571 }
572
573 async fn wait_until_idle(&self) {
574 while self.in_progress.load(Ordering::Acquire) {
575 let notified = self.idle.notified();
576 if !self.in_progress.load(Ordering::Acquire) {
577 break;
578 }
579 notified.await;
580 }
581 }
582}
583
584struct WorkerState<D: DirectoryWriter + 'static> {
586 directory: Arc<D>,
587 schema: Arc<Schema>,
588 builder_config: SegmentBuilderConfig,
589 tokenizers: parking_lot::RwLock<FxHashMap<Field, BoxedTokenizer>>,
590 memory_budget_per_worker: usize,
592 segment_build_limiter: SegmentBuildLimiter,
595 segment_manager: Arc<crate::merge::SegmentManager<D>>,
597 built_segments: parking_lot::Mutex<Vec<PreparedSegment<D>>>,
600 cycle_error: parking_lot::Mutex<Option<String>>,
605 cycle_failed: AtomicBool,
606
607 flush_count: AtomicUsize,
614 flush_mutex: parking_lot::Mutex<()>,
616 flush_cvar: parking_lot::Condvar,
617 resume_receiver: parking_lot::Mutex<Option<async_channel::Receiver<QueuedDocument>>>,
619 resume_epoch: AtomicUsize,
622 resume_cvar: parking_lot::Condvar,
624 shutdown: AtomicBool,
626 num_workers: usize,
628}
629
630struct PreparedSegment<D: DirectoryWriter + 'static> {
636 id: String,
637 segment_id: SegmentId,
638 num_docs: u32,
639 staged_rows: Arc<StagedSegment>,
640 segment_manager: Arc<crate::merge::SegmentManager<D>>,
641 operation: Option<crate::merge::SegmentOperationGuard>,
642 runtime: tokio::runtime::Handle,
643 needs_vector_upgrade: bool,
644 published: bool,
645}
646
647impl<D: DirectoryWriter + 'static> PreparedSegment<D> {
648 fn metadata_entry(&self) -> (String, u32) {
649 (self.id.clone(), self.num_docs)
650 }
651
652 fn mark_published(&mut self) {
653 self.published = true;
654 drop(self.operation.take());
656 }
657}
658
659impl<D: DirectoryWriter + 'static> WorkerState<D> {
660 fn record_cycle_error(&self, error: impl Into<String>) {
661 let mut first_error = self.cycle_error.lock();
662 if first_error.is_none() {
663 *first_error = Some(error.into());
664 }
665 drop(first_error);
666 self.cycle_failed.store(true, Ordering::Release);
667 }
668}
669
670impl<D: DirectoryWriter + 'static> Drop for PreparedSegment<D> {
671 fn drop(&mut self) {
672 if self.published {
673 return;
674 }
675 let Some(operation) = self.operation.take() else {
676 return;
677 };
678 self.segment_manager.schedule_unpublished_segment_cleanup(
679 self.segment_id,
680 operation,
681 self.runtime.clone(),
682 );
683 }
684}
685
686impl<D: DirectoryWriter + 'static> IndexWriter<D> {
687 pub async fn create(directory: D, schema: Schema, config: IndexConfig) -> Result<Self> {
689 let builder_config = default_builder_config(&config);
690 Self::create_with_config(directory, schema, config, builder_config).await
691 }
692
693 pub async fn create_with_config(
695 directory: D,
696 schema: Schema,
697 config: IndexConfig,
698 builder_config: SegmentBuilderConfig,
699 ) -> Result<Self> {
700 schema.validate()?;
701 crate::dsl::reject_removed_vector_index_types(&schema).map_err(Error::Schema)?;
702 let directory = Arc::new(directory);
703 let schema = Arc::new(schema);
704 directory.set_index_label(schema.index_label());
706
707 let writer_lock = try_acquire_writer_lock(directory.as_ref())?;
709 if let WriterLock::Unavailable { reason } = &writer_lock {
710 return Err(Error::Internal(reason.clone()));
711 }
712 if directory
716 .exists(std::path::Path::new(super::INDEX_META_FILENAME))
717 .await?
718 {
719 return Err(Error::Internal(format!(
720 "refusing to create index: {} already exists in this directory; \
721 use IndexWriter::open to open the existing index, or delete the \
722 directory first if you really want to start over",
723 super::INDEX_META_FILENAME
724 )));
725 }
726
727 let metadata = super::IndexMetadata::new((*schema).clone());
728
729 let mut segment_config = config.clone();
733 segment_config.optimization = builder_config.optimization;
734 segment_config.posting_codec = Some(builder_config.posting_codec);
735 segment_config.quantized_norms = builder_config.quantized_norms;
736 segment_config.compact_text = builder_config.compact_text;
737 segment_config.term_dict_block_size = builder_config.term_dict_block_size;
738 let segment_manager =
739 super::segment_manager_from_config(&directory, &schema, metadata, &segment_config)?;
740 segment_manager.update_metadata(|_| {}).await?;
741
742 Ok(Self::new_with_parts(
743 directory,
744 schema,
745 config,
746 builder_config,
747 segment_manager,
748 writer_lock,
749 ))
750 }
751
752 pub async fn open(directory: D, config: IndexConfig) -> Result<Self> {
761 let builder_config = default_builder_config(&config);
762 Self::open_with_config(directory, config, builder_config).await
763 }
764
765 pub async fn open_with_config(
767 directory: D,
768 config: IndexConfig,
769 builder_config: SegmentBuilderConfig,
770 ) -> Result<Self> {
771 let directory = Arc::new(directory);
772
773 let writer_lock = try_acquire_writer_lock(directory.as_ref())?;
776 if let WriterLock::Unavailable { reason } = &writer_lock {
777 return Err(Error::Internal(reason.clone()));
778 }
779
780 let metadata = super::IndexMetadata::load_persisting_migration(directory.as_ref()).await?;
781 let schema = Arc::new(metadata.schema.clone());
782 directory.set_index_label(schema.index_label());
784
785 let mut segment_config = config.clone();
787 segment_config.optimization = builder_config.optimization;
788 segment_config.posting_codec = Some(builder_config.posting_codec);
789 segment_config.quantized_norms = builder_config.quantized_norms;
790 segment_config.compact_text = builder_config.compact_text;
791 segment_config.term_dict_block_size = builder_config.term_dict_block_size;
792 segment_config.posting_ratio_bounds = builder_config.posting_ratio_bounds;
793 segment_config.posting_impact_bounds = builder_config.posting_impact_bounds;
794 super::log_posting_bounds_policy(directory.as_ref(), &metadata, &segment_config).await;
795 let segment_manager =
796 super::segment_manager_from_config(&directory, &schema, metadata, &segment_config)?;
797 let swept = segment_manager.cleanup_orphan_segments().await?;
798 if swept > 0 {
799 log::warn!(
800 "[segment_cleanup] swept {} orphan segment(s) while opening writer",
801 swept
802 );
803 }
804 segment_manager.try_load_and_publish_trained().await?;
805
806 Ok(Self::new_with_parts(
807 directory,
808 schema,
809 config,
810 builder_config,
811 segment_manager,
812 writer_lock,
813 ))
814 }
815
816 pub fn from_index(index: &super::Index<D>) -> Self {
823 let writer_lock = match try_acquire_writer_lock(index.directory.as_ref()) {
824 Ok(lock) => lock,
825 Err(error) => WriterLock::Unavailable {
826 reason: format!("failed to acquire the single-writer lock: {error}"),
827 },
828 };
829 if let WriterLock::Unavailable { reason } = &writer_lock {
830 log::error!("[writer_lock] {reason}");
831 }
832 let builder_config = default_builder_config(&index.config);
833 Self::new_with_parts(
834 Arc::clone(&index.directory),
835 index.schema_arc(),
836 index.config.clone(),
837 builder_config,
838 Arc::clone(&index.segment_manager),
839 writer_lock,
840 )
841 }
842
843 fn new_with_parts(
849 directory: Arc<D>,
850 schema: Arc<Schema>,
851 config: IndexConfig,
852 builder_config: SegmentBuilderConfig,
853 segment_manager: Arc<crate::merge::SegmentManager<D>>,
854 writer_lock: WriterLock,
855 ) -> Self {
856 let registry = crate::tokenizer::TokenizerRegistry::new();
858 let mut tokenizers = FxHashMap::default();
859 for (field, entry) in schema.fields() {
860 if matches!(entry.field_type, crate::dsl::FieldType::Text)
861 && let Some(ref tok_name) = entry.tokenizer
862 && let Some(tok) = registry.get(tok_name)
863 {
864 tokenizers.insert(field, tok);
865 }
866 }
867
868 let num_workers = config.num_indexing_threads.max(1);
869 let worker_state = Arc::new(WorkerState {
870 directory: Arc::clone(&directory),
871 schema: Arc::clone(&schema),
872 builder_config,
873 tokenizers: parking_lot::RwLock::new(tokenizers),
874 memory_budget_per_worker: config.max_indexing_memory_bytes / num_workers,
875 segment_build_limiter: SegmentBuildLimiter::new(num_workers),
876 segment_manager: Arc::clone(&segment_manager),
877 built_segments: parking_lot::Mutex::new(Vec::new()),
878 cycle_error: parking_lot::Mutex::new(None),
879 cycle_failed: AtomicBool::new(false),
880 flush_count: AtomicUsize::new(0),
881 flush_mutex: parking_lot::Mutex::new(()),
882 flush_cvar: parking_lot::Condvar::new(),
883 resume_receiver: parking_lot::Mutex::new(None),
884 resume_epoch: AtomicUsize::new(0),
885 resume_cvar: parking_lot::Condvar::new(),
886 shutdown: AtomicBool::new(false),
887 num_workers,
888 });
889 let (doc_sender, workers) = Self::spawn_workers(&worker_state, num_workers);
890 let primary_key_index = Arc::new(parking_lot::RwLock::new(None));
891 let primary_key_refresh_lock = Arc::new(tokio::sync::Mutex::new(()));
892
893 Self {
894 directory,
895 schema,
896 config,
897 doc_sender: Arc::new(parking_lot::RwLock::new(doc_sender)),
898 workers,
899 worker_state,
900 segment_manager,
901 flushed_segments: Arc::new(parking_lot::Mutex::new(Vec::new())),
902 primary_key_index,
903 primary_key_refresh_lock,
904 commit_finalization: Arc::new(CommitFinalizationState::default()),
905 pk_reservations_retained: Arc::new(AtomicBool::new(false)),
906 writer_lock: parking_lot::RwLock::new(writer_lock),
907 }
908 }
909
910 fn ensure_writer_lock(&self) -> Result<()> {
918 if !matches!(&*self.writer_lock.read(), WriterLock::Unavailable { .. }) {
920 return Ok(());
921 }
922
923 let mut lock = self.writer_lock.write();
924 if !matches!(&*lock, WriterLock::Unavailable { .. }) {
926 return Ok(());
927 }
928 match try_acquire_writer_lock(self.directory.as_ref())? {
929 acquired @ (WriterLock::Held { .. } | WriterLock::NotApplicable) => {
930 log::info!(
931 "[writer_lock] index={} single-writer lock acquired after retry; \
932 the previous holder has released it — resuming writes",
933 self.schema.index_label()
934 );
935 *lock = acquired;
936 Ok(())
937 }
938 WriterLock::Unavailable { reason } => {
939 let err = Error::Internal(reason.clone());
940 *lock = WriterLock::Unavailable { reason };
941 Err(err)
942 }
943 }
944 }
945
946 fn clear_uncommitted_pk_reservations(&self) {
955 if self.pk_reservations_retained.load(Ordering::Acquire) {
956 log::warn!(
957 "[primary_key] index={} keeping uncommitted reservations through abort: a \
958 failed post-commit refresh left them as the only record of \
959 committed keys; they are cleared by the next successful commit",
960 self.schema.index_label()
961 );
962 return;
963 }
964 if let Some(pk_index) = self.primary_key_index.write().as_mut() {
965 pk_index.clear_uncommitted();
966 }
967 }
968
969 fn spawn_workers(
970 worker_state: &Arc<WorkerState<D>>,
971 num_workers: usize,
972 ) -> (
973 async_channel::Sender<QueuedDocument>,
974 Vec<std::thread::JoinHandle<()>>,
975 ) {
976 let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
977 let handle = tokio::runtime::Handle::current();
978 let mut workers = Vec::with_capacity(num_workers);
979 for i in 0..num_workers {
980 let state = Arc::clone(worker_state);
981 let rx = receiver.clone();
982 let rt = handle.clone();
983 workers.push(
984 std::thread::Builder::new()
985 .name(format!("index-worker-{}", i))
986 .spawn(move || Self::worker_loop(state, rx, rt, i))
987 .expect("failed to spawn index worker thread"),
988 );
989 }
990 (sender, workers)
991 }
992
993 pub fn schema(&self) -> Arc<Schema> {
995 self.segment_manager.published_generation().schema.clone()
996 }
997
998 pub fn set_tokenizer<T: crate::tokenizer::Tokenizer>(&mut self, field: Field, tokenizer: T) {
1001 self.worker_state
1002 .tokenizers
1003 .write()
1004 .insert(field, Box::new(tokenizer));
1005 }
1006
1007 pub async fn init_primary_key_dedup(&mut self) -> Result<()> {
1023 use super::primary_key::{PK_BLOOM_FILE, deserialize_pk_bloom};
1024
1025 self.commit_finalization.wait_until_idle().await;
1026 self.ensure_writer_lock()?;
1027
1028 let field = match self.schema.primary_field() {
1029 Some(f) => f,
1030 None => return Ok(()),
1031 };
1032
1033 let _refresh_guard = self.primary_key_refresh_lock.lock().await;
1038 {
1039 let callback_directory = Arc::clone(&self.directory);
1040 let callback_schema = Arc::clone(&self.schema);
1041 let callback_manager = Arc::downgrade(&self.segment_manager);
1042 let callback_primary_key = Arc::downgrade(&self.primary_key_index);
1043 let callback_refresh_lock = Arc::downgrade(&self.primary_key_refresh_lock);
1044 self.segment_manager.set_replacement_refresh(move || {
1045 let directory = Arc::clone(&callback_directory);
1046 let schema = Arc::clone(&callback_schema);
1047 let manager = callback_manager.clone();
1048 let primary_key = callback_primary_key.clone();
1049 let refresh_lock = callback_refresh_lock.clone();
1050 async move {
1051 let (Some(manager), Some(primary_key), Some(refresh_lock)) = (
1052 manager.upgrade(),
1053 primary_key.upgrade(),
1054 refresh_lock.upgrade(),
1055 ) else {
1056 return Ok(());
1057 };
1058 refresh_primary_key_snapshot(
1059 &directory,
1060 &schema,
1061 &manager,
1062 &primary_key,
1063 &refresh_lock,
1064 PrimaryKeyRefresh::Replacement,
1065 )
1066 .await
1067 }
1068 });
1069 }
1070
1071 let snapshot = self.segment_manager.acquire_snapshot().await;
1072 let current_seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
1073
1074 let cached = match self
1076 .directory
1077 .open_read(std::path::Path::new(PK_BLOOM_FILE))
1078 .await
1079 {
1080 Ok(handle) => {
1081 let data = handle.read_bytes_range(0..handle.len()).await;
1082 match data {
1083 Ok(bytes) => deserialize_pk_bloom(bytes.as_slice()),
1084 Err(_) => None,
1085 }
1086 }
1087 Err(_) => None,
1088 };
1089
1090 let load_futures: Vec<_> = current_seg_ids
1092 .iter()
1093 .map(|seg_id_str| {
1094 let seg_id_str = seg_id_str.clone();
1095 let dir = self.directory.as_ref();
1096 let schema = Arc::clone(&self.schema);
1097 let deletion = snapshot.deletions().get(&seg_id_str).cloned();
1098 async move { load_pk_segment_data(dir, &seg_id_str, &schema, deletion).await }
1099 })
1100 .collect();
1101 let all_data = futures::stream::iter(load_futures)
1102 .buffer_unordered(4)
1103 .try_collect::<Vec<_>>()
1104 .await?;
1105
1106 if let Some((persisted_seg_ids, bloom)) = cached {
1107 let mut pk_data = Vec::with_capacity(all_data.len());
1109 let mut new_data = Vec::new();
1110 for d in all_data {
1111 if persisted_seg_ids.contains(&d.segment_id) {
1112 pk_data.push(d);
1113 } else {
1114 new_data.push(d);
1115 }
1116 }
1117 let needs_persist = !new_data.is_empty();
1118 let new_start = pk_data.len();
1119 pk_data.extend(new_data);
1120
1121 let pk_index = if new_start == pk_data.len() {
1122 super::primary_key::PrimaryKeyIndex::from_persisted(field, bloom, pk_data, snapshot)
1124 } else {
1125 let index_label = self.schema.index_label().to_owned();
1127 tokio::task::spawn_blocking(move || {
1128 let mut bloom = bloom;
1131 let mut added = 0usize;
1132 let num_new = pk_data.len() - new_start;
1133 for data in &pk_data[new_start..] {
1134 if let Some(ff) = data.fast_fields.get(&field.0)
1135 && let Some(dict) = ff.text_dict()
1136 {
1137 for key in dict.iter() {
1138 bloom.insert(key.as_bytes());
1139 added += 1;
1140 }
1141 }
1142 }
1143 if added > 0 {
1144 log::info!(
1145 "[primary_key] index={index_label} bloom: added {} keys from {} new segment(s)",
1146 added,
1147 num_new,
1148 );
1149 }
1150 super::primary_key::PrimaryKeyIndex::from_persisted(
1151 field, bloom, pk_data, snapshot,
1152 )
1153 })
1154 .await
1155 .map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?
1156 };
1157
1158 if needs_persist {
1159 self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
1160 }
1161
1162 *self.primary_key_index.write() = Some(pk_index);
1163 } else {
1164 let pk_index = tokio::task::spawn_blocking(move || {
1166 super::primary_key::PrimaryKeyIndex::new(field, all_data, snapshot)
1167 })
1168 .await
1169 .map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?;
1170
1171 self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
1172 *self.primary_key_index.write() = Some(pk_index);
1173 }
1174
1175 self.pk_reservations_retained
1179 .store(false, Ordering::Release);
1180
1181 Ok(())
1182 }
1183
1184 async fn persist_pk_bloom(
1187 &self,
1188 pk_index: &super::primary_key::PrimaryKeyIndex,
1189 segment_ids: &[String],
1190 ) {
1191 use super::primary_key::PK_BLOOM_FILE;
1192
1193 let writer = match self
1194 .directory
1195 .streaming_writer(std::path::Path::new(PK_BLOOM_FILE))
1196 .await
1197 {
1198 Ok(writer) => writer,
1199 Err(error) => {
1200 log::warn!(
1201 "[primary_key] index={} failed to open bloom cache: {}",
1202 self.schema.index_label(),
1203 error
1204 );
1205 return;
1206 }
1207 };
1208 let result = crate::segment::block_in_place_if_multithread(|| {
1209 write_pk_bloom_stream(pk_index, segment_ids, writer)
1210 });
1211 if let Err(e) = result {
1212 log::warn!(
1213 "[primary_key] index={} failed to persist bloom cache: {}",
1214 self.schema.index_label(),
1215 e
1216 );
1217 }
1218 }
1219
1220 pub fn add_document(&self, doc: Document) -> Result<()> {
1226 self.enqueue_document(doc, false)
1227 }
1228
1229 fn enqueue_document(&self, doc: Document, replace: bool) -> Result<()> {
1230 self.ensure_writer_lock()?;
1231 if self.worker_state.shutdown.load(Ordering::Acquire) {
1232 return Err(Error::IndexClosed);
1233 }
1234 if self.commit_finalization.in_progress.load(Ordering::Acquire) {
1235 return Err(Error::CommitInProgress);
1236 }
1237 let sender = self.doc_sender.read().clone();
1238 if sender.is_closed() {
1242 return Err(Error::CommitInProgress);
1243 }
1244 validate_vector_value_counts(&doc, &self.schema)?;
1248 super::content_hash::document_hash(&doc, &self.schema)?;
1249 let primary_key_index = self.primary_key_index.read();
1250 let enqueue = |doc, row| {
1251 sender
1252 .try_send(QueuedDocument { doc, row })
1253 .map_err(|error| match error {
1254 async_channel::TrySendError::Full(_) => Error::QueueFull,
1255 async_channel::TrySendError::Closed(_) => Error::CommitInProgress,
1256 })
1257 };
1258 if let Some(pk) = primary_key_index.as_ref() {
1259 pk.admit_document(doc, &self.schema, replace, |doc, row| {
1260 enqueue(doc, Some(row))
1261 })
1262 } else {
1263 enqueue(doc, None)
1264 }
1265 }
1266
1267 pub fn delete_primary_key(&mut self, key: &str) -> Result<()> {
1270 self.ensure_writer_lock()?;
1271 if self.worker_state.shutdown.load(Ordering::Acquire) {
1272 return Err(Error::IndexClosed);
1273 }
1274 if self.commit_finalization.in_progress.load(Ordering::Acquire)
1275 || self.doc_sender.read().is_closed()
1276 {
1277 return Err(Error::CommitInProgress);
1278 }
1279 if self.pk_reservations_retained.load(Ordering::Acquire) {
1280 return Err(Error::CommitInProgress);
1281 }
1282 let guard = self.primary_key_index.read();
1283 let pk = guard.as_ref().ok_or_else(|| {
1284 Error::Schema("row deletion requires initialized primary-key deduplication".into())
1285 })?;
1286 pk.delete(key)?;
1287 Ok(())
1288 }
1289
1290 pub async fn upsert_document(&mut self, doc: Document) -> Result<()> {
1296 let field = self
1297 .schema
1298 .primary_field()
1299 .ok_or_else(|| Error::Schema("upserts require a primary key".into()))?;
1300 let key = super::primary_key::document_key(&doc, field)?;
1301 validate_vector_value_counts(&doc, &self.schema)?;
1302 let hash = super::content_hash::document_hash(&doc, &self.schema)?;
1303 self.ensure_writer_lock()?;
1305 if self.worker_state.shutdown.load(Ordering::Acquire) {
1306 return Err(Error::IndexClosed);
1307 }
1308 if self.commit_finalization.in_progress.load(Ordering::Acquire)
1309 || self.doc_sender.read().is_closed()
1310 || self.pk_reservations_retained.load(Ordering::Acquire)
1311 {
1312 return Err(Error::CommitInProgress);
1313 }
1314 if let Some(hash) = hash {
1315 let target = {
1316 let guard = self.primary_key_index.read();
1317 let pk = guard.as_ref().ok_or_else(|| {
1318 Error::Schema("upserts require initialized primary-key deduplication".into())
1319 })?;
1320 match pk.staged_hash_matches(key, hash) {
1321 Some(true) => {
1322 log::debug!(
1323 "[content_hash] index={} skipped unchanged staged upsert",
1324 self.schema.index_label()
1325 );
1326 return Ok(());
1327 }
1328 Some(false) => None,
1329 None => pk.content_hash_target(key)?,
1330 }
1331 };
1332 if let Some(target) = target
1333 && target.matches(hash, &self.schema).await?
1334 {
1335 return Ok(());
1336 }
1337 }
1338 if self.primary_key_index.read().is_none() {
1339 return Err(Error::Schema(
1340 "upserts require initialized primary-key deduplication".into(),
1341 ));
1342 }
1343 self.enqueue_document(doc, true)
1344 }
1345
1346 pub fn add_documents(&self, documents: Vec<Document>) -> Result<usize> {
1351 let total = documents.len();
1352 for (i, doc) in documents.into_iter().enumerate() {
1353 match self.add_document(doc) {
1354 Ok(()) => {}
1355 Err(Error::QueueFull | Error::CommitInProgress) => return Ok(i),
1356 Err(e) => return Err(e),
1357 }
1358 }
1359 Ok(total)
1360 }
1361
1362 fn worker_loop(
1375 state: Arc<WorkerState<D>>,
1376 initial_receiver: async_channel::Receiver<QueuedDocument>,
1377 handle: tokio::runtime::Handle,
1378 worker_id: usize,
1379 ) {
1380 let mut receiver = initial_receiver;
1381 let mut my_epoch = 0usize;
1382 let soft_flush_threshold =
1383 soft_flush_threshold(state.memory_budget_per_worker, worker_id, state.num_workers);
1384 let hard_flush_threshold = hard_flush_threshold(state.memory_budget_per_worker);
1385
1386 loop {
1387 let build_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1391 let mut builder: Option<SegmentBuilder> = None;
1392 let mut staged_rows = Arc::new(StagedSegment::default());
1393
1394 while let Ok(doc) = receiver.recv_blocking() {
1395 if state.shutdown.load(Ordering::Acquire) {
1396 break;
1397 }
1398 if state.cycle_failed.load(Ordering::Acquire) {
1403 continue;
1404 }
1405 if builder.is_none() {
1407 match SegmentBuilder::new(
1408 state.segment_manager.published_generation().schema.clone(),
1409 state.builder_config.clone(),
1410 ) {
1411 Ok(mut b) => {
1412 for (field, tokenizer) in state.tokenizers.read().iter() {
1413 b.set_tokenizer(*field, tokenizer.clone_box());
1414 }
1415 builder = Some(b);
1416 }
1417 Err(e) => {
1418 log::error!("Failed to create segment builder: {:?}", e);
1419 state.record_cycle_error(format!(
1420 "failed to create segment builder: {e}"
1421 ));
1422 continue;
1423 }
1424 }
1425 }
1426
1427 let b = builder.as_mut().unwrap();
1428 if let Some(row) = &doc.row
1429 && !row.attach(&staged_rows, b.num_docs())
1430 {
1431 continue;
1432 }
1433 if let Err(e) = b.add_document(doc.doc) {
1434 log::error!("Failed to index document: {:?}", e);
1435 state.record_cycle_error(format!("failed to index document: {e}"));
1436 continue;
1437 }
1438
1439 let builder_memory = b.estimated_memory_bytes();
1440
1441 if b.num_docs() & 0x3FFF == 0 {
1442 log::debug!(
1443 "[indexing] index={} docs={}, memory={}, budget={}",
1444 state.schema.index_label(),
1445 b.num_docs(),
1446 crate::format_bytes(builder_memory as u64),
1447 crate::format_bytes(state.memory_budget_per_worker as u64)
1448 );
1449 }
1450
1451 const MIN_DOCS_BEFORE_FLUSH: u32 = 100;
1453
1454 if b.num_docs() >= MIN_DOCS_BEFORE_FLUSH
1455 && let Some(_build_permit) = state.segment_build_limiter.reserve_if_due(
1456 builder_memory,
1457 soft_flush_threshold,
1458 hard_flush_threshold,
1459 )
1460 {
1461 log::info!(
1462 "[indexing] index={} memory budget reached, building segment: \
1463 worker={}, docs={}, memory={}, soft_budget={}, hard_budget={}",
1464 state.schema.index_label(),
1465 worker_id,
1466 b.num_docs(),
1467 crate::format_bytes(builder_memory as u64),
1468 crate::format_bytes(soft_flush_threshold as u64),
1469 crate::format_bytes(hard_flush_threshold as u64),
1470 );
1471 let full_builder = builder.take().unwrap();
1472 Self::build_segment_inline(
1473 &state,
1474 full_builder,
1475 std::mem::take(&mut staged_rows),
1476 &handle,
1477 );
1478 }
1479 }
1480
1481 if !state.cycle_failed.load(Ordering::Acquire)
1483 && let Some(b) = builder.take()
1484 && b.num_docs() > 0
1485 {
1486 let _build_permit = state.segment_build_limiter.acquire_flush();
1487 Self::build_segment_inline(&state, b, staged_rows, &handle);
1488 }
1489 }));
1490
1491 if build_result.is_err() {
1492 log::error!(
1493 "[worker] index={} panic during indexing cycle — documents in this cycle may be lost",
1494 state.schema.index_label()
1495 );
1496 state.record_cycle_error("indexing worker panicked while building the batch");
1497 }
1498
1499 let prev = state.flush_count.fetch_add(1, Ordering::Release);
1502 if prev + 1 == state.num_workers {
1503 let _lock = state.flush_mutex.lock();
1510 state.flush_cvar.notify_all();
1511 }
1512
1513 {
1517 let mut lock = state.resume_receiver.lock();
1518 loop {
1519 if state.shutdown.load(Ordering::Acquire) {
1520 return;
1521 }
1522 let current_epoch = state.resume_epoch.load(Ordering::Acquire);
1523 if current_epoch > my_epoch
1524 && let Some(rx) = lock.as_ref()
1525 {
1526 receiver = rx.clone();
1527 my_epoch = current_epoch;
1528 break;
1529 }
1530 state.resume_cvar.wait(&mut lock);
1531 }
1532 }
1533 }
1534 }
1535
1536 fn build_segment_inline(
1540 state: &WorkerState<D>,
1541 builder: SegmentBuilder,
1542 staged_rows: Arc<StagedSegment>,
1543 handle: &tokio::runtime::Handle,
1544 ) {
1545 let segment_id = SegmentId::new();
1546 let segment_hex = segment_id.to_hex();
1547 let operation = match state
1550 .segment_manager
1551 .protect_new_segment(segment_hex.clone())
1552 {
1553 Ok(operation) => operation,
1554 Err(e) => {
1555 log::error!(
1556 "[segment_build_failed] index={} segment_id={} lifecycle_error={}",
1557 state.schema.index_label(),
1558 segment_hex,
1559 e,
1560 );
1561 state.record_cycle_error(format!(
1562 "failed to claim segment {segment_hex} for building: {e}"
1563 ));
1564 return;
1565 }
1566 };
1567 let trained = state.segment_manager.trained_for_segment_build();
1568 let doc_count = builder.num_docs();
1569 let build_start = std::time::Instant::now();
1570
1571 log::info!(
1572 "[segment_build] index={} segment_id={} doc_count={} ann={}",
1573 state.schema.index_label(),
1574 segment_hex,
1575 doc_count,
1576 trained.is_some()
1577 );
1578
1579 let mut prepared = PreparedSegment {
1583 id: segment_hex.clone(),
1584 segment_id,
1585 num_docs: doc_count,
1586 staged_rows,
1587 segment_manager: Arc::clone(&state.segment_manager),
1588 operation: Some(operation),
1589 runtime: handle.clone(),
1590 needs_vector_upgrade: trained.is_none(),
1591 published: false,
1592 };
1593
1594 match handle.block_on(builder.build(
1595 state.directory.as_ref(),
1596 segment_id,
1597 trained.as_deref(),
1598 )) {
1599 Ok(meta) if meta.num_docs == doc_count && meta.num_docs > 0 => {
1600 let duration_ms = build_start.elapsed().as_millis() as u64;
1601 log::info!(
1602 "[segment_build_done] index={} segment_id={} doc_count={} duration_ms={}",
1603 state.schema.index_label(),
1604 segment_hex,
1605 meta.num_docs,
1606 duration_ms,
1607 );
1608 prepared.num_docs = meta.num_docs;
1609 state.built_segments.lock().push(prepared);
1610 }
1611 Ok(meta) => {
1612 let error = format!(
1613 "segment {segment_hex} built {} docs from a {doc_count}-document builder",
1614 meta.num_docs
1615 );
1616 log::error!(
1617 "[segment_build_failed] index={} {error}",
1618 state.schema.index_label()
1619 );
1620 state.record_cycle_error(error);
1621 }
1622 Err(e) => {
1623 log::error!(
1624 "[segment_build_failed] index={} segment_id={} error={:?}",
1625 state.schema.index_label(),
1626 segment_hex,
1627 e
1628 );
1629 state.record_cycle_error(format!("failed to build segment {segment_hex}: {e}"));
1632 }
1633 }
1634 }
1635
1636 pub async fn maybe_merge(&self) {
1642 self.segment_manager.maybe_merge().await;
1643 }
1644
1645 pub async fn abort_merges(&self) {
1648 self.segment_manager.abort_merges().await;
1649 }
1650
1651 pub async fn shutdown(&mut self) -> Result<()> {
1656 self.segment_manager.begin_shutdown();
1657 self.signal_worker_shutdown();
1658
1659 self.commit_finalization.wait_until_idle().await;
1664
1665 let workers = std::mem::take(&mut self.workers);
1666 let panicked = tokio::task::spawn_blocking(move || {
1667 workers
1668 .into_iter()
1669 .map(|worker| worker.join().is_err())
1670 .filter(|panicked| *panicked)
1671 .count()
1672 })
1673 .await
1674 .map_err(|error| Error::Internal(format!("failed to join index workers: {}", error)))?;
1675 if panicked > 0 {
1676 log::error!(
1677 "[index_shutdown] index={} {} indexing worker(s) panicked",
1678 self.schema.index_label(),
1679 panicked
1680 );
1681 }
1682
1683 self.flushed_segments.lock().clear();
1686 self.worker_state.built_segments.lock().clear();
1687 if let Some(pk_index) = self.primary_key_index.write().as_mut() {
1688 pk_index.clear_uncommitted();
1689 }
1690 Ok(())
1691 }
1692
1693 pub async fn wait_for_merging_thread(&self) {
1695 self.segment_manager.wait_for_merging_thread().await;
1696 }
1697
1698 pub async fn wait_for_all_merges(&self) {
1700 self.segment_manager.wait_for_all_merges().await;
1701 }
1702
1703 pub async fn wait_for_commit_finalization(&self) {
1708 self.commit_finalization.wait_until_idle().await;
1709 }
1710
1711 pub fn tracker(&self) -> std::sync::Arc<crate::segment::SegmentTracker> {
1713 self.segment_manager.tracker()
1714 }
1715
1716 pub async fn acquire_snapshot(&self) -> crate::segment::SegmentSnapshot {
1718 self.segment_manager.acquire_snapshot().await
1719 }
1720
1721 pub async fn cleanup_orphan_segments(&self) -> Result<usize> {
1726 self.ensure_writer_lock()?;
1727 self.segment_manager.cleanup_orphan_segments().await
1728 }
1729
1730 pub async fn prepare_commit(&mut self) -> Result<PreparedCommit<'_, D>> {
1743 self.prepare_commit_with_timeout(std::time::Duration::from_secs(300))
1744 .await
1745 }
1746
1747 pub(super) async fn prepare_commit_with_timeout(
1748 &mut self,
1749 flush_timeout: std::time::Duration,
1750 ) -> Result<PreparedCommit<'_, D>> {
1751 self.ensure_writer_lock()?;
1752 if self.worker_state.shutdown.load(Ordering::Acquire) {
1753 return Err(Error::IndexClosed);
1754 }
1755 if self.commit_finalization.in_progress.load(Ordering::Acquire) {
1756 return Err(Error::CommitInProgress);
1757 }
1758 self.doc_sender.read().close();
1760 self.worker_state.segment_build_limiter.begin_flush();
1761
1762 self.worker_state.resume_cvar.notify_all();
1766
1767 let state = Arc::clone(&self.worker_state);
1770 let index_label = self.schema.index_label().to_owned();
1771 let all_flushed = tokio::task::spawn_blocking(move || {
1772 let mut lock = state.flush_mutex.lock();
1773 let deadline = std::time::Instant::now() + flush_timeout;
1774 while state.flush_count.load(Ordering::Acquire) < state.num_workers {
1775 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
1776 if remaining.is_zero() {
1777 log::error!(
1778 "[prepare_commit] index={index_label} timed out waiting for workers: {}/{} flushed",
1779 state.flush_count.load(Ordering::Acquire),
1780 state.num_workers
1781 );
1782 return false;
1783 }
1784 state.flush_cvar.wait_for(&mut lock, remaining);
1785 }
1786 true
1787 })
1788 .await
1789 .map_err(|e| Error::Internal(format!("Failed to wait for workers: {}", e)))?;
1790
1791 if !all_flushed {
1792 return Err(Error::CommitFlushTimeout {
1800 flushed_workers: self.worker_state.flush_count.load(Ordering::Acquire),
1801 total_workers: self.worker_state.num_workers,
1802 });
1803 }
1804
1805 let cycle_error = { self.worker_state.cycle_error.lock().take() };
1806 if let Some(error) = cycle_error {
1807 self.flushed_segments.lock().clear();
1812 self.worker_state.built_segments.lock().clear();
1813 self.clear_uncommitted_pk_reservations();
1814 self.resume_workers();
1815 return Err(Error::Internal(format!(
1816 "indexing generation failed; no documents from this batch were committed: {error}"
1817 )));
1818 }
1819
1820 let built = std::mem::take(&mut *self.worker_state.built_segments.lock());
1822 self.flushed_segments.lock().extend(built);
1823
1824 Ok(PreparedCommit {
1825 writer: self,
1826 is_resolved: false,
1827 })
1828 }
1829
1830 pub async fn commit(&mut self) -> Result<bool> {
1835 self.prepare_commit().await?.commit().await
1836 }
1837
1838 pub async fn compact_segment(
1842 &mut self,
1843 segment_id: &str,
1844 memory_budget: usize,
1845 ) -> Result<bool> {
1846 if memory_budget < 1024 * 1024 {
1847 return Err(Error::Schema(
1848 "compaction memory budget must be at least 1 MiB".into(),
1849 ));
1850 }
1851 if crate::segment::SegmentId::from_hex(segment_id).is_none() {
1852 return Err(Error::Document("invalid compaction segment ID".into()));
1853 }
1854 self.commit().await?;
1855 let changed = self
1856 .segment_manager
1857 .compact_segment(segment_id, memory_budget)
1858 .await?;
1859 self.persist_replacement_snapshot().await?;
1860 Ok(changed)
1861 }
1862
1863 pub async fn compact(&mut self, memory_budget: usize) -> Result<usize> {
1866 if memory_budget < 1024 * 1024 {
1867 return Err(Error::Schema(
1868 "compaction memory budget must be at least 1 MiB".into(),
1869 ));
1870 }
1871 self.commit().await?;
1872 self.wait_for_merging_thread().await;
1873 let ids = self.segment_manager.get_segment_ids().await;
1874 let mut count = 0;
1875 for id in ids {
1876 count += usize::from(
1877 self.segment_manager
1878 .compact_segment(&id, memory_budget)
1879 .await?,
1880 );
1881 }
1882 self.persist_replacement_snapshot().await?;
1883 Ok(count)
1884 }
1885
1886 pub async fn force_merge(&mut self) -> Result<()> {
1888 self.force_merge_with_snapshot_refresh(|| std::future::ready(Ok(())))
1889 .await
1890 }
1891
1892 pub async fn force_merge_with_snapshot_refresh<F, Fut>(
1899 &mut self,
1900 refresh_external: F,
1901 ) -> Result<()>
1902 where
1903 F: FnMut() -> Fut,
1904 Fut: std::future::Future<Output = Result<()>>,
1905 {
1906 self.force_merge_with_compaction_and_snapshot_refresh(false, refresh_external)
1907 .await
1908 }
1909
1910 pub async fn force_merge_with_compaction(&mut self, compact: bool) -> Result<()> {
1913 self.force_merge_with_compaction_and_snapshot_refresh(compact, || {
1914 std::future::ready(Ok(()))
1915 })
1916 .await
1917 }
1918
1919 pub async fn force_merge_with_compaction_and_snapshot_refresh<F, Fut>(
1920 &mut self,
1921 compact: bool,
1922 refresh_external: F,
1923 ) -> Result<()>
1924 where
1925 F: FnMut() -> Fut,
1926 Fut: std::future::Future<Output = Result<()>>,
1927 {
1928 let budget = compact.then_some(self.config.compaction_memory_budget_bytes);
1929 if budget.is_some_and(|bytes| bytes < 1024 * 1024) {
1930 return Err(Error::Schema(
1931 "compaction memory budget must be at least 1 MiB".into(),
1932 ));
1933 }
1934 self.prepare_commit().await?.commit().await?;
1935
1936 self.segment_manager
1937 .force_merge_with_compaction_and_snapshot_refresh(budget, refresh_external)
1938 .await?;
1939
1940 self.persist_replacement_snapshot().await
1944 }
1945
1946 pub async fn reorder(&mut self) -> Result<()> {
1952 self.reorder_with_snapshot_refresh(|| std::future::ready(Ok(())))
1953 .await
1954 }
1955
1956 pub async fn reorder_with_shared_writer<F, Fut>(
1960 writer: &Arc<tokio::sync::RwLock<Self>>,
1961 refresh_external: F,
1962 ) -> Result<()>
1963 where
1964 F: FnMut() -> Fut,
1965 Fut: std::future::Future<Output = Result<()>>,
1966 {
1967 let segment_manager = {
1968 let mut writer = writer.write().await;
1969 writer.commit().await?;
1970 Arc::clone(&writer.segment_manager)
1971 };
1972 segment_manager
1973 .reorder_segments_with_snapshot_refresh(refresh_external)
1974 .await?;
1975 writer.read().await.persist_replacement_snapshot().await
1976 }
1977
1978 pub async fn reorder_with_snapshot_refresh<F, Fut>(&mut self, refresh_external: F) -> Result<()>
1981 where
1982 F: FnMut() -> Fut,
1983 Fut: std::future::Future<Output = Result<()>>,
1984 {
1985 self.prepare_commit().await?.commit().await?;
1986
1987 self.segment_manager
1988 .reorder_segments_with_snapshot_refresh(refresh_external)
1989 .await?;
1990 self.persist_replacement_snapshot().await
1991 }
1992
1993 async fn persist_replacement_snapshot(&self) -> Result<()> {
1995 refresh_primary_key_snapshot(
1996 &self.directory,
1997 &self.schema,
1998 &self.segment_manager,
1999 &self.primary_key_index,
2000 &self.primary_key_refresh_lock,
2001 PrimaryKeyRefresh::FinalReplacement,
2002 )
2003 .await
2004 }
2005
2006 pub fn segment_manager(&self) -> &Arc<crate::merge::SegmentManager<D>> {
2008 &self.segment_manager
2009 }
2010
2011 fn resume_workers(&mut self) {
2016 Self::resume_workers_shared(&self.worker_state, &self.doc_sender);
2017 }
2018
2019 fn resume_workers_shared(
2020 worker_state: &Arc<WorkerState<D>>,
2021 doc_sender: &Arc<parking_lot::RwLock<async_channel::Sender<QueuedDocument>>>,
2022 ) {
2023 if worker_state.shutdown.load(Ordering::Acquire) {
2024 return;
2025 }
2026 if tokio::runtime::Handle::try_current().is_err() {
2027 worker_state.shutdown.store(true, Ordering::Release);
2030 worker_state.resume_cvar.notify_all();
2031 return;
2032 }
2033
2034 worker_state.segment_build_limiter.end_flush();
2036 worker_state.flush_count.store(0, Ordering::Release);
2037 *worker_state.cycle_error.lock() = None;
2038 worker_state.cycle_failed.store(false, Ordering::Release);
2039
2040 let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
2042 *doc_sender.write() = sender;
2043
2044 {
2046 let mut lock = worker_state.resume_receiver.lock();
2047 *lock = Some(receiver);
2048 }
2049 worker_state.resume_epoch.fetch_add(1, Ordering::Release);
2050 worker_state.resume_cvar.notify_all();
2051 }
2052
2053 fn signal_worker_shutdown(&self) {
2054 self.worker_state.shutdown.store(true, Ordering::Release);
2055 self.doc_sender.read().close();
2056 self.worker_state.segment_build_limiter.begin_flush();
2057 self.worker_state.resume_cvar.notify_all();
2058 }
2059
2060 }
2062
2063impl<D: DirectoryWriter + 'static> Drop for IndexWriter<D> {
2064 fn drop(&mut self) {
2065 self.signal_worker_shutdown();
2066 for w in std::mem::take(&mut self.workers) {
2067 let _ = w.join();
2068 }
2069 }
2070}
2071
2072pub struct PreparedCommit<'a, D: DirectoryWriter + 'static> {
2079 writer: &'a mut IndexWriter<D>,
2080 is_resolved: bool,
2081}
2082
2083struct PreparedSegmentsGuard<D: DirectoryWriter + 'static> {
2088 segments: Option<Vec<PreparedSegment<D>>>,
2089 retry_slot: Arc<parking_lot::Mutex<Vec<PreparedSegment<D>>>>,
2090}
2091
2092impl<D: DirectoryWriter + 'static> PreparedSegmentsGuard<D> {
2093 fn metadata_entries(&self) -> Vec<(String, u32)> {
2094 self.segments
2095 .as_deref()
2096 .unwrap_or_default()
2097 .iter()
2098 .map(PreparedSegment::metadata_entry)
2099 .collect()
2100 }
2101
2102 fn staged_deletions(&self) -> Vec<(String, Arc<StagedSegment>)> {
2103 self.segments
2104 .as_deref()
2105 .unwrap_or_default()
2106 .iter()
2107 .filter(|segment| segment.staged_rows.is_dirty())
2108 .map(|segment| (segment.id.clone(), Arc::clone(&segment.staged_rows)))
2109 .collect()
2110 }
2111
2112 fn take_published(&mut self) -> Vec<PreparedSegment<D>> {
2113 self.segments.take().unwrap_or_default()
2114 }
2115
2116 fn vector_upgrade_segment_ids(&self) -> Vec<String> {
2117 self.segments
2118 .as_deref()
2119 .unwrap_or_default()
2120 .iter()
2121 .filter(|segment| segment.needs_vector_upgrade)
2122 .map(|segment| segment.id.clone())
2123 .collect()
2124 }
2125}
2126
2127impl<D: DirectoryWriter + 'static> Drop for PreparedSegmentsGuard<D> {
2128 fn drop(&mut self) {
2129 if let Some(segments) = self.segments.take() {
2130 self.retry_slot.lock().extend(segments);
2131 }
2132 }
2133}
2134
2135struct CommitFinalizationGuard<D: DirectoryWriter + 'static> {
2140 state: Arc<CommitFinalizationState>,
2141 worker_state: Arc<WorkerState<D>>,
2142 doc_sender: Arc<parking_lot::RwLock<async_channel::Sender<QueuedDocument>>>,
2143 resume_workers: bool,
2144}
2145
2146impl<D: DirectoryWriter + 'static> CommitFinalizationGuard<D> {
2147 fn resume_on_drop(&mut self) {
2148 self.resume_workers = true;
2149 }
2150}
2151
2152impl<D: DirectoryWriter + 'static> Drop for CommitFinalizationGuard<D> {
2153 fn drop(&mut self) {
2154 if self.resume_workers {
2155 IndexWriter::<D>::resume_workers_shared(&self.worker_state, &self.doc_sender);
2156 }
2157 self.state.finish();
2158 }
2159}
2160
2161struct OwnedCommitFinalization<D: DirectoryWriter + 'static> {
2166 directory: Arc<D>,
2167 schema: Arc<Schema>,
2168 segment_manager: Arc<crate::merge::SegmentManager<D>>,
2169 primary_key_index: Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
2170 primary_key_refresh_lock: Arc<tokio::sync::Mutex<()>>,
2171 prepared: PreparedSegmentsGuard<D>,
2172 finalization: Option<CommitFinalizationGuard<D>>,
2173 publication_observed: Arc<AtomicBool>,
2174 pk_reservations_retained: Arc<AtomicBool>,
2175}
2176
2177#[derive(Clone, Copy)]
2178enum PrimaryKeyRefresh {
2179 Commit,
2181 Replacement,
2184 FinalReplacement,
2187}
2188
2189async fn refresh_primary_key_snapshot<D: DirectoryWriter + 'static>(
2190 directory: &Arc<D>,
2191 schema: &Arc<Schema>,
2192 segment_manager: &Arc<crate::merge::SegmentManager<D>>,
2193 primary_key_index: &Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
2194 primary_key_refresh_lock: &Arc<tokio::sync::Mutex<()>>,
2195 refresh: PrimaryKeyRefresh,
2196) -> Result<()> {
2197 let _refresh_guard = primary_key_refresh_lock.lock().await;
2198 let snapshot = segment_manager.acquire_snapshot().await;
2199 let existing_ids: std::collections::HashSet<String> = {
2200 let guard = primary_key_index.read();
2201 let Some(pk_index) = guard.as_ref() else {
2202 return Ok(());
2203 };
2204 pk_index
2205 .committed_visibility()
2206 .filter(|(id, deletion)| *deletion == snapshot.deletions().get(*id).map(|(_, d)| d))
2207 .map(|(id, _)| id.to_owned())
2208 .collect()
2209 };
2210
2211 let load_futures: Vec<_> = snapshot
2212 .segment_ids()
2213 .iter()
2214 .filter(|id| !existing_ids.contains(id.as_str()))
2215 .map(|seg_id_str| {
2216 let seg_id_str = seg_id_str.clone();
2217 let dir = directory.as_ref();
2218 let schema = Arc::clone(schema);
2219 let deletion = snapshot.deletions().get(&seg_id_str).cloned();
2220 async move { load_pk_segment_data(dir, &seg_id_str, &schema, deletion).await }
2221 })
2222 .collect();
2223 let mut new_data = futures::stream::iter(load_futures)
2224 .buffer_unordered(4)
2225 .try_collect::<Vec<_>>()
2226 .await?;
2227 if let Some(field) = schema.primary_field() {
2228 new_data = tokio::task::spawn_blocking(move || {
2229 for data in &mut new_data {
2230 data.prepare_live_keys(field);
2231 }
2232 new_data
2233 })
2234 .await
2235 .map_err(|error| {
2236 Error::Internal(format!("primary-key visibility refresh failed: {error}"))
2237 })?;
2238 }
2239 let seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
2240
2241 let persist_bloom = {
2242 let mut guard = primary_key_index.write();
2243 let Some(pk_index) = guard.as_mut() else {
2244 return Ok(());
2245 };
2246 match refresh {
2247 PrimaryKeyRefresh::Commit => pk_index.refresh_incremental(new_data, snapshot),
2248 PrimaryKeyRefresh::Replacement | PrimaryKeyRefresh::FinalReplacement => {
2249 pk_index.refresh_replacement(new_data, snapshot);
2250 }
2251 }
2252 matches!(
2253 refresh,
2254 PrimaryKeyRefresh::Commit | PrimaryKeyRefresh::FinalReplacement
2255 )
2256 };
2257
2258 if persist_bloom {
2259 let writer = match directory
2260 .streaming_writer(std::path::Path::new(super::primary_key::PK_BLOOM_FILE))
2261 .await
2262 {
2263 Ok(writer) => writer,
2264 Err(error) => {
2265 log::warn!(
2266 "[primary_key] index={} failed to open bloom cache: {}",
2267 schema.index_label(),
2268 error
2269 );
2270 return Ok(());
2271 }
2272 };
2273 let guard = primary_key_index.read();
2276 if let Some(pk_index) = guard.as_ref()
2277 && let Err(error) = crate::segment::block_in_place_if_multithread(|| {
2278 write_pk_bloom_stream(pk_index, &seg_ids, writer)
2279 })
2280 {
2281 log::warn!(
2282 "[primary_key] index={} failed to persist bloom cache: {}",
2283 schema.index_label(),
2284 error
2285 );
2286 }
2287 }
2288 Ok(())
2289}
2290
2291fn write_pk_bloom_stream(
2292 pk_index: &super::primary_key::PrimaryKeyIndex,
2293 segment_ids: &[String],
2294 mut writer: Box<dyn crate::directories::StreamingWriter>,
2295) -> std::io::Result<()> {
2296 pk_index.write_bloom_cache(segment_ids, writer.as_mut())?;
2297 writer.finish()
2298}
2299
2300async fn finalize_prepared_commit<D: DirectoryWriter + 'static>(
2301 mut commit: OwnedCommitFinalization<D>,
2302) -> Result<bool> {
2303 let metadata_entries = commit.prepared.metadata_entries();
2304 let published_segment_ids = commit.prepared.vector_upgrade_segment_ids();
2305
2306 let deletes = commit
2310 .primary_key_index
2311 .read()
2312 .as_ref()
2313 .map_or_else(Vec::new, |pk| pk.pending_deletes());
2314 commit
2315 .segment_manager
2316 .commit_with_deletes(
2317 &metadata_entries,
2318 deletes,
2319 commit.prepared.staged_deletions(),
2320 )
2321 .await?;
2322 commit.publication_observed.store(true, Ordering::Release);
2323 if let Some(pk) = commit.primary_key_index.read().as_ref() {
2324 pk.mark_deletes_published();
2325 }
2326
2327 let mut published = commit.prepared.take_published();
2328 for segment in &mut published {
2329 segment.mark_published();
2330 }
2331 drop(published);
2332 commit
2333 .segment_manager
2334 .schedule_vector_segment_upgrades(published_segment_ids);
2335 if let Some(finalization) = commit.finalization.as_mut() {
2339 finalization.resume_on_drop();
2340 } else {
2341 log::error!("owned commit finalization guard was already released after publication");
2342 }
2343
2344 match refresh_primary_key_snapshot(
2349 &commit.directory,
2350 &commit.schema,
2351 &commit.segment_manager,
2352 &commit.primary_key_index,
2353 &commit.primary_key_refresh_lock,
2354 PrimaryKeyRefresh::Commit,
2355 )
2356 .await
2357 {
2358 Ok(()) => commit
2361 .pk_reservations_retained
2362 .store(false, Ordering::Release),
2363 Err(error) => {
2364 commit
2369 .pk_reservations_retained
2370 .store(true, Ordering::Release);
2371 log::error!(
2372 "[primary_key] committed metadata but failed to refresh dedup state; \
2373 retaining reservations until a later successful commit: {}",
2374 error,
2375 );
2376 }
2377 }
2378
2379 drop(commit.finalization.take());
2383 commit.segment_manager.maybe_merge().await;
2384 Ok(true)
2385}
2386
2387impl<'a, D: DirectoryWriter + 'static> PreparedCommit<'a, D> {
2388 pub async fn commit(mut self) -> Result<bool> {
2392 let segments = std::mem::take(&mut *self.writer.flushed_segments.lock());
2393
2394 if segments.is_empty()
2396 && self
2397 .writer
2398 .primary_key_index
2399 .read()
2400 .as_ref()
2401 .is_none_or(|pk| pk.pending_deletes().is_empty())
2402 {
2403 log::debug!(
2404 "[commit] index={} no segments to commit, skipping",
2405 self.writer.schema.index_label()
2406 );
2407 self.is_resolved = true;
2408 self.writer.resume_workers();
2409 return Ok(false);
2410 }
2411
2412 if !self.writer.commit_finalization.begin() {
2413 self.writer.flushed_segments.lock().extend(segments);
2414 self.is_resolved = true;
2418 return Err(Error::CommitInProgress);
2419 }
2420
2421 let publication_observed = Arc::new(AtomicBool::new(false));
2422 let owned = OwnedCommitFinalization {
2423 directory: Arc::clone(&self.writer.directory),
2424 schema: Arc::clone(&self.writer.schema),
2425 segment_manager: Arc::clone(&self.writer.segment_manager),
2426 primary_key_index: Arc::clone(&self.writer.primary_key_index),
2427 primary_key_refresh_lock: Arc::clone(&self.writer.primary_key_refresh_lock),
2428 prepared: PreparedSegmentsGuard {
2429 segments: Some(segments),
2430 retry_slot: Arc::clone(&self.writer.flushed_segments),
2431 },
2432 finalization: Some(CommitFinalizationGuard {
2433 state: Arc::clone(&self.writer.commit_finalization),
2434 worker_state: Arc::clone(&self.writer.worker_state),
2435 doc_sender: Arc::clone(&self.writer.doc_sender),
2436 resume_workers: false,
2437 }),
2438 publication_observed: Arc::clone(&publication_observed),
2439 pk_reservations_retained: Arc::clone(&self.writer.pk_reservations_retained),
2440 };
2441
2442 self.is_resolved = true;
2447 let task_publication = Arc::clone(&publication_observed);
2448 let task = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2449 tokio::spawn(async move {
2450 match std::panic::AssertUnwindSafe(finalize_prepared_commit(owned))
2451 .catch_unwind()
2452 .await
2453 {
2454 Ok(result) => result,
2455 Err(_) if task_publication.load(Ordering::Acquire) => {
2456 log::error!(
2457 "owned commit finalizer panicked after metadata publication; \
2458 treating the durable generation as committed"
2459 );
2460 Ok(true)
2461 }
2462 Err(_) => Err(Error::Internal(
2463 "owned commit finalizer panicked before metadata publication".into(),
2464 )),
2465 }
2466 })
2467 }))
2468 .map_err(|_| Error::Internal("runtime rejected owned commit finalizer".into()))?;
2469
2470 match task.await {
2471 Ok(result) => result,
2472 Err(error) if publication_observed.load(Ordering::Acquire) => {
2473 log::error!(
2474 "owned commit finalizer terminated after metadata publication: {}; \
2475 treating the durable generation as committed",
2476 error,
2477 );
2478 Ok(true)
2479 }
2480 Err(error) => Err(Error::Internal(format!(
2481 "owned commit finalizer terminated unexpectedly: {error}"
2482 ))),
2483 }
2484 }
2485
2486 pub fn abort(mut self) {
2489 self.is_resolved = true;
2490 self.writer.flushed_segments.lock().clear();
2491 self.writer.clear_uncommitted_pk_reservations();
2492 self.writer.resume_workers();
2493 }
2494}
2495
2496impl<D: DirectoryWriter + 'static> Drop for PreparedCommit<'_, D> {
2497 fn drop(&mut self) {
2498 if !self.is_resolved {
2499 log::warn!("PreparedCommit dropped without commit/abort — auto-aborting");
2500 self.writer.flushed_segments.lock().clear();
2501 self.writer.clear_uncommitted_pk_reservations();
2502 self.writer.resume_workers();
2503 }
2504 }
2505}
2506
2507#[cfg(test)]
2508#[path = "tests/staged_admission.rs"]
2509mod staged_admission;