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::{
48 SegmentBuilder, SegmentBuilderConfig, SegmentId, validate_vector_value_counts,
49};
50use crate::tokenizer::BoxedTokenizer;
51
52use super::IndexConfig;
53
54const PIPELINE_MAX_SIZE_IN_DOCS: usize = 10_000;
56
57const SOFT_FLUSH_MIN_PERCENT: usize = 70;
63const SOFT_FLUSH_MAX_PERCENT: usize = 80;
64
65pub const WRITER_LOCK_FILENAME: &str = ".hermes_writer.lock";
67
68fn soft_flush_threshold(memory_budget: usize, worker_id: usize, num_workers: usize) -> usize {
75 if num_workers <= 1 {
76 return hard_flush_threshold(memory_budget);
77 }
78
79 let worker_id = worker_id.min(num_workers - 1);
80 let span = SOFT_FLUSH_MAX_PERCENT - SOFT_FLUSH_MIN_PERCENT;
81 let denominator = 100u128 * (num_workers - 1) as u128;
82 let numerator = (SOFT_FLUSH_MIN_PERCENT * (num_workers - 1) + span * worker_id) as u128;
83 ((memory_budget as u128 * numerator) / denominator) as usize
84}
85
86fn hard_flush_threshold(memory_budget: usize) -> usize {
90 memory_budget.saturating_mul(SOFT_FLUSH_MAX_PERCENT) / 100
91}
92
93fn default_builder_config(index_config: &IndexConfig) -> SegmentBuilderConfig {
100 SegmentBuilderConfig {
101 num_compression_threads: index_config.num_compression_threads,
102 ..SegmentBuilderConfig::default()
103 }
104}
105
106struct SegmentBuildLimiter {
114 live_max_active: usize,
115 flush_max_active: usize,
116 active: AtomicUsize,
117 flushing: AtomicBool,
118 wait_mutex: parking_lot::Mutex<()>,
119 available: parking_lot::Condvar,
120}
121
122impl SegmentBuildLimiter {
123 fn new(num_workers: usize) -> Self {
124 Self {
125 live_max_active: num_workers.saturating_sub(1).max(1),
128 flush_max_active: num_workers.max(1),
131 active: AtomicUsize::new(0),
132 flushing: AtomicBool::new(false),
133 wait_mutex: parking_lot::Mutex::new(()),
134 available: parking_lot::Condvar::new(),
135 }
136 }
137
138 fn try_acquire(&self) -> Option<SegmentBuildPermit<'_>> {
139 self.try_acquire_up_to(self.live_max_active)
140 }
141
142 fn try_acquire_up_to(&self, limit: usize) -> Option<SegmentBuildPermit<'_>> {
143 let mut active = self.active.load(Ordering::Acquire);
144 loop {
145 if active >= limit {
146 return None;
147 }
148 match self.active.compare_exchange_weak(
149 active,
150 active + 1,
151 Ordering::AcqRel,
152 Ordering::Acquire,
153 ) {
154 Ok(_) => return Some(SegmentBuildPermit { limiter: self }),
155 Err(observed) => active = observed,
156 }
157 }
158 }
159
160 fn acquire(&self) -> SegmentBuildPermit<'_> {
161 let mut wait = self.wait_mutex.lock();
162 loop {
163 let limit = if self.flushing.load(Ordering::Acquire) {
164 self.flush_max_active
165 } else {
166 self.live_max_active
167 };
168 if let Some(permit) = self.try_acquire_up_to(limit) {
169 return permit;
170 }
171 self.available.wait(&mut wait);
172 }
173 }
174
175 fn acquire_flush(&self) -> SegmentBuildPermit<'_> {
176 self.acquire_up_to(self.flush_max_active)
177 }
178
179 fn acquire_up_to(&self, limit: usize) -> SegmentBuildPermit<'_> {
180 let mut wait = self.wait_mutex.lock();
181 loop {
182 if let Some(permit) = self.try_acquire_up_to(limit) {
183 return permit;
184 }
185 self.available.wait(&mut wait);
186 }
187 }
188
189 fn begin_flush(&self) {
193 self.flushing.store(true, Ordering::Release);
194 let _wait = self.wait_mutex.lock();
195 self.available.notify_all();
196 }
197
198 fn end_flush(&self) {
199 self.flushing.store(false, Ordering::Release);
200 }
201
202 fn reserve_if_due(
208 &self,
209 builder_memory: usize,
210 soft_threshold: usize,
211 hard_budget: usize,
212 ) -> Option<SegmentBuildPermit<'_>> {
213 if builder_memory < soft_threshold {
214 return None;
215 }
216 if let Some(permit) = self.try_acquire() {
217 return Some(permit);
218 }
219 if builder_memory < hard_budget {
220 return None;
221 }
222 Some(self.acquire())
223 }
224}
225
226struct SegmentBuildPermit<'a> {
227 limiter: &'a SegmentBuildLimiter,
228}
229
230impl Drop for SegmentBuildPermit<'_> {
231 fn drop(&mut self) {
232 let previous = self.limiter.active.fetch_sub(1, Ordering::AcqRel);
233 debug_assert!(previous > 0);
234 let _wait = self.limiter.wait_mutex.lock();
237 self.limiter.available.notify_all();
240 }
241}
242
243#[cfg(test)]
244mod indexing_pipeline_tests {
245 use std::sync::Arc;
246 use std::time::Duration;
247
248 use super::{
249 SOFT_FLUSH_MAX_PERCENT, SOFT_FLUSH_MIN_PERCENT, SegmentBuildLimiter,
250 default_builder_config, hard_flush_threshold, soft_flush_threshold,
251 };
252
253 #[test]
254 fn standard_builder_config_honors_index_compression_width() {
255 let index_config = crate::index::IndexConfig {
256 num_compression_threads: 7,
257 ..Default::default()
258 };
259
260 let builder_config = default_builder_config(&index_config);
261
262 assert_eq!(builder_config.num_compression_threads, 7);
263 }
264
265 #[test]
266 fn flush_thresholds_are_staggered_without_increasing_memory_budget() {
267 const WORKERS: usize = 12;
268 const PER_WORKER_BUDGET: usize = 1024 * 1024 * 1024;
269
270 let thresholds: Vec<_> = (0..WORKERS)
271 .map(|worker| soft_flush_threshold(PER_WORKER_BUDGET, worker, WORKERS))
272 .collect();
273
274 assert_eq!(
275 thresholds[0],
276 PER_WORKER_BUDGET * SOFT_FLUSH_MIN_PERCENT / 100
277 );
278 assert_eq!(
279 thresholds[WORKERS - 1],
280 PER_WORKER_BUDGET * SOFT_FLUSH_MAX_PERCENT / 100
281 );
282 assert!(
283 thresholds.windows(2).all(|pair| pair[0] < pair[1]),
284 "production-width workers must not reach identical flush thresholds: {thresholds:?}"
285 );
286
287 let staggered_total: usize = thresholds.iter().sum();
288 let former_uniform_total = WORKERS * (PER_WORKER_BUDGET * SOFT_FLUSH_MAX_PERCENT / 100);
289 assert!(
290 staggered_total <= former_uniform_total,
291 "staggering must not increase aggregate builder memory"
292 );
293
294 assert_eq!(
295 soft_flush_threshold(PER_WORKER_BUDGET, 0, 1),
296 PER_WORKER_BUDGET * SOFT_FLUSH_MAX_PERCENT / 100,
297 "single-worker behavior retains the former 80% build headroom"
298 );
299
300 let hard_threshold = hard_flush_threshold(PER_WORKER_BUDGET);
301 let build_scratch = PER_WORKER_BUDGET - hard_threshold;
302 let steady_state_peak = (WORKERS - 1) * (hard_threshold + build_scratch) + hard_threshold;
303 assert!(
304 steady_state_peak <= WORKERS * PER_WORKER_BUDGET,
305 "rotated hard-threshold builds must retain aggregate scratch headroom"
306 );
307 }
308
309 #[test]
310 fn full_build_gate_leaves_soft_threshold_worker_draining() {
311 const WORKERS: usize = 12;
312 let limiter = SegmentBuildLimiter::new(WORKERS);
313 let mut active_builds: Vec<_> = (0..WORKERS - 1)
314 .map(|_| {
315 limiter
316 .try_acquire()
317 .expect("N - 1 builds should be admitted")
318 })
319 .collect();
320
321 assert!(
322 limiter.try_acquire().is_none(),
323 "the final worker must be reserved from concurrent segment builds"
324 );
325 assert!(
326 limiter.reserve_if_due(750, 700, 800).is_none(),
327 "a worker below its hard budget must keep draining when builds are saturated"
328 );
329
330 drop(active_builds.pop());
331 let replacement = limiter
332 .reserve_if_due(750, 700, 800)
333 .expect("a completed build must immediately rotate draining capacity");
334 assert!(limiter.try_acquire().is_none());
335 drop(replacement);
336 drop(active_builds);
337 }
338
339 #[test]
340 fn closed_queue_flush_uses_the_reserved_build_slot() {
341 let limiter = SegmentBuildLimiter::new(2);
342 let live_build = limiter
343 .try_acquire()
344 .expect("one live build should be admitted");
345 assert!(limiter.try_acquire().is_none());
346
347 let tail_build = limiter.acquire_flush();
348 assert!(
349 limiter
350 .try_acquire_up_to(limiter.flush_max_active)
351 .is_none(),
352 "closed-queue flushes must remain bounded by the worker count"
353 );
354
355 drop(tail_build);
356 drop(live_build);
357 }
358
359 #[test]
360 fn closing_input_promotes_an_existing_live_waiter() {
361 let limiter = Arc::new(SegmentBuildLimiter::new(2));
362 let live_build = limiter.try_acquire().unwrap();
363 let waiter_limiter = Arc::clone(&limiter);
364 let (started_tx, started_rx) = std::sync::mpsc::channel();
365 let (acquired_tx, acquired_rx) = std::sync::mpsc::channel();
366
367 let waiter = std::thread::spawn(move || {
368 started_tx.send(()).unwrap();
369 let _permit = waiter_limiter
370 .reserve_if_due(800, 700, 800)
371 .expect("closed input must promote a hard-boundary waiter");
372 acquired_tx.send(()).unwrap();
373 });
374
375 started_rx.recv_timeout(Duration::from_secs(1)).unwrap();
376 assert!(acquired_rx.recv_timeout(Duration::from_millis(50)).is_err());
377
378 limiter.begin_flush();
379 acquired_rx
380 .recv_timeout(Duration::from_secs(1))
381 .expect("live waiter did not adopt the closed-queue build limit");
382 waiter.join().unwrap();
383 limiter.end_flush();
384 drop(live_build);
385 }
386
387 #[test]
388 fn hard_budget_waiter_resumes_when_a_build_finishes() {
389 let limiter = Arc::new(SegmentBuildLimiter::new(3));
390 let first = limiter.try_acquire().unwrap();
391 let second = limiter.try_acquire().unwrap();
392 let waiter_limiter = Arc::clone(&limiter);
393 let (started_tx, started_rx) = std::sync::mpsc::channel();
394 let (acquired_tx, acquired_rx) = std::sync::mpsc::channel();
395
396 let waiter = std::thread::spawn(move || {
397 started_tx.send(()).unwrap();
398 let _permit = waiter_limiter
399 .reserve_if_due(800, 700, 800)
400 .expect("hard-budget worker must eventually acquire a build slot");
401 acquired_tx.send(()).unwrap();
402 });
403
404 started_rx.recv_timeout(Duration::from_secs(1)).unwrap();
405 assert!(
406 acquired_rx.recv_timeout(Duration::from_millis(50)).is_err(),
407 "hard-budget worker must not over-subscribe segment builds"
408 );
409 drop(first);
410 acquired_rx
411 .recv_timeout(Duration::from_secs(1))
412 .expect("hard-budget worker did not wake after a build completed");
413 waiter.join().unwrap();
414 drop(second);
415 }
416}
417
418enum WriterLock {
427 Held { _file: std::fs::File },
429 NotApplicable,
432 Unavailable { reason: String },
435}
436
437fn writer_lock_root<D: DirectoryWriter + 'static>(directory: &D) -> Option<std::path::PathBuf> {
440 let any: &dyn std::any::Any = directory;
441 if let Some(mmap) = any.downcast_ref::<crate::directories::MmapDirectory>() {
442 return Some(mmap.root().to_path_buf());
443 }
444 if any
448 .downcast_ref::<crate::directories::FsDirectory>()
449 .is_some()
450 {
451 log::warn!(
452 "[writer_lock] FsDirectory exposes no root path; single-writer locking \
453 is not enforced for this writer — do not open a second writer for the \
454 same index directory"
455 );
456 }
457 None
458}
459
460fn try_acquire_writer_lock<D: DirectoryWriter + 'static>(directory: &D) -> Result<WriterLock> {
465 let Some(root) = writer_lock_root(directory) else {
466 return Ok(WriterLock::NotApplicable);
467 };
468 std::fs::create_dir_all(&root)?;
469 let lock_path = root.join(WRITER_LOCK_FILENAME);
470 let file = std::fs::OpenOptions::new()
471 .create(true)
472 .truncate(false)
473 .write(true)
474 .open(&lock_path)?;
475 match file.try_lock() {
476 Ok(()) => Ok(WriterLock::Held { _file: file }),
477 Err(std::fs::TryLockError::WouldBlock) => Ok(WriterLock::Unavailable {
478 reason: format!(
479 "another IndexWriter already holds the single-writer lock for this \
480 index ({}); Hermes supports one writer per index directory — stop \
481 the other writer (e.g. a running hermes-server or hermes-tool) \
482 before opening this one",
483 lock_path.display()
484 ),
485 }),
486 Err(std::fs::TryLockError::Error(error)) => Err(Error::Io(error)),
487 }
488}
489
490pub struct IndexWriter<D: DirectoryWriter + 'static> {
504 pub(super) directory: Arc<D>,
505 pub(super) schema: Arc<Schema>,
506 pub(super) config: IndexConfig,
507 doc_sender: Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
510 workers: Vec<std::thread::JoinHandle<()>>,
512 worker_state: Arc<WorkerState<D>>,
514 pub(super) segment_manager: Arc<crate::merge::SegmentManager<D>>,
516 flushed_segments: Arc<parking_lot::Mutex<Vec<PreparedSegment<D>>>>,
519 primary_key_index: Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
521 primary_key_refresh_lock: Arc<tokio::sync::Mutex<()>>,
524 commit_finalization: Arc<CommitFinalizationState>,
528 pk_reservations_retained: Arc<AtomicBool>,
533 writer_lock: parking_lot::RwLock<WriterLock>,
538}
539
540#[derive(Default)]
541struct CommitFinalizationState {
542 in_progress: AtomicBool,
543 idle: tokio::sync::Notify,
544}
545
546impl CommitFinalizationState {
547 fn begin(&self) -> bool {
548 self.in_progress
549 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
550 .is_ok()
551 }
552
553 fn finish(&self) {
554 self.in_progress.store(false, Ordering::Release);
555 self.idle.notify_waiters();
556 }
557
558 async fn wait_until_idle(&self) {
559 while self.in_progress.load(Ordering::Acquire) {
560 let notified = self.idle.notified();
561 if !self.in_progress.load(Ordering::Acquire) {
562 break;
563 }
564 notified.await;
565 }
566 }
567}
568
569struct WorkerState<D: DirectoryWriter + 'static> {
571 directory: Arc<D>,
572 schema: Arc<Schema>,
573 builder_config: SegmentBuilderConfig,
574 tokenizers: parking_lot::RwLock<FxHashMap<Field, BoxedTokenizer>>,
575 memory_budget_per_worker: usize,
577 segment_build_limiter: SegmentBuildLimiter,
580 segment_manager: Arc<crate::merge::SegmentManager<D>>,
582 built_segments: parking_lot::Mutex<Vec<PreparedSegment<D>>>,
585 cycle_error: parking_lot::Mutex<Option<String>>,
590 cycle_failed: AtomicBool,
591
592 flush_count: AtomicUsize,
599 flush_mutex: parking_lot::Mutex<()>,
601 flush_cvar: parking_lot::Condvar,
602 resume_receiver: parking_lot::Mutex<Option<async_channel::Receiver<Document>>>,
604 resume_epoch: AtomicUsize,
607 resume_cvar: parking_lot::Condvar,
609 shutdown: AtomicBool,
611 num_workers: usize,
613}
614
615struct PreparedSegment<D: DirectoryWriter + 'static> {
621 id: String,
622 segment_id: SegmentId,
623 num_docs: u32,
624 segment_manager: Arc<crate::merge::SegmentManager<D>>,
625 operation: Option<crate::merge::SegmentOperationGuard>,
626 runtime: tokio::runtime::Handle,
627 needs_vector_upgrade: bool,
628 published: bool,
629}
630
631impl<D: DirectoryWriter + 'static> PreparedSegment<D> {
632 fn metadata_entry(&self) -> (String, u32) {
633 (self.id.clone(), self.num_docs)
634 }
635
636 fn mark_published(&mut self) {
637 self.published = true;
638 drop(self.operation.take());
640 }
641}
642
643impl<D: DirectoryWriter + 'static> WorkerState<D> {
644 fn record_cycle_error(&self, error: impl Into<String>) {
645 let mut first_error = self.cycle_error.lock();
646 if first_error.is_none() {
647 *first_error = Some(error.into());
648 }
649 drop(first_error);
650 self.cycle_failed.store(true, Ordering::Release);
651 }
652}
653
654impl<D: DirectoryWriter + 'static> Drop for PreparedSegment<D> {
655 fn drop(&mut self) {
656 if self.published {
657 return;
658 }
659 let Some(operation) = self.operation.take() else {
660 return;
661 };
662 self.segment_manager.schedule_unpublished_segment_cleanup(
663 self.segment_id,
664 operation,
665 self.runtime.clone(),
666 );
667 }
668}
669
670impl<D: DirectoryWriter + 'static> IndexWriter<D> {
671 pub async fn create(directory: D, schema: Schema, config: IndexConfig) -> Result<Self> {
673 let builder_config = default_builder_config(&config);
674 Self::create_with_config(directory, schema, config, builder_config).await
675 }
676
677 pub async fn create_with_config(
679 directory: D,
680 schema: Schema,
681 config: IndexConfig,
682 builder_config: SegmentBuilderConfig,
683 ) -> Result<Self> {
684 crate::dsl::reject_removed_vector_index_types(&schema).map_err(Error::Schema)?;
685 let directory = Arc::new(directory);
686 let schema = Arc::new(schema);
687 directory.set_index_label(schema.index_label());
689
690 let writer_lock = try_acquire_writer_lock(directory.as_ref())?;
692 if let WriterLock::Unavailable { reason } = &writer_lock {
693 return Err(Error::Internal(reason.clone()));
694 }
695 if directory
699 .exists(std::path::Path::new(super::INDEX_META_FILENAME))
700 .await?
701 {
702 return Err(Error::Internal(format!(
703 "refusing to create index: {} already exists in this directory; \
704 use IndexWriter::open to open the existing index, or delete the \
705 directory first if you really want to start over",
706 super::INDEX_META_FILENAME
707 )));
708 }
709
710 let metadata = super::IndexMetadata::new((*schema).clone());
711
712 let segment_manager =
713 super::segment_manager_from_config(&directory, &schema, metadata, &config);
714 segment_manager.update_metadata(|_| {}).await?;
715
716 Ok(Self::new_with_parts(
717 directory,
718 schema,
719 config,
720 builder_config,
721 segment_manager,
722 writer_lock,
723 ))
724 }
725
726 pub async fn open(directory: D, config: IndexConfig) -> Result<Self> {
735 let builder_config = default_builder_config(&config);
736 Self::open_with_config(directory, config, builder_config).await
737 }
738
739 pub async fn open_with_config(
741 directory: D,
742 config: IndexConfig,
743 builder_config: SegmentBuilderConfig,
744 ) -> Result<Self> {
745 let directory = Arc::new(directory);
746
747 let writer_lock = try_acquire_writer_lock(directory.as_ref())?;
750 if let WriterLock::Unavailable { reason } = &writer_lock {
751 return Err(Error::Internal(reason.clone()));
752 }
753
754 let metadata = super::IndexMetadata::load(directory.as_ref()).await?;
755 let schema = Arc::new(metadata.schema.clone());
756 directory.set_index_label(schema.index_label());
758
759 let segment_manager =
760 super::segment_manager_from_config(&directory, &schema, metadata, &config);
761 let swept = segment_manager.cleanup_orphan_segments().await?;
762 if swept > 0 {
763 log::warn!(
764 "[segment_cleanup] swept {} orphan segment(s) while opening writer",
765 swept
766 );
767 }
768 segment_manager.try_load_and_publish_trained().await?;
769
770 Ok(Self::new_with_parts(
771 directory,
772 schema,
773 config,
774 builder_config,
775 segment_manager,
776 writer_lock,
777 ))
778 }
779
780 pub fn from_index(index: &super::Index<D>) -> Self {
787 let writer_lock = match try_acquire_writer_lock(index.directory.as_ref()) {
788 Ok(lock) => lock,
789 Err(error) => WriterLock::Unavailable {
790 reason: format!("failed to acquire the single-writer lock: {error}"),
791 },
792 };
793 if let WriterLock::Unavailable { reason } = &writer_lock {
794 log::error!("[writer_lock] {reason}");
795 }
796 let builder_config = default_builder_config(&index.config);
797 Self::new_with_parts(
798 Arc::clone(&index.directory),
799 index.schema_arc(),
800 index.config.clone(),
801 builder_config,
802 Arc::clone(&index.segment_manager),
803 writer_lock,
804 )
805 }
806
807 fn new_with_parts(
813 directory: Arc<D>,
814 schema: Arc<Schema>,
815 config: IndexConfig,
816 builder_config: SegmentBuilderConfig,
817 segment_manager: Arc<crate::merge::SegmentManager<D>>,
818 writer_lock: WriterLock,
819 ) -> Self {
820 let registry = crate::tokenizer::TokenizerRegistry::new();
822 let mut tokenizers = FxHashMap::default();
823 for (field, entry) in schema.fields() {
824 if matches!(entry.field_type, crate::dsl::FieldType::Text)
825 && let Some(ref tok_name) = entry.tokenizer
826 && let Some(tok) = registry.get(tok_name)
827 {
828 tokenizers.insert(field, tok);
829 }
830 }
831
832 let num_workers = config.num_indexing_threads.max(1);
833 let worker_state = Arc::new(WorkerState {
834 directory: Arc::clone(&directory),
835 schema: Arc::clone(&schema),
836 builder_config,
837 tokenizers: parking_lot::RwLock::new(tokenizers),
838 memory_budget_per_worker: config.max_indexing_memory_bytes / num_workers,
839 segment_build_limiter: SegmentBuildLimiter::new(num_workers),
840 segment_manager: Arc::clone(&segment_manager),
841 built_segments: parking_lot::Mutex::new(Vec::new()),
842 cycle_error: parking_lot::Mutex::new(None),
843 cycle_failed: AtomicBool::new(false),
844 flush_count: AtomicUsize::new(0),
845 flush_mutex: parking_lot::Mutex::new(()),
846 flush_cvar: parking_lot::Condvar::new(),
847 resume_receiver: parking_lot::Mutex::new(None),
848 resume_epoch: AtomicUsize::new(0),
849 resume_cvar: parking_lot::Condvar::new(),
850 shutdown: AtomicBool::new(false),
851 num_workers,
852 });
853 let (doc_sender, workers) = Self::spawn_workers(&worker_state, num_workers);
854 let primary_key_index = Arc::new(parking_lot::RwLock::new(None));
855 let primary_key_refresh_lock = Arc::new(tokio::sync::Mutex::new(()));
856
857 Self {
858 directory,
859 schema,
860 config,
861 doc_sender: Arc::new(parking_lot::RwLock::new(doc_sender)),
862 workers,
863 worker_state,
864 segment_manager,
865 flushed_segments: Arc::new(parking_lot::Mutex::new(Vec::new())),
866 primary_key_index,
867 primary_key_refresh_lock,
868 commit_finalization: Arc::new(CommitFinalizationState::default()),
869 pk_reservations_retained: Arc::new(AtomicBool::new(false)),
870 writer_lock: parking_lot::RwLock::new(writer_lock),
871 }
872 }
873
874 fn ensure_writer_lock(&self) -> Result<()> {
882 if !matches!(&*self.writer_lock.read(), WriterLock::Unavailable { .. }) {
884 return Ok(());
885 }
886
887 let mut lock = self.writer_lock.write();
888 if !matches!(&*lock, WriterLock::Unavailable { .. }) {
890 return Ok(());
891 }
892 match try_acquire_writer_lock(self.directory.as_ref())? {
893 acquired @ (WriterLock::Held { .. } | WriterLock::NotApplicable) => {
894 log::info!(
895 "[writer_lock] index={} single-writer lock acquired after retry; \
896 the previous holder has released it — resuming writes",
897 self.schema.index_label()
898 );
899 *lock = acquired;
900 Ok(())
901 }
902 WriterLock::Unavailable { reason } => {
903 let err = Error::Internal(reason.clone());
904 *lock = WriterLock::Unavailable { reason };
905 Err(err)
906 }
907 }
908 }
909
910 fn clear_uncommitted_pk_reservations(&self) {
919 if self.pk_reservations_retained.load(Ordering::Acquire) {
920 log::warn!(
921 "[primary_key] index={} keeping uncommitted reservations through abort: a \
922 failed post-commit refresh left them as the only record of \
923 committed keys; they are cleared by the next successful commit",
924 self.schema.index_label()
925 );
926 return;
927 }
928 if let Some(pk_index) = self.primary_key_index.write().as_mut() {
929 pk_index.clear_uncommitted();
930 }
931 }
932
933 fn spawn_workers(
934 worker_state: &Arc<WorkerState<D>>,
935 num_workers: usize,
936 ) -> (
937 async_channel::Sender<Document>,
938 Vec<std::thread::JoinHandle<()>>,
939 ) {
940 let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
941 let handle = tokio::runtime::Handle::current();
942 let mut workers = Vec::with_capacity(num_workers);
943 for i in 0..num_workers {
944 let state = Arc::clone(worker_state);
945 let rx = receiver.clone();
946 let rt = handle.clone();
947 workers.push(
948 std::thread::Builder::new()
949 .name(format!("index-worker-{}", i))
950 .spawn(move || Self::worker_loop(state, rx, rt, i))
951 .expect("failed to spawn index worker thread"),
952 );
953 }
954 (sender, workers)
955 }
956
957 pub fn schema(&self) -> Arc<Schema> {
959 self.segment_manager.published_generation().schema.clone()
960 }
961
962 pub fn set_tokenizer<T: crate::tokenizer::Tokenizer>(&mut self, field: Field, tokenizer: T) {
965 self.worker_state
966 .tokenizers
967 .write()
968 .insert(field, Box::new(tokenizer));
969 }
970
971 pub async fn init_primary_key_dedup(&mut self) -> Result<()> {
987 use super::primary_key::{PK_BLOOM_FILE, deserialize_pk_bloom};
988
989 self.commit_finalization.wait_until_idle().await;
990 self.ensure_writer_lock()?;
991
992 let field = match self.schema.primary_field() {
993 Some(f) => f,
994 None => return Ok(()),
995 };
996
997 let _refresh_guard = self.primary_key_refresh_lock.lock().await;
1002 {
1003 let callback_directory = Arc::clone(&self.directory);
1004 let callback_schema = Arc::clone(&self.schema);
1005 let callback_manager = Arc::downgrade(&self.segment_manager);
1006 let callback_primary_key = Arc::downgrade(&self.primary_key_index);
1007 let callback_refresh_lock = Arc::downgrade(&self.primary_key_refresh_lock);
1008 self.segment_manager.set_replacement_refresh(move || {
1009 let directory = Arc::clone(&callback_directory);
1010 let schema = Arc::clone(&callback_schema);
1011 let manager = callback_manager.clone();
1012 let primary_key = callback_primary_key.clone();
1013 let refresh_lock = callback_refresh_lock.clone();
1014 async move {
1015 let (Some(manager), Some(primary_key), Some(refresh_lock)) = (
1016 manager.upgrade(),
1017 primary_key.upgrade(),
1018 refresh_lock.upgrade(),
1019 ) else {
1020 return Ok(());
1021 };
1022 refresh_primary_key_snapshot(
1023 &directory,
1024 &schema,
1025 &manager,
1026 &primary_key,
1027 &refresh_lock,
1028 PrimaryKeyRefresh::Replacement,
1029 )
1030 .await
1031 }
1032 });
1033 }
1034
1035 let snapshot = self.segment_manager.acquire_snapshot().await;
1036 let current_seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
1037
1038 let cached = match self
1040 .directory
1041 .open_read(std::path::Path::new(PK_BLOOM_FILE))
1042 .await
1043 {
1044 Ok(handle) => {
1045 let data = handle.read_bytes_range(0..handle.len()).await;
1046 match data {
1047 Ok(bytes) => deserialize_pk_bloom(bytes.as_slice()),
1048 Err(_) => None,
1049 }
1050 }
1051 Err(_) => None,
1052 };
1053
1054 let load_futures: Vec<_> = current_seg_ids
1056 .iter()
1057 .map(|seg_id_str| {
1058 let seg_id_str = seg_id_str.clone();
1059 let dir = self.directory.as_ref();
1060 let schema = Arc::clone(&self.schema);
1061 async move { load_pk_segment_data(dir, &seg_id_str, &schema).await }
1062 })
1063 .collect();
1064 let all_data = futures::future::try_join_all(load_futures).await?;
1065
1066 if let Some((persisted_seg_ids, bloom)) = cached {
1067 let mut pk_data = Vec::with_capacity(all_data.len());
1069 let mut new_data = Vec::new();
1070 for d in all_data {
1071 if persisted_seg_ids.contains(&d.segment_id) {
1072 pk_data.push(d);
1073 } else {
1074 new_data.push(d);
1075 }
1076 }
1077 let needs_persist = !new_data.is_empty();
1078 let new_start = pk_data.len();
1079 pk_data.extend(new_data);
1080
1081 let pk_index = if new_start == pk_data.len() {
1082 super::primary_key::PrimaryKeyIndex::from_persisted(field, bloom, pk_data, snapshot)
1084 } else {
1085 let index_label = self.schema.index_label().to_owned();
1087 tokio::task::spawn_blocking(move || {
1088 let mut bloom = bloom;
1091 let mut added = 0usize;
1092 let num_new = pk_data.len() - new_start;
1093 for data in &pk_data[new_start..] {
1094 if let Some(ff) = data.fast_fields.get(&field.0)
1095 && let Some(dict) = ff.text_dict()
1096 {
1097 for key in dict.iter() {
1098 bloom.insert(key.as_bytes());
1099 added += 1;
1100 }
1101 }
1102 }
1103 if added > 0 {
1104 log::info!(
1105 "[primary_key] index={index_label} bloom: added {} keys from {} new segment(s)",
1106 added,
1107 num_new,
1108 );
1109 }
1110 super::primary_key::PrimaryKeyIndex::from_persisted(
1111 field, bloom, pk_data, snapshot,
1112 )
1113 })
1114 .await
1115 .map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?
1116 };
1117
1118 if needs_persist {
1119 self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
1120 }
1121
1122 *self.primary_key_index.write() = Some(pk_index);
1123 } else {
1124 let pk_index = tokio::task::spawn_blocking(move || {
1126 super::primary_key::PrimaryKeyIndex::new(field, all_data, snapshot)
1127 })
1128 .await
1129 .map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?;
1130
1131 self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
1132 *self.primary_key_index.write() = Some(pk_index);
1133 }
1134
1135 self.pk_reservations_retained
1139 .store(false, Ordering::Release);
1140
1141 Ok(())
1142 }
1143
1144 async fn persist_pk_bloom(
1147 &self,
1148 pk_index: &super::primary_key::PrimaryKeyIndex,
1149 segment_ids: &[String],
1150 ) {
1151 use super::primary_key::PK_BLOOM_FILE;
1152
1153 let writer = match self
1154 .directory
1155 .streaming_writer(std::path::Path::new(PK_BLOOM_FILE))
1156 .await
1157 {
1158 Ok(writer) => writer,
1159 Err(error) => {
1160 log::warn!(
1161 "[primary_key] index={} failed to open bloom cache: {}",
1162 self.schema.index_label(),
1163 error
1164 );
1165 return;
1166 }
1167 };
1168 let result = crate::segment::block_in_place_if_multithread(|| {
1169 write_pk_bloom_stream(pk_index, segment_ids, writer)
1170 });
1171 if let Err(e) = result {
1172 log::warn!(
1173 "[primary_key] index={} failed to persist bloom cache: {}",
1174 self.schema.index_label(),
1175 e
1176 );
1177 }
1178 }
1179
1180 pub fn add_document(&self, doc: Document) -> Result<()> {
1186 self.ensure_writer_lock()?;
1187 if self.worker_state.shutdown.load(Ordering::Acquire) {
1188 return Err(Error::IndexClosed);
1189 }
1190 if self.commit_finalization.in_progress.load(Ordering::Acquire) {
1191 return Err(Error::CommitInProgress);
1192 }
1193 let sender = self.doc_sender.read().clone();
1194 if sender.is_closed() {
1198 return Err(Error::CommitInProgress);
1199 }
1200 validate_vector_value_counts(&doc, &self.schema)?;
1204 let primary_key_index = self.primary_key_index.read();
1205 if let Some(ref pk_index) = *primary_key_index {
1206 pk_index.check_and_insert(&doc)?;
1207 }
1208 match sender.try_send(doc) {
1209 Ok(()) => Ok(()),
1210 Err(async_channel::TrySendError::Full(doc)) => {
1211 if let Some(ref pk_index) = *primary_key_index {
1213 pk_index.rollback_uncommitted_key(&doc);
1214 }
1215 Err(Error::QueueFull)
1216 }
1217 Err(async_channel::TrySendError::Closed(doc)) => {
1218 if let Some(ref pk_index) = *primary_key_index {
1220 pk_index.rollback_uncommitted_key(&doc);
1221 }
1222 Err(Error::CommitInProgress)
1223 }
1224 }
1225 }
1226
1227 pub fn add_documents(&self, documents: Vec<Document>) -> Result<usize> {
1232 let total = documents.len();
1233 for (i, doc) in documents.into_iter().enumerate() {
1234 match self.add_document(doc) {
1235 Ok(()) => {}
1236 Err(Error::QueueFull | Error::CommitInProgress) => return Ok(i),
1237 Err(e) => return Err(e),
1238 }
1239 }
1240 Ok(total)
1241 }
1242
1243 fn worker_loop(
1256 state: Arc<WorkerState<D>>,
1257 initial_receiver: async_channel::Receiver<Document>,
1258 handle: tokio::runtime::Handle,
1259 worker_id: usize,
1260 ) {
1261 let mut receiver = initial_receiver;
1262 let mut my_epoch = 0usize;
1263 let soft_flush_threshold =
1264 soft_flush_threshold(state.memory_budget_per_worker, worker_id, state.num_workers);
1265 let hard_flush_threshold = hard_flush_threshold(state.memory_budget_per_worker);
1266
1267 loop {
1268 let build_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1272 let mut builder: Option<SegmentBuilder> = None;
1273
1274 while let Ok(doc) = receiver.recv_blocking() {
1275 if state.shutdown.load(Ordering::Acquire) {
1276 break;
1277 }
1278 if state.cycle_failed.load(Ordering::Acquire) {
1283 continue;
1284 }
1285 if builder.is_none() {
1287 match SegmentBuilder::new(
1288 state.segment_manager.published_generation().schema.clone(),
1289 state.builder_config.clone(),
1290 ) {
1291 Ok(mut b) => {
1292 for (field, tokenizer) in state.tokenizers.read().iter() {
1293 b.set_tokenizer(*field, tokenizer.clone_box());
1294 }
1295 builder = Some(b);
1296 }
1297 Err(e) => {
1298 log::error!("Failed to create segment builder: {:?}", e);
1299 state.record_cycle_error(format!(
1300 "failed to create segment builder: {e}"
1301 ));
1302 continue;
1303 }
1304 }
1305 }
1306
1307 let b = builder.as_mut().unwrap();
1308 if let Err(e) = b.add_document(doc) {
1309 log::error!("Failed to index document: {:?}", e);
1310 state.record_cycle_error(format!("failed to index document: {e}"));
1311 continue;
1312 }
1313
1314 let builder_memory = b.estimated_memory_bytes();
1315
1316 if b.num_docs() & 0x3FFF == 0 {
1317 log::debug!(
1318 "[indexing] index={} docs={}, memory={}, budget={}",
1319 state.schema.index_label(),
1320 b.num_docs(),
1321 crate::format_bytes(builder_memory as u64),
1322 crate::format_bytes(state.memory_budget_per_worker as u64)
1323 );
1324 }
1325
1326 const MIN_DOCS_BEFORE_FLUSH: u32 = 100;
1328
1329 if b.num_docs() >= MIN_DOCS_BEFORE_FLUSH
1330 && let Some(_build_permit) = state.segment_build_limiter.reserve_if_due(
1331 builder_memory,
1332 soft_flush_threshold,
1333 hard_flush_threshold,
1334 )
1335 {
1336 log::info!(
1337 "[indexing] index={} memory budget reached, building segment: \
1338 worker={}, docs={}, memory={}, soft_budget={}, hard_budget={}",
1339 state.schema.index_label(),
1340 worker_id,
1341 b.num_docs(),
1342 crate::format_bytes(builder_memory as u64),
1343 crate::format_bytes(soft_flush_threshold as u64),
1344 crate::format_bytes(hard_flush_threshold as u64),
1345 );
1346 let full_builder = builder.take().unwrap();
1347 Self::build_segment_inline(&state, full_builder, &handle);
1348 }
1349 }
1350
1351 if !state.cycle_failed.load(Ordering::Acquire)
1353 && let Some(b) = builder.take()
1354 && b.num_docs() > 0
1355 {
1356 let _build_permit = state.segment_build_limiter.acquire_flush();
1357 Self::build_segment_inline(&state, b, &handle);
1358 }
1359 }));
1360
1361 if build_result.is_err() {
1362 log::error!(
1363 "[worker] index={} panic during indexing cycle — documents in this cycle may be lost",
1364 state.schema.index_label()
1365 );
1366 state.record_cycle_error("indexing worker panicked while building the batch");
1367 }
1368
1369 let prev = state.flush_count.fetch_add(1, Ordering::Release);
1372 if prev + 1 == state.num_workers {
1373 let _lock = state.flush_mutex.lock();
1380 state.flush_cvar.notify_all();
1381 }
1382
1383 {
1387 let mut lock = state.resume_receiver.lock();
1388 loop {
1389 if state.shutdown.load(Ordering::Acquire) {
1390 return;
1391 }
1392 let current_epoch = state.resume_epoch.load(Ordering::Acquire);
1393 if current_epoch > my_epoch
1394 && let Some(rx) = lock.as_ref()
1395 {
1396 receiver = rx.clone();
1397 my_epoch = current_epoch;
1398 break;
1399 }
1400 state.resume_cvar.wait(&mut lock);
1401 }
1402 }
1403 }
1404 }
1405
1406 fn build_segment_inline(
1410 state: &WorkerState<D>,
1411 builder: SegmentBuilder,
1412 handle: &tokio::runtime::Handle,
1413 ) {
1414 let segment_id = SegmentId::new();
1415 let segment_hex = segment_id.to_hex();
1416 let operation = match state
1419 .segment_manager
1420 .protect_new_segment(segment_hex.clone())
1421 {
1422 Ok(operation) => operation,
1423 Err(e) => {
1424 log::error!(
1425 "[segment_build_failed] index={} segment_id={} lifecycle_error={}",
1426 state.schema.index_label(),
1427 segment_hex,
1428 e,
1429 );
1430 state.record_cycle_error(format!(
1431 "failed to claim segment {segment_hex} for building: {e}"
1432 ));
1433 return;
1434 }
1435 };
1436 let trained = state.segment_manager.trained_for_segment_build();
1437 let doc_count = builder.num_docs();
1438 let build_start = std::time::Instant::now();
1439
1440 log::info!(
1441 "[segment_build] index={} segment_id={} doc_count={} ann={}",
1442 state.schema.index_label(),
1443 segment_hex,
1444 doc_count,
1445 trained.is_some()
1446 );
1447
1448 let mut prepared = PreparedSegment {
1452 id: segment_hex.clone(),
1453 segment_id,
1454 num_docs: doc_count,
1455 segment_manager: Arc::clone(&state.segment_manager),
1456 operation: Some(operation),
1457 runtime: handle.clone(),
1458 needs_vector_upgrade: trained.is_none(),
1459 published: false,
1460 };
1461
1462 match handle.block_on(builder.build(
1463 state.directory.as_ref(),
1464 segment_id,
1465 trained.as_deref(),
1466 )) {
1467 Ok(meta) if meta.num_docs == doc_count && meta.num_docs > 0 => {
1468 let duration_ms = build_start.elapsed().as_millis() as u64;
1469 log::info!(
1470 "[segment_build_done] index={} segment_id={} doc_count={} duration_ms={}",
1471 state.schema.index_label(),
1472 segment_hex,
1473 meta.num_docs,
1474 duration_ms,
1475 );
1476 prepared.num_docs = meta.num_docs;
1477 state.built_segments.lock().push(prepared);
1478 }
1479 Ok(meta) => {
1480 let error = format!(
1481 "segment {segment_hex} built {} docs from a {doc_count}-document builder",
1482 meta.num_docs
1483 );
1484 log::error!(
1485 "[segment_build_failed] index={} {error}",
1486 state.schema.index_label()
1487 );
1488 state.record_cycle_error(error);
1489 }
1490 Err(e) => {
1491 log::error!(
1492 "[segment_build_failed] index={} segment_id={} error={:?}",
1493 state.schema.index_label(),
1494 segment_hex,
1495 e
1496 );
1497 state.record_cycle_error(format!("failed to build segment {segment_hex}: {e}"));
1500 }
1501 }
1502 }
1503
1504 pub async fn maybe_merge(&self) {
1510 self.segment_manager.maybe_merge().await;
1511 }
1512
1513 pub async fn abort_merges(&self) {
1516 self.segment_manager.abort_merges().await;
1517 }
1518
1519 pub async fn shutdown(&mut self) -> Result<()> {
1524 self.segment_manager.begin_shutdown();
1525 self.signal_worker_shutdown();
1526
1527 self.commit_finalization.wait_until_idle().await;
1532
1533 let workers = std::mem::take(&mut self.workers);
1534 let panicked = tokio::task::spawn_blocking(move || {
1535 workers
1536 .into_iter()
1537 .map(|worker| worker.join().is_err())
1538 .filter(|panicked| *panicked)
1539 .count()
1540 })
1541 .await
1542 .map_err(|error| Error::Internal(format!("failed to join index workers: {}", error)))?;
1543 if panicked > 0 {
1544 log::error!(
1545 "[index_shutdown] index={} {} indexing worker(s) panicked",
1546 self.schema.index_label(),
1547 panicked
1548 );
1549 }
1550
1551 self.flushed_segments.lock().clear();
1554 self.worker_state.built_segments.lock().clear();
1555 if let Some(pk_index) = self.primary_key_index.write().as_mut() {
1556 pk_index.clear_uncommitted();
1557 }
1558 Ok(())
1559 }
1560
1561 pub async fn wait_for_merging_thread(&self) {
1563 self.segment_manager.wait_for_merging_thread().await;
1564 }
1565
1566 pub async fn wait_for_all_merges(&self) {
1568 self.segment_manager.wait_for_all_merges().await;
1569 }
1570
1571 pub async fn wait_for_commit_finalization(&self) {
1576 self.commit_finalization.wait_until_idle().await;
1577 }
1578
1579 pub fn tracker(&self) -> std::sync::Arc<crate::segment::SegmentTracker> {
1581 self.segment_manager.tracker()
1582 }
1583
1584 pub async fn acquire_snapshot(&self) -> crate::segment::SegmentSnapshot {
1586 self.segment_manager.acquire_snapshot().await
1587 }
1588
1589 pub async fn cleanup_orphan_segments(&self) -> Result<usize> {
1594 self.ensure_writer_lock()?;
1595 self.segment_manager.cleanup_orphan_segments().await
1596 }
1597
1598 pub async fn prepare_commit(&mut self) -> Result<PreparedCommit<'_, D>> {
1609 self.ensure_writer_lock()?;
1610 if self.worker_state.shutdown.load(Ordering::Acquire) {
1611 return Err(Error::IndexClosed);
1612 }
1613 if self.commit_finalization.in_progress.load(Ordering::Acquire) {
1614 return Err(Error::CommitInProgress);
1615 }
1616 self.doc_sender.read().close();
1618 self.worker_state.segment_build_limiter.begin_flush();
1619
1620 self.worker_state.resume_cvar.notify_all();
1624
1625 let state = Arc::clone(&self.worker_state);
1628 let index_label = self.schema.index_label().to_owned();
1629 let all_flushed = tokio::task::spawn_blocking(move || {
1630 let mut lock = state.flush_mutex.lock();
1631 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(300);
1632 while state.flush_count.load(Ordering::Acquire) < state.num_workers {
1633 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
1634 if remaining.is_zero() {
1635 log::error!(
1636 "[prepare_commit] index={index_label} timed out waiting for workers: {}/{} flushed",
1637 state.flush_count.load(Ordering::Acquire),
1638 state.num_workers
1639 );
1640 return false;
1641 }
1642 state.flush_cvar.wait_for(&mut lock, remaining);
1643 }
1644 true
1645 })
1646 .await
1647 .map_err(|e| Error::Internal(format!("Failed to wait for workers: {}", e)))?;
1648
1649 if !all_flushed {
1650 return Err(Error::Internal(format!(
1658 "prepare_commit timed out: {}/{} workers flushed; writer remains paused, retry commit",
1659 self.worker_state.flush_count.load(Ordering::Acquire),
1660 self.worker_state.num_workers
1661 )));
1662 }
1663
1664 let cycle_error = { self.worker_state.cycle_error.lock().take() };
1665 if let Some(error) = cycle_error {
1666 self.flushed_segments.lock().clear();
1671 self.worker_state.built_segments.lock().clear();
1672 self.clear_uncommitted_pk_reservations();
1673 self.resume_workers();
1674 return Err(Error::Internal(format!(
1675 "indexing generation failed; no documents from this batch were committed: {error}"
1676 )));
1677 }
1678
1679 let built = std::mem::take(&mut *self.worker_state.built_segments.lock());
1681 self.flushed_segments.lock().extend(built);
1682
1683 Ok(PreparedCommit {
1684 writer: self,
1685 is_resolved: false,
1686 })
1687 }
1688
1689 pub async fn commit(&mut self) -> Result<bool> {
1694 self.prepare_commit().await?.commit().await
1695 }
1696
1697 pub async fn force_merge(&mut self) -> Result<()> {
1699 self.force_merge_with_snapshot_refresh(|| std::future::ready(Ok(())))
1700 .await
1701 }
1702
1703 pub async fn force_merge_with_snapshot_refresh<F, Fut>(
1710 &mut self,
1711 refresh_external: F,
1712 ) -> Result<()>
1713 where
1714 F: FnMut() -> Fut,
1715 Fut: std::future::Future<Output = Result<()>>,
1716 {
1717 self.prepare_commit().await?.commit().await?;
1718
1719 self.segment_manager
1720 .force_merge_with_snapshot_refresh(refresh_external)
1721 .await?;
1722
1723 self.persist_replacement_snapshot().await
1727 }
1728
1729 pub async fn reorder(&mut self) -> Result<()> {
1734 self.reorder_with_snapshot_refresh(|| std::future::ready(Ok(())))
1735 .await
1736 }
1737
1738 pub async fn reorder_with_snapshot_refresh<F, Fut>(&mut self, refresh_external: F) -> Result<()>
1741 where
1742 F: FnMut() -> Fut,
1743 Fut: std::future::Future<Output = Result<()>>,
1744 {
1745 self.prepare_commit().await?.commit().await?;
1746
1747 self.segment_manager
1748 .reorder_segments_with_snapshot_refresh(refresh_external)
1749 .await?;
1750 self.persist_replacement_snapshot().await
1751 }
1752
1753 async fn persist_replacement_snapshot(&self) -> Result<()> {
1755 refresh_primary_key_snapshot(
1756 &self.directory,
1757 &self.schema,
1758 &self.segment_manager,
1759 &self.primary_key_index,
1760 &self.primary_key_refresh_lock,
1761 PrimaryKeyRefresh::FinalReplacement,
1762 )
1763 .await
1764 }
1765
1766 pub fn segment_manager(&self) -> &Arc<crate::merge::SegmentManager<D>> {
1768 &self.segment_manager
1769 }
1770
1771 fn resume_workers(&mut self) {
1776 Self::resume_workers_shared(&self.worker_state, &self.doc_sender);
1777 }
1778
1779 fn resume_workers_shared(
1780 worker_state: &Arc<WorkerState<D>>,
1781 doc_sender: &Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
1782 ) {
1783 if worker_state.shutdown.load(Ordering::Acquire) {
1784 return;
1785 }
1786 if tokio::runtime::Handle::try_current().is_err() {
1787 worker_state.shutdown.store(true, Ordering::Release);
1790 worker_state.resume_cvar.notify_all();
1791 return;
1792 }
1793
1794 worker_state.segment_build_limiter.end_flush();
1796 worker_state.flush_count.store(0, Ordering::Release);
1797 *worker_state.cycle_error.lock() = None;
1798 worker_state.cycle_failed.store(false, Ordering::Release);
1799
1800 let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
1802 *doc_sender.write() = sender;
1803
1804 {
1806 let mut lock = worker_state.resume_receiver.lock();
1807 *lock = Some(receiver);
1808 }
1809 worker_state.resume_epoch.fetch_add(1, Ordering::Release);
1810 worker_state.resume_cvar.notify_all();
1811 }
1812
1813 fn signal_worker_shutdown(&self) {
1814 self.worker_state.shutdown.store(true, Ordering::Release);
1815 self.doc_sender.read().close();
1816 self.worker_state.segment_build_limiter.begin_flush();
1817 self.worker_state.resume_cvar.notify_all();
1818 }
1819
1820 }
1822
1823impl<D: DirectoryWriter + 'static> Drop for IndexWriter<D> {
1824 fn drop(&mut self) {
1825 self.signal_worker_shutdown();
1826 for w in std::mem::take(&mut self.workers) {
1827 let _ = w.join();
1828 }
1829 }
1830}
1831
1832pub struct PreparedCommit<'a, D: DirectoryWriter + 'static> {
1839 writer: &'a mut IndexWriter<D>,
1840 is_resolved: bool,
1841}
1842
1843struct PreparedSegmentsGuard<D: DirectoryWriter + 'static> {
1848 segments: Option<Vec<PreparedSegment<D>>>,
1849 retry_slot: Arc<parking_lot::Mutex<Vec<PreparedSegment<D>>>>,
1850}
1851
1852impl<D: DirectoryWriter + 'static> PreparedSegmentsGuard<D> {
1853 fn metadata_entries(&self) -> Vec<(String, u32)> {
1854 self.segments
1855 .as_deref()
1856 .unwrap_or_default()
1857 .iter()
1858 .map(PreparedSegment::metadata_entry)
1859 .collect()
1860 }
1861
1862 fn take_published(&mut self) -> Vec<PreparedSegment<D>> {
1863 self.segments.take().unwrap_or_default()
1864 }
1865
1866 fn vector_upgrade_segment_ids(&self) -> Vec<String> {
1867 self.segments
1868 .as_deref()
1869 .unwrap_or_default()
1870 .iter()
1871 .filter(|segment| segment.needs_vector_upgrade)
1872 .map(|segment| segment.id.clone())
1873 .collect()
1874 }
1875}
1876
1877impl<D: DirectoryWriter + 'static> Drop for PreparedSegmentsGuard<D> {
1878 fn drop(&mut self) {
1879 if let Some(segments) = self.segments.take() {
1880 self.retry_slot.lock().extend(segments);
1881 }
1882 }
1883}
1884
1885struct CommitFinalizationGuard<D: DirectoryWriter + 'static> {
1890 state: Arc<CommitFinalizationState>,
1891 worker_state: Arc<WorkerState<D>>,
1892 doc_sender: Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
1893 resume_workers: bool,
1894}
1895
1896impl<D: DirectoryWriter + 'static> CommitFinalizationGuard<D> {
1897 fn resume_on_drop(&mut self) {
1898 self.resume_workers = true;
1899 }
1900}
1901
1902impl<D: DirectoryWriter + 'static> Drop for CommitFinalizationGuard<D> {
1903 fn drop(&mut self) {
1904 if self.resume_workers {
1905 IndexWriter::<D>::resume_workers_shared(&self.worker_state, &self.doc_sender);
1906 }
1907 self.state.finish();
1908 }
1909}
1910
1911struct OwnedCommitFinalization<D: DirectoryWriter + 'static> {
1916 directory: Arc<D>,
1917 schema: Arc<Schema>,
1918 segment_manager: Arc<crate::merge::SegmentManager<D>>,
1919 primary_key_index: Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
1920 primary_key_refresh_lock: Arc<tokio::sync::Mutex<()>>,
1921 prepared: PreparedSegmentsGuard<D>,
1922 finalization: Option<CommitFinalizationGuard<D>>,
1923 publication_observed: Arc<AtomicBool>,
1924 pk_reservations_retained: Arc<AtomicBool>,
1925}
1926
1927#[derive(Clone, Copy)]
1928enum PrimaryKeyRefresh {
1929 Commit,
1931 Replacement,
1934 FinalReplacement,
1937}
1938
1939async fn refresh_primary_key_snapshot<D: DirectoryWriter + 'static>(
1940 directory: &Arc<D>,
1941 schema: &Arc<Schema>,
1942 segment_manager: &Arc<crate::merge::SegmentManager<D>>,
1943 primary_key_index: &Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
1944 primary_key_refresh_lock: &Arc<tokio::sync::Mutex<()>>,
1945 refresh: PrimaryKeyRefresh,
1946) -> Result<()> {
1947 let _refresh_guard = primary_key_refresh_lock.lock().await;
1948 let existing_ids: std::collections::HashSet<String> = {
1949 let guard = primary_key_index.read();
1950 let Some(pk_index) = guard.as_ref() else {
1951 return Ok(());
1952 };
1953 pk_index
1954 .committed_segment_ids()
1955 .map(ToOwned::to_owned)
1956 .collect()
1957 };
1958
1959 let snapshot = segment_manager.acquire_snapshot().await;
1960 let load_futures: Vec<_> = snapshot
1961 .segment_ids()
1962 .iter()
1963 .filter(|id| !existing_ids.contains(id.as_str()))
1964 .map(|seg_id_str| {
1965 let seg_id_str = seg_id_str.clone();
1966 let dir = directory.as_ref();
1967 let schema = Arc::clone(schema);
1968 async move { load_pk_segment_data(dir, &seg_id_str, &schema).await }
1969 })
1970 .collect();
1971 let new_data = futures::future::try_join_all(load_futures).await?;
1972 let seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
1973
1974 let persist_bloom = {
1975 let mut guard = primary_key_index.write();
1976 let Some(pk_index) = guard.as_mut() else {
1977 return Ok(());
1978 };
1979 match refresh {
1980 PrimaryKeyRefresh::Commit => pk_index.refresh_incremental(new_data, snapshot),
1981 PrimaryKeyRefresh::Replacement | PrimaryKeyRefresh::FinalReplacement => {
1982 pk_index.refresh_replacement(new_data, snapshot);
1983 }
1984 }
1985 matches!(
1986 refresh,
1987 PrimaryKeyRefresh::Commit | PrimaryKeyRefresh::FinalReplacement
1988 )
1989 };
1990
1991 if persist_bloom {
1992 let writer = match directory
1993 .streaming_writer(std::path::Path::new(super::primary_key::PK_BLOOM_FILE))
1994 .await
1995 {
1996 Ok(writer) => writer,
1997 Err(error) => {
1998 log::warn!(
1999 "[primary_key] index={} failed to open bloom cache: {}",
2000 schema.index_label(),
2001 error
2002 );
2003 return Ok(());
2004 }
2005 };
2006 let guard = primary_key_index.read();
2009 if let Some(pk_index) = guard.as_ref()
2010 && let Err(error) = crate::segment::block_in_place_if_multithread(|| {
2011 write_pk_bloom_stream(pk_index, &seg_ids, writer)
2012 })
2013 {
2014 log::warn!(
2015 "[primary_key] index={} failed to persist bloom cache: {}",
2016 schema.index_label(),
2017 error
2018 );
2019 }
2020 }
2021 Ok(())
2022}
2023
2024fn write_pk_bloom_stream(
2025 pk_index: &super::primary_key::PrimaryKeyIndex,
2026 segment_ids: &[String],
2027 mut writer: Box<dyn crate::directories::StreamingWriter>,
2028) -> std::io::Result<()> {
2029 pk_index.write_bloom_cache(segment_ids, writer.as_mut())?;
2030 writer.finish()
2031}
2032
2033async fn finalize_prepared_commit<D: DirectoryWriter + 'static>(
2034 mut commit: OwnedCommitFinalization<D>,
2035) -> Result<bool> {
2036 let metadata_entries = commit.prepared.metadata_entries();
2037 let published_segment_ids = commit.prepared.vector_upgrade_segment_ids();
2038
2039 commit.segment_manager.commit(&metadata_entries).await?;
2043 commit.publication_observed.store(true, Ordering::Release);
2044
2045 let mut published = commit.prepared.take_published();
2046 for segment in &mut published {
2047 segment.mark_published();
2048 }
2049 drop(published);
2050 commit
2051 .segment_manager
2052 .schedule_vector_segment_upgrades(published_segment_ids);
2053 if let Some(finalization) = commit.finalization.as_mut() {
2057 finalization.resume_on_drop();
2058 } else {
2059 log::error!("owned commit finalization guard was already released after publication");
2060 }
2061
2062 match refresh_primary_key_snapshot(
2067 &commit.directory,
2068 &commit.schema,
2069 &commit.segment_manager,
2070 &commit.primary_key_index,
2071 &commit.primary_key_refresh_lock,
2072 PrimaryKeyRefresh::Commit,
2073 )
2074 .await
2075 {
2076 Ok(()) => commit
2079 .pk_reservations_retained
2080 .store(false, Ordering::Release),
2081 Err(error) => {
2082 commit
2087 .pk_reservations_retained
2088 .store(true, Ordering::Release);
2089 log::error!(
2090 "[primary_key] committed metadata but failed to refresh dedup state; \
2091 retaining reservations until a later successful commit: {}",
2092 error,
2093 );
2094 }
2095 }
2096
2097 drop(commit.finalization.take());
2101 commit.segment_manager.maybe_merge().await;
2102 Ok(true)
2103}
2104
2105impl<'a, D: DirectoryWriter + 'static> PreparedCommit<'a, D> {
2106 pub async fn commit(mut self) -> Result<bool> {
2110 let segments = std::mem::take(&mut *self.writer.flushed_segments.lock());
2111
2112 if segments.is_empty() {
2114 log::debug!(
2115 "[commit] index={} no segments to commit, skipping",
2116 self.writer.schema.index_label()
2117 );
2118 self.is_resolved = true;
2119 self.writer.resume_workers();
2120 return Ok(false);
2121 }
2122
2123 if !self.writer.commit_finalization.begin() {
2124 self.writer.flushed_segments.lock().extend(segments);
2125 self.is_resolved = true;
2129 return Err(Error::CommitInProgress);
2130 }
2131
2132 let publication_observed = Arc::new(AtomicBool::new(false));
2133 let owned = OwnedCommitFinalization {
2134 directory: Arc::clone(&self.writer.directory),
2135 schema: Arc::clone(&self.writer.schema),
2136 segment_manager: Arc::clone(&self.writer.segment_manager),
2137 primary_key_index: Arc::clone(&self.writer.primary_key_index),
2138 primary_key_refresh_lock: Arc::clone(&self.writer.primary_key_refresh_lock),
2139 prepared: PreparedSegmentsGuard {
2140 segments: Some(segments),
2141 retry_slot: Arc::clone(&self.writer.flushed_segments),
2142 },
2143 finalization: Some(CommitFinalizationGuard {
2144 state: Arc::clone(&self.writer.commit_finalization),
2145 worker_state: Arc::clone(&self.writer.worker_state),
2146 doc_sender: Arc::clone(&self.writer.doc_sender),
2147 resume_workers: false,
2148 }),
2149 publication_observed: Arc::clone(&publication_observed),
2150 pk_reservations_retained: Arc::clone(&self.writer.pk_reservations_retained),
2151 };
2152
2153 self.is_resolved = true;
2158 let task_publication = Arc::clone(&publication_observed);
2159 let task = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2160 tokio::spawn(async move {
2161 match std::panic::AssertUnwindSafe(finalize_prepared_commit(owned))
2162 .catch_unwind()
2163 .await
2164 {
2165 Ok(result) => result,
2166 Err(_) if task_publication.load(Ordering::Acquire) => {
2167 log::error!(
2168 "owned commit finalizer panicked after metadata publication; \
2169 treating the durable generation as committed"
2170 );
2171 Ok(true)
2172 }
2173 Err(_) => Err(Error::Internal(
2174 "owned commit finalizer panicked before metadata publication".into(),
2175 )),
2176 }
2177 })
2178 }))
2179 .map_err(|_| Error::Internal("runtime rejected owned commit finalizer".into()))?;
2180
2181 match task.await {
2182 Ok(result) => result,
2183 Err(error) if publication_observed.load(Ordering::Acquire) => {
2184 log::error!(
2185 "owned commit finalizer terminated after metadata publication: {}; \
2186 treating the durable generation as committed",
2187 error,
2188 );
2189 Ok(true)
2190 }
2191 Err(error) => Err(Error::Internal(format!(
2192 "owned commit finalizer terminated unexpectedly: {error}"
2193 ))),
2194 }
2195 }
2196
2197 pub fn abort(mut self) {
2200 self.is_resolved = true;
2201 self.writer.flushed_segments.lock().clear();
2202 self.writer.clear_uncommitted_pk_reservations();
2203 self.writer.resume_workers();
2204 }
2205}
2206
2207impl<D: DirectoryWriter + 'static> Drop for PreparedCommit<'_, D> {
2208 fn drop(&mut self) {
2209 if !self.is_resolved {
2210 log::warn!("PreparedCommit dropped without commit/abort — auto-aborting");
2211 self.writer.flushed_segments.lock().clear();
2212 self.writer.clear_uncommitted_pk_reservations();
2213 self.writer.resume_workers();
2214 }
2215 }
2216}
2217
2218async fn load_pk_segment_data<D: crate::directories::Directory>(
2220 dir: &D,
2221 seg_id_str: &str,
2222 schema: &Arc<crate::dsl::Schema>,
2223) -> Result<super::primary_key::PkSegmentData> {
2224 let seg_id = crate::segment::SegmentId::from_hex(seg_id_str)
2225 .ok_or_else(|| Error::Internal(format!("Invalid segment id: {}", seg_id_str)))?;
2226 let files = crate::segment::SegmentFiles::new(seg_id.0);
2227 let fast_fields =
2228 crate::segment::reader::loader::load_fast_fields_file(dir, &files, schema).await?;
2229 Ok(super::primary_key::PkSegmentData {
2230 segment_id: seg_id_str.to_string(),
2231 fast_fields,
2232 })
2233}