1use std::sync::Arc;
33use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
34
35use rustc_hash::FxHashMap;
36
37use crate::directories::DirectoryWriter;
38use crate::dsl::{Document, Field, Schema};
39use crate::error::{Error, Result};
40use crate::segment::{SegmentBuilder, SegmentBuilderConfig, SegmentId};
41use crate::tokenizer::BoxedTokenizer;
42
43use super::IndexConfig;
44
45const PIPELINE_MAX_SIZE_IN_DOCS: usize = 10_000;
47
48pub struct IndexWriter<D: DirectoryWriter + 'static> {
60 pub(super) directory: Arc<D>,
61 pub(super) schema: Arc<Schema>,
62 pub(super) config: IndexConfig,
63 doc_sender: async_channel::Sender<Document>,
66 workers: Vec<std::thread::JoinHandle<()>>,
68 worker_state: Arc<WorkerState<D>>,
70 pub(super) segment_manager: Arc<crate::merge::SegmentManager<D>>,
72 flushed_segments: Vec<PreparedSegment<D>>,
75 primary_key_index: Option<super::primary_key::PrimaryKeyIndex>,
77}
78
79struct WorkerState<D: DirectoryWriter + 'static> {
81 directory: Arc<D>,
82 schema: Arc<Schema>,
83 builder_config: SegmentBuilderConfig,
84 tokenizers: parking_lot::RwLock<FxHashMap<Field, BoxedTokenizer>>,
85 memory_budget_per_worker: usize,
87 segment_manager: Arc<crate::merge::SegmentManager<D>>,
89 built_segments: parking_lot::Mutex<Vec<PreparedSegment<D>>>,
92 cycle_error: parking_lot::Mutex<Option<String>>,
97 cycle_failed: AtomicBool,
98
99 flush_count: AtomicUsize,
106 flush_mutex: parking_lot::Mutex<()>,
108 flush_cvar: parking_lot::Condvar,
109 resume_receiver: parking_lot::Mutex<Option<async_channel::Receiver<Document>>>,
111 resume_epoch: AtomicUsize,
114 resume_cvar: parking_lot::Condvar,
116 shutdown: AtomicBool,
118 num_workers: usize,
120}
121
122struct PreparedSegment<D: DirectoryWriter + 'static> {
128 id: String,
129 segment_id: SegmentId,
130 num_docs: u32,
131 segment_manager: Arc<crate::merge::SegmentManager<D>>,
132 operation: Option<crate::merge::SegmentOperationGuard>,
133 runtime: tokio::runtime::Handle,
134 published: bool,
135}
136
137impl<D: DirectoryWriter + 'static> PreparedSegment<D> {
138 fn metadata_entry(&self) -> (String, u32) {
139 (self.id.clone(), self.num_docs)
140 }
141
142 fn mark_published(&mut self) {
143 self.published = true;
144 drop(self.operation.take());
146 }
147}
148
149impl<D: DirectoryWriter + 'static> WorkerState<D> {
150 fn record_cycle_error(&self, error: impl Into<String>) {
151 let mut first_error = self.cycle_error.lock();
152 if first_error.is_none() {
153 *first_error = Some(error.into());
154 }
155 drop(first_error);
156 self.cycle_failed.store(true, Ordering::Release);
157 }
158}
159
160impl<D: DirectoryWriter + 'static> Drop for PreparedSegment<D> {
161 fn drop(&mut self) {
162 if self.published {
163 return;
164 }
165 let Some(operation) = self.operation.take() else {
166 return;
167 };
168 self.segment_manager.schedule_unpublished_segment_cleanup(
169 self.segment_id,
170 operation,
171 self.runtime.clone(),
172 );
173 }
174}
175
176impl<D: DirectoryWriter + 'static> IndexWriter<D> {
177 pub async fn create(directory: D, schema: Schema, config: IndexConfig) -> Result<Self> {
179 Self::create_with_config(directory, schema, config, SegmentBuilderConfig::default()).await
180 }
181
182 pub async fn create_with_config(
184 directory: D,
185 schema: Schema,
186 config: IndexConfig,
187 builder_config: SegmentBuilderConfig,
188 ) -> Result<Self> {
189 let directory = Arc::new(directory);
190 let schema = Arc::new(schema);
191 directory.set_index_label(schema.index_label());
193 let metadata = super::IndexMetadata::new((*schema).clone());
194
195 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
196 Arc::clone(&directory),
197 Arc::clone(&schema),
198 metadata,
199 config.merge_policy.clone_box(),
200 config.term_cache_blocks,
201 config.max_concurrent_merges,
202 Arc::clone(&config.background_merge_permits),
203 config.merge_bp_time_budget,
204 config.bp_memory_budget_bytes,
205 Arc::clone(&config.background_reorder_permits),
206 config.background_reorder_pool.clone(),
207 ));
208 segment_manager.update_metadata(|_| {}).await?;
209
210 Ok(Self::new_with_parts(
211 directory,
212 schema,
213 config,
214 builder_config,
215 segment_manager,
216 ))
217 }
218
219 pub async fn open(directory: D, config: IndexConfig) -> Result<Self> {
226 Self::open_with_config(directory, config, SegmentBuilderConfig::default()).await
227 }
228
229 pub async fn open_with_config(
231 directory: D,
232 config: IndexConfig,
233 builder_config: SegmentBuilderConfig,
234 ) -> Result<Self> {
235 let directory = Arc::new(directory);
236 let metadata = super::IndexMetadata::load(directory.as_ref()).await?;
237 let schema = Arc::new(metadata.schema.clone());
238 directory.set_index_label(schema.index_label());
240
241 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
242 Arc::clone(&directory),
243 Arc::clone(&schema),
244 metadata,
245 config.merge_policy.clone_box(),
246 config.term_cache_blocks,
247 config.max_concurrent_merges,
248 Arc::clone(&config.background_merge_permits),
249 config.merge_bp_time_budget,
250 config.bp_memory_budget_bytes,
251 Arc::clone(&config.background_reorder_permits),
252 config.background_reorder_pool.clone(),
253 ));
254 let swept = segment_manager.cleanup_orphan_segments().await?;
255 if swept > 0 {
256 log::warn!(
257 "[segment_cleanup] swept {} orphan segment(s) while opening writer",
258 swept
259 );
260 }
261 segment_manager.load_and_publish_trained().await;
262
263 Ok(Self::new_with_parts(
264 directory,
265 schema,
266 config,
267 builder_config,
268 segment_manager,
269 ))
270 }
271
272 pub fn from_index(index: &super::Index<D>) -> Self {
275 Self::new_with_parts(
276 Arc::clone(&index.directory),
277 Arc::clone(&index.schema),
278 index.config.clone(),
279 SegmentBuilderConfig::default(),
280 Arc::clone(&index.segment_manager),
281 )
282 }
283
284 fn new_with_parts(
290 directory: Arc<D>,
291 schema: Arc<Schema>,
292 config: IndexConfig,
293 builder_config: SegmentBuilderConfig,
294 segment_manager: Arc<crate::merge::SegmentManager<D>>,
295 ) -> Self {
296 let registry = crate::tokenizer::TokenizerRegistry::new();
298 let mut tokenizers = FxHashMap::default();
299 for (field, entry) in schema.fields() {
300 if matches!(entry.field_type, crate::dsl::FieldType::Text)
301 && let Some(ref tok_name) = entry.tokenizer
302 && let Some(tok) = registry.get(tok_name)
303 {
304 tokenizers.insert(field, tok);
305 }
306 }
307
308 let num_workers = config.num_indexing_threads.max(1);
309 let worker_state = Arc::new(WorkerState {
310 directory: Arc::clone(&directory),
311 schema: Arc::clone(&schema),
312 builder_config,
313 tokenizers: parking_lot::RwLock::new(tokenizers),
314 memory_budget_per_worker: config.max_indexing_memory_bytes / num_workers,
315 segment_manager: Arc::clone(&segment_manager),
316 built_segments: parking_lot::Mutex::new(Vec::new()),
317 cycle_error: parking_lot::Mutex::new(None),
318 cycle_failed: AtomicBool::new(false),
319 flush_count: AtomicUsize::new(0),
320 flush_mutex: parking_lot::Mutex::new(()),
321 flush_cvar: parking_lot::Condvar::new(),
322 resume_receiver: parking_lot::Mutex::new(None),
323 resume_epoch: AtomicUsize::new(0),
324 resume_cvar: parking_lot::Condvar::new(),
325 shutdown: AtomicBool::new(false),
326 num_workers,
327 });
328 let (doc_sender, workers) = Self::spawn_workers(&worker_state, num_workers);
329
330 Self {
331 directory,
332 schema,
333 config,
334 doc_sender,
335 workers,
336 worker_state,
337 segment_manager,
338 flushed_segments: Vec::new(),
339 primary_key_index: None,
340 }
341 }
342
343 fn spawn_workers(
344 worker_state: &Arc<WorkerState<D>>,
345 num_workers: usize,
346 ) -> (
347 async_channel::Sender<Document>,
348 Vec<std::thread::JoinHandle<()>>,
349 ) {
350 let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
351 let handle = tokio::runtime::Handle::current();
352 let mut workers = Vec::with_capacity(num_workers);
353 for i in 0..num_workers {
354 let state = Arc::clone(worker_state);
355 let rx = receiver.clone();
356 let rt = handle.clone();
357 workers.push(
358 std::thread::Builder::new()
359 .name(format!("index-worker-{}", i))
360 .spawn(move || Self::worker_loop(state, rx, rt))
361 .expect("failed to spawn index worker thread"),
362 );
363 }
364 (sender, workers)
365 }
366
367 pub fn schema(&self) -> &Schema {
369 &self.schema
370 }
371
372 pub fn set_tokenizer<T: crate::tokenizer::Tokenizer>(&mut self, field: Field, tokenizer: T) {
375 self.worker_state
376 .tokenizers
377 .write()
378 .insert(field, Box::new(tokenizer));
379 }
380
381 pub async fn init_primary_key_dedup(&mut self) -> Result<()> {
397 use super::primary_key::{PK_BLOOM_FILE, deserialize_pk_bloom};
398
399 let field = match self.schema.primary_field() {
400 Some(f) => f,
401 None => return Ok(()),
402 };
403
404 let snapshot = self.segment_manager.acquire_snapshot().await;
405 let current_seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
406
407 let cached = match self
409 .directory
410 .open_read(std::path::Path::new(PK_BLOOM_FILE))
411 .await
412 {
413 Ok(handle) => {
414 let data = handle.read_bytes_range(0..handle.len()).await;
415 match data {
416 Ok(bytes) => deserialize_pk_bloom(bytes.as_slice()),
417 Err(_) => None,
418 }
419 }
420 Err(_) => None,
421 };
422
423 let load_futures: Vec<_> = current_seg_ids
425 .iter()
426 .map(|seg_id_str| {
427 let seg_id_str = seg_id_str.clone();
428 let dir = self.directory.as_ref();
429 let schema = Arc::clone(&self.schema);
430 async move { load_pk_segment_data(dir, &seg_id_str, &schema).await }
431 })
432 .collect();
433 let all_data = futures::future::try_join_all(load_futures).await?;
434
435 if let Some((persisted_seg_ids, bloom)) = cached {
436 let mut pk_data = Vec::with_capacity(all_data.len());
438 let mut new_data = Vec::new();
439 for d in all_data {
440 if persisted_seg_ids.contains(&d.segment_id) {
441 pk_data.push(d);
442 } else {
443 new_data.push(d);
444 }
445 }
446 let needs_persist = !new_data.is_empty();
447 let new_start = pk_data.len();
448 pk_data.extend(new_data);
449
450 let pk_index = if new_start == pk_data.len() {
451 super::primary_key::PrimaryKeyIndex::from_persisted(
453 field,
454 bloom,
455 pk_data,
456 &[],
457 snapshot,
458 )
459 } else {
460 tokio::task::spawn_blocking(move || {
462 let mut bloom = bloom;
465 let mut added = 0usize;
466 let num_new = pk_data.len() - new_start;
467 for data in &pk_data[new_start..] {
468 if let Some(ff) = data.fast_fields.get(&field.0)
469 && let Some(dict) = ff.text_dict()
470 {
471 for key in dict.iter() {
472 bloom.insert(key.as_bytes());
473 added += 1;
474 }
475 }
476 }
477 if added > 0 {
478 log::info!(
479 "[primary_key] bloom: added {} keys from {} new segment(s)",
480 added,
481 num_new,
482 );
483 }
484 super::primary_key::PrimaryKeyIndex::from_persisted(
485 field,
486 bloom,
487 pk_data,
488 &[],
489 snapshot,
490 )
491 })
492 .await
493 .map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?
494 };
495
496 if needs_persist {
497 self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
498 }
499
500 self.primary_key_index = Some(pk_index);
501 } else {
502 let pk_index = tokio::task::spawn_blocking(move || {
504 super::primary_key::PrimaryKeyIndex::new(field, all_data, snapshot)
505 })
506 .await
507 .map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?;
508
509 self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
510 self.primary_key_index = Some(pk_index);
511 }
512
513 Ok(())
514 }
515
516 async fn persist_pk_bloom(
519 &self,
520 pk_index: &super::primary_key::PrimaryKeyIndex,
521 segment_ids: &[String],
522 ) {
523 use super::primary_key::{PK_BLOOM_FILE, serialize_pk_bloom};
524
525 let bloom_bytes = pk_index.bloom_to_bytes();
526 let data = serialize_pk_bloom(segment_ids, &bloom_bytes);
527 if let Err(e) = self
528 .directory
529 .write(std::path::Path::new(PK_BLOOM_FILE), &data)
530 .await
531 {
532 log::warn!("[primary_key] failed to persist bloom cache: {}", e);
533 }
534 }
535
536 pub fn add_document(&self, doc: Document) -> Result<()> {
541 if self.worker_state.shutdown.load(Ordering::Acquire) {
542 return Err(Error::IndexClosed);
543 }
544 if let Some(ref pk_index) = self.primary_key_index {
545 pk_index.check_and_insert(&doc)?;
546 }
547 match self.doc_sender.try_send(doc) {
548 Ok(()) => Ok(()),
549 Err(async_channel::TrySendError::Full(doc)) => {
550 if let Some(ref pk_index) = self.primary_key_index {
552 pk_index.rollback_uncommitted_key(&doc);
553 }
554 Err(Error::QueueFull)
555 }
556 Err(async_channel::TrySendError::Closed(doc)) => {
557 if let Some(ref pk_index) = self.primary_key_index {
559 pk_index.rollback_uncommitted_key(&doc);
560 }
561 Err(Error::Internal("Document channel closed".into()))
562 }
563 }
564 }
565
566 pub fn add_documents(&self, documents: Vec<Document>) -> Result<usize> {
571 let total = documents.len();
572 for (i, doc) in documents.into_iter().enumerate() {
573 match self.add_document(doc) {
574 Ok(()) => {}
575 Err(Error::QueueFull) => return Ok(i),
576 Err(e) => return Err(e),
577 }
578 }
579 Ok(total)
580 }
581
582 fn worker_loop(
595 state: Arc<WorkerState<D>>,
596 initial_receiver: async_channel::Receiver<Document>,
597 handle: tokio::runtime::Handle,
598 ) {
599 let mut receiver = initial_receiver;
600 let mut my_epoch = 0usize;
601
602 loop {
603 let build_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
607 let mut builder: Option<SegmentBuilder> = None;
608
609 while let Ok(doc) = receiver.recv_blocking() {
610 if state.shutdown.load(Ordering::Acquire) {
611 break;
612 }
613 if state.cycle_failed.load(Ordering::Acquire) {
618 continue;
619 }
620 if builder.is_none() {
622 match SegmentBuilder::new(
623 Arc::clone(&state.schema),
624 state.builder_config.clone(),
625 ) {
626 Ok(mut b) => {
627 for (field, tokenizer) in state.tokenizers.read().iter() {
628 b.set_tokenizer(*field, tokenizer.clone_box());
629 }
630 builder = Some(b);
631 }
632 Err(e) => {
633 log::error!("Failed to create segment builder: {:?}", e);
634 state.record_cycle_error(format!(
635 "failed to create segment builder: {e}"
636 ));
637 continue;
638 }
639 }
640 }
641
642 let b = builder.as_mut().unwrap();
643 if let Err(e) = b.add_document(doc) {
644 log::error!("Failed to index document: {:?}", e);
645 state.record_cycle_error(format!("failed to index document: {e}"));
646 continue;
647 }
648
649 let builder_memory = b.estimated_memory_bytes();
650
651 if b.num_docs() & 0x3FFF == 0 {
652 log::debug!(
653 "[indexing] docs={}, memory={:.2} MB, budget={:.2} MB",
654 b.num_docs(),
655 builder_memory as f64 / (1024.0 * 1024.0),
656 state.memory_budget_per_worker as f64 / (1024.0 * 1024.0)
657 );
658 }
659
660 const MIN_DOCS_BEFORE_FLUSH: u32 = 100;
662
663 let effective_budget = state.memory_budget_per_worker * 4 / 5;
667
668 if builder_memory >= effective_budget && b.num_docs() >= MIN_DOCS_BEFORE_FLUSH {
669 log::info!(
670 "[indexing] memory budget reached, building segment: \
671 docs={}, memory={:.2} MB, budget={:.2} MB",
672 b.num_docs(),
673 builder_memory as f64 / (1024.0 * 1024.0),
674 state.memory_budget_per_worker as f64 / (1024.0 * 1024.0),
675 );
676 let full_builder = builder.take().unwrap();
677 Self::build_segment_inline(&state, full_builder, &handle);
678 }
679 }
680
681 if !state.cycle_failed.load(Ordering::Acquire)
683 && let Some(b) = builder.take()
684 && b.num_docs() > 0
685 {
686 Self::build_segment_inline(&state, b, &handle);
687 }
688 }));
689
690 if build_result.is_err() {
691 log::error!(
692 "[worker] panic during indexing cycle — documents in this cycle may be lost"
693 );
694 state.record_cycle_error("indexing worker panicked while building the batch");
695 }
696
697 let prev = state.flush_count.fetch_add(1, Ordering::Release);
700 if prev + 1 == state.num_workers {
701 let _lock = state.flush_mutex.lock();
703 state.flush_cvar.notify_one();
704 }
705
706 {
710 let mut lock = state.resume_receiver.lock();
711 loop {
712 if state.shutdown.load(Ordering::Acquire) {
713 return;
714 }
715 let current_epoch = state.resume_epoch.load(Ordering::Acquire);
716 if current_epoch > my_epoch
717 && let Some(rx) = lock.as_ref()
718 {
719 receiver = rx.clone();
720 my_epoch = current_epoch;
721 break;
722 }
723 state.resume_cvar.wait(&mut lock);
724 }
725 }
726 }
727 }
728
729 fn build_segment_inline(
733 state: &WorkerState<D>,
734 builder: SegmentBuilder,
735 handle: &tokio::runtime::Handle,
736 ) {
737 let segment_id = SegmentId::new();
738 let segment_hex = segment_id.to_hex();
739 let operation = match state
742 .segment_manager
743 .protect_new_segment(segment_hex.clone())
744 {
745 Ok(operation) => operation,
746 Err(e) => {
747 log::error!(
748 "[segment_build_failed] segment_id={} lifecycle_error={}",
749 segment_hex,
750 e,
751 );
752 state.record_cycle_error(format!(
753 "failed to claim segment {segment_hex} for building: {e}"
754 ));
755 return;
756 }
757 };
758 let trained = state.segment_manager.trained();
759 let doc_count = builder.num_docs();
760 let build_start = std::time::Instant::now();
761
762 log::info!(
763 "[segment_build] segment_id={} doc_count={} ann={}",
764 segment_hex,
765 doc_count,
766 trained.is_some()
767 );
768
769 let mut prepared = PreparedSegment {
773 id: segment_hex.clone(),
774 segment_id,
775 num_docs: doc_count,
776 segment_manager: Arc::clone(&state.segment_manager),
777 operation: Some(operation),
778 runtime: handle.clone(),
779 published: false,
780 };
781
782 match handle.block_on(builder.build(
783 state.directory.as_ref(),
784 segment_id,
785 trained.as_deref(),
786 )) {
787 Ok(meta) if meta.num_docs == doc_count && meta.num_docs > 0 => {
788 let duration_ms = build_start.elapsed().as_millis() as u64;
789 log::info!(
790 "[segment_build_done] segment_id={} doc_count={} duration_ms={}",
791 segment_hex,
792 meta.num_docs,
793 duration_ms,
794 );
795 prepared.num_docs = meta.num_docs;
796 state.built_segments.lock().push(prepared);
797 }
798 Ok(meta) => {
799 let error = format!(
800 "segment {segment_hex} built {} docs from a {doc_count}-document builder",
801 meta.num_docs
802 );
803 log::error!("[segment_build_failed] {error}");
804 state.record_cycle_error(error);
805 }
806 Err(e) => {
807 log::error!(
808 "[segment_build_failed] segment_id={} error={:?}",
809 segment_hex,
810 e
811 );
812 state.record_cycle_error(format!("failed to build segment {segment_hex}: {e}"));
815 }
816 }
817 }
818
819 pub async fn maybe_merge(&self) {
825 self.segment_manager.maybe_merge().await;
826 }
827
828 pub async fn abort_merges(&self) {
831 self.segment_manager.abort_merges().await;
832 }
833
834 pub async fn shutdown(&mut self) -> Result<()> {
839 self.segment_manager.begin_shutdown();
840 self.signal_worker_shutdown();
841
842 let workers = std::mem::take(&mut self.workers);
843 let panicked = tokio::task::spawn_blocking(move || {
844 workers
845 .into_iter()
846 .map(|worker| worker.join().is_err())
847 .filter(|panicked| *panicked)
848 .count()
849 })
850 .await
851 .map_err(|error| Error::Internal(format!("failed to join index workers: {}", error)))?;
852 if panicked > 0 {
853 log::error!("[index_shutdown] {} indexing worker(s) panicked", panicked);
854 }
855
856 self.flushed_segments.clear();
859 self.worker_state.built_segments.lock().clear();
860 if let Some(pk_index) = &mut self.primary_key_index {
861 pk_index.clear_uncommitted();
862 }
863 Ok(())
864 }
865
866 pub async fn wait_for_merging_thread(&self) {
868 self.segment_manager.wait_for_merging_thread().await;
869 }
870
871 pub async fn wait_for_all_merges(&self) {
873 self.segment_manager.wait_for_all_merges().await;
874 }
875
876 pub fn tracker(&self) -> std::sync::Arc<crate::segment::SegmentTracker> {
878 self.segment_manager.tracker()
879 }
880
881 pub async fn acquire_snapshot(&self) -> crate::segment::SegmentSnapshot {
883 self.segment_manager.acquire_snapshot().await
884 }
885
886 pub async fn cleanup_orphan_segments(&self) -> Result<usize> {
888 self.segment_manager.cleanup_orphan_segments().await
889 }
890
891 pub async fn prepare_commit(&mut self) -> Result<PreparedCommit<'_, D>> {
902 if self.worker_state.shutdown.load(Ordering::Acquire) {
903 return Err(Error::IndexClosed);
904 }
905 self.doc_sender.close();
907
908 self.worker_state.resume_cvar.notify_all();
912
913 let state = Arc::clone(&self.worker_state);
916 let all_flushed = tokio::task::spawn_blocking(move || {
917 let mut lock = state.flush_mutex.lock();
918 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(300);
919 while state.flush_count.load(Ordering::Acquire) < state.num_workers {
920 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
921 if remaining.is_zero() {
922 log::error!(
923 "[prepare_commit] timed out waiting for workers: {}/{} flushed",
924 state.flush_count.load(Ordering::Acquire),
925 state.num_workers
926 );
927 return false;
928 }
929 state.flush_cvar.wait_for(&mut lock, remaining);
930 }
931 true
932 })
933 .await
934 .map_err(|e| Error::Internal(format!("Failed to wait for workers: {}", e)))?;
935
936 if !all_flushed {
937 return Err(Error::Internal(format!(
945 "prepare_commit timed out: {}/{} workers flushed; writer remains paused, retry commit",
946 self.worker_state.flush_count.load(Ordering::Acquire),
947 self.worker_state.num_workers
948 )));
949 }
950
951 let cycle_error = { self.worker_state.cycle_error.lock().take() };
952 if let Some(error) = cycle_error {
953 self.flushed_segments.clear();
958 self.worker_state.built_segments.lock().clear();
959 if let Some(pk_index) = &mut self.primary_key_index {
960 pk_index.clear_uncommitted();
961 }
962 self.resume_workers();
963 return Err(Error::Internal(format!(
964 "indexing generation failed; no documents from this batch were committed: {error}"
965 )));
966 }
967
968 let built = std::mem::take(&mut *self.worker_state.built_segments.lock());
970 self.flushed_segments.extend(built);
971
972 Ok(PreparedCommit {
973 writer: self,
974 is_resolved: false,
975 is_published: false,
976 })
977 }
978
979 pub async fn commit(&mut self) -> Result<bool> {
984 self.prepare_commit().await?.commit().await
985 }
986
987 pub async fn force_merge(&mut self) -> Result<()> {
989 self.prepare_commit().await?.commit().await?;
990 self.segment_manager.force_merge().await
991 }
992
993 pub async fn reorder(&mut self) -> Result<()> {
998 self.prepare_commit().await?.commit().await?;
999 self.segment_manager.reorder_segments().await
1000 }
1001
1002 pub fn segment_manager(&self) -> &Arc<crate::merge::SegmentManager<D>> {
1004 &self.segment_manager
1005 }
1006
1007 fn resume_workers(&mut self) {
1012 if self.worker_state.shutdown.load(Ordering::Acquire) {
1013 return;
1014 }
1015 if tokio::runtime::Handle::try_current().is_err() {
1016 self.worker_state.shutdown.store(true, Ordering::Release);
1019 self.worker_state.resume_cvar.notify_all();
1020 return;
1021 }
1022
1023 self.worker_state.flush_count.store(0, Ordering::Release);
1025 *self.worker_state.cycle_error.lock() = None;
1026 self.worker_state
1027 .cycle_failed
1028 .store(false, Ordering::Release);
1029
1030 let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
1032 self.doc_sender = sender;
1033
1034 {
1036 let mut lock = self.worker_state.resume_receiver.lock();
1037 *lock = Some(receiver);
1038 }
1039 self.worker_state
1040 .resume_epoch
1041 .fetch_add(1, Ordering::Release);
1042 self.worker_state.resume_cvar.notify_all();
1043 }
1044
1045 fn signal_worker_shutdown(&self) {
1046 self.worker_state.shutdown.store(true, Ordering::Release);
1047 self.doc_sender.close();
1048 self.worker_state.resume_cvar.notify_all();
1049 }
1050
1051 }
1053
1054impl<D: DirectoryWriter + 'static> Drop for IndexWriter<D> {
1055 fn drop(&mut self) {
1056 self.signal_worker_shutdown();
1057 for w in std::mem::take(&mut self.workers) {
1058 let _ = w.join();
1059 }
1060 }
1061}
1062
1063pub struct PreparedCommit<'a, D: DirectoryWriter + 'static> {
1070 writer: &'a mut IndexWriter<D>,
1071 is_resolved: bool,
1072 is_published: bool,
1075}
1076
1077impl<'a, D: DirectoryWriter + 'static> PreparedCommit<'a, D> {
1078 pub async fn commit(mut self) -> Result<bool> {
1082 let mut segments = std::mem::take(&mut self.writer.flushed_segments);
1083
1084 if segments.is_empty() {
1086 log::debug!("[commit] no segments to commit, skipping");
1087 self.is_resolved = true;
1088 self.writer.resume_workers();
1089 return Ok(false);
1090 }
1091
1092 let metadata_entries: Vec<(String, u32)> = segments
1093 .iter()
1094 .map(PreparedSegment::metadata_entry)
1095 .collect();
1096 if let Err(error) = self.writer.segment_manager.commit(&metadata_entries).await {
1097 self.writer.flushed_segments = segments;
1103 self.is_resolved = true;
1104 return Err(error);
1105 }
1106 self.is_published = true;
1107
1108 for segment in &mut segments {
1111 segment.mark_published();
1112 }
1113 drop(segments);
1114
1115 let post_publish_result = async {
1117 if let Some(ref mut pk_index) = self.writer.primary_key_index {
1118 let snapshot = self.writer.segment_manager.acquire_snapshot().await;
1119 let existing_ids: std::collections::HashSet<&str> =
1120 pk_index.committed_segment_ids().collect();
1121
1122 let load_futures: Vec<_> = snapshot
1124 .segment_ids()
1125 .iter()
1126 .filter(|id| !existing_ids.contains(id.as_str()))
1127 .map(|seg_id_str| {
1128 let seg_id_str = seg_id_str.clone();
1129 let dir = self.writer.directory.as_ref();
1130 let schema = Arc::clone(&self.writer.schema);
1131 async move { load_pk_segment_data(dir, &seg_id_str, &schema).await }
1132 })
1133 .collect();
1134 let new_data = futures::future::try_join_all(load_futures).await?;
1135
1136 let seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
1137 pk_index.refresh_incremental(new_data, snapshot);
1138
1139 let bloom_bytes = pk_index.bloom_to_bytes();
1141 let data = super::primary_key::serialize_pk_bloom(&seg_ids, &bloom_bytes);
1142 if let Err(e) = self
1143 .writer
1144 .directory
1145 .write(
1146 std::path::Path::new(super::primary_key::PK_BLOOM_FILE),
1147 &data,
1148 )
1149 .await
1150 {
1151 log::warn!("[primary_key] failed to persist bloom cache: {}", e);
1152 }
1153 }
1154
1155 self.writer.segment_manager.maybe_merge().await;
1156 Ok(())
1157 }
1158 .await;
1159
1160 self.is_resolved = true;
1163 self.writer.resume_workers();
1164 post_publish_result.map(|()| true)
1165 }
1166
1167 pub fn abort(mut self) {
1170 self.is_resolved = true;
1171 self.writer.flushed_segments.clear();
1172 if let Some(ref mut pk_index) = self.writer.primary_key_index {
1173 pk_index.clear_uncommitted();
1174 }
1175 self.writer.resume_workers();
1176 }
1177}
1178
1179impl<D: DirectoryWriter + 'static> Drop for PreparedCommit<'_, D> {
1180 fn drop(&mut self) {
1181 if !self.is_resolved {
1182 if self.is_published {
1183 log::warn!("PreparedCommit dropped after metadata publication — resuming workers");
1184 } else {
1185 log::warn!("PreparedCommit dropped without commit/abort — auto-aborting");
1186 self.writer.flushed_segments.clear();
1187 if let Some(ref mut pk_index) = self.writer.primary_key_index {
1188 pk_index.clear_uncommitted();
1189 }
1190 }
1191 self.writer.resume_workers();
1192 }
1193 }
1194}
1195
1196async fn load_pk_segment_data<D: crate::directories::Directory>(
1198 dir: &D,
1199 seg_id_str: &str,
1200 schema: &Arc<crate::dsl::Schema>,
1201) -> Result<super::primary_key::PkSegmentData> {
1202 let seg_id = crate::segment::SegmentId::from_hex(seg_id_str)
1203 .ok_or_else(|| Error::Internal(format!("Invalid segment id: {}", seg_id_str)))?;
1204 let files = crate::segment::SegmentFiles::new(seg_id.0);
1205 let fast_fields =
1206 crate::segment::reader::loader::load_fast_fields_file(dir, &files, schema).await?;
1207 Ok(super::primary_key::PkSegmentData {
1208 segment_id: seg_id_str.to_string(),
1209 fast_fields,
1210 })
1211}