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 struct IndexWriter<D: DirectoryWriter + 'static> {
63 pub(super) directory: Arc<D>,
64 pub(super) schema: Arc<Schema>,
65 pub(super) config: IndexConfig,
66 doc_sender: Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
69 workers: Vec<std::thread::JoinHandle<()>>,
71 worker_state: Arc<WorkerState<D>>,
73 pub(super) segment_manager: Arc<crate::merge::SegmentManager<D>>,
75 flushed_segments: Arc<parking_lot::Mutex<Vec<PreparedSegment<D>>>>,
78 primary_key_index: Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
80 commit_finalization: Arc<CommitFinalizationState>,
84}
85
86#[derive(Default)]
87struct CommitFinalizationState {
88 in_progress: AtomicBool,
89 idle: tokio::sync::Notify,
90}
91
92impl CommitFinalizationState {
93 fn begin(&self) -> bool {
94 self.in_progress
95 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
96 .is_ok()
97 }
98
99 fn finish(&self) {
100 self.in_progress.store(false, Ordering::Release);
101 self.idle.notify_waiters();
102 }
103
104 async fn wait_until_idle(&self) {
105 while self.in_progress.load(Ordering::Acquire) {
106 let notified = self.idle.notified();
107 if !self.in_progress.load(Ordering::Acquire) {
108 break;
109 }
110 notified.await;
111 }
112 }
113}
114
115struct WorkerState<D: DirectoryWriter + 'static> {
117 directory: Arc<D>,
118 schema: Arc<Schema>,
119 builder_config: SegmentBuilderConfig,
120 tokenizers: parking_lot::RwLock<FxHashMap<Field, BoxedTokenizer>>,
121 memory_budget_per_worker: usize,
123 segment_manager: Arc<crate::merge::SegmentManager<D>>,
125 built_segments: parking_lot::Mutex<Vec<PreparedSegment<D>>>,
128 cycle_error: parking_lot::Mutex<Option<String>>,
133 cycle_failed: AtomicBool,
134
135 flush_count: AtomicUsize,
142 flush_mutex: parking_lot::Mutex<()>,
144 flush_cvar: parking_lot::Condvar,
145 resume_receiver: parking_lot::Mutex<Option<async_channel::Receiver<Document>>>,
147 resume_epoch: AtomicUsize,
150 resume_cvar: parking_lot::Condvar,
152 shutdown: AtomicBool,
154 num_workers: usize,
156}
157
158struct PreparedSegment<D: DirectoryWriter + 'static> {
164 id: String,
165 segment_id: SegmentId,
166 num_docs: u32,
167 segment_manager: Arc<crate::merge::SegmentManager<D>>,
168 operation: Option<crate::merge::SegmentOperationGuard>,
169 runtime: tokio::runtime::Handle,
170 published: bool,
171}
172
173impl<D: DirectoryWriter + 'static> PreparedSegment<D> {
174 fn metadata_entry(&self) -> (String, u32) {
175 (self.id.clone(), self.num_docs)
176 }
177
178 fn mark_published(&mut self) {
179 self.published = true;
180 drop(self.operation.take());
182 }
183}
184
185impl<D: DirectoryWriter + 'static> WorkerState<D> {
186 fn record_cycle_error(&self, error: impl Into<String>) {
187 let mut first_error = self.cycle_error.lock();
188 if first_error.is_none() {
189 *first_error = Some(error.into());
190 }
191 drop(first_error);
192 self.cycle_failed.store(true, Ordering::Release);
193 }
194}
195
196impl<D: DirectoryWriter + 'static> Drop for PreparedSegment<D> {
197 fn drop(&mut self) {
198 if self.published {
199 return;
200 }
201 let Some(operation) = self.operation.take() else {
202 return;
203 };
204 self.segment_manager.schedule_unpublished_segment_cleanup(
205 self.segment_id,
206 operation,
207 self.runtime.clone(),
208 );
209 }
210}
211
212impl<D: DirectoryWriter + 'static> IndexWriter<D> {
213 pub async fn create(directory: D, schema: Schema, config: IndexConfig) -> Result<Self> {
215 Self::create_with_config(directory, schema, config, SegmentBuilderConfig::default()).await
216 }
217
218 pub async fn create_with_config(
220 directory: D,
221 schema: Schema,
222 config: IndexConfig,
223 builder_config: SegmentBuilderConfig,
224 ) -> Result<Self> {
225 let directory = Arc::new(directory);
226 let schema = Arc::new(schema);
227 directory.set_index_label(schema.index_label());
229 let metadata = super::IndexMetadata::new((*schema).clone());
230
231 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
232 Arc::clone(&directory),
233 Arc::clone(&schema),
234 metadata,
235 config.merge_policy.clone_box(),
236 config.term_cache_blocks,
237 config.max_concurrent_merges,
238 Arc::clone(&config.background_merge_permits),
239 config.merge_bp_time_budget,
240 config.bp_memory_budget_bytes,
241 Arc::clone(&config.background_reorder_permits),
242 config.background_reorder_pool.clone(),
243 ));
244 segment_manager.update_metadata(|_| {}).await?;
245
246 Ok(Self::new_with_parts(
247 directory,
248 schema,
249 config,
250 builder_config,
251 segment_manager,
252 ))
253 }
254
255 pub async fn open(directory: D, config: IndexConfig) -> Result<Self> {
262 Self::open_with_config(directory, config, SegmentBuilderConfig::default()).await
263 }
264
265 pub async fn open_with_config(
267 directory: D,
268 config: IndexConfig,
269 builder_config: SegmentBuilderConfig,
270 ) -> Result<Self> {
271 let directory = Arc::new(directory);
272 let metadata = super::IndexMetadata::load(directory.as_ref()).await?;
273 let schema = Arc::new(metadata.schema.clone());
274 directory.set_index_label(schema.index_label());
276
277 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
278 Arc::clone(&directory),
279 Arc::clone(&schema),
280 metadata,
281 config.merge_policy.clone_box(),
282 config.term_cache_blocks,
283 config.max_concurrent_merges,
284 Arc::clone(&config.background_merge_permits),
285 config.merge_bp_time_budget,
286 config.bp_memory_budget_bytes,
287 Arc::clone(&config.background_reorder_permits),
288 config.background_reorder_pool.clone(),
289 ));
290 let swept = segment_manager.cleanup_orphan_segments().await?;
291 if swept > 0 {
292 log::warn!(
293 "[segment_cleanup] swept {} orphan segment(s) while opening writer",
294 swept
295 );
296 }
297 segment_manager.load_and_publish_trained().await;
298
299 Ok(Self::new_with_parts(
300 directory,
301 schema,
302 config,
303 builder_config,
304 segment_manager,
305 ))
306 }
307
308 pub fn from_index(index: &super::Index<D>) -> Self {
311 Self::new_with_parts(
312 Arc::clone(&index.directory),
313 Arc::clone(&index.schema),
314 index.config.clone(),
315 SegmentBuilderConfig::default(),
316 Arc::clone(&index.segment_manager),
317 )
318 }
319
320 fn new_with_parts(
326 directory: Arc<D>,
327 schema: Arc<Schema>,
328 config: IndexConfig,
329 builder_config: SegmentBuilderConfig,
330 segment_manager: Arc<crate::merge::SegmentManager<D>>,
331 ) -> Self {
332 let registry = crate::tokenizer::TokenizerRegistry::new();
334 let mut tokenizers = FxHashMap::default();
335 for (field, entry) in schema.fields() {
336 if matches!(entry.field_type, crate::dsl::FieldType::Text)
337 && let Some(ref tok_name) = entry.tokenizer
338 && let Some(tok) = registry.get(tok_name)
339 {
340 tokenizers.insert(field, tok);
341 }
342 }
343
344 let num_workers = config.num_indexing_threads.max(1);
345 let worker_state = Arc::new(WorkerState {
346 directory: Arc::clone(&directory),
347 schema: Arc::clone(&schema),
348 builder_config,
349 tokenizers: parking_lot::RwLock::new(tokenizers),
350 memory_budget_per_worker: config.max_indexing_memory_bytes / num_workers,
351 segment_manager: Arc::clone(&segment_manager),
352 built_segments: parking_lot::Mutex::new(Vec::new()),
353 cycle_error: parking_lot::Mutex::new(None),
354 cycle_failed: AtomicBool::new(false),
355 flush_count: AtomicUsize::new(0),
356 flush_mutex: parking_lot::Mutex::new(()),
357 flush_cvar: parking_lot::Condvar::new(),
358 resume_receiver: parking_lot::Mutex::new(None),
359 resume_epoch: AtomicUsize::new(0),
360 resume_cvar: parking_lot::Condvar::new(),
361 shutdown: AtomicBool::new(false),
362 num_workers,
363 });
364 let (doc_sender, workers) = Self::spawn_workers(&worker_state, num_workers);
365
366 Self {
367 directory,
368 schema,
369 config,
370 doc_sender: Arc::new(parking_lot::RwLock::new(doc_sender)),
371 workers,
372 worker_state,
373 segment_manager,
374 flushed_segments: Arc::new(parking_lot::Mutex::new(Vec::new())),
375 primary_key_index: Arc::new(parking_lot::RwLock::new(None)),
376 commit_finalization: Arc::new(CommitFinalizationState::default()),
377 }
378 }
379
380 fn spawn_workers(
381 worker_state: &Arc<WorkerState<D>>,
382 num_workers: usize,
383 ) -> (
384 async_channel::Sender<Document>,
385 Vec<std::thread::JoinHandle<()>>,
386 ) {
387 let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
388 let handle = tokio::runtime::Handle::current();
389 let mut workers = Vec::with_capacity(num_workers);
390 for i in 0..num_workers {
391 let state = Arc::clone(worker_state);
392 let rx = receiver.clone();
393 let rt = handle.clone();
394 workers.push(
395 std::thread::Builder::new()
396 .name(format!("index-worker-{}", i))
397 .spawn(move || Self::worker_loop(state, rx, rt))
398 .expect("failed to spawn index worker thread"),
399 );
400 }
401 (sender, workers)
402 }
403
404 pub fn schema(&self) -> &Schema {
406 &self.schema
407 }
408
409 pub fn set_tokenizer<T: crate::tokenizer::Tokenizer>(&mut self, field: Field, tokenizer: T) {
412 self.worker_state
413 .tokenizers
414 .write()
415 .insert(field, Box::new(tokenizer));
416 }
417
418 pub async fn init_primary_key_dedup(&mut self) -> Result<()> {
434 use super::primary_key::{PK_BLOOM_FILE, deserialize_pk_bloom};
435
436 self.commit_finalization.wait_until_idle().await;
437
438 let field = match self.schema.primary_field() {
439 Some(f) => f,
440 None => return Ok(()),
441 };
442
443 let snapshot = self.segment_manager.acquire_snapshot().await;
444 let current_seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
445
446 let cached = match self
448 .directory
449 .open_read(std::path::Path::new(PK_BLOOM_FILE))
450 .await
451 {
452 Ok(handle) => {
453 let data = handle.read_bytes_range(0..handle.len()).await;
454 match data {
455 Ok(bytes) => deserialize_pk_bloom(bytes.as_slice()),
456 Err(_) => None,
457 }
458 }
459 Err(_) => None,
460 };
461
462 let load_futures: Vec<_> = current_seg_ids
464 .iter()
465 .map(|seg_id_str| {
466 let seg_id_str = seg_id_str.clone();
467 let dir = self.directory.as_ref();
468 let schema = Arc::clone(&self.schema);
469 async move { load_pk_segment_data(dir, &seg_id_str, &schema).await }
470 })
471 .collect();
472 let all_data = futures::future::try_join_all(load_futures).await?;
473
474 if let Some((persisted_seg_ids, bloom)) = cached {
475 let mut pk_data = Vec::with_capacity(all_data.len());
477 let mut new_data = Vec::new();
478 for d in all_data {
479 if persisted_seg_ids.contains(&d.segment_id) {
480 pk_data.push(d);
481 } else {
482 new_data.push(d);
483 }
484 }
485 let needs_persist = !new_data.is_empty();
486 let new_start = pk_data.len();
487 pk_data.extend(new_data);
488
489 let pk_index = if new_start == pk_data.len() {
490 super::primary_key::PrimaryKeyIndex::from_persisted(
492 field,
493 bloom,
494 pk_data,
495 &[],
496 snapshot,
497 )
498 } else {
499 tokio::task::spawn_blocking(move || {
501 let mut bloom = bloom;
504 let mut added = 0usize;
505 let num_new = pk_data.len() - new_start;
506 for data in &pk_data[new_start..] {
507 if let Some(ff) = data.fast_fields.get(&field.0)
508 && let Some(dict) = ff.text_dict()
509 {
510 for key in dict.iter() {
511 bloom.insert(key.as_bytes());
512 added += 1;
513 }
514 }
515 }
516 if added > 0 {
517 log::info!(
518 "[primary_key] bloom: added {} keys from {} new segment(s)",
519 added,
520 num_new,
521 );
522 }
523 super::primary_key::PrimaryKeyIndex::from_persisted(
524 field,
525 bloom,
526 pk_data,
527 &[],
528 snapshot,
529 )
530 })
531 .await
532 .map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?
533 };
534
535 if needs_persist {
536 self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
537 }
538
539 *self.primary_key_index.write() = Some(pk_index);
540 } else {
541 let pk_index = tokio::task::spawn_blocking(move || {
543 super::primary_key::PrimaryKeyIndex::new(field, all_data, snapshot)
544 })
545 .await
546 .map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?;
547
548 self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
549 *self.primary_key_index.write() = Some(pk_index);
550 }
551
552 Ok(())
553 }
554
555 async fn persist_pk_bloom(
558 &self,
559 pk_index: &super::primary_key::PrimaryKeyIndex,
560 segment_ids: &[String],
561 ) {
562 use super::primary_key::{PK_BLOOM_FILE, serialize_pk_bloom};
563
564 let bloom_bytes = pk_index.bloom_to_bytes();
565 let data = serialize_pk_bloom(segment_ids, &bloom_bytes);
566 if let Err(e) = self
567 .directory
568 .write(std::path::Path::new(PK_BLOOM_FILE), &data)
569 .await
570 {
571 log::warn!("[primary_key] failed to persist bloom cache: {}", e);
572 }
573 }
574
575 pub fn add_document(&self, doc: Document) -> Result<()> {
581 if self.worker_state.shutdown.load(Ordering::Acquire) {
582 return Err(Error::IndexClosed);
583 }
584 if self.commit_finalization.in_progress.load(Ordering::Acquire) {
585 return Err(Error::CommitInProgress);
586 }
587 let sender = self.doc_sender.read().clone();
588 if sender.is_closed() {
592 return Err(Error::CommitInProgress);
593 }
594 let primary_key_index = self.primary_key_index.read();
595 if let Some(ref pk_index) = *primary_key_index {
596 pk_index.check_and_insert(&doc)?;
597 }
598 match sender.try_send(doc) {
599 Ok(()) => Ok(()),
600 Err(async_channel::TrySendError::Full(doc)) => {
601 if let Some(ref pk_index) = *primary_key_index {
603 pk_index.rollback_uncommitted_key(&doc);
604 }
605 Err(Error::QueueFull)
606 }
607 Err(async_channel::TrySendError::Closed(doc)) => {
608 if let Some(ref pk_index) = *primary_key_index {
610 pk_index.rollback_uncommitted_key(&doc);
611 }
612 Err(Error::CommitInProgress)
613 }
614 }
615 }
616
617 pub fn add_documents(&self, documents: Vec<Document>) -> Result<usize> {
622 let total = documents.len();
623 for (i, doc) in documents.into_iter().enumerate() {
624 match self.add_document(doc) {
625 Ok(()) => {}
626 Err(Error::QueueFull | Error::CommitInProgress) => return Ok(i),
627 Err(e) => return Err(e),
628 }
629 }
630 Ok(total)
631 }
632
633 fn worker_loop(
646 state: Arc<WorkerState<D>>,
647 initial_receiver: async_channel::Receiver<Document>,
648 handle: tokio::runtime::Handle,
649 ) {
650 let mut receiver = initial_receiver;
651 let mut my_epoch = 0usize;
652
653 loop {
654 let build_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
658 let mut builder: Option<SegmentBuilder> = None;
659
660 while let Ok(doc) = receiver.recv_blocking() {
661 if state.shutdown.load(Ordering::Acquire) {
662 break;
663 }
664 if state.cycle_failed.load(Ordering::Acquire) {
669 continue;
670 }
671 if builder.is_none() {
673 match SegmentBuilder::new(
674 Arc::clone(&state.schema),
675 state.builder_config.clone(),
676 ) {
677 Ok(mut b) => {
678 for (field, tokenizer) in state.tokenizers.read().iter() {
679 b.set_tokenizer(*field, tokenizer.clone_box());
680 }
681 builder = Some(b);
682 }
683 Err(e) => {
684 log::error!("Failed to create segment builder: {:?}", e);
685 state.record_cycle_error(format!(
686 "failed to create segment builder: {e}"
687 ));
688 continue;
689 }
690 }
691 }
692
693 let b = builder.as_mut().unwrap();
694 if let Err(e) = b.add_document(doc) {
695 log::error!("Failed to index document: {:?}", e);
696 state.record_cycle_error(format!("failed to index document: {e}"));
697 continue;
698 }
699
700 let builder_memory = b.estimated_memory_bytes();
701
702 if b.num_docs() & 0x3FFF == 0 {
703 log::debug!(
704 "[indexing] docs={}, memory={:.2} MB, budget={:.2} MB",
705 b.num_docs(),
706 builder_memory as f64 / (1024.0 * 1024.0),
707 state.memory_budget_per_worker as f64 / (1024.0 * 1024.0)
708 );
709 }
710
711 const MIN_DOCS_BEFORE_FLUSH: u32 = 100;
713
714 let effective_budget = state.memory_budget_per_worker * 4 / 5;
718
719 if builder_memory >= effective_budget && b.num_docs() >= MIN_DOCS_BEFORE_FLUSH {
720 log::info!(
721 "[indexing] memory budget reached, building segment: \
722 docs={}, memory={:.2} MB, budget={:.2} MB",
723 b.num_docs(),
724 builder_memory as f64 / (1024.0 * 1024.0),
725 state.memory_budget_per_worker as f64 / (1024.0 * 1024.0),
726 );
727 let full_builder = builder.take().unwrap();
728 Self::build_segment_inline(&state, full_builder, &handle);
729 }
730 }
731
732 if !state.cycle_failed.load(Ordering::Acquire)
734 && let Some(b) = builder.take()
735 && b.num_docs() > 0
736 {
737 Self::build_segment_inline(&state, b, &handle);
738 }
739 }));
740
741 if build_result.is_err() {
742 log::error!(
743 "[worker] panic during indexing cycle — documents in this cycle may be lost"
744 );
745 state.record_cycle_error("indexing worker panicked while building the batch");
746 }
747
748 let prev = state.flush_count.fetch_add(1, Ordering::Release);
751 if prev + 1 == state.num_workers {
752 let _lock = state.flush_mutex.lock();
754 state.flush_cvar.notify_one();
755 }
756
757 {
761 let mut lock = state.resume_receiver.lock();
762 loop {
763 if state.shutdown.load(Ordering::Acquire) {
764 return;
765 }
766 let current_epoch = state.resume_epoch.load(Ordering::Acquire);
767 if current_epoch > my_epoch
768 && let Some(rx) = lock.as_ref()
769 {
770 receiver = rx.clone();
771 my_epoch = current_epoch;
772 break;
773 }
774 state.resume_cvar.wait(&mut lock);
775 }
776 }
777 }
778 }
779
780 fn build_segment_inline(
784 state: &WorkerState<D>,
785 builder: SegmentBuilder,
786 handle: &tokio::runtime::Handle,
787 ) {
788 let segment_id = SegmentId::new();
789 let segment_hex = segment_id.to_hex();
790 let operation = match state
793 .segment_manager
794 .protect_new_segment(segment_hex.clone())
795 {
796 Ok(operation) => operation,
797 Err(e) => {
798 log::error!(
799 "[segment_build_failed] segment_id={} lifecycle_error={}",
800 segment_hex,
801 e,
802 );
803 state.record_cycle_error(format!(
804 "failed to claim segment {segment_hex} for building: {e}"
805 ));
806 return;
807 }
808 };
809 let trained = state.segment_manager.trained();
810 let doc_count = builder.num_docs();
811 let build_start = std::time::Instant::now();
812
813 log::info!(
814 "[segment_build] segment_id={} doc_count={} ann={}",
815 segment_hex,
816 doc_count,
817 trained.is_some()
818 );
819
820 let mut prepared = PreparedSegment {
824 id: segment_hex.clone(),
825 segment_id,
826 num_docs: doc_count,
827 segment_manager: Arc::clone(&state.segment_manager),
828 operation: Some(operation),
829 runtime: handle.clone(),
830 published: false,
831 };
832
833 match handle.block_on(builder.build(
834 state.directory.as_ref(),
835 segment_id,
836 trained.as_deref(),
837 )) {
838 Ok(meta) if meta.num_docs == doc_count && meta.num_docs > 0 => {
839 let duration_ms = build_start.elapsed().as_millis() as u64;
840 log::info!(
841 "[segment_build_done] segment_id={} doc_count={} duration_ms={}",
842 segment_hex,
843 meta.num_docs,
844 duration_ms,
845 );
846 prepared.num_docs = meta.num_docs;
847 state.built_segments.lock().push(prepared);
848 }
849 Ok(meta) => {
850 let error = format!(
851 "segment {segment_hex} built {} docs from a {doc_count}-document builder",
852 meta.num_docs
853 );
854 log::error!("[segment_build_failed] {error}");
855 state.record_cycle_error(error);
856 }
857 Err(e) => {
858 log::error!(
859 "[segment_build_failed] segment_id={} error={:?}",
860 segment_hex,
861 e
862 );
863 state.record_cycle_error(format!("failed to build segment {segment_hex}: {e}"));
866 }
867 }
868 }
869
870 pub async fn maybe_merge(&self) {
876 self.segment_manager.maybe_merge().await;
877 }
878
879 pub async fn abort_merges(&self) {
882 self.segment_manager.abort_merges().await;
883 }
884
885 pub async fn shutdown(&mut self) -> Result<()> {
890 self.segment_manager.begin_shutdown();
891 self.signal_worker_shutdown();
892
893 self.commit_finalization.wait_until_idle().await;
898
899 let workers = std::mem::take(&mut self.workers);
900 let panicked = tokio::task::spawn_blocking(move || {
901 workers
902 .into_iter()
903 .map(|worker| worker.join().is_err())
904 .filter(|panicked| *panicked)
905 .count()
906 })
907 .await
908 .map_err(|error| Error::Internal(format!("failed to join index workers: {}", error)))?;
909 if panicked > 0 {
910 log::error!("[index_shutdown] {} indexing worker(s) panicked", panicked);
911 }
912
913 self.flushed_segments.lock().clear();
916 self.worker_state.built_segments.lock().clear();
917 if let Some(pk_index) = self.primary_key_index.write().as_mut() {
918 pk_index.clear_uncommitted();
919 }
920 Ok(())
921 }
922
923 pub async fn wait_for_merging_thread(&self) {
925 self.segment_manager.wait_for_merging_thread().await;
926 }
927
928 pub async fn wait_for_all_merges(&self) {
930 self.segment_manager.wait_for_all_merges().await;
931 }
932
933 pub async fn wait_for_commit_finalization(&self) {
938 self.commit_finalization.wait_until_idle().await;
939 }
940
941 pub fn tracker(&self) -> std::sync::Arc<crate::segment::SegmentTracker> {
943 self.segment_manager.tracker()
944 }
945
946 pub async fn acquire_snapshot(&self) -> crate::segment::SegmentSnapshot {
948 self.segment_manager.acquire_snapshot().await
949 }
950
951 pub async fn cleanup_orphan_segments(&self) -> Result<usize> {
953 self.segment_manager.cleanup_orphan_segments().await
954 }
955
956 pub async fn prepare_commit(&mut self) -> Result<PreparedCommit<'_, D>> {
967 if self.worker_state.shutdown.load(Ordering::Acquire) {
968 return Err(Error::IndexClosed);
969 }
970 if self.commit_finalization.in_progress.load(Ordering::Acquire) {
971 return Err(Error::CommitInProgress);
972 }
973 self.doc_sender.read().close();
975
976 self.worker_state.resume_cvar.notify_all();
980
981 let state = Arc::clone(&self.worker_state);
984 let all_flushed = tokio::task::spawn_blocking(move || {
985 let mut lock = state.flush_mutex.lock();
986 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(300);
987 while state.flush_count.load(Ordering::Acquire) < state.num_workers {
988 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
989 if remaining.is_zero() {
990 log::error!(
991 "[prepare_commit] timed out waiting for workers: {}/{} flushed",
992 state.flush_count.load(Ordering::Acquire),
993 state.num_workers
994 );
995 return false;
996 }
997 state.flush_cvar.wait_for(&mut lock, remaining);
998 }
999 true
1000 })
1001 .await
1002 .map_err(|e| Error::Internal(format!("Failed to wait for workers: {}", e)))?;
1003
1004 if !all_flushed {
1005 return Err(Error::Internal(format!(
1013 "prepare_commit timed out: {}/{} workers flushed; writer remains paused, retry commit",
1014 self.worker_state.flush_count.load(Ordering::Acquire),
1015 self.worker_state.num_workers
1016 )));
1017 }
1018
1019 let cycle_error = { self.worker_state.cycle_error.lock().take() };
1020 if let Some(error) = cycle_error {
1021 self.flushed_segments.lock().clear();
1026 self.worker_state.built_segments.lock().clear();
1027 if let Some(pk_index) = self.primary_key_index.write().as_mut() {
1028 pk_index.clear_uncommitted();
1029 }
1030 self.resume_workers();
1031 return Err(Error::Internal(format!(
1032 "indexing generation failed; no documents from this batch were committed: {error}"
1033 )));
1034 }
1035
1036 let built = std::mem::take(&mut *self.worker_state.built_segments.lock());
1038 self.flushed_segments.lock().extend(built);
1039
1040 Ok(PreparedCommit {
1041 writer: self,
1042 is_resolved: false,
1043 })
1044 }
1045
1046 pub async fn commit(&mut self) -> Result<bool> {
1051 self.prepare_commit().await?.commit().await
1052 }
1053
1054 pub async fn force_merge(&mut self) -> Result<()> {
1056 self.prepare_commit().await?.commit().await?;
1057 self.segment_manager.force_merge().await
1058 }
1059
1060 pub async fn reorder(&mut self) -> Result<()> {
1065 self.prepare_commit().await?.commit().await?;
1066 self.segment_manager.reorder_segments().await
1067 }
1068
1069 pub fn segment_manager(&self) -> &Arc<crate::merge::SegmentManager<D>> {
1071 &self.segment_manager
1072 }
1073
1074 fn resume_workers(&mut self) {
1079 Self::resume_workers_shared(&self.worker_state, &self.doc_sender);
1080 }
1081
1082 fn resume_workers_shared(
1083 worker_state: &Arc<WorkerState<D>>,
1084 doc_sender: &Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
1085 ) {
1086 if worker_state.shutdown.load(Ordering::Acquire) {
1087 return;
1088 }
1089 if tokio::runtime::Handle::try_current().is_err() {
1090 worker_state.shutdown.store(true, Ordering::Release);
1093 worker_state.resume_cvar.notify_all();
1094 return;
1095 }
1096
1097 worker_state.flush_count.store(0, Ordering::Release);
1099 *worker_state.cycle_error.lock() = None;
1100 worker_state.cycle_failed.store(false, Ordering::Release);
1101
1102 let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
1104 *doc_sender.write() = sender;
1105
1106 {
1108 let mut lock = worker_state.resume_receiver.lock();
1109 *lock = Some(receiver);
1110 }
1111 worker_state.resume_epoch.fetch_add(1, Ordering::Release);
1112 worker_state.resume_cvar.notify_all();
1113 }
1114
1115 fn signal_worker_shutdown(&self) {
1116 self.worker_state.shutdown.store(true, Ordering::Release);
1117 self.doc_sender.read().close();
1118 self.worker_state.resume_cvar.notify_all();
1119 }
1120
1121 }
1123
1124impl<D: DirectoryWriter + 'static> Drop for IndexWriter<D> {
1125 fn drop(&mut self) {
1126 self.signal_worker_shutdown();
1127 for w in std::mem::take(&mut self.workers) {
1128 let _ = w.join();
1129 }
1130 }
1131}
1132
1133pub struct PreparedCommit<'a, D: DirectoryWriter + 'static> {
1140 writer: &'a mut IndexWriter<D>,
1141 is_resolved: bool,
1142}
1143
1144struct PreparedSegmentsGuard<D: DirectoryWriter + 'static> {
1149 segments: Option<Vec<PreparedSegment<D>>>,
1150 retry_slot: Arc<parking_lot::Mutex<Vec<PreparedSegment<D>>>>,
1151}
1152
1153impl<D: DirectoryWriter + 'static> PreparedSegmentsGuard<D> {
1154 fn metadata_entries(&self) -> Vec<(String, u32)> {
1155 self.segments
1156 .as_deref()
1157 .unwrap_or_default()
1158 .iter()
1159 .map(PreparedSegment::metadata_entry)
1160 .collect()
1161 }
1162
1163 fn take_published(&mut self) -> Vec<PreparedSegment<D>> {
1164 self.segments.take().unwrap_or_default()
1165 }
1166}
1167
1168impl<D: DirectoryWriter + 'static> Drop for PreparedSegmentsGuard<D> {
1169 fn drop(&mut self) {
1170 if let Some(segments) = self.segments.take() {
1171 self.retry_slot.lock().extend(segments);
1172 }
1173 }
1174}
1175
1176struct CommitFinalizationGuard<D: DirectoryWriter + 'static> {
1181 state: Arc<CommitFinalizationState>,
1182 worker_state: Arc<WorkerState<D>>,
1183 doc_sender: Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
1184 resume_workers: bool,
1185}
1186
1187impl<D: DirectoryWriter + 'static> CommitFinalizationGuard<D> {
1188 fn resume_on_drop(&mut self) {
1189 self.resume_workers = true;
1190 }
1191}
1192
1193impl<D: DirectoryWriter + 'static> Drop for CommitFinalizationGuard<D> {
1194 fn drop(&mut self) {
1195 if self.resume_workers {
1196 IndexWriter::<D>::resume_workers_shared(&self.worker_state, &self.doc_sender);
1197 }
1198 self.state.finish();
1199 }
1200}
1201
1202struct OwnedCommitFinalization<D: DirectoryWriter + 'static> {
1207 directory: Arc<D>,
1208 schema: Arc<Schema>,
1209 segment_manager: Arc<crate::merge::SegmentManager<D>>,
1210 primary_key_index: Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
1211 prepared: PreparedSegmentsGuard<D>,
1212 finalization: Option<CommitFinalizationGuard<D>>,
1213 publication_observed: Arc<AtomicBool>,
1214}
1215
1216async fn refresh_primary_key_after_commit<D: DirectoryWriter + 'static>(
1217 directory: &Arc<D>,
1218 schema: &Arc<Schema>,
1219 segment_manager: &Arc<crate::merge::SegmentManager<D>>,
1220 primary_key_index: &Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
1221) -> Result<()> {
1222 let existing_ids: std::collections::HashSet<String> = {
1223 let guard = primary_key_index.read();
1224 let Some(pk_index) = guard.as_ref() else {
1225 return Ok(());
1226 };
1227 pk_index
1228 .committed_segment_ids()
1229 .map(ToOwned::to_owned)
1230 .collect()
1231 };
1232
1233 let snapshot = segment_manager.acquire_snapshot().await;
1234 let load_futures: Vec<_> = snapshot
1235 .segment_ids()
1236 .iter()
1237 .filter(|id| !existing_ids.contains(id.as_str()))
1238 .map(|seg_id_str| {
1239 let seg_id_str = seg_id_str.clone();
1240 let dir = directory.as_ref();
1241 let schema = Arc::clone(schema);
1242 async move { load_pk_segment_data(dir, &seg_id_str, &schema).await }
1243 })
1244 .collect();
1245 let new_data = futures::future::try_join_all(load_futures).await?;
1246 let seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
1247
1248 let bloom_file = {
1249 let mut guard = primary_key_index.write();
1250 let Some(pk_index) = guard.as_mut() else {
1251 return Ok(());
1252 };
1253 pk_index.refresh_incremental(new_data, snapshot);
1254 let bloom_bytes = pk_index.bloom_to_bytes();
1255 super::primary_key::serialize_pk_bloom(&seg_ids, &bloom_bytes)
1256 };
1257
1258 if let Err(error) = directory
1259 .write(
1260 std::path::Path::new(super::primary_key::PK_BLOOM_FILE),
1261 &bloom_file,
1262 )
1263 .await
1264 {
1265 log::warn!("[primary_key] failed to persist bloom cache: {}", error);
1266 }
1267 Ok(())
1268}
1269
1270async fn finalize_prepared_commit<D: DirectoryWriter + 'static>(
1271 mut commit: OwnedCommitFinalization<D>,
1272) -> Result<bool> {
1273 let metadata_entries = commit.prepared.metadata_entries();
1274
1275 commit.segment_manager.commit(&metadata_entries).await?;
1279 commit.publication_observed.store(true, Ordering::Release);
1280
1281 let mut published = commit.prepared.take_published();
1282 for segment in &mut published {
1283 segment.mark_published();
1284 }
1285 drop(published);
1286 if let Some(finalization) = commit.finalization.as_mut() {
1290 finalization.resume_on_drop();
1291 } else {
1292 log::error!("owned commit finalization guard was already released after publication");
1293 }
1294
1295 if let Err(error) = refresh_primary_key_after_commit(
1300 &commit.directory,
1301 &commit.schema,
1302 &commit.segment_manager,
1303 &commit.primary_key_index,
1304 )
1305 .await
1306 {
1307 log::error!(
1308 "[primary_key] committed metadata but failed to refresh dedup state; \
1309 retaining reservations until a later successful commit: {}",
1310 error,
1311 );
1312 }
1313
1314 drop(commit.finalization.take());
1318 commit.segment_manager.maybe_merge().await;
1319 Ok(true)
1320}
1321
1322impl<'a, D: DirectoryWriter + 'static> PreparedCommit<'a, D> {
1323 pub async fn commit(mut self) -> Result<bool> {
1327 let segments = std::mem::take(&mut *self.writer.flushed_segments.lock());
1328
1329 if segments.is_empty() {
1331 log::debug!("[commit] no segments to commit, skipping");
1332 self.is_resolved = true;
1333 self.writer.resume_workers();
1334 return Ok(false);
1335 }
1336
1337 if !self.writer.commit_finalization.begin() {
1338 self.writer.flushed_segments.lock().extend(segments);
1339 self.is_resolved = true;
1343 return Err(Error::CommitInProgress);
1344 }
1345
1346 let publication_observed = Arc::new(AtomicBool::new(false));
1347 let owned = OwnedCommitFinalization {
1348 directory: Arc::clone(&self.writer.directory),
1349 schema: Arc::clone(&self.writer.schema),
1350 segment_manager: Arc::clone(&self.writer.segment_manager),
1351 primary_key_index: Arc::clone(&self.writer.primary_key_index),
1352 prepared: PreparedSegmentsGuard {
1353 segments: Some(segments),
1354 retry_slot: Arc::clone(&self.writer.flushed_segments),
1355 },
1356 finalization: Some(CommitFinalizationGuard {
1357 state: Arc::clone(&self.writer.commit_finalization),
1358 worker_state: Arc::clone(&self.writer.worker_state),
1359 doc_sender: Arc::clone(&self.writer.doc_sender),
1360 resume_workers: false,
1361 }),
1362 publication_observed: Arc::clone(&publication_observed),
1363 };
1364
1365 self.is_resolved = true;
1370 let task_publication = Arc::clone(&publication_observed);
1371 let task = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1372 tokio::spawn(async move {
1373 match std::panic::AssertUnwindSafe(finalize_prepared_commit(owned))
1374 .catch_unwind()
1375 .await
1376 {
1377 Ok(result) => result,
1378 Err(_) if task_publication.load(Ordering::Acquire) => {
1379 log::error!(
1380 "owned commit finalizer panicked after metadata publication; \
1381 treating the durable generation as committed"
1382 );
1383 Ok(true)
1384 }
1385 Err(_) => Err(Error::Internal(
1386 "owned commit finalizer panicked before metadata publication".into(),
1387 )),
1388 }
1389 })
1390 }))
1391 .map_err(|_| Error::Internal("runtime rejected owned commit finalizer".into()))?;
1392
1393 match task.await {
1394 Ok(result) => result,
1395 Err(error) if publication_observed.load(Ordering::Acquire) => {
1396 log::error!(
1397 "owned commit finalizer terminated after metadata publication: {}; \
1398 treating the durable generation as committed",
1399 error,
1400 );
1401 Ok(true)
1402 }
1403 Err(error) => Err(Error::Internal(format!(
1404 "owned commit finalizer terminated unexpectedly: {error}"
1405 ))),
1406 }
1407 }
1408
1409 pub fn abort(mut self) {
1412 self.is_resolved = true;
1413 self.writer.flushed_segments.lock().clear();
1414 if let Some(pk_index) = self.writer.primary_key_index.write().as_mut() {
1415 pk_index.clear_uncommitted();
1416 }
1417 self.writer.resume_workers();
1418 }
1419}
1420
1421impl<D: DirectoryWriter + 'static> Drop for PreparedCommit<'_, D> {
1422 fn drop(&mut self) {
1423 if !self.is_resolved {
1424 log::warn!("PreparedCommit dropped without commit/abort — auto-aborting");
1425 self.writer.flushed_segments.lock().clear();
1426 if let Some(pk_index) = self.writer.primary_key_index.write().as_mut() {
1427 pk_index.clear_uncommitted();
1428 }
1429 self.writer.resume_workers();
1430 }
1431 }
1432}
1433
1434async fn load_pk_segment_data<D: crate::directories::Directory>(
1436 dir: &D,
1437 seg_id_str: &str,
1438 schema: &Arc<crate::dsl::Schema>,
1439) -> Result<super::primary_key::PkSegmentData> {
1440 let seg_id = crate::segment::SegmentId::from_hex(seg_id_str)
1441 .ok_or_else(|| Error::Internal(format!("Invalid segment id: {}", seg_id_str)))?;
1442 let files = crate::segment::SegmentFiles::new(seg_id.0);
1443 let fast_fields =
1444 crate::segment::reader::loader::load_fast_fields_file(dir, &files, schema).await?;
1445 Ok(super::primary_key::PkSegmentData {
1446 segment_id: seg_id_str.to_string(),
1447 fast_fields,
1448 })
1449}