1use std::sync::Arc;
33use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
34
35use futures::FutureExt;
36use rustc_hash::FxHashMap;
37
38use crate::directories::DirectoryWriter;
39use crate::dsl::{Document, Field, Schema};
40use crate::error::{Error, Result};
41use crate::segment::{SegmentBuilder, SegmentBuilderConfig, SegmentId};
42use crate::tokenizer::BoxedTokenizer;
43
44use super::IndexConfig;
45
46const PIPELINE_MAX_SIZE_IN_DOCS: usize = 10_000;
48
49pub const WRITER_LOCK_FILENAME: &str = ".hermes_writer.lock";
51
52enum WriterLock {
61 Held { _file: std::fs::File },
63 NotApplicable,
66 Unavailable { reason: String },
69}
70
71fn writer_lock_root<D: DirectoryWriter + 'static>(directory: &D) -> Option<std::path::PathBuf> {
74 let any: &dyn std::any::Any = directory;
75 if let Some(mmap) = any.downcast_ref::<crate::directories::MmapDirectory>() {
76 return Some(mmap.root().to_path_buf());
77 }
78 if any
82 .downcast_ref::<crate::directories::FsDirectory>()
83 .is_some()
84 {
85 log::warn!(
86 "[writer_lock] FsDirectory exposes no root path; single-writer locking \
87 is not enforced for this writer — do not open a second writer for the \
88 same index directory"
89 );
90 }
91 None
92}
93
94fn try_acquire_writer_lock<D: DirectoryWriter + 'static>(directory: &D) -> Result<WriterLock> {
99 let Some(root) = writer_lock_root(directory) else {
100 return Ok(WriterLock::NotApplicable);
101 };
102 std::fs::create_dir_all(&root)?;
103 let lock_path = root.join(WRITER_LOCK_FILENAME);
104 let file = std::fs::OpenOptions::new()
105 .create(true)
106 .truncate(false)
107 .write(true)
108 .open(&lock_path)?;
109 match file.try_lock() {
110 Ok(()) => Ok(WriterLock::Held { _file: file }),
111 Err(std::fs::TryLockError::WouldBlock) => Ok(WriterLock::Unavailable {
112 reason: format!(
113 "another IndexWriter already holds the single-writer lock for this \
114 index ({}); Hermes supports one writer per index directory — stop \
115 the other writer (e.g. a running hermes-server or hermes-tool) \
116 before opening this one",
117 lock_path.display()
118 ),
119 }),
120 Err(std::fs::TryLockError::Error(error)) => Err(Error::Io(error)),
121 }
122}
123
124pub struct IndexWriter<D: DirectoryWriter + 'static> {
138 pub(super) directory: Arc<D>,
139 pub(super) schema: Arc<Schema>,
140 pub(super) config: IndexConfig,
141 doc_sender: Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
144 workers: Vec<std::thread::JoinHandle<()>>,
146 worker_state: Arc<WorkerState<D>>,
148 pub(super) segment_manager: Arc<crate::merge::SegmentManager<D>>,
150 flushed_segments: Arc<parking_lot::Mutex<Vec<PreparedSegment<D>>>>,
153 primary_key_index: Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
155 commit_finalization: Arc<CommitFinalizationState>,
159 pk_reservations_retained: Arc<AtomicBool>,
164 writer_lock: parking_lot::RwLock<WriterLock>,
169}
170
171#[derive(Default)]
172struct CommitFinalizationState {
173 in_progress: AtomicBool,
174 idle: tokio::sync::Notify,
175}
176
177impl CommitFinalizationState {
178 fn begin(&self) -> bool {
179 self.in_progress
180 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
181 .is_ok()
182 }
183
184 fn finish(&self) {
185 self.in_progress.store(false, Ordering::Release);
186 self.idle.notify_waiters();
187 }
188
189 async fn wait_until_idle(&self) {
190 while self.in_progress.load(Ordering::Acquire) {
191 let notified = self.idle.notified();
192 if !self.in_progress.load(Ordering::Acquire) {
193 break;
194 }
195 notified.await;
196 }
197 }
198}
199
200struct WorkerState<D: DirectoryWriter + 'static> {
202 directory: Arc<D>,
203 schema: Arc<Schema>,
204 builder_config: SegmentBuilderConfig,
205 tokenizers: parking_lot::RwLock<FxHashMap<Field, BoxedTokenizer>>,
206 memory_budget_per_worker: usize,
208 segment_manager: Arc<crate::merge::SegmentManager<D>>,
210 built_segments: parking_lot::Mutex<Vec<PreparedSegment<D>>>,
213 cycle_error: parking_lot::Mutex<Option<String>>,
218 cycle_failed: AtomicBool,
219
220 flush_count: AtomicUsize,
227 flush_mutex: parking_lot::Mutex<()>,
229 flush_cvar: parking_lot::Condvar,
230 resume_receiver: parking_lot::Mutex<Option<async_channel::Receiver<Document>>>,
232 resume_epoch: AtomicUsize,
235 resume_cvar: parking_lot::Condvar,
237 shutdown: AtomicBool,
239 num_workers: usize,
241}
242
243struct PreparedSegment<D: DirectoryWriter + 'static> {
249 id: String,
250 segment_id: SegmentId,
251 num_docs: u32,
252 segment_manager: Arc<crate::merge::SegmentManager<D>>,
253 operation: Option<crate::merge::SegmentOperationGuard>,
254 runtime: tokio::runtime::Handle,
255 published: bool,
256}
257
258impl<D: DirectoryWriter + 'static> PreparedSegment<D> {
259 fn metadata_entry(&self) -> (String, u32) {
260 (self.id.clone(), self.num_docs)
261 }
262
263 fn mark_published(&mut self) {
264 self.published = true;
265 drop(self.operation.take());
267 }
268}
269
270impl<D: DirectoryWriter + 'static> WorkerState<D> {
271 fn record_cycle_error(&self, error: impl Into<String>) {
272 let mut first_error = self.cycle_error.lock();
273 if first_error.is_none() {
274 *first_error = Some(error.into());
275 }
276 drop(first_error);
277 self.cycle_failed.store(true, Ordering::Release);
278 }
279}
280
281impl<D: DirectoryWriter + 'static> Drop for PreparedSegment<D> {
282 fn drop(&mut self) {
283 if self.published {
284 return;
285 }
286 let Some(operation) = self.operation.take() else {
287 return;
288 };
289 self.segment_manager.schedule_unpublished_segment_cleanup(
290 self.segment_id,
291 operation,
292 self.runtime.clone(),
293 );
294 }
295}
296
297impl<D: DirectoryWriter + 'static> IndexWriter<D> {
298 pub async fn create(directory: D, schema: Schema, config: IndexConfig) -> Result<Self> {
300 Self::create_with_config(directory, schema, config, SegmentBuilderConfig::default()).await
301 }
302
303 pub async fn create_with_config(
305 directory: D,
306 schema: Schema,
307 config: IndexConfig,
308 builder_config: SegmentBuilderConfig,
309 ) -> Result<Self> {
310 let directory = Arc::new(directory);
311 let schema = Arc::new(schema);
312 directory.set_index_label(schema.index_label());
314
315 let writer_lock = try_acquire_writer_lock(directory.as_ref())?;
317 if let WriterLock::Unavailable { reason } = &writer_lock {
318 return Err(Error::Internal(reason.clone()));
319 }
320 if directory
324 .exists(std::path::Path::new(super::INDEX_META_FILENAME))
325 .await?
326 {
327 return Err(Error::Internal(format!(
328 "refusing to create index: {} already exists in this directory; \
329 use IndexWriter::open to open the existing index, or delete the \
330 directory first if you really want to start over",
331 super::INDEX_META_FILENAME
332 )));
333 }
334
335 let metadata = super::IndexMetadata::new((*schema).clone());
336
337 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
338 Arc::clone(&directory),
339 Arc::clone(&schema),
340 metadata,
341 config.merge_policy.clone_box(),
342 config.term_cache_blocks,
343 config.max_concurrent_merges,
344 Arc::clone(&config.background_merge_permits),
345 config.merge_bp_time_budget,
346 config.bp_memory_budget_bytes,
347 Arc::clone(&config.background_reorder_permits),
348 config.background_reorder_pool.clone(),
349 ));
350 segment_manager.update_metadata(|_| {}).await?;
351
352 Ok(Self::new_with_parts(
353 directory,
354 schema,
355 config,
356 builder_config,
357 segment_manager,
358 writer_lock,
359 ))
360 }
361
362 pub async fn open(directory: D, config: IndexConfig) -> Result<Self> {
371 Self::open_with_config(directory, config, SegmentBuilderConfig::default()).await
372 }
373
374 pub async fn open_with_config(
376 directory: D,
377 config: IndexConfig,
378 builder_config: SegmentBuilderConfig,
379 ) -> Result<Self> {
380 let directory = Arc::new(directory);
381
382 let writer_lock = try_acquire_writer_lock(directory.as_ref())?;
385 if let WriterLock::Unavailable { reason } = &writer_lock {
386 return Err(Error::Internal(reason.clone()));
387 }
388
389 let metadata = super::IndexMetadata::load(directory.as_ref()).await?;
390 let schema = Arc::new(metadata.schema.clone());
391 directory.set_index_label(schema.index_label());
393
394 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
395 Arc::clone(&directory),
396 Arc::clone(&schema),
397 metadata,
398 config.merge_policy.clone_box(),
399 config.term_cache_blocks,
400 config.max_concurrent_merges,
401 Arc::clone(&config.background_merge_permits),
402 config.merge_bp_time_budget,
403 config.bp_memory_budget_bytes,
404 Arc::clone(&config.background_reorder_permits),
405 config.background_reorder_pool.clone(),
406 ));
407 let swept = segment_manager.cleanup_orphan_segments().await?;
408 if swept > 0 {
409 log::warn!(
410 "[segment_cleanup] swept {} orphan segment(s) while opening writer",
411 swept
412 );
413 }
414 segment_manager.try_load_and_publish_trained().await?;
415
416 Ok(Self::new_with_parts(
417 directory,
418 schema,
419 config,
420 builder_config,
421 segment_manager,
422 writer_lock,
423 ))
424 }
425
426 pub fn from_index(index: &super::Index<D>) -> Self {
433 let writer_lock = match try_acquire_writer_lock(index.directory.as_ref()) {
434 Ok(lock) => lock,
435 Err(error) => WriterLock::Unavailable {
436 reason: format!("failed to acquire the single-writer lock: {error}"),
437 },
438 };
439 if let WriterLock::Unavailable { reason } = &writer_lock {
440 log::error!("[writer_lock] {reason}");
441 }
442 Self::new_with_parts(
443 Arc::clone(&index.directory),
444 Arc::clone(&index.schema),
445 index.config.clone(),
446 SegmentBuilderConfig::default(),
447 Arc::clone(&index.segment_manager),
448 writer_lock,
449 )
450 }
451
452 fn new_with_parts(
458 directory: Arc<D>,
459 schema: Arc<Schema>,
460 config: IndexConfig,
461 builder_config: SegmentBuilderConfig,
462 segment_manager: Arc<crate::merge::SegmentManager<D>>,
463 writer_lock: WriterLock,
464 ) -> Self {
465 let registry = crate::tokenizer::TokenizerRegistry::new();
467 let mut tokenizers = FxHashMap::default();
468 for (field, entry) in schema.fields() {
469 if matches!(entry.field_type, crate::dsl::FieldType::Text)
470 && let Some(ref tok_name) = entry.tokenizer
471 && let Some(tok) = registry.get(tok_name)
472 {
473 tokenizers.insert(field, tok);
474 }
475 }
476
477 let num_workers = config.num_indexing_threads.max(1);
478 let worker_state = Arc::new(WorkerState {
479 directory: Arc::clone(&directory),
480 schema: Arc::clone(&schema),
481 builder_config,
482 tokenizers: parking_lot::RwLock::new(tokenizers),
483 memory_budget_per_worker: config.max_indexing_memory_bytes / num_workers,
484 segment_manager: Arc::clone(&segment_manager),
485 built_segments: parking_lot::Mutex::new(Vec::new()),
486 cycle_error: parking_lot::Mutex::new(None),
487 cycle_failed: AtomicBool::new(false),
488 flush_count: AtomicUsize::new(0),
489 flush_mutex: parking_lot::Mutex::new(()),
490 flush_cvar: parking_lot::Condvar::new(),
491 resume_receiver: parking_lot::Mutex::new(None),
492 resume_epoch: AtomicUsize::new(0),
493 resume_cvar: parking_lot::Condvar::new(),
494 shutdown: AtomicBool::new(false),
495 num_workers,
496 });
497 let (doc_sender, workers) = Self::spawn_workers(&worker_state, num_workers);
498
499 Self {
500 directory,
501 schema,
502 config,
503 doc_sender: Arc::new(parking_lot::RwLock::new(doc_sender)),
504 workers,
505 worker_state,
506 segment_manager,
507 flushed_segments: Arc::new(parking_lot::Mutex::new(Vec::new())),
508 primary_key_index: Arc::new(parking_lot::RwLock::new(None)),
509 commit_finalization: Arc::new(CommitFinalizationState::default()),
510 pk_reservations_retained: Arc::new(AtomicBool::new(false)),
511 writer_lock: parking_lot::RwLock::new(writer_lock),
512 }
513 }
514
515 fn ensure_writer_lock(&self) -> Result<()> {
523 if !matches!(&*self.writer_lock.read(), WriterLock::Unavailable { .. }) {
525 return Ok(());
526 }
527
528 let mut lock = self.writer_lock.write();
529 if !matches!(&*lock, WriterLock::Unavailable { .. }) {
531 return Ok(());
532 }
533 match try_acquire_writer_lock(self.directory.as_ref())? {
534 acquired @ (WriterLock::Held { .. } | WriterLock::NotApplicable) => {
535 log::info!(
536 "[writer_lock] single-writer lock acquired after retry; \
537 the previous holder has released it — resuming writes"
538 );
539 *lock = acquired;
540 Ok(())
541 }
542 WriterLock::Unavailable { reason } => {
543 let err = Error::Internal(reason.clone());
544 *lock = WriterLock::Unavailable { reason };
545 Err(err)
546 }
547 }
548 }
549
550 fn clear_uncommitted_pk_reservations(&self) {
559 if self.pk_reservations_retained.load(Ordering::Acquire) {
560 log::warn!(
561 "[primary_key] keeping uncommitted reservations through abort: a \
562 failed post-commit refresh left them as the only record of \
563 committed keys; they are cleared by the next successful commit"
564 );
565 return;
566 }
567 if let Some(pk_index) = self.primary_key_index.write().as_mut() {
568 pk_index.clear_uncommitted();
569 }
570 }
571
572 fn spawn_workers(
573 worker_state: &Arc<WorkerState<D>>,
574 num_workers: usize,
575 ) -> (
576 async_channel::Sender<Document>,
577 Vec<std::thread::JoinHandle<()>>,
578 ) {
579 let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
580 let handle = tokio::runtime::Handle::current();
581 let mut workers = Vec::with_capacity(num_workers);
582 for i in 0..num_workers {
583 let state = Arc::clone(worker_state);
584 let rx = receiver.clone();
585 let rt = handle.clone();
586 workers.push(
587 std::thread::Builder::new()
588 .name(format!("index-worker-{}", i))
589 .spawn(move || Self::worker_loop(state, rx, rt))
590 .expect("failed to spawn index worker thread"),
591 );
592 }
593 (sender, workers)
594 }
595
596 pub fn schema(&self) -> &Schema {
598 &self.schema
599 }
600
601 pub fn set_tokenizer<T: crate::tokenizer::Tokenizer>(&mut self, field: Field, tokenizer: T) {
604 self.worker_state
605 .tokenizers
606 .write()
607 .insert(field, Box::new(tokenizer));
608 }
609
610 pub async fn init_primary_key_dedup(&mut self) -> Result<()> {
626 use super::primary_key::{PK_BLOOM_FILE, deserialize_pk_bloom};
627
628 self.commit_finalization.wait_until_idle().await;
629
630 let field = match self.schema.primary_field() {
631 Some(f) => f,
632 None => return Ok(()),
633 };
634
635 let snapshot = self.segment_manager.acquire_snapshot().await;
636 let current_seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
637
638 let cached = match self
640 .directory
641 .open_read(std::path::Path::new(PK_BLOOM_FILE))
642 .await
643 {
644 Ok(handle) => {
645 let data = handle.read_bytes_range(0..handle.len()).await;
646 match data {
647 Ok(bytes) => deserialize_pk_bloom(bytes.as_slice()),
648 Err(_) => None,
649 }
650 }
651 Err(_) => None,
652 };
653
654 let load_futures: Vec<_> = current_seg_ids
656 .iter()
657 .map(|seg_id_str| {
658 let seg_id_str = seg_id_str.clone();
659 let dir = self.directory.as_ref();
660 let schema = Arc::clone(&self.schema);
661 async move { load_pk_segment_data(dir, &seg_id_str, &schema).await }
662 })
663 .collect();
664 let all_data = futures::future::try_join_all(load_futures).await?;
665
666 if let Some((persisted_seg_ids, bloom)) = cached {
667 let mut pk_data = Vec::with_capacity(all_data.len());
669 let mut new_data = Vec::new();
670 for d in all_data {
671 if persisted_seg_ids.contains(&d.segment_id) {
672 pk_data.push(d);
673 } else {
674 new_data.push(d);
675 }
676 }
677 let needs_persist = !new_data.is_empty();
678 let new_start = pk_data.len();
679 pk_data.extend(new_data);
680
681 let pk_index = if new_start == pk_data.len() {
682 super::primary_key::PrimaryKeyIndex::from_persisted(
684 field,
685 bloom,
686 pk_data,
687 &[],
688 snapshot,
689 )
690 } else {
691 tokio::task::spawn_blocking(move || {
693 let mut bloom = bloom;
696 let mut added = 0usize;
697 let num_new = pk_data.len() - new_start;
698 for data in &pk_data[new_start..] {
699 if let Some(ff) = data.fast_fields.get(&field.0)
700 && let Some(dict) = ff.text_dict()
701 {
702 for key in dict.iter() {
703 bloom.insert(key.as_bytes());
704 added += 1;
705 }
706 }
707 }
708 if added > 0 {
709 log::info!(
710 "[primary_key] bloom: added {} keys from {} new segment(s)",
711 added,
712 num_new,
713 );
714 }
715 super::primary_key::PrimaryKeyIndex::from_persisted(
716 field,
717 bloom,
718 pk_data,
719 &[],
720 snapshot,
721 )
722 })
723 .await
724 .map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?
725 };
726
727 if needs_persist {
728 self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
729 }
730
731 *self.primary_key_index.write() = Some(pk_index);
732 } else {
733 let pk_index = tokio::task::spawn_blocking(move || {
735 super::primary_key::PrimaryKeyIndex::new(field, all_data, snapshot)
736 })
737 .await
738 .map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?;
739
740 self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
741 *self.primary_key_index.write() = Some(pk_index);
742 }
743
744 self.pk_reservations_retained
748 .store(false, Ordering::Release);
749
750 Ok(())
751 }
752
753 async fn persist_pk_bloom(
756 &self,
757 pk_index: &super::primary_key::PrimaryKeyIndex,
758 segment_ids: &[String],
759 ) {
760 use super::primary_key::{PK_BLOOM_FILE, serialize_pk_bloom};
761
762 let bloom_bytes = pk_index.bloom_to_bytes();
763 let data = serialize_pk_bloom(segment_ids, &bloom_bytes);
764 if let Err(e) = self
765 .directory
766 .write(std::path::Path::new(PK_BLOOM_FILE), &data)
767 .await
768 {
769 log::warn!("[primary_key] failed to persist bloom cache: {}", e);
770 }
771 }
772
773 pub fn add_document(&self, doc: Document) -> Result<()> {
779 self.ensure_writer_lock()?;
780 if self.worker_state.shutdown.load(Ordering::Acquire) {
781 return Err(Error::IndexClosed);
782 }
783 if self.commit_finalization.in_progress.load(Ordering::Acquire) {
784 return Err(Error::CommitInProgress);
785 }
786 let sender = self.doc_sender.read().clone();
787 if sender.is_closed() {
791 return Err(Error::CommitInProgress);
792 }
793 let primary_key_index = self.primary_key_index.read();
794 if let Some(ref pk_index) = *primary_key_index {
795 pk_index.check_and_insert(&doc)?;
796 }
797 match sender.try_send(doc) {
798 Ok(()) => Ok(()),
799 Err(async_channel::TrySendError::Full(doc)) => {
800 if let Some(ref pk_index) = *primary_key_index {
802 pk_index.rollback_uncommitted_key(&doc);
803 }
804 Err(Error::QueueFull)
805 }
806 Err(async_channel::TrySendError::Closed(doc)) => {
807 if let Some(ref pk_index) = *primary_key_index {
809 pk_index.rollback_uncommitted_key(&doc);
810 }
811 Err(Error::CommitInProgress)
812 }
813 }
814 }
815
816 pub fn add_documents(&self, documents: Vec<Document>) -> Result<usize> {
821 let total = documents.len();
822 for (i, doc) in documents.into_iter().enumerate() {
823 match self.add_document(doc) {
824 Ok(()) => {}
825 Err(Error::QueueFull | Error::CommitInProgress) => return Ok(i),
826 Err(e) => return Err(e),
827 }
828 }
829 Ok(total)
830 }
831
832 fn worker_loop(
845 state: Arc<WorkerState<D>>,
846 initial_receiver: async_channel::Receiver<Document>,
847 handle: tokio::runtime::Handle,
848 ) {
849 let mut receiver = initial_receiver;
850 let mut my_epoch = 0usize;
851
852 loop {
853 let build_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
857 let mut builder: Option<SegmentBuilder> = None;
858
859 while let Ok(doc) = receiver.recv_blocking() {
860 if state.shutdown.load(Ordering::Acquire) {
861 break;
862 }
863 if state.cycle_failed.load(Ordering::Acquire) {
868 continue;
869 }
870 if builder.is_none() {
872 match SegmentBuilder::new(
873 Arc::clone(&state.schema),
874 state.builder_config.clone(),
875 ) {
876 Ok(mut b) => {
877 for (field, tokenizer) in state.tokenizers.read().iter() {
878 b.set_tokenizer(*field, tokenizer.clone_box());
879 }
880 builder = Some(b);
881 }
882 Err(e) => {
883 log::error!("Failed to create segment builder: {:?}", e);
884 state.record_cycle_error(format!(
885 "failed to create segment builder: {e}"
886 ));
887 continue;
888 }
889 }
890 }
891
892 let b = builder.as_mut().unwrap();
893 if let Err(e) = b.add_document(doc) {
894 log::error!("Failed to index document: {:?}", e);
895 state.record_cycle_error(format!("failed to index document: {e}"));
896 continue;
897 }
898
899 let builder_memory = b.estimated_memory_bytes();
900
901 if b.num_docs() & 0x3FFF == 0 {
902 log::debug!(
903 "[indexing] docs={}, memory={:.2} MB, budget={:.2} MB",
904 b.num_docs(),
905 builder_memory as f64 / (1024.0 * 1024.0),
906 state.memory_budget_per_worker as f64 / (1024.0 * 1024.0)
907 );
908 }
909
910 const MIN_DOCS_BEFORE_FLUSH: u32 = 100;
912
913 let effective_budget = state.memory_budget_per_worker * 4 / 5;
917
918 if builder_memory >= effective_budget && b.num_docs() >= MIN_DOCS_BEFORE_FLUSH {
919 log::info!(
920 "[indexing] memory budget reached, building segment: \
921 docs={}, memory={:.2} MB, budget={:.2} MB",
922 b.num_docs(),
923 builder_memory as f64 / (1024.0 * 1024.0),
924 state.memory_budget_per_worker as f64 / (1024.0 * 1024.0),
925 );
926 let full_builder = builder.take().unwrap();
927 Self::build_segment_inline(&state, full_builder, &handle);
928 }
929 }
930
931 if !state.cycle_failed.load(Ordering::Acquire)
933 && let Some(b) = builder.take()
934 && b.num_docs() > 0
935 {
936 Self::build_segment_inline(&state, b, &handle);
937 }
938 }));
939
940 if build_result.is_err() {
941 log::error!(
942 "[worker] panic during indexing cycle — documents in this cycle may be lost"
943 );
944 state.record_cycle_error("indexing worker panicked while building the batch");
945 }
946
947 let prev = state.flush_count.fetch_add(1, Ordering::Release);
950 if prev + 1 == state.num_workers {
951 let _lock = state.flush_mutex.lock();
958 state.flush_cvar.notify_all();
959 }
960
961 {
965 let mut lock = state.resume_receiver.lock();
966 loop {
967 if state.shutdown.load(Ordering::Acquire) {
968 return;
969 }
970 let current_epoch = state.resume_epoch.load(Ordering::Acquire);
971 if current_epoch > my_epoch
972 && let Some(rx) = lock.as_ref()
973 {
974 receiver = rx.clone();
975 my_epoch = current_epoch;
976 break;
977 }
978 state.resume_cvar.wait(&mut lock);
979 }
980 }
981 }
982 }
983
984 fn build_segment_inline(
988 state: &WorkerState<D>,
989 builder: SegmentBuilder,
990 handle: &tokio::runtime::Handle,
991 ) {
992 let segment_id = SegmentId::new();
993 let segment_hex = segment_id.to_hex();
994 let operation = match state
997 .segment_manager
998 .protect_new_segment(segment_hex.clone())
999 {
1000 Ok(operation) => operation,
1001 Err(e) => {
1002 log::error!(
1003 "[segment_build_failed] segment_id={} lifecycle_error={}",
1004 segment_hex,
1005 e,
1006 );
1007 state.record_cycle_error(format!(
1008 "failed to claim segment {segment_hex} for building: {e}"
1009 ));
1010 return;
1011 }
1012 };
1013 let trained = state.segment_manager.trained_for_segment_build();
1014 let doc_count = builder.num_docs();
1015 let build_start = std::time::Instant::now();
1016
1017 log::info!(
1018 "[segment_build] segment_id={} doc_count={} ann={}",
1019 segment_hex,
1020 doc_count,
1021 trained.is_some()
1022 );
1023
1024 let mut prepared = PreparedSegment {
1028 id: segment_hex.clone(),
1029 segment_id,
1030 num_docs: doc_count,
1031 segment_manager: Arc::clone(&state.segment_manager),
1032 operation: Some(operation),
1033 runtime: handle.clone(),
1034 published: false,
1035 };
1036
1037 match handle.block_on(builder.build(
1038 state.directory.as_ref(),
1039 segment_id,
1040 trained.as_deref(),
1041 )) {
1042 Ok(meta) if meta.num_docs == doc_count && meta.num_docs > 0 => {
1043 let duration_ms = build_start.elapsed().as_millis() as u64;
1044 log::info!(
1045 "[segment_build_done] segment_id={} doc_count={} duration_ms={}",
1046 segment_hex,
1047 meta.num_docs,
1048 duration_ms,
1049 );
1050 prepared.num_docs = meta.num_docs;
1051 state.built_segments.lock().push(prepared);
1052 }
1053 Ok(meta) => {
1054 let error = format!(
1055 "segment {segment_hex} built {} docs from a {doc_count}-document builder",
1056 meta.num_docs
1057 );
1058 log::error!("[segment_build_failed] {error}");
1059 state.record_cycle_error(error);
1060 }
1061 Err(e) => {
1062 log::error!(
1063 "[segment_build_failed] segment_id={} error={:?}",
1064 segment_hex,
1065 e
1066 );
1067 state.record_cycle_error(format!("failed to build segment {segment_hex}: {e}"));
1070 }
1071 }
1072 }
1073
1074 pub async fn maybe_merge(&self) {
1080 self.segment_manager.maybe_merge().await;
1081 }
1082
1083 pub async fn abort_merges(&self) {
1086 self.segment_manager.abort_merges().await;
1087 }
1088
1089 pub async fn shutdown(&mut self) -> Result<()> {
1094 self.segment_manager.begin_shutdown();
1095 self.signal_worker_shutdown();
1096
1097 self.commit_finalization.wait_until_idle().await;
1102
1103 let workers = std::mem::take(&mut self.workers);
1104 let panicked = tokio::task::spawn_blocking(move || {
1105 workers
1106 .into_iter()
1107 .map(|worker| worker.join().is_err())
1108 .filter(|panicked| *panicked)
1109 .count()
1110 })
1111 .await
1112 .map_err(|error| Error::Internal(format!("failed to join index workers: {}", error)))?;
1113 if panicked > 0 {
1114 log::error!("[index_shutdown] {} indexing worker(s) panicked", panicked);
1115 }
1116
1117 self.flushed_segments.lock().clear();
1120 self.worker_state.built_segments.lock().clear();
1121 if let Some(pk_index) = self.primary_key_index.write().as_mut() {
1122 pk_index.clear_uncommitted();
1123 }
1124 Ok(())
1125 }
1126
1127 pub async fn wait_for_merging_thread(&self) {
1129 self.segment_manager.wait_for_merging_thread().await;
1130 }
1131
1132 pub async fn wait_for_all_merges(&self) {
1134 self.segment_manager.wait_for_all_merges().await;
1135 }
1136
1137 pub async fn wait_for_commit_finalization(&self) {
1142 self.commit_finalization.wait_until_idle().await;
1143 }
1144
1145 pub fn tracker(&self) -> std::sync::Arc<crate::segment::SegmentTracker> {
1147 self.segment_manager.tracker()
1148 }
1149
1150 pub async fn acquire_snapshot(&self) -> crate::segment::SegmentSnapshot {
1152 self.segment_manager.acquire_snapshot().await
1153 }
1154
1155 pub async fn cleanup_orphan_segments(&self) -> Result<usize> {
1160 self.ensure_writer_lock()?;
1161 self.segment_manager.cleanup_orphan_segments().await
1162 }
1163
1164 pub async fn prepare_commit(&mut self) -> Result<PreparedCommit<'_, D>> {
1175 self.ensure_writer_lock()?;
1176 if self.worker_state.shutdown.load(Ordering::Acquire) {
1177 return Err(Error::IndexClosed);
1178 }
1179 if self.commit_finalization.in_progress.load(Ordering::Acquire) {
1180 return Err(Error::CommitInProgress);
1181 }
1182 self.doc_sender.read().close();
1184
1185 self.worker_state.resume_cvar.notify_all();
1189
1190 let state = Arc::clone(&self.worker_state);
1193 let all_flushed = tokio::task::spawn_blocking(move || {
1194 let mut lock = state.flush_mutex.lock();
1195 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(300);
1196 while state.flush_count.load(Ordering::Acquire) < state.num_workers {
1197 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
1198 if remaining.is_zero() {
1199 log::error!(
1200 "[prepare_commit] timed out waiting for workers: {}/{} flushed",
1201 state.flush_count.load(Ordering::Acquire),
1202 state.num_workers
1203 );
1204 return false;
1205 }
1206 state.flush_cvar.wait_for(&mut lock, remaining);
1207 }
1208 true
1209 })
1210 .await
1211 .map_err(|e| Error::Internal(format!("Failed to wait for workers: {}", e)))?;
1212
1213 if !all_flushed {
1214 return Err(Error::Internal(format!(
1222 "prepare_commit timed out: {}/{} workers flushed; writer remains paused, retry commit",
1223 self.worker_state.flush_count.load(Ordering::Acquire),
1224 self.worker_state.num_workers
1225 )));
1226 }
1227
1228 let cycle_error = { self.worker_state.cycle_error.lock().take() };
1229 if let Some(error) = cycle_error {
1230 self.flushed_segments.lock().clear();
1235 self.worker_state.built_segments.lock().clear();
1236 self.clear_uncommitted_pk_reservations();
1237 self.resume_workers();
1238 return Err(Error::Internal(format!(
1239 "indexing generation failed; no documents from this batch were committed: {error}"
1240 )));
1241 }
1242
1243 let built = std::mem::take(&mut *self.worker_state.built_segments.lock());
1245 self.flushed_segments.lock().extend(built);
1246
1247 Ok(PreparedCommit {
1248 writer: self,
1249 is_resolved: false,
1250 })
1251 }
1252
1253 pub async fn commit(&mut self) -> Result<bool> {
1258 self.prepare_commit().await?.commit().await
1259 }
1260
1261 pub async fn force_merge(&mut self) -> Result<()> {
1263 self.prepare_commit().await?.commit().await?;
1264 self.segment_manager.force_merge().await
1265 }
1266
1267 pub async fn reorder(&mut self) -> Result<()> {
1272 self.prepare_commit().await?.commit().await?;
1273 self.segment_manager.reorder_segments().await
1274 }
1275
1276 pub fn segment_manager(&self) -> &Arc<crate::merge::SegmentManager<D>> {
1278 &self.segment_manager
1279 }
1280
1281 fn resume_workers(&mut self) {
1286 Self::resume_workers_shared(&self.worker_state, &self.doc_sender);
1287 }
1288
1289 fn resume_workers_shared(
1290 worker_state: &Arc<WorkerState<D>>,
1291 doc_sender: &Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
1292 ) {
1293 if worker_state.shutdown.load(Ordering::Acquire) {
1294 return;
1295 }
1296 if tokio::runtime::Handle::try_current().is_err() {
1297 worker_state.shutdown.store(true, Ordering::Release);
1300 worker_state.resume_cvar.notify_all();
1301 return;
1302 }
1303
1304 worker_state.flush_count.store(0, Ordering::Release);
1306 *worker_state.cycle_error.lock() = None;
1307 worker_state.cycle_failed.store(false, Ordering::Release);
1308
1309 let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
1311 *doc_sender.write() = sender;
1312
1313 {
1315 let mut lock = worker_state.resume_receiver.lock();
1316 *lock = Some(receiver);
1317 }
1318 worker_state.resume_epoch.fetch_add(1, Ordering::Release);
1319 worker_state.resume_cvar.notify_all();
1320 }
1321
1322 fn signal_worker_shutdown(&self) {
1323 self.worker_state.shutdown.store(true, Ordering::Release);
1324 self.doc_sender.read().close();
1325 self.worker_state.resume_cvar.notify_all();
1326 }
1327
1328 }
1330
1331impl<D: DirectoryWriter + 'static> Drop for IndexWriter<D> {
1332 fn drop(&mut self) {
1333 self.signal_worker_shutdown();
1334 for w in std::mem::take(&mut self.workers) {
1335 let _ = w.join();
1336 }
1337 }
1338}
1339
1340pub struct PreparedCommit<'a, D: DirectoryWriter + 'static> {
1347 writer: &'a mut IndexWriter<D>,
1348 is_resolved: bool,
1349}
1350
1351struct PreparedSegmentsGuard<D: DirectoryWriter + 'static> {
1356 segments: Option<Vec<PreparedSegment<D>>>,
1357 retry_slot: Arc<parking_lot::Mutex<Vec<PreparedSegment<D>>>>,
1358}
1359
1360impl<D: DirectoryWriter + 'static> PreparedSegmentsGuard<D> {
1361 fn metadata_entries(&self) -> Vec<(String, u32)> {
1362 self.segments
1363 .as_deref()
1364 .unwrap_or_default()
1365 .iter()
1366 .map(PreparedSegment::metadata_entry)
1367 .collect()
1368 }
1369
1370 fn take_published(&mut self) -> Vec<PreparedSegment<D>> {
1371 self.segments.take().unwrap_or_default()
1372 }
1373}
1374
1375impl<D: DirectoryWriter + 'static> Drop for PreparedSegmentsGuard<D> {
1376 fn drop(&mut self) {
1377 if let Some(segments) = self.segments.take() {
1378 self.retry_slot.lock().extend(segments);
1379 }
1380 }
1381}
1382
1383struct CommitFinalizationGuard<D: DirectoryWriter + 'static> {
1388 state: Arc<CommitFinalizationState>,
1389 worker_state: Arc<WorkerState<D>>,
1390 doc_sender: Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
1391 resume_workers: bool,
1392}
1393
1394impl<D: DirectoryWriter + 'static> CommitFinalizationGuard<D> {
1395 fn resume_on_drop(&mut self) {
1396 self.resume_workers = true;
1397 }
1398}
1399
1400impl<D: DirectoryWriter + 'static> Drop for CommitFinalizationGuard<D> {
1401 fn drop(&mut self) {
1402 if self.resume_workers {
1403 IndexWriter::<D>::resume_workers_shared(&self.worker_state, &self.doc_sender);
1404 }
1405 self.state.finish();
1406 }
1407}
1408
1409struct OwnedCommitFinalization<D: DirectoryWriter + 'static> {
1414 directory: Arc<D>,
1415 schema: Arc<Schema>,
1416 segment_manager: Arc<crate::merge::SegmentManager<D>>,
1417 primary_key_index: Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
1418 prepared: PreparedSegmentsGuard<D>,
1419 finalization: Option<CommitFinalizationGuard<D>>,
1420 publication_observed: Arc<AtomicBool>,
1421 pk_reservations_retained: Arc<AtomicBool>,
1422}
1423
1424async fn refresh_primary_key_after_commit<D: DirectoryWriter + 'static>(
1425 directory: &Arc<D>,
1426 schema: &Arc<Schema>,
1427 segment_manager: &Arc<crate::merge::SegmentManager<D>>,
1428 primary_key_index: &Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
1429) -> Result<()> {
1430 let existing_ids: std::collections::HashSet<String> = {
1431 let guard = primary_key_index.read();
1432 let Some(pk_index) = guard.as_ref() else {
1433 return Ok(());
1434 };
1435 pk_index
1436 .committed_segment_ids()
1437 .map(ToOwned::to_owned)
1438 .collect()
1439 };
1440
1441 let snapshot = segment_manager.acquire_snapshot().await;
1442 let load_futures: Vec<_> = snapshot
1443 .segment_ids()
1444 .iter()
1445 .filter(|id| !existing_ids.contains(id.as_str()))
1446 .map(|seg_id_str| {
1447 let seg_id_str = seg_id_str.clone();
1448 let dir = directory.as_ref();
1449 let schema = Arc::clone(schema);
1450 async move { load_pk_segment_data(dir, &seg_id_str, &schema).await }
1451 })
1452 .collect();
1453 let new_data = futures::future::try_join_all(load_futures).await?;
1454 let seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
1455
1456 let bloom_file = {
1457 let mut guard = primary_key_index.write();
1458 let Some(pk_index) = guard.as_mut() else {
1459 return Ok(());
1460 };
1461 pk_index.refresh_incremental(new_data, snapshot);
1462 let bloom_bytes = pk_index.bloom_to_bytes();
1463 super::primary_key::serialize_pk_bloom(&seg_ids, &bloom_bytes)
1464 };
1465
1466 if let Err(error) = directory
1467 .write(
1468 std::path::Path::new(super::primary_key::PK_BLOOM_FILE),
1469 &bloom_file,
1470 )
1471 .await
1472 {
1473 log::warn!("[primary_key] failed to persist bloom cache: {}", error);
1474 }
1475 Ok(())
1476}
1477
1478async fn finalize_prepared_commit<D: DirectoryWriter + 'static>(
1479 mut commit: OwnedCommitFinalization<D>,
1480) -> Result<bool> {
1481 let metadata_entries = commit.prepared.metadata_entries();
1482
1483 commit.segment_manager.commit(&metadata_entries).await?;
1487 commit.publication_observed.store(true, Ordering::Release);
1488
1489 let mut published = commit.prepared.take_published();
1490 for segment in &mut published {
1491 segment.mark_published();
1492 }
1493 drop(published);
1494 if let Some(finalization) = commit.finalization.as_mut() {
1498 finalization.resume_on_drop();
1499 } else {
1500 log::error!("owned commit finalization guard was already released after publication");
1501 }
1502
1503 match refresh_primary_key_after_commit(
1508 &commit.directory,
1509 &commit.schema,
1510 &commit.segment_manager,
1511 &commit.primary_key_index,
1512 )
1513 .await
1514 {
1515 Ok(()) => commit
1518 .pk_reservations_retained
1519 .store(false, Ordering::Release),
1520 Err(error) => {
1521 commit
1526 .pk_reservations_retained
1527 .store(true, Ordering::Release);
1528 log::error!(
1529 "[primary_key] committed metadata but failed to refresh dedup state; \
1530 retaining reservations until a later successful commit: {}",
1531 error,
1532 );
1533 }
1534 }
1535
1536 drop(commit.finalization.take());
1540 commit.segment_manager.maybe_merge().await;
1541 Ok(true)
1542}
1543
1544impl<'a, D: DirectoryWriter + 'static> PreparedCommit<'a, D> {
1545 pub async fn commit(mut self) -> Result<bool> {
1549 let segments = std::mem::take(&mut *self.writer.flushed_segments.lock());
1550
1551 if segments.is_empty() {
1553 log::debug!("[commit] no segments to commit, skipping");
1554 self.is_resolved = true;
1555 self.writer.resume_workers();
1556 return Ok(false);
1557 }
1558
1559 if !self.writer.commit_finalization.begin() {
1560 self.writer.flushed_segments.lock().extend(segments);
1561 self.is_resolved = true;
1565 return Err(Error::CommitInProgress);
1566 }
1567
1568 let publication_observed = Arc::new(AtomicBool::new(false));
1569 let owned = OwnedCommitFinalization {
1570 directory: Arc::clone(&self.writer.directory),
1571 schema: Arc::clone(&self.writer.schema),
1572 segment_manager: Arc::clone(&self.writer.segment_manager),
1573 primary_key_index: Arc::clone(&self.writer.primary_key_index),
1574 prepared: PreparedSegmentsGuard {
1575 segments: Some(segments),
1576 retry_slot: Arc::clone(&self.writer.flushed_segments),
1577 },
1578 finalization: Some(CommitFinalizationGuard {
1579 state: Arc::clone(&self.writer.commit_finalization),
1580 worker_state: Arc::clone(&self.writer.worker_state),
1581 doc_sender: Arc::clone(&self.writer.doc_sender),
1582 resume_workers: false,
1583 }),
1584 publication_observed: Arc::clone(&publication_observed),
1585 pk_reservations_retained: Arc::clone(&self.writer.pk_reservations_retained),
1586 };
1587
1588 self.is_resolved = true;
1593 let task_publication = Arc::clone(&publication_observed);
1594 let task = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1595 tokio::spawn(async move {
1596 match std::panic::AssertUnwindSafe(finalize_prepared_commit(owned))
1597 .catch_unwind()
1598 .await
1599 {
1600 Ok(result) => result,
1601 Err(_) if task_publication.load(Ordering::Acquire) => {
1602 log::error!(
1603 "owned commit finalizer panicked after metadata publication; \
1604 treating the durable generation as committed"
1605 );
1606 Ok(true)
1607 }
1608 Err(_) => Err(Error::Internal(
1609 "owned commit finalizer panicked before metadata publication".into(),
1610 )),
1611 }
1612 })
1613 }))
1614 .map_err(|_| Error::Internal("runtime rejected owned commit finalizer".into()))?;
1615
1616 match task.await {
1617 Ok(result) => result,
1618 Err(error) if publication_observed.load(Ordering::Acquire) => {
1619 log::error!(
1620 "owned commit finalizer terminated after metadata publication: {}; \
1621 treating the durable generation as committed",
1622 error,
1623 );
1624 Ok(true)
1625 }
1626 Err(error) => Err(Error::Internal(format!(
1627 "owned commit finalizer terminated unexpectedly: {error}"
1628 ))),
1629 }
1630 }
1631
1632 pub fn abort(mut self) {
1635 self.is_resolved = true;
1636 self.writer.flushed_segments.lock().clear();
1637 self.writer.clear_uncommitted_pk_reservations();
1638 self.writer.resume_workers();
1639 }
1640}
1641
1642impl<D: DirectoryWriter + 'static> Drop for PreparedCommit<'_, D> {
1643 fn drop(&mut self) {
1644 if !self.is_resolved {
1645 log::warn!("PreparedCommit dropped without commit/abort — auto-aborting");
1646 self.writer.flushed_segments.lock().clear();
1647 self.writer.clear_uncommitted_pk_reservations();
1648 self.writer.resume_workers();
1649 }
1650 }
1651}
1652
1653async fn load_pk_segment_data<D: crate::directories::Directory>(
1655 dir: &D,
1656 seg_id_str: &str,
1657 schema: &Arc<crate::dsl::Schema>,
1658) -> Result<super::primary_key::PkSegmentData> {
1659 let seg_id = crate::segment::SegmentId::from_hex(seg_id_str)
1660 .ok_or_else(|| Error::Internal(format!("Invalid segment id: {}", seg_id_str)))?;
1661 let files = crate::segment::SegmentFiles::new(seg_id.0);
1662 let fast_fields =
1663 crate::segment::reader::loader::load_fast_fields_file(dir, &files, schema).await?;
1664 Ok(super::primary_key::PkSegmentData {
1665 segment_id: seg_id_str.to_string(),
1666 fast_fields,
1667 })
1668}