1use std::sync::Arc;
39use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
40
41use futures::FutureExt;
42use rustc_hash::FxHashMap;
43
44use crate::directories::DirectoryWriter;
45use crate::dsl::{Document, Field, Schema};
46use crate::error::{Error, Result};
47use crate::segment::{SegmentBuilder, SegmentBuilderConfig, SegmentId};
48use crate::tokenizer::BoxedTokenizer;
49
50use super::IndexConfig;
51
52const PIPELINE_MAX_SIZE_IN_DOCS: usize = 10_000;
54
55const SOFT_FLUSH_MIN_PERCENT: usize = 70;
61const SOFT_FLUSH_MAX_PERCENT: usize = 80;
62
63pub const WRITER_LOCK_FILENAME: &str = ".hermes_writer.lock";
65
66fn soft_flush_threshold(memory_budget: usize, worker_id: usize, num_workers: usize) -> usize {
73 if num_workers <= 1 {
74 return hard_flush_threshold(memory_budget);
75 }
76
77 let worker_id = worker_id.min(num_workers - 1);
78 let span = SOFT_FLUSH_MAX_PERCENT - SOFT_FLUSH_MIN_PERCENT;
79 let denominator = 100u128 * (num_workers - 1) as u128;
80 let numerator = (SOFT_FLUSH_MIN_PERCENT * (num_workers - 1) + span * worker_id) as u128;
81 ((memory_budget as u128 * numerator) / denominator) as usize
82}
83
84fn hard_flush_threshold(memory_budget: usize) -> usize {
88 memory_budget.saturating_mul(SOFT_FLUSH_MAX_PERCENT) / 100
89}
90
91fn default_builder_config(index_config: &IndexConfig) -> SegmentBuilderConfig {
98 SegmentBuilderConfig {
99 num_compression_threads: index_config.num_compression_threads,
100 ..SegmentBuilderConfig::default()
101 }
102}
103
104struct SegmentBuildLimiter {
112 live_max_active: usize,
113 flush_max_active: usize,
114 active: AtomicUsize,
115 flushing: AtomicBool,
116 wait_mutex: parking_lot::Mutex<()>,
117 available: parking_lot::Condvar,
118}
119
120impl SegmentBuildLimiter {
121 fn new(num_workers: usize) -> Self {
122 Self {
123 live_max_active: num_workers.saturating_sub(1).max(1),
126 flush_max_active: num_workers.max(1),
129 active: AtomicUsize::new(0),
130 flushing: AtomicBool::new(false),
131 wait_mutex: parking_lot::Mutex::new(()),
132 available: parking_lot::Condvar::new(),
133 }
134 }
135
136 fn try_acquire(&self) -> Option<SegmentBuildPermit<'_>> {
137 self.try_acquire_up_to(self.live_max_active)
138 }
139
140 fn try_acquire_up_to(&self, limit: usize) -> Option<SegmentBuildPermit<'_>> {
141 let mut active = self.active.load(Ordering::Acquire);
142 loop {
143 if active >= limit {
144 return None;
145 }
146 match self.active.compare_exchange_weak(
147 active,
148 active + 1,
149 Ordering::AcqRel,
150 Ordering::Acquire,
151 ) {
152 Ok(_) => return Some(SegmentBuildPermit { limiter: self }),
153 Err(observed) => active = observed,
154 }
155 }
156 }
157
158 fn acquire(&self) -> SegmentBuildPermit<'_> {
159 let mut wait = self.wait_mutex.lock();
160 loop {
161 let limit = if self.flushing.load(Ordering::Acquire) {
162 self.flush_max_active
163 } else {
164 self.live_max_active
165 };
166 if let Some(permit) = self.try_acquire_up_to(limit) {
167 return permit;
168 }
169 self.available.wait(&mut wait);
170 }
171 }
172
173 fn acquire_flush(&self) -> SegmentBuildPermit<'_> {
174 self.acquire_up_to(self.flush_max_active)
175 }
176
177 fn acquire_up_to(&self, limit: usize) -> SegmentBuildPermit<'_> {
178 let mut wait = self.wait_mutex.lock();
179 loop {
180 if let Some(permit) = self.try_acquire_up_to(limit) {
181 return permit;
182 }
183 self.available.wait(&mut wait);
184 }
185 }
186
187 fn begin_flush(&self) {
191 self.flushing.store(true, Ordering::Release);
192 let _wait = self.wait_mutex.lock();
193 self.available.notify_all();
194 }
195
196 fn end_flush(&self) {
197 self.flushing.store(false, Ordering::Release);
198 }
199
200 fn reserve_if_due(
206 &self,
207 builder_memory: usize,
208 soft_threshold: usize,
209 hard_budget: usize,
210 ) -> Option<SegmentBuildPermit<'_>> {
211 if builder_memory < soft_threshold {
212 return None;
213 }
214 if let Some(permit) = self.try_acquire() {
215 return Some(permit);
216 }
217 if builder_memory < hard_budget {
218 return None;
219 }
220 Some(self.acquire())
221 }
222}
223
224struct SegmentBuildPermit<'a> {
225 limiter: &'a SegmentBuildLimiter,
226}
227
228impl Drop for SegmentBuildPermit<'_> {
229 fn drop(&mut self) {
230 let previous = self.limiter.active.fetch_sub(1, Ordering::AcqRel);
231 debug_assert!(previous > 0);
232 let _wait = self.limiter.wait_mutex.lock();
235 self.limiter.available.notify_all();
238 }
239}
240
241#[cfg(test)]
242mod indexing_pipeline_tests {
243 use std::sync::Arc;
244 use std::time::Duration;
245
246 use super::{
247 SOFT_FLUSH_MAX_PERCENT, SOFT_FLUSH_MIN_PERCENT, SegmentBuildLimiter,
248 default_builder_config, hard_flush_threshold, soft_flush_threshold,
249 };
250
251 #[test]
252 fn standard_builder_config_honors_index_compression_width() {
253 let index_config = crate::index::IndexConfig {
254 num_compression_threads: 7,
255 ..Default::default()
256 };
257
258 let builder_config = default_builder_config(&index_config);
259
260 assert_eq!(builder_config.num_compression_threads, 7);
261 }
262
263 #[test]
264 fn flush_thresholds_are_staggered_without_increasing_memory_budget() {
265 const WORKERS: usize = 12;
266 const PER_WORKER_BUDGET: usize = 1024 * 1024 * 1024;
267
268 let thresholds: Vec<_> = (0..WORKERS)
269 .map(|worker| soft_flush_threshold(PER_WORKER_BUDGET, worker, WORKERS))
270 .collect();
271
272 assert_eq!(
273 thresholds[0],
274 PER_WORKER_BUDGET * SOFT_FLUSH_MIN_PERCENT / 100
275 );
276 assert_eq!(
277 thresholds[WORKERS - 1],
278 PER_WORKER_BUDGET * SOFT_FLUSH_MAX_PERCENT / 100
279 );
280 assert!(
281 thresholds.windows(2).all(|pair| pair[0] < pair[1]),
282 "production-width workers must not reach identical flush thresholds: {thresholds:?}"
283 );
284
285 let staggered_total: usize = thresholds.iter().sum();
286 let former_uniform_total = WORKERS * (PER_WORKER_BUDGET * SOFT_FLUSH_MAX_PERCENT / 100);
287 assert!(
288 staggered_total <= former_uniform_total,
289 "staggering must not increase aggregate builder memory"
290 );
291
292 assert_eq!(
293 soft_flush_threshold(PER_WORKER_BUDGET, 0, 1),
294 PER_WORKER_BUDGET * SOFT_FLUSH_MAX_PERCENT / 100,
295 "single-worker behavior retains the former 80% build headroom"
296 );
297
298 let hard_threshold = hard_flush_threshold(PER_WORKER_BUDGET);
299 let build_scratch = PER_WORKER_BUDGET - hard_threshold;
300 let steady_state_peak = (WORKERS - 1) * (hard_threshold + build_scratch) + hard_threshold;
301 assert!(
302 steady_state_peak <= WORKERS * PER_WORKER_BUDGET,
303 "rotated hard-threshold builds must retain aggregate scratch headroom"
304 );
305 }
306
307 #[test]
308 fn full_build_gate_leaves_soft_threshold_worker_draining() {
309 const WORKERS: usize = 12;
310 let limiter = SegmentBuildLimiter::new(WORKERS);
311 let mut active_builds: Vec<_> = (0..WORKERS - 1)
312 .map(|_| {
313 limiter
314 .try_acquire()
315 .expect("N - 1 builds should be admitted")
316 })
317 .collect();
318
319 assert!(
320 limiter.try_acquire().is_none(),
321 "the final worker must be reserved from concurrent segment builds"
322 );
323 assert!(
324 limiter.reserve_if_due(750, 700, 800).is_none(),
325 "a worker below its hard budget must keep draining when builds are saturated"
326 );
327
328 drop(active_builds.pop());
329 let replacement = limiter
330 .reserve_if_due(750, 700, 800)
331 .expect("a completed build must immediately rotate draining capacity");
332 assert!(limiter.try_acquire().is_none());
333 drop(replacement);
334 drop(active_builds);
335 }
336
337 #[test]
338 fn closed_queue_flush_uses_the_reserved_build_slot() {
339 let limiter = SegmentBuildLimiter::new(2);
340 let live_build = limiter
341 .try_acquire()
342 .expect("one live build should be admitted");
343 assert!(limiter.try_acquire().is_none());
344
345 let tail_build = limiter.acquire_flush();
346 assert!(
347 limiter
348 .try_acquire_up_to(limiter.flush_max_active)
349 .is_none(),
350 "closed-queue flushes must remain bounded by the worker count"
351 );
352
353 drop(tail_build);
354 drop(live_build);
355 }
356
357 #[test]
358 fn closing_input_promotes_an_existing_live_waiter() {
359 let limiter = Arc::new(SegmentBuildLimiter::new(2));
360 let live_build = limiter.try_acquire().unwrap();
361 let waiter_limiter = Arc::clone(&limiter);
362 let (started_tx, started_rx) = std::sync::mpsc::channel();
363 let (acquired_tx, acquired_rx) = std::sync::mpsc::channel();
364
365 let waiter = std::thread::spawn(move || {
366 started_tx.send(()).unwrap();
367 let _permit = waiter_limiter
368 .reserve_if_due(800, 700, 800)
369 .expect("closed input must promote a hard-boundary waiter");
370 acquired_tx.send(()).unwrap();
371 });
372
373 started_rx.recv_timeout(Duration::from_secs(1)).unwrap();
374 assert!(acquired_rx.recv_timeout(Duration::from_millis(50)).is_err());
375
376 limiter.begin_flush();
377 acquired_rx
378 .recv_timeout(Duration::from_secs(1))
379 .expect("live waiter did not adopt the closed-queue build limit");
380 waiter.join().unwrap();
381 limiter.end_flush();
382 drop(live_build);
383 }
384
385 #[test]
386 fn hard_budget_waiter_resumes_when_a_build_finishes() {
387 let limiter = Arc::new(SegmentBuildLimiter::new(3));
388 let first = limiter.try_acquire().unwrap();
389 let second = limiter.try_acquire().unwrap();
390 let waiter_limiter = Arc::clone(&limiter);
391 let (started_tx, started_rx) = std::sync::mpsc::channel();
392 let (acquired_tx, acquired_rx) = std::sync::mpsc::channel();
393
394 let waiter = std::thread::spawn(move || {
395 started_tx.send(()).unwrap();
396 let _permit = waiter_limiter
397 .reserve_if_due(800, 700, 800)
398 .expect("hard-budget worker must eventually acquire a build slot");
399 acquired_tx.send(()).unwrap();
400 });
401
402 started_rx.recv_timeout(Duration::from_secs(1)).unwrap();
403 assert!(
404 acquired_rx.recv_timeout(Duration::from_millis(50)).is_err(),
405 "hard-budget worker must not over-subscribe segment builds"
406 );
407 drop(first);
408 acquired_rx
409 .recv_timeout(Duration::from_secs(1))
410 .expect("hard-budget worker did not wake after a build completed");
411 waiter.join().unwrap();
412 drop(second);
413 }
414}
415
416enum WriterLock {
425 Held { _file: std::fs::File },
427 NotApplicable,
430 Unavailable { reason: String },
433}
434
435fn writer_lock_root<D: DirectoryWriter + 'static>(directory: &D) -> Option<std::path::PathBuf> {
438 let any: &dyn std::any::Any = directory;
439 if let Some(mmap) = any.downcast_ref::<crate::directories::MmapDirectory>() {
440 return Some(mmap.root().to_path_buf());
441 }
442 if any
446 .downcast_ref::<crate::directories::FsDirectory>()
447 .is_some()
448 {
449 log::warn!(
450 "[writer_lock] FsDirectory exposes no root path; single-writer locking \
451 is not enforced for this writer — do not open a second writer for the \
452 same index directory"
453 );
454 }
455 None
456}
457
458fn try_acquire_writer_lock<D: DirectoryWriter + 'static>(directory: &D) -> Result<WriterLock> {
463 let Some(root) = writer_lock_root(directory) else {
464 return Ok(WriterLock::NotApplicable);
465 };
466 std::fs::create_dir_all(&root)?;
467 let lock_path = root.join(WRITER_LOCK_FILENAME);
468 let file = std::fs::OpenOptions::new()
469 .create(true)
470 .truncate(false)
471 .write(true)
472 .open(&lock_path)?;
473 match file.try_lock() {
474 Ok(()) => Ok(WriterLock::Held { _file: file }),
475 Err(std::fs::TryLockError::WouldBlock) => Ok(WriterLock::Unavailable {
476 reason: format!(
477 "another IndexWriter already holds the single-writer lock for this \
478 index ({}); Hermes supports one writer per index directory — stop \
479 the other writer (e.g. a running hermes-server or hermes-tool) \
480 before opening this one",
481 lock_path.display()
482 ),
483 }),
484 Err(std::fs::TryLockError::Error(error)) => Err(Error::Io(error)),
485 }
486}
487
488pub struct IndexWriter<D: DirectoryWriter + 'static> {
502 pub(super) directory: Arc<D>,
503 pub(super) schema: Arc<Schema>,
504 pub(super) config: IndexConfig,
505 doc_sender: Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
508 workers: Vec<std::thread::JoinHandle<()>>,
510 worker_state: Arc<WorkerState<D>>,
512 pub(super) segment_manager: Arc<crate::merge::SegmentManager<D>>,
514 flushed_segments: Arc<parking_lot::Mutex<Vec<PreparedSegment<D>>>>,
517 primary_key_index: Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
519 primary_key_refresh_lock: Arc<tokio::sync::Mutex<()>>,
522 commit_finalization: Arc<CommitFinalizationState>,
526 pk_reservations_retained: Arc<AtomicBool>,
531 writer_lock: parking_lot::RwLock<WriterLock>,
536}
537
538#[derive(Default)]
539struct CommitFinalizationState {
540 in_progress: AtomicBool,
541 idle: tokio::sync::Notify,
542}
543
544impl CommitFinalizationState {
545 fn begin(&self) -> bool {
546 self.in_progress
547 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
548 .is_ok()
549 }
550
551 fn finish(&self) {
552 self.in_progress.store(false, Ordering::Release);
553 self.idle.notify_waiters();
554 }
555
556 async fn wait_until_idle(&self) {
557 while self.in_progress.load(Ordering::Acquire) {
558 let notified = self.idle.notified();
559 if !self.in_progress.load(Ordering::Acquire) {
560 break;
561 }
562 notified.await;
563 }
564 }
565}
566
567struct WorkerState<D: DirectoryWriter + 'static> {
569 directory: Arc<D>,
570 schema: Arc<Schema>,
571 builder_config: SegmentBuilderConfig,
572 tokenizers: parking_lot::RwLock<FxHashMap<Field, BoxedTokenizer>>,
573 memory_budget_per_worker: usize,
575 segment_build_limiter: SegmentBuildLimiter,
578 segment_manager: Arc<crate::merge::SegmentManager<D>>,
580 built_segments: parking_lot::Mutex<Vec<PreparedSegment<D>>>,
583 cycle_error: parking_lot::Mutex<Option<String>>,
588 cycle_failed: AtomicBool,
589
590 flush_count: AtomicUsize,
597 flush_mutex: parking_lot::Mutex<()>,
599 flush_cvar: parking_lot::Condvar,
600 resume_receiver: parking_lot::Mutex<Option<async_channel::Receiver<Document>>>,
602 resume_epoch: AtomicUsize,
605 resume_cvar: parking_lot::Condvar,
607 shutdown: AtomicBool,
609 num_workers: usize,
611}
612
613struct PreparedSegment<D: DirectoryWriter + 'static> {
619 id: String,
620 segment_id: SegmentId,
621 num_docs: u32,
622 segment_manager: Arc<crate::merge::SegmentManager<D>>,
623 operation: Option<crate::merge::SegmentOperationGuard>,
624 runtime: tokio::runtime::Handle,
625 needs_vector_upgrade: bool,
626 published: bool,
627}
628
629impl<D: DirectoryWriter + 'static> PreparedSegment<D> {
630 fn metadata_entry(&self) -> (String, u32) {
631 (self.id.clone(), self.num_docs)
632 }
633
634 fn mark_published(&mut self) {
635 self.published = true;
636 drop(self.operation.take());
638 }
639}
640
641impl<D: DirectoryWriter + 'static> WorkerState<D> {
642 fn record_cycle_error(&self, error: impl Into<String>) {
643 let mut first_error = self.cycle_error.lock();
644 if first_error.is_none() {
645 *first_error = Some(error.into());
646 }
647 drop(first_error);
648 self.cycle_failed.store(true, Ordering::Release);
649 }
650}
651
652impl<D: DirectoryWriter + 'static> Drop for PreparedSegment<D> {
653 fn drop(&mut self) {
654 if self.published {
655 return;
656 }
657 let Some(operation) = self.operation.take() else {
658 return;
659 };
660 self.segment_manager.schedule_unpublished_segment_cleanup(
661 self.segment_id,
662 operation,
663 self.runtime.clone(),
664 );
665 }
666}
667
668impl<D: DirectoryWriter + 'static> IndexWriter<D> {
669 pub async fn create(directory: D, schema: Schema, config: IndexConfig) -> Result<Self> {
671 let builder_config = default_builder_config(&config);
672 Self::create_with_config(directory, schema, config, builder_config).await
673 }
674
675 pub async fn create_with_config(
677 directory: D,
678 schema: Schema,
679 config: IndexConfig,
680 builder_config: SegmentBuilderConfig,
681 ) -> Result<Self> {
682 crate::dsl::reject_removed_vector_index_types(&schema).map_err(Error::Schema)?;
683 let directory = Arc::new(directory);
684 let schema = Arc::new(schema);
685 directory.set_index_label(schema.index_label());
687
688 let writer_lock = try_acquire_writer_lock(directory.as_ref())?;
690 if let WriterLock::Unavailable { reason } = &writer_lock {
691 return Err(Error::Internal(reason.clone()));
692 }
693 if directory
697 .exists(std::path::Path::new(super::INDEX_META_FILENAME))
698 .await?
699 {
700 return Err(Error::Internal(format!(
701 "refusing to create index: {} already exists in this directory; \
702 use IndexWriter::open to open the existing index, or delete the \
703 directory first if you really want to start over",
704 super::INDEX_META_FILENAME
705 )));
706 }
707
708 let metadata = super::IndexMetadata::new((*schema).clone());
709
710 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
711 Arc::clone(&directory),
712 Arc::clone(&schema),
713 metadata,
714 config.merge_policy.clone_box(),
715 config.term_cache_blocks,
716 config.max_concurrent_merges,
717 Arc::clone(&config.background_merge_permits),
718 config.merge_bp_time_budget,
719 config.bp_memory_budget_bytes,
720 Arc::clone(&config.background_reorder_permits),
721 config.background_reorder_pool.clone(),
722 ));
723 segment_manager.update_metadata(|_| {}).await?;
724
725 Ok(Self::new_with_parts(
726 directory,
727 schema,
728 config,
729 builder_config,
730 segment_manager,
731 writer_lock,
732 ))
733 }
734
735 pub async fn open(directory: D, config: IndexConfig) -> Result<Self> {
744 let builder_config = default_builder_config(&config);
745 Self::open_with_config(directory, config, builder_config).await
746 }
747
748 pub async fn open_with_config(
750 directory: D,
751 config: IndexConfig,
752 builder_config: SegmentBuilderConfig,
753 ) -> Result<Self> {
754 let directory = Arc::new(directory);
755
756 let writer_lock = try_acquire_writer_lock(directory.as_ref())?;
759 if let WriterLock::Unavailable { reason } = &writer_lock {
760 return Err(Error::Internal(reason.clone()));
761 }
762
763 let metadata = super::IndexMetadata::load(directory.as_ref()).await?;
764 let schema = Arc::new(metadata.schema.clone());
765 directory.set_index_label(schema.index_label());
767
768 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
769 Arc::clone(&directory),
770 Arc::clone(&schema),
771 metadata,
772 config.merge_policy.clone_box(),
773 config.term_cache_blocks,
774 config.max_concurrent_merges,
775 Arc::clone(&config.background_merge_permits),
776 config.merge_bp_time_budget,
777 config.bp_memory_budget_bytes,
778 Arc::clone(&config.background_reorder_permits),
779 config.background_reorder_pool.clone(),
780 ));
781 let swept = segment_manager.cleanup_orphan_segments().await?;
782 if swept > 0 {
783 log::warn!(
784 "[segment_cleanup] swept {} orphan segment(s) while opening writer",
785 swept
786 );
787 }
788 segment_manager.try_load_and_publish_trained().await?;
789
790 Ok(Self::new_with_parts(
791 directory,
792 schema,
793 config,
794 builder_config,
795 segment_manager,
796 writer_lock,
797 ))
798 }
799
800 pub fn from_index(index: &super::Index<D>) -> Self {
807 let writer_lock = match try_acquire_writer_lock(index.directory.as_ref()) {
808 Ok(lock) => lock,
809 Err(error) => WriterLock::Unavailable {
810 reason: format!("failed to acquire the single-writer lock: {error}"),
811 },
812 };
813 if let WriterLock::Unavailable { reason } = &writer_lock {
814 log::error!("[writer_lock] {reason}");
815 }
816 let builder_config = default_builder_config(&index.config);
817 Self::new_with_parts(
818 Arc::clone(&index.directory),
819 Arc::clone(&index.schema),
820 index.config.clone(),
821 builder_config,
822 Arc::clone(&index.segment_manager),
823 writer_lock,
824 )
825 }
826
827 fn new_with_parts(
833 directory: Arc<D>,
834 schema: Arc<Schema>,
835 config: IndexConfig,
836 builder_config: SegmentBuilderConfig,
837 segment_manager: Arc<crate::merge::SegmentManager<D>>,
838 writer_lock: WriterLock,
839 ) -> Self {
840 let registry = crate::tokenizer::TokenizerRegistry::new();
842 let mut tokenizers = FxHashMap::default();
843 for (field, entry) in schema.fields() {
844 if matches!(entry.field_type, crate::dsl::FieldType::Text)
845 && let Some(ref tok_name) = entry.tokenizer
846 && let Some(tok) = registry.get(tok_name)
847 {
848 tokenizers.insert(field, tok);
849 }
850 }
851
852 let num_workers = config.num_indexing_threads.max(1);
853 let worker_state = Arc::new(WorkerState {
854 directory: Arc::clone(&directory),
855 schema: Arc::clone(&schema),
856 builder_config,
857 tokenizers: parking_lot::RwLock::new(tokenizers),
858 memory_budget_per_worker: config.max_indexing_memory_bytes / num_workers,
859 segment_build_limiter: SegmentBuildLimiter::new(num_workers),
860 segment_manager: Arc::clone(&segment_manager),
861 built_segments: parking_lot::Mutex::new(Vec::new()),
862 cycle_error: parking_lot::Mutex::new(None),
863 cycle_failed: AtomicBool::new(false),
864 flush_count: AtomicUsize::new(0),
865 flush_mutex: parking_lot::Mutex::new(()),
866 flush_cvar: parking_lot::Condvar::new(),
867 resume_receiver: parking_lot::Mutex::new(None),
868 resume_epoch: AtomicUsize::new(0),
869 resume_cvar: parking_lot::Condvar::new(),
870 shutdown: AtomicBool::new(false),
871 num_workers,
872 });
873 let (doc_sender, workers) = Self::spawn_workers(&worker_state, num_workers);
874 let primary_key_index = Arc::new(parking_lot::RwLock::new(None));
875 let primary_key_refresh_lock = Arc::new(tokio::sync::Mutex::new(()));
876
877 Self {
878 directory,
879 schema,
880 config,
881 doc_sender: Arc::new(parking_lot::RwLock::new(doc_sender)),
882 workers,
883 worker_state,
884 segment_manager,
885 flushed_segments: Arc::new(parking_lot::Mutex::new(Vec::new())),
886 primary_key_index,
887 primary_key_refresh_lock,
888 commit_finalization: Arc::new(CommitFinalizationState::default()),
889 pk_reservations_retained: Arc::new(AtomicBool::new(false)),
890 writer_lock: parking_lot::RwLock::new(writer_lock),
891 }
892 }
893
894 fn ensure_writer_lock(&self) -> Result<()> {
902 if !matches!(&*self.writer_lock.read(), WriterLock::Unavailable { .. }) {
904 return Ok(());
905 }
906
907 let mut lock = self.writer_lock.write();
908 if !matches!(&*lock, WriterLock::Unavailable { .. }) {
910 return Ok(());
911 }
912 match try_acquire_writer_lock(self.directory.as_ref())? {
913 acquired @ (WriterLock::Held { .. } | WriterLock::NotApplicable) => {
914 log::info!(
915 "[writer_lock] index={} single-writer lock acquired after retry; \
916 the previous holder has released it — resuming writes",
917 self.schema.index_label()
918 );
919 *lock = acquired;
920 Ok(())
921 }
922 WriterLock::Unavailable { reason } => {
923 let err = Error::Internal(reason.clone());
924 *lock = WriterLock::Unavailable { reason };
925 Err(err)
926 }
927 }
928 }
929
930 fn clear_uncommitted_pk_reservations(&self) {
939 if self.pk_reservations_retained.load(Ordering::Acquire) {
940 log::warn!(
941 "[primary_key] index={} keeping uncommitted reservations through abort: a \
942 failed post-commit refresh left them as the only record of \
943 committed keys; they are cleared by the next successful commit",
944 self.schema.index_label()
945 );
946 return;
947 }
948 if let Some(pk_index) = self.primary_key_index.write().as_mut() {
949 pk_index.clear_uncommitted();
950 }
951 }
952
953 fn spawn_workers(
954 worker_state: &Arc<WorkerState<D>>,
955 num_workers: usize,
956 ) -> (
957 async_channel::Sender<Document>,
958 Vec<std::thread::JoinHandle<()>>,
959 ) {
960 let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
961 let handle = tokio::runtime::Handle::current();
962 let mut workers = Vec::with_capacity(num_workers);
963 for i in 0..num_workers {
964 let state = Arc::clone(worker_state);
965 let rx = receiver.clone();
966 let rt = handle.clone();
967 workers.push(
968 std::thread::Builder::new()
969 .name(format!("index-worker-{}", i))
970 .spawn(move || Self::worker_loop(state, rx, rt, i))
971 .expect("failed to spawn index worker thread"),
972 );
973 }
974 (sender, workers)
975 }
976
977 pub fn schema(&self) -> &Schema {
979 &self.schema
980 }
981
982 pub fn set_tokenizer<T: crate::tokenizer::Tokenizer>(&mut self, field: Field, tokenizer: T) {
985 self.worker_state
986 .tokenizers
987 .write()
988 .insert(field, Box::new(tokenizer));
989 }
990
991 pub async fn init_primary_key_dedup(&mut self) -> Result<()> {
1007 use super::primary_key::{PK_BLOOM_FILE, deserialize_pk_bloom};
1008
1009 self.commit_finalization.wait_until_idle().await;
1010 self.ensure_writer_lock()?;
1011
1012 let field = match self.schema.primary_field() {
1013 Some(f) => f,
1014 None => return Ok(()),
1015 };
1016
1017 let _refresh_guard = self.primary_key_refresh_lock.lock().await;
1022 {
1023 let callback_directory = Arc::clone(&self.directory);
1024 let callback_schema = Arc::clone(&self.schema);
1025 let callback_manager = Arc::downgrade(&self.segment_manager);
1026 let callback_primary_key = Arc::downgrade(&self.primary_key_index);
1027 let callback_refresh_lock = Arc::downgrade(&self.primary_key_refresh_lock);
1028 self.segment_manager.set_replacement_refresh(move || {
1029 let directory = Arc::clone(&callback_directory);
1030 let schema = Arc::clone(&callback_schema);
1031 let manager = callback_manager.clone();
1032 let primary_key = callback_primary_key.clone();
1033 let refresh_lock = callback_refresh_lock.clone();
1034 async move {
1035 let (Some(manager), Some(primary_key), Some(refresh_lock)) = (
1036 manager.upgrade(),
1037 primary_key.upgrade(),
1038 refresh_lock.upgrade(),
1039 ) else {
1040 return Ok(());
1041 };
1042 refresh_primary_key_snapshot(
1043 &directory,
1044 &schema,
1045 &manager,
1046 &primary_key,
1047 &refresh_lock,
1048 PrimaryKeyRefresh::Replacement,
1049 )
1050 .await
1051 }
1052 });
1053 }
1054
1055 let snapshot = self.segment_manager.acquire_snapshot().await;
1056 let current_seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
1057
1058 let cached = match self
1060 .directory
1061 .open_read(std::path::Path::new(PK_BLOOM_FILE))
1062 .await
1063 {
1064 Ok(handle) => {
1065 let data = handle.read_bytes_range(0..handle.len()).await;
1066 match data {
1067 Ok(bytes) => deserialize_pk_bloom(bytes.as_slice()),
1068 Err(_) => None,
1069 }
1070 }
1071 Err(_) => None,
1072 };
1073
1074 let load_futures: Vec<_> = current_seg_ids
1076 .iter()
1077 .map(|seg_id_str| {
1078 let seg_id_str = seg_id_str.clone();
1079 let dir = self.directory.as_ref();
1080 let schema = Arc::clone(&self.schema);
1081 async move { load_pk_segment_data(dir, &seg_id_str, &schema).await }
1082 })
1083 .collect();
1084 let all_data = futures::future::try_join_all(load_futures).await?;
1085
1086 if let Some((persisted_seg_ids, bloom)) = cached {
1087 let mut pk_data = Vec::with_capacity(all_data.len());
1089 let mut new_data = Vec::new();
1090 for d in all_data {
1091 if persisted_seg_ids.contains(&d.segment_id) {
1092 pk_data.push(d);
1093 } else {
1094 new_data.push(d);
1095 }
1096 }
1097 let needs_persist = !new_data.is_empty();
1098 let new_start = pk_data.len();
1099 pk_data.extend(new_data);
1100
1101 let pk_index = if new_start == pk_data.len() {
1102 super::primary_key::PrimaryKeyIndex::from_persisted(field, bloom, pk_data, snapshot)
1104 } else {
1105 let index_label = self.schema.index_label().to_owned();
1107 tokio::task::spawn_blocking(move || {
1108 let mut bloom = bloom;
1111 let mut added = 0usize;
1112 let num_new = pk_data.len() - new_start;
1113 for data in &pk_data[new_start..] {
1114 if let Some(ff) = data.fast_fields.get(&field.0)
1115 && let Some(dict) = ff.text_dict()
1116 {
1117 for key in dict.iter() {
1118 bloom.insert(key.as_bytes());
1119 added += 1;
1120 }
1121 }
1122 }
1123 if added > 0 {
1124 log::info!(
1125 "[primary_key] index={index_label} bloom: added {} keys from {} new segment(s)",
1126 added,
1127 num_new,
1128 );
1129 }
1130 super::primary_key::PrimaryKeyIndex::from_persisted(
1131 field, bloom, pk_data, snapshot,
1132 )
1133 })
1134 .await
1135 .map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?
1136 };
1137
1138 if needs_persist {
1139 self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
1140 }
1141
1142 *self.primary_key_index.write() = Some(pk_index);
1143 } else {
1144 let pk_index = tokio::task::spawn_blocking(move || {
1146 super::primary_key::PrimaryKeyIndex::new(field, all_data, snapshot)
1147 })
1148 .await
1149 .map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?;
1150
1151 self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
1152 *self.primary_key_index.write() = Some(pk_index);
1153 }
1154
1155 self.pk_reservations_retained
1159 .store(false, Ordering::Release);
1160
1161 Ok(())
1162 }
1163
1164 async fn persist_pk_bloom(
1167 &self,
1168 pk_index: &super::primary_key::PrimaryKeyIndex,
1169 segment_ids: &[String],
1170 ) {
1171 use super::primary_key::PK_BLOOM_FILE;
1172
1173 let writer = match self
1174 .directory
1175 .streaming_writer(std::path::Path::new(PK_BLOOM_FILE))
1176 .await
1177 {
1178 Ok(writer) => writer,
1179 Err(error) => {
1180 log::warn!(
1181 "[primary_key] index={} failed to open bloom cache: {}",
1182 self.schema.index_label(),
1183 error
1184 );
1185 return;
1186 }
1187 };
1188 let result = crate::segment::block_in_place_if_multithread(|| {
1189 write_pk_bloom_stream(pk_index, segment_ids, writer)
1190 });
1191 if let Err(e) = result {
1192 log::warn!(
1193 "[primary_key] index={} failed to persist bloom cache: {}",
1194 self.schema.index_label(),
1195 e
1196 );
1197 }
1198 }
1199
1200 pub fn add_document(&self, doc: Document) -> Result<()> {
1206 self.ensure_writer_lock()?;
1207 if self.worker_state.shutdown.load(Ordering::Acquire) {
1208 return Err(Error::IndexClosed);
1209 }
1210 if self.commit_finalization.in_progress.load(Ordering::Acquire) {
1211 return Err(Error::CommitInProgress);
1212 }
1213 let sender = self.doc_sender.read().clone();
1214 if sender.is_closed() {
1218 return Err(Error::CommitInProgress);
1219 }
1220 let primary_key_index = self.primary_key_index.read();
1221 if let Some(ref pk_index) = *primary_key_index {
1222 pk_index.check_and_insert(&doc)?;
1223 }
1224 match sender.try_send(doc) {
1225 Ok(()) => Ok(()),
1226 Err(async_channel::TrySendError::Full(doc)) => {
1227 if let Some(ref pk_index) = *primary_key_index {
1229 pk_index.rollback_uncommitted_key(&doc);
1230 }
1231 Err(Error::QueueFull)
1232 }
1233 Err(async_channel::TrySendError::Closed(doc)) => {
1234 if let Some(ref pk_index) = *primary_key_index {
1236 pk_index.rollback_uncommitted_key(&doc);
1237 }
1238 Err(Error::CommitInProgress)
1239 }
1240 }
1241 }
1242
1243 pub fn add_documents(&self, documents: Vec<Document>) -> Result<usize> {
1248 let total = documents.len();
1249 for (i, doc) in documents.into_iter().enumerate() {
1250 match self.add_document(doc) {
1251 Ok(()) => {}
1252 Err(Error::QueueFull | Error::CommitInProgress) => return Ok(i),
1253 Err(e) => return Err(e),
1254 }
1255 }
1256 Ok(total)
1257 }
1258
1259 fn worker_loop(
1272 state: Arc<WorkerState<D>>,
1273 initial_receiver: async_channel::Receiver<Document>,
1274 handle: tokio::runtime::Handle,
1275 worker_id: usize,
1276 ) {
1277 let mut receiver = initial_receiver;
1278 let mut my_epoch = 0usize;
1279 let soft_flush_threshold =
1280 soft_flush_threshold(state.memory_budget_per_worker, worker_id, state.num_workers);
1281 let hard_flush_threshold = hard_flush_threshold(state.memory_budget_per_worker);
1282
1283 loop {
1284 let build_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1288 let mut builder: Option<SegmentBuilder> = None;
1289
1290 while let Ok(doc) = receiver.recv_blocking() {
1291 if state.shutdown.load(Ordering::Acquire) {
1292 break;
1293 }
1294 if state.cycle_failed.load(Ordering::Acquire) {
1299 continue;
1300 }
1301 if builder.is_none() {
1303 match SegmentBuilder::new(
1304 Arc::clone(&state.schema),
1305 state.builder_config.clone(),
1306 ) {
1307 Ok(mut b) => {
1308 for (field, tokenizer) in state.tokenizers.read().iter() {
1309 b.set_tokenizer(*field, tokenizer.clone_box());
1310 }
1311 builder = Some(b);
1312 }
1313 Err(e) => {
1314 log::error!("Failed to create segment builder: {:?}", e);
1315 state.record_cycle_error(format!(
1316 "failed to create segment builder: {e}"
1317 ));
1318 continue;
1319 }
1320 }
1321 }
1322
1323 let b = builder.as_mut().unwrap();
1324 if let Err(e) = b.add_document(doc) {
1325 log::error!("Failed to index document: {:?}", e);
1326 state.record_cycle_error(format!("failed to index document: {e}"));
1327 continue;
1328 }
1329
1330 let builder_memory = b.estimated_memory_bytes();
1331
1332 if b.num_docs() & 0x3FFF == 0 {
1333 log::debug!(
1334 "[indexing] index={} docs={}, memory={}, budget={}",
1335 state.schema.index_label(),
1336 b.num_docs(),
1337 crate::format_bytes(builder_memory as u64),
1338 crate::format_bytes(state.memory_budget_per_worker as u64)
1339 );
1340 }
1341
1342 const MIN_DOCS_BEFORE_FLUSH: u32 = 100;
1344
1345 if b.num_docs() >= MIN_DOCS_BEFORE_FLUSH
1346 && let Some(_build_permit) = state.segment_build_limiter.reserve_if_due(
1347 builder_memory,
1348 soft_flush_threshold,
1349 hard_flush_threshold,
1350 )
1351 {
1352 log::info!(
1353 "[indexing] index={} memory budget reached, building segment: \
1354 worker={}, docs={}, memory={}, soft_budget={}, hard_budget={}",
1355 state.schema.index_label(),
1356 worker_id,
1357 b.num_docs(),
1358 crate::format_bytes(builder_memory as u64),
1359 crate::format_bytes(soft_flush_threshold as u64),
1360 crate::format_bytes(hard_flush_threshold as u64),
1361 );
1362 let full_builder = builder.take().unwrap();
1363 Self::build_segment_inline(&state, full_builder, &handle);
1364 }
1365 }
1366
1367 if !state.cycle_failed.load(Ordering::Acquire)
1369 && let Some(b) = builder.take()
1370 && b.num_docs() > 0
1371 {
1372 let _build_permit = state.segment_build_limiter.acquire_flush();
1373 Self::build_segment_inline(&state, b, &handle);
1374 }
1375 }));
1376
1377 if build_result.is_err() {
1378 log::error!(
1379 "[worker] index={} panic during indexing cycle — documents in this cycle may be lost",
1380 state.schema.index_label()
1381 );
1382 state.record_cycle_error("indexing worker panicked while building the batch");
1383 }
1384
1385 let prev = state.flush_count.fetch_add(1, Ordering::Release);
1388 if prev + 1 == state.num_workers {
1389 let _lock = state.flush_mutex.lock();
1396 state.flush_cvar.notify_all();
1397 }
1398
1399 {
1403 let mut lock = state.resume_receiver.lock();
1404 loop {
1405 if state.shutdown.load(Ordering::Acquire) {
1406 return;
1407 }
1408 let current_epoch = state.resume_epoch.load(Ordering::Acquire);
1409 if current_epoch > my_epoch
1410 && let Some(rx) = lock.as_ref()
1411 {
1412 receiver = rx.clone();
1413 my_epoch = current_epoch;
1414 break;
1415 }
1416 state.resume_cvar.wait(&mut lock);
1417 }
1418 }
1419 }
1420 }
1421
1422 fn build_segment_inline(
1426 state: &WorkerState<D>,
1427 builder: SegmentBuilder,
1428 handle: &tokio::runtime::Handle,
1429 ) {
1430 let segment_id = SegmentId::new();
1431 let segment_hex = segment_id.to_hex();
1432 let operation = match state
1435 .segment_manager
1436 .protect_new_segment(segment_hex.clone())
1437 {
1438 Ok(operation) => operation,
1439 Err(e) => {
1440 log::error!(
1441 "[segment_build_failed] index={} segment_id={} lifecycle_error={}",
1442 state.schema.index_label(),
1443 segment_hex,
1444 e,
1445 );
1446 state.record_cycle_error(format!(
1447 "failed to claim segment {segment_hex} for building: {e}"
1448 ));
1449 return;
1450 }
1451 };
1452 let trained = state.segment_manager.trained_for_segment_build();
1453 let doc_count = builder.num_docs();
1454 let build_start = std::time::Instant::now();
1455
1456 log::info!(
1457 "[segment_build] index={} segment_id={} doc_count={} ann={}",
1458 state.schema.index_label(),
1459 segment_hex,
1460 doc_count,
1461 trained.is_some()
1462 );
1463
1464 let mut prepared = PreparedSegment {
1468 id: segment_hex.clone(),
1469 segment_id,
1470 num_docs: doc_count,
1471 segment_manager: Arc::clone(&state.segment_manager),
1472 operation: Some(operation),
1473 runtime: handle.clone(),
1474 needs_vector_upgrade: trained.is_none(),
1475 published: false,
1476 };
1477
1478 match handle.block_on(builder.build(
1479 state.directory.as_ref(),
1480 segment_id,
1481 trained.as_deref(),
1482 )) {
1483 Ok(meta) if meta.num_docs == doc_count && meta.num_docs > 0 => {
1484 let duration_ms = build_start.elapsed().as_millis() as u64;
1485 log::info!(
1486 "[segment_build_done] index={} segment_id={} doc_count={} duration_ms={}",
1487 state.schema.index_label(),
1488 segment_hex,
1489 meta.num_docs,
1490 duration_ms,
1491 );
1492 prepared.num_docs = meta.num_docs;
1493 state.built_segments.lock().push(prepared);
1494 }
1495 Ok(meta) => {
1496 let error = format!(
1497 "segment {segment_hex} built {} docs from a {doc_count}-document builder",
1498 meta.num_docs
1499 );
1500 log::error!(
1501 "[segment_build_failed] index={} {error}",
1502 state.schema.index_label()
1503 );
1504 state.record_cycle_error(error);
1505 }
1506 Err(e) => {
1507 log::error!(
1508 "[segment_build_failed] index={} segment_id={} error={:?}",
1509 state.schema.index_label(),
1510 segment_hex,
1511 e
1512 );
1513 state.record_cycle_error(format!("failed to build segment {segment_hex}: {e}"));
1516 }
1517 }
1518 }
1519
1520 pub async fn maybe_merge(&self) {
1526 self.segment_manager.maybe_merge().await;
1527 }
1528
1529 pub async fn abort_merges(&self) {
1532 self.segment_manager.abort_merges().await;
1533 }
1534
1535 pub async fn shutdown(&mut self) -> Result<()> {
1540 self.segment_manager.begin_shutdown();
1541 self.signal_worker_shutdown();
1542
1543 self.commit_finalization.wait_until_idle().await;
1548
1549 let workers = std::mem::take(&mut self.workers);
1550 let panicked = tokio::task::spawn_blocking(move || {
1551 workers
1552 .into_iter()
1553 .map(|worker| worker.join().is_err())
1554 .filter(|panicked| *panicked)
1555 .count()
1556 })
1557 .await
1558 .map_err(|error| Error::Internal(format!("failed to join index workers: {}", error)))?;
1559 if panicked > 0 {
1560 log::error!(
1561 "[index_shutdown] index={} {} indexing worker(s) panicked",
1562 self.schema.index_label(),
1563 panicked
1564 );
1565 }
1566
1567 self.flushed_segments.lock().clear();
1570 self.worker_state.built_segments.lock().clear();
1571 if let Some(pk_index) = self.primary_key_index.write().as_mut() {
1572 pk_index.clear_uncommitted();
1573 }
1574 Ok(())
1575 }
1576
1577 pub async fn wait_for_merging_thread(&self) {
1579 self.segment_manager.wait_for_merging_thread().await;
1580 }
1581
1582 pub async fn wait_for_all_merges(&self) {
1584 self.segment_manager.wait_for_all_merges().await;
1585 }
1586
1587 pub async fn wait_for_commit_finalization(&self) {
1592 self.commit_finalization.wait_until_idle().await;
1593 }
1594
1595 pub fn tracker(&self) -> std::sync::Arc<crate::segment::SegmentTracker> {
1597 self.segment_manager.tracker()
1598 }
1599
1600 pub async fn acquire_snapshot(&self) -> crate::segment::SegmentSnapshot {
1602 self.segment_manager.acquire_snapshot().await
1603 }
1604
1605 pub async fn cleanup_orphan_segments(&self) -> Result<usize> {
1610 self.ensure_writer_lock()?;
1611 self.segment_manager.cleanup_orphan_segments().await
1612 }
1613
1614 pub async fn prepare_commit(&mut self) -> Result<PreparedCommit<'_, D>> {
1625 self.ensure_writer_lock()?;
1626 if self.worker_state.shutdown.load(Ordering::Acquire) {
1627 return Err(Error::IndexClosed);
1628 }
1629 if self.commit_finalization.in_progress.load(Ordering::Acquire) {
1630 return Err(Error::CommitInProgress);
1631 }
1632 self.doc_sender.read().close();
1634 self.worker_state.segment_build_limiter.begin_flush();
1635
1636 self.worker_state.resume_cvar.notify_all();
1640
1641 let state = Arc::clone(&self.worker_state);
1644 let index_label = self.schema.index_label().to_owned();
1645 let all_flushed = tokio::task::spawn_blocking(move || {
1646 let mut lock = state.flush_mutex.lock();
1647 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(300);
1648 while state.flush_count.load(Ordering::Acquire) < state.num_workers {
1649 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
1650 if remaining.is_zero() {
1651 log::error!(
1652 "[prepare_commit] index={index_label} timed out waiting for workers: {}/{} flushed",
1653 state.flush_count.load(Ordering::Acquire),
1654 state.num_workers
1655 );
1656 return false;
1657 }
1658 state.flush_cvar.wait_for(&mut lock, remaining);
1659 }
1660 true
1661 })
1662 .await
1663 .map_err(|e| Error::Internal(format!("Failed to wait for workers: {}", e)))?;
1664
1665 if !all_flushed {
1666 return Err(Error::Internal(format!(
1674 "prepare_commit timed out: {}/{} workers flushed; writer remains paused, retry commit",
1675 self.worker_state.flush_count.load(Ordering::Acquire),
1676 self.worker_state.num_workers
1677 )));
1678 }
1679
1680 let cycle_error = { self.worker_state.cycle_error.lock().take() };
1681 if let Some(error) = cycle_error {
1682 self.flushed_segments.lock().clear();
1687 self.worker_state.built_segments.lock().clear();
1688 self.clear_uncommitted_pk_reservations();
1689 self.resume_workers();
1690 return Err(Error::Internal(format!(
1691 "indexing generation failed; no documents from this batch were committed: {error}"
1692 )));
1693 }
1694
1695 let built = std::mem::take(&mut *self.worker_state.built_segments.lock());
1697 self.flushed_segments.lock().extend(built);
1698
1699 Ok(PreparedCommit {
1700 writer: self,
1701 is_resolved: false,
1702 })
1703 }
1704
1705 pub async fn commit(&mut self) -> Result<bool> {
1710 self.prepare_commit().await?.commit().await
1711 }
1712
1713 pub async fn force_merge(&mut self) -> Result<()> {
1715 self.force_merge_with_snapshot_refresh(|| std::future::ready(Ok(())))
1716 .await
1717 }
1718
1719 pub async fn force_merge_with_snapshot_refresh<F, Fut>(
1726 &mut self,
1727 refresh_external: F,
1728 ) -> Result<()>
1729 where
1730 F: FnMut() -> Fut,
1731 Fut: std::future::Future<Output = Result<()>>,
1732 {
1733 self.prepare_commit().await?.commit().await?;
1734
1735 self.segment_manager
1736 .force_merge_with_snapshot_refresh(refresh_external)
1737 .await?;
1738
1739 self.persist_replacement_snapshot().await
1743 }
1744
1745 pub async fn reorder(&mut self) -> Result<()> {
1750 self.reorder_with_snapshot_refresh(|| std::future::ready(Ok(())))
1751 .await
1752 }
1753
1754 pub async fn reorder_with_snapshot_refresh<F, Fut>(&mut self, refresh_external: F) -> Result<()>
1757 where
1758 F: FnMut() -> Fut,
1759 Fut: std::future::Future<Output = Result<()>>,
1760 {
1761 self.prepare_commit().await?.commit().await?;
1762
1763 self.segment_manager
1764 .reorder_segments_with_snapshot_refresh(refresh_external)
1765 .await?;
1766 self.persist_replacement_snapshot().await
1767 }
1768
1769 async fn persist_replacement_snapshot(&self) -> Result<()> {
1771 refresh_primary_key_snapshot(
1772 &self.directory,
1773 &self.schema,
1774 &self.segment_manager,
1775 &self.primary_key_index,
1776 &self.primary_key_refresh_lock,
1777 PrimaryKeyRefresh::FinalReplacement,
1778 )
1779 .await
1780 }
1781
1782 pub fn segment_manager(&self) -> &Arc<crate::merge::SegmentManager<D>> {
1784 &self.segment_manager
1785 }
1786
1787 fn resume_workers(&mut self) {
1792 Self::resume_workers_shared(&self.worker_state, &self.doc_sender);
1793 }
1794
1795 fn resume_workers_shared(
1796 worker_state: &Arc<WorkerState<D>>,
1797 doc_sender: &Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
1798 ) {
1799 if worker_state.shutdown.load(Ordering::Acquire) {
1800 return;
1801 }
1802 if tokio::runtime::Handle::try_current().is_err() {
1803 worker_state.shutdown.store(true, Ordering::Release);
1806 worker_state.resume_cvar.notify_all();
1807 return;
1808 }
1809
1810 worker_state.segment_build_limiter.end_flush();
1812 worker_state.flush_count.store(0, Ordering::Release);
1813 *worker_state.cycle_error.lock() = None;
1814 worker_state.cycle_failed.store(false, Ordering::Release);
1815
1816 let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
1818 *doc_sender.write() = sender;
1819
1820 {
1822 let mut lock = worker_state.resume_receiver.lock();
1823 *lock = Some(receiver);
1824 }
1825 worker_state.resume_epoch.fetch_add(1, Ordering::Release);
1826 worker_state.resume_cvar.notify_all();
1827 }
1828
1829 fn signal_worker_shutdown(&self) {
1830 self.worker_state.shutdown.store(true, Ordering::Release);
1831 self.doc_sender.read().close();
1832 self.worker_state.segment_build_limiter.begin_flush();
1833 self.worker_state.resume_cvar.notify_all();
1834 }
1835
1836 }
1838
1839impl<D: DirectoryWriter + 'static> Drop for IndexWriter<D> {
1840 fn drop(&mut self) {
1841 self.signal_worker_shutdown();
1842 for w in std::mem::take(&mut self.workers) {
1843 let _ = w.join();
1844 }
1845 }
1846}
1847
1848pub struct PreparedCommit<'a, D: DirectoryWriter + 'static> {
1855 writer: &'a mut IndexWriter<D>,
1856 is_resolved: bool,
1857}
1858
1859struct PreparedSegmentsGuard<D: DirectoryWriter + 'static> {
1864 segments: Option<Vec<PreparedSegment<D>>>,
1865 retry_slot: Arc<parking_lot::Mutex<Vec<PreparedSegment<D>>>>,
1866}
1867
1868impl<D: DirectoryWriter + 'static> PreparedSegmentsGuard<D> {
1869 fn metadata_entries(&self) -> Vec<(String, u32)> {
1870 self.segments
1871 .as_deref()
1872 .unwrap_or_default()
1873 .iter()
1874 .map(PreparedSegment::metadata_entry)
1875 .collect()
1876 }
1877
1878 fn take_published(&mut self) -> Vec<PreparedSegment<D>> {
1879 self.segments.take().unwrap_or_default()
1880 }
1881
1882 fn vector_upgrade_segment_ids(&self) -> Vec<String> {
1883 self.segments
1884 .as_deref()
1885 .unwrap_or_default()
1886 .iter()
1887 .filter(|segment| segment.needs_vector_upgrade)
1888 .map(|segment| segment.id.clone())
1889 .collect()
1890 }
1891}
1892
1893impl<D: DirectoryWriter + 'static> Drop for PreparedSegmentsGuard<D> {
1894 fn drop(&mut self) {
1895 if let Some(segments) = self.segments.take() {
1896 self.retry_slot.lock().extend(segments);
1897 }
1898 }
1899}
1900
1901struct CommitFinalizationGuard<D: DirectoryWriter + 'static> {
1906 state: Arc<CommitFinalizationState>,
1907 worker_state: Arc<WorkerState<D>>,
1908 doc_sender: Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
1909 resume_workers: bool,
1910}
1911
1912impl<D: DirectoryWriter + 'static> CommitFinalizationGuard<D> {
1913 fn resume_on_drop(&mut self) {
1914 self.resume_workers = true;
1915 }
1916}
1917
1918impl<D: DirectoryWriter + 'static> Drop for CommitFinalizationGuard<D> {
1919 fn drop(&mut self) {
1920 if self.resume_workers {
1921 IndexWriter::<D>::resume_workers_shared(&self.worker_state, &self.doc_sender);
1922 }
1923 self.state.finish();
1924 }
1925}
1926
1927struct OwnedCommitFinalization<D: DirectoryWriter + 'static> {
1932 directory: Arc<D>,
1933 schema: Arc<Schema>,
1934 segment_manager: Arc<crate::merge::SegmentManager<D>>,
1935 primary_key_index: Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
1936 primary_key_refresh_lock: Arc<tokio::sync::Mutex<()>>,
1937 prepared: PreparedSegmentsGuard<D>,
1938 finalization: Option<CommitFinalizationGuard<D>>,
1939 publication_observed: Arc<AtomicBool>,
1940 pk_reservations_retained: Arc<AtomicBool>,
1941}
1942
1943#[derive(Clone, Copy)]
1944enum PrimaryKeyRefresh {
1945 Commit,
1947 Replacement,
1950 FinalReplacement,
1953}
1954
1955async fn refresh_primary_key_snapshot<D: DirectoryWriter + 'static>(
1956 directory: &Arc<D>,
1957 schema: &Arc<Schema>,
1958 segment_manager: &Arc<crate::merge::SegmentManager<D>>,
1959 primary_key_index: &Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
1960 primary_key_refresh_lock: &Arc<tokio::sync::Mutex<()>>,
1961 refresh: PrimaryKeyRefresh,
1962) -> Result<()> {
1963 let _refresh_guard = primary_key_refresh_lock.lock().await;
1964 let existing_ids: std::collections::HashSet<String> = {
1965 let guard = primary_key_index.read();
1966 let Some(pk_index) = guard.as_ref() else {
1967 return Ok(());
1968 };
1969 pk_index
1970 .committed_segment_ids()
1971 .map(ToOwned::to_owned)
1972 .collect()
1973 };
1974
1975 let snapshot = segment_manager.acquire_snapshot().await;
1976 let load_futures: Vec<_> = snapshot
1977 .segment_ids()
1978 .iter()
1979 .filter(|id| !existing_ids.contains(id.as_str()))
1980 .map(|seg_id_str| {
1981 let seg_id_str = seg_id_str.clone();
1982 let dir = directory.as_ref();
1983 let schema = Arc::clone(schema);
1984 async move { load_pk_segment_data(dir, &seg_id_str, &schema).await }
1985 })
1986 .collect();
1987 let new_data = futures::future::try_join_all(load_futures).await?;
1988 let seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
1989
1990 let persist_bloom = {
1991 let mut guard = primary_key_index.write();
1992 let Some(pk_index) = guard.as_mut() else {
1993 return Ok(());
1994 };
1995 match refresh {
1996 PrimaryKeyRefresh::Commit => pk_index.refresh_incremental(new_data, snapshot),
1997 PrimaryKeyRefresh::Replacement | PrimaryKeyRefresh::FinalReplacement => {
1998 pk_index.refresh_replacement(new_data, snapshot);
1999 }
2000 }
2001 matches!(
2002 refresh,
2003 PrimaryKeyRefresh::Commit | PrimaryKeyRefresh::FinalReplacement
2004 )
2005 };
2006
2007 if persist_bloom {
2008 let writer = match directory
2009 .streaming_writer(std::path::Path::new(super::primary_key::PK_BLOOM_FILE))
2010 .await
2011 {
2012 Ok(writer) => writer,
2013 Err(error) => {
2014 log::warn!(
2015 "[primary_key] index={} failed to open bloom cache: {}",
2016 schema.index_label(),
2017 error
2018 );
2019 return Ok(());
2020 }
2021 };
2022 let guard = primary_key_index.read();
2025 if let Some(pk_index) = guard.as_ref()
2026 && let Err(error) = crate::segment::block_in_place_if_multithread(|| {
2027 write_pk_bloom_stream(pk_index, &seg_ids, writer)
2028 })
2029 {
2030 log::warn!(
2031 "[primary_key] index={} failed to persist bloom cache: {}",
2032 schema.index_label(),
2033 error
2034 );
2035 }
2036 }
2037 Ok(())
2038}
2039
2040fn write_pk_bloom_stream(
2041 pk_index: &super::primary_key::PrimaryKeyIndex,
2042 segment_ids: &[String],
2043 mut writer: Box<dyn crate::directories::StreamingWriter>,
2044) -> std::io::Result<()> {
2045 pk_index.write_bloom_cache(segment_ids, writer.as_mut())?;
2046 writer.finish()
2047}
2048
2049async fn finalize_prepared_commit<D: DirectoryWriter + 'static>(
2050 mut commit: OwnedCommitFinalization<D>,
2051) -> Result<bool> {
2052 let metadata_entries = commit.prepared.metadata_entries();
2053 let published_segment_ids = commit.prepared.vector_upgrade_segment_ids();
2054
2055 commit.segment_manager.commit(&metadata_entries).await?;
2059 commit.publication_observed.store(true, Ordering::Release);
2060
2061 let mut published = commit.prepared.take_published();
2062 for segment in &mut published {
2063 segment.mark_published();
2064 }
2065 drop(published);
2066 commit
2067 .segment_manager
2068 .schedule_vector_segment_upgrades(published_segment_ids);
2069 if let Some(finalization) = commit.finalization.as_mut() {
2073 finalization.resume_on_drop();
2074 } else {
2075 log::error!("owned commit finalization guard was already released after publication");
2076 }
2077
2078 match refresh_primary_key_snapshot(
2083 &commit.directory,
2084 &commit.schema,
2085 &commit.segment_manager,
2086 &commit.primary_key_index,
2087 &commit.primary_key_refresh_lock,
2088 PrimaryKeyRefresh::Commit,
2089 )
2090 .await
2091 {
2092 Ok(()) => commit
2095 .pk_reservations_retained
2096 .store(false, Ordering::Release),
2097 Err(error) => {
2098 commit
2103 .pk_reservations_retained
2104 .store(true, Ordering::Release);
2105 log::error!(
2106 "[primary_key] committed metadata but failed to refresh dedup state; \
2107 retaining reservations until a later successful commit: {}",
2108 error,
2109 );
2110 }
2111 }
2112
2113 drop(commit.finalization.take());
2117 commit.segment_manager.maybe_merge().await;
2118 Ok(true)
2119}
2120
2121impl<'a, D: DirectoryWriter + 'static> PreparedCommit<'a, D> {
2122 pub async fn commit(mut self) -> Result<bool> {
2126 let segments = std::mem::take(&mut *self.writer.flushed_segments.lock());
2127
2128 if segments.is_empty() {
2130 log::debug!(
2131 "[commit] index={} no segments to commit, skipping",
2132 self.writer.schema.index_label()
2133 );
2134 self.is_resolved = true;
2135 self.writer.resume_workers();
2136 return Ok(false);
2137 }
2138
2139 if !self.writer.commit_finalization.begin() {
2140 self.writer.flushed_segments.lock().extend(segments);
2141 self.is_resolved = true;
2145 return Err(Error::CommitInProgress);
2146 }
2147
2148 let publication_observed = Arc::new(AtomicBool::new(false));
2149 let owned = OwnedCommitFinalization {
2150 directory: Arc::clone(&self.writer.directory),
2151 schema: Arc::clone(&self.writer.schema),
2152 segment_manager: Arc::clone(&self.writer.segment_manager),
2153 primary_key_index: Arc::clone(&self.writer.primary_key_index),
2154 primary_key_refresh_lock: Arc::clone(&self.writer.primary_key_refresh_lock),
2155 prepared: PreparedSegmentsGuard {
2156 segments: Some(segments),
2157 retry_slot: Arc::clone(&self.writer.flushed_segments),
2158 },
2159 finalization: Some(CommitFinalizationGuard {
2160 state: Arc::clone(&self.writer.commit_finalization),
2161 worker_state: Arc::clone(&self.writer.worker_state),
2162 doc_sender: Arc::clone(&self.writer.doc_sender),
2163 resume_workers: false,
2164 }),
2165 publication_observed: Arc::clone(&publication_observed),
2166 pk_reservations_retained: Arc::clone(&self.writer.pk_reservations_retained),
2167 };
2168
2169 self.is_resolved = true;
2174 let task_publication = Arc::clone(&publication_observed);
2175 let task = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2176 tokio::spawn(async move {
2177 match std::panic::AssertUnwindSafe(finalize_prepared_commit(owned))
2178 .catch_unwind()
2179 .await
2180 {
2181 Ok(result) => result,
2182 Err(_) if task_publication.load(Ordering::Acquire) => {
2183 log::error!(
2184 "owned commit finalizer panicked after metadata publication; \
2185 treating the durable generation as committed"
2186 );
2187 Ok(true)
2188 }
2189 Err(_) => Err(Error::Internal(
2190 "owned commit finalizer panicked before metadata publication".into(),
2191 )),
2192 }
2193 })
2194 }))
2195 .map_err(|_| Error::Internal("runtime rejected owned commit finalizer".into()))?;
2196
2197 match task.await {
2198 Ok(result) => result,
2199 Err(error) if publication_observed.load(Ordering::Acquire) => {
2200 log::error!(
2201 "owned commit finalizer terminated after metadata publication: {}; \
2202 treating the durable generation as committed",
2203 error,
2204 );
2205 Ok(true)
2206 }
2207 Err(error) => Err(Error::Internal(format!(
2208 "owned commit finalizer terminated unexpectedly: {error}"
2209 ))),
2210 }
2211 }
2212
2213 pub fn abort(mut self) {
2216 self.is_resolved = true;
2217 self.writer.flushed_segments.lock().clear();
2218 self.writer.clear_uncommitted_pk_reservations();
2219 self.writer.resume_workers();
2220 }
2221}
2222
2223impl<D: DirectoryWriter + 'static> Drop for PreparedCommit<'_, D> {
2224 fn drop(&mut self) {
2225 if !self.is_resolved {
2226 log::warn!("PreparedCommit dropped without commit/abort — auto-aborting");
2227 self.writer.flushed_segments.lock().clear();
2228 self.writer.clear_uncommitted_pk_reservations();
2229 self.writer.resume_workers();
2230 }
2231 }
2232}
2233
2234async fn load_pk_segment_data<D: crate::directories::Directory>(
2236 dir: &D,
2237 seg_id_str: &str,
2238 schema: &Arc<crate::dsl::Schema>,
2239) -> Result<super::primary_key::PkSegmentData> {
2240 let seg_id = crate::segment::SegmentId::from_hex(seg_id_str)
2241 .ok_or_else(|| Error::Internal(format!("Invalid segment id: {}", seg_id_str)))?;
2242 let files = crate::segment::SegmentFiles::new(seg_id.0);
2243 let fast_fields =
2244 crate::segment::reader::loader::load_fast_fields_file(dir, &files, schema).await?;
2245 Ok(super::primary_key::PkSegmentData {
2246 segment_id: seg_id_str.to_string(),
2247 fast_fields,
2248 })
2249}