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 primary_key_refresh_lock: Arc<tokio::sync::Mutex<()>>,
158 commit_finalization: Arc<CommitFinalizationState>,
162 pk_reservations_retained: Arc<AtomicBool>,
167 writer_lock: parking_lot::RwLock<WriterLock>,
172}
173
174#[derive(Default)]
175struct CommitFinalizationState {
176 in_progress: AtomicBool,
177 idle: tokio::sync::Notify,
178}
179
180impl CommitFinalizationState {
181 fn begin(&self) -> bool {
182 self.in_progress
183 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
184 .is_ok()
185 }
186
187 fn finish(&self) {
188 self.in_progress.store(false, Ordering::Release);
189 self.idle.notify_waiters();
190 }
191
192 async fn wait_until_idle(&self) {
193 while self.in_progress.load(Ordering::Acquire) {
194 let notified = self.idle.notified();
195 if !self.in_progress.load(Ordering::Acquire) {
196 break;
197 }
198 notified.await;
199 }
200 }
201}
202
203struct WorkerState<D: DirectoryWriter + 'static> {
205 directory: Arc<D>,
206 schema: Arc<Schema>,
207 builder_config: SegmentBuilderConfig,
208 tokenizers: parking_lot::RwLock<FxHashMap<Field, BoxedTokenizer>>,
209 memory_budget_per_worker: usize,
211 segment_manager: Arc<crate::merge::SegmentManager<D>>,
213 built_segments: parking_lot::Mutex<Vec<PreparedSegment<D>>>,
216 cycle_error: parking_lot::Mutex<Option<String>>,
221 cycle_failed: AtomicBool,
222
223 flush_count: AtomicUsize,
230 flush_mutex: parking_lot::Mutex<()>,
232 flush_cvar: parking_lot::Condvar,
233 resume_receiver: parking_lot::Mutex<Option<async_channel::Receiver<Document>>>,
235 resume_epoch: AtomicUsize,
238 resume_cvar: parking_lot::Condvar,
240 shutdown: AtomicBool,
242 num_workers: usize,
244}
245
246struct PreparedSegment<D: DirectoryWriter + 'static> {
252 id: String,
253 segment_id: SegmentId,
254 num_docs: u32,
255 segment_manager: Arc<crate::merge::SegmentManager<D>>,
256 operation: Option<crate::merge::SegmentOperationGuard>,
257 runtime: tokio::runtime::Handle,
258 needs_vector_upgrade: bool,
259 published: bool,
260}
261
262impl<D: DirectoryWriter + 'static> PreparedSegment<D> {
263 fn metadata_entry(&self) -> (String, u32) {
264 (self.id.clone(), self.num_docs)
265 }
266
267 fn mark_published(&mut self) {
268 self.published = true;
269 drop(self.operation.take());
271 }
272}
273
274impl<D: DirectoryWriter + 'static> WorkerState<D> {
275 fn record_cycle_error(&self, error: impl Into<String>) {
276 let mut first_error = self.cycle_error.lock();
277 if first_error.is_none() {
278 *first_error = Some(error.into());
279 }
280 drop(first_error);
281 self.cycle_failed.store(true, Ordering::Release);
282 }
283}
284
285impl<D: DirectoryWriter + 'static> Drop for PreparedSegment<D> {
286 fn drop(&mut self) {
287 if self.published {
288 return;
289 }
290 let Some(operation) = self.operation.take() else {
291 return;
292 };
293 self.segment_manager.schedule_unpublished_segment_cleanup(
294 self.segment_id,
295 operation,
296 self.runtime.clone(),
297 );
298 }
299}
300
301impl<D: DirectoryWriter + 'static> IndexWriter<D> {
302 pub async fn create(directory: D, schema: Schema, config: IndexConfig) -> Result<Self> {
304 Self::create_with_config(directory, schema, config, SegmentBuilderConfig::default()).await
305 }
306
307 pub async fn create_with_config(
309 directory: D,
310 schema: Schema,
311 config: IndexConfig,
312 builder_config: SegmentBuilderConfig,
313 ) -> Result<Self> {
314 crate::dsl::reject_removed_vector_index_types(&schema).map_err(Error::Schema)?;
315 let directory = Arc::new(directory);
316 let schema = Arc::new(schema);
317 directory.set_index_label(schema.index_label());
319
320 let writer_lock = try_acquire_writer_lock(directory.as_ref())?;
322 if let WriterLock::Unavailable { reason } = &writer_lock {
323 return Err(Error::Internal(reason.clone()));
324 }
325 if directory
329 .exists(std::path::Path::new(super::INDEX_META_FILENAME))
330 .await?
331 {
332 return Err(Error::Internal(format!(
333 "refusing to create index: {} already exists in this directory; \
334 use IndexWriter::open to open the existing index, or delete the \
335 directory first if you really want to start over",
336 super::INDEX_META_FILENAME
337 )));
338 }
339
340 let metadata = super::IndexMetadata::new((*schema).clone());
341
342 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
343 Arc::clone(&directory),
344 Arc::clone(&schema),
345 metadata,
346 config.merge_policy.clone_box(),
347 config.term_cache_blocks,
348 config.max_concurrent_merges,
349 Arc::clone(&config.background_merge_permits),
350 config.merge_bp_time_budget,
351 config.bp_memory_budget_bytes,
352 Arc::clone(&config.background_reorder_permits),
353 config.background_reorder_pool.clone(),
354 ));
355 segment_manager.update_metadata(|_| {}).await?;
356
357 Ok(Self::new_with_parts(
358 directory,
359 schema,
360 config,
361 builder_config,
362 segment_manager,
363 writer_lock,
364 ))
365 }
366
367 pub async fn open(directory: D, config: IndexConfig) -> Result<Self> {
376 Self::open_with_config(directory, config, SegmentBuilderConfig::default()).await
377 }
378
379 pub async fn open_with_config(
381 directory: D,
382 config: IndexConfig,
383 builder_config: SegmentBuilderConfig,
384 ) -> Result<Self> {
385 let directory = Arc::new(directory);
386
387 let writer_lock = try_acquire_writer_lock(directory.as_ref())?;
390 if let WriterLock::Unavailable { reason } = &writer_lock {
391 return Err(Error::Internal(reason.clone()));
392 }
393
394 let metadata = super::IndexMetadata::load(directory.as_ref()).await?;
395 let schema = Arc::new(metadata.schema.clone());
396 directory.set_index_label(schema.index_label());
398
399 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
400 Arc::clone(&directory),
401 Arc::clone(&schema),
402 metadata,
403 config.merge_policy.clone_box(),
404 config.term_cache_blocks,
405 config.max_concurrent_merges,
406 Arc::clone(&config.background_merge_permits),
407 config.merge_bp_time_budget,
408 config.bp_memory_budget_bytes,
409 Arc::clone(&config.background_reorder_permits),
410 config.background_reorder_pool.clone(),
411 ));
412 let swept = segment_manager.cleanup_orphan_segments().await?;
413 if swept > 0 {
414 log::warn!(
415 "[segment_cleanup] swept {} orphan segment(s) while opening writer",
416 swept
417 );
418 }
419 segment_manager.try_load_and_publish_trained().await?;
420
421 Ok(Self::new_with_parts(
422 directory,
423 schema,
424 config,
425 builder_config,
426 segment_manager,
427 writer_lock,
428 ))
429 }
430
431 pub fn from_index(index: &super::Index<D>) -> Self {
438 let writer_lock = match try_acquire_writer_lock(index.directory.as_ref()) {
439 Ok(lock) => lock,
440 Err(error) => WriterLock::Unavailable {
441 reason: format!("failed to acquire the single-writer lock: {error}"),
442 },
443 };
444 if let WriterLock::Unavailable { reason } = &writer_lock {
445 log::error!("[writer_lock] {reason}");
446 }
447 Self::new_with_parts(
448 Arc::clone(&index.directory),
449 Arc::clone(&index.schema),
450 index.config.clone(),
451 SegmentBuilderConfig::default(),
452 Arc::clone(&index.segment_manager),
453 writer_lock,
454 )
455 }
456
457 fn new_with_parts(
463 directory: Arc<D>,
464 schema: Arc<Schema>,
465 config: IndexConfig,
466 builder_config: SegmentBuilderConfig,
467 segment_manager: Arc<crate::merge::SegmentManager<D>>,
468 writer_lock: WriterLock,
469 ) -> Self {
470 let registry = crate::tokenizer::TokenizerRegistry::new();
472 let mut tokenizers = FxHashMap::default();
473 for (field, entry) in schema.fields() {
474 if matches!(entry.field_type, crate::dsl::FieldType::Text)
475 && let Some(ref tok_name) = entry.tokenizer
476 && let Some(tok) = registry.get(tok_name)
477 {
478 tokenizers.insert(field, tok);
479 }
480 }
481
482 let num_workers = config.num_indexing_threads.max(1);
483 let worker_state = Arc::new(WorkerState {
484 directory: Arc::clone(&directory),
485 schema: Arc::clone(&schema),
486 builder_config,
487 tokenizers: parking_lot::RwLock::new(tokenizers),
488 memory_budget_per_worker: config.max_indexing_memory_bytes / num_workers,
489 segment_manager: Arc::clone(&segment_manager),
490 built_segments: parking_lot::Mutex::new(Vec::new()),
491 cycle_error: parking_lot::Mutex::new(None),
492 cycle_failed: AtomicBool::new(false),
493 flush_count: AtomicUsize::new(0),
494 flush_mutex: parking_lot::Mutex::new(()),
495 flush_cvar: parking_lot::Condvar::new(),
496 resume_receiver: parking_lot::Mutex::new(None),
497 resume_epoch: AtomicUsize::new(0),
498 resume_cvar: parking_lot::Condvar::new(),
499 shutdown: AtomicBool::new(false),
500 num_workers,
501 });
502 let (doc_sender, workers) = Self::spawn_workers(&worker_state, num_workers);
503 let primary_key_index = Arc::new(parking_lot::RwLock::new(None));
504 let primary_key_refresh_lock = Arc::new(tokio::sync::Mutex::new(()));
505
506 Self {
507 directory,
508 schema,
509 config,
510 doc_sender: Arc::new(parking_lot::RwLock::new(doc_sender)),
511 workers,
512 worker_state,
513 segment_manager,
514 flushed_segments: Arc::new(parking_lot::Mutex::new(Vec::new())),
515 primary_key_index,
516 primary_key_refresh_lock,
517 commit_finalization: Arc::new(CommitFinalizationState::default()),
518 pk_reservations_retained: Arc::new(AtomicBool::new(false)),
519 writer_lock: parking_lot::RwLock::new(writer_lock),
520 }
521 }
522
523 fn ensure_writer_lock(&self) -> Result<()> {
531 if !matches!(&*self.writer_lock.read(), WriterLock::Unavailable { .. }) {
533 return Ok(());
534 }
535
536 let mut lock = self.writer_lock.write();
537 if !matches!(&*lock, WriterLock::Unavailable { .. }) {
539 return Ok(());
540 }
541 match try_acquire_writer_lock(self.directory.as_ref())? {
542 acquired @ (WriterLock::Held { .. } | WriterLock::NotApplicable) => {
543 log::info!(
544 "[writer_lock] single-writer lock acquired after retry; \
545 the previous holder has released it — resuming writes"
546 );
547 *lock = acquired;
548 Ok(())
549 }
550 WriterLock::Unavailable { reason } => {
551 let err = Error::Internal(reason.clone());
552 *lock = WriterLock::Unavailable { reason };
553 Err(err)
554 }
555 }
556 }
557
558 fn clear_uncommitted_pk_reservations(&self) {
567 if self.pk_reservations_retained.load(Ordering::Acquire) {
568 log::warn!(
569 "[primary_key] keeping uncommitted reservations through abort: a \
570 failed post-commit refresh left them as the only record of \
571 committed keys; they are cleared by the next successful commit"
572 );
573 return;
574 }
575 if let Some(pk_index) = self.primary_key_index.write().as_mut() {
576 pk_index.clear_uncommitted();
577 }
578 }
579
580 fn spawn_workers(
581 worker_state: &Arc<WorkerState<D>>,
582 num_workers: usize,
583 ) -> (
584 async_channel::Sender<Document>,
585 Vec<std::thread::JoinHandle<()>>,
586 ) {
587 let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
588 let handle = tokio::runtime::Handle::current();
589 let mut workers = Vec::with_capacity(num_workers);
590 for i in 0..num_workers {
591 let state = Arc::clone(worker_state);
592 let rx = receiver.clone();
593 let rt = handle.clone();
594 workers.push(
595 std::thread::Builder::new()
596 .name(format!("index-worker-{}", i))
597 .spawn(move || Self::worker_loop(state, rx, rt))
598 .expect("failed to spawn index worker thread"),
599 );
600 }
601 (sender, workers)
602 }
603
604 pub fn schema(&self) -> &Schema {
606 &self.schema
607 }
608
609 pub fn set_tokenizer<T: crate::tokenizer::Tokenizer>(&mut self, field: Field, tokenizer: T) {
612 self.worker_state
613 .tokenizers
614 .write()
615 .insert(field, Box::new(tokenizer));
616 }
617
618 pub async fn init_primary_key_dedup(&mut self) -> Result<()> {
634 use super::primary_key::{PK_BLOOM_FILE, deserialize_pk_bloom};
635
636 self.commit_finalization.wait_until_idle().await;
637 self.ensure_writer_lock()?;
638
639 let field = match self.schema.primary_field() {
640 Some(f) => f,
641 None => return Ok(()),
642 };
643
644 let _refresh_guard = self.primary_key_refresh_lock.lock().await;
649 {
650 let callback_directory = Arc::clone(&self.directory);
651 let callback_schema = Arc::clone(&self.schema);
652 let callback_manager = Arc::downgrade(&self.segment_manager);
653 let callback_primary_key = Arc::downgrade(&self.primary_key_index);
654 let callback_refresh_lock = Arc::downgrade(&self.primary_key_refresh_lock);
655 self.segment_manager.set_replacement_refresh(move || {
656 let directory = Arc::clone(&callback_directory);
657 let schema = Arc::clone(&callback_schema);
658 let manager = callback_manager.clone();
659 let primary_key = callback_primary_key.clone();
660 let refresh_lock = callback_refresh_lock.clone();
661 async move {
662 let (Some(manager), Some(primary_key), Some(refresh_lock)) = (
663 manager.upgrade(),
664 primary_key.upgrade(),
665 refresh_lock.upgrade(),
666 ) else {
667 return Ok(());
668 };
669 refresh_primary_key_snapshot(
670 &directory,
671 &schema,
672 &manager,
673 &primary_key,
674 &refresh_lock,
675 PrimaryKeyRefresh::Replacement,
676 )
677 .await
678 }
679 });
680 }
681
682 let snapshot = self.segment_manager.acquire_snapshot().await;
683 let current_seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
684
685 let cached = match self
687 .directory
688 .open_read(std::path::Path::new(PK_BLOOM_FILE))
689 .await
690 {
691 Ok(handle) => {
692 let data = handle.read_bytes_range(0..handle.len()).await;
693 match data {
694 Ok(bytes) => deserialize_pk_bloom(bytes.as_slice()),
695 Err(_) => None,
696 }
697 }
698 Err(_) => None,
699 };
700
701 let load_futures: Vec<_> = current_seg_ids
703 .iter()
704 .map(|seg_id_str| {
705 let seg_id_str = seg_id_str.clone();
706 let dir = self.directory.as_ref();
707 let schema = Arc::clone(&self.schema);
708 async move { load_pk_segment_data(dir, &seg_id_str, &schema).await }
709 })
710 .collect();
711 let all_data = futures::future::try_join_all(load_futures).await?;
712
713 if let Some((persisted_seg_ids, bloom)) = cached {
714 let mut pk_data = Vec::with_capacity(all_data.len());
716 let mut new_data = Vec::new();
717 for d in all_data {
718 if persisted_seg_ids.contains(&d.segment_id) {
719 pk_data.push(d);
720 } else {
721 new_data.push(d);
722 }
723 }
724 let needs_persist = !new_data.is_empty();
725 let new_start = pk_data.len();
726 pk_data.extend(new_data);
727
728 let pk_index = if new_start == pk_data.len() {
729 super::primary_key::PrimaryKeyIndex::from_persisted(field, bloom, pk_data, snapshot)
731 } else {
732 tokio::task::spawn_blocking(move || {
734 let mut bloom = bloom;
737 let mut added = 0usize;
738 let num_new = pk_data.len() - new_start;
739 for data in &pk_data[new_start..] {
740 if let Some(ff) = data.fast_fields.get(&field.0)
741 && let Some(dict) = ff.text_dict()
742 {
743 for key in dict.iter() {
744 bloom.insert(key.as_bytes());
745 added += 1;
746 }
747 }
748 }
749 if added > 0 {
750 log::info!(
751 "[primary_key] bloom: added {} keys from {} new segment(s)",
752 added,
753 num_new,
754 );
755 }
756 super::primary_key::PrimaryKeyIndex::from_persisted(
757 field, bloom, pk_data, snapshot,
758 )
759 })
760 .await
761 .map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?
762 };
763
764 if needs_persist {
765 self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
766 }
767
768 *self.primary_key_index.write() = Some(pk_index);
769 } else {
770 let pk_index = tokio::task::spawn_blocking(move || {
772 super::primary_key::PrimaryKeyIndex::new(field, all_data, snapshot)
773 })
774 .await
775 .map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?;
776
777 self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
778 *self.primary_key_index.write() = Some(pk_index);
779 }
780
781 self.pk_reservations_retained
785 .store(false, Ordering::Release);
786
787 Ok(())
788 }
789
790 async fn persist_pk_bloom(
793 &self,
794 pk_index: &super::primary_key::PrimaryKeyIndex,
795 segment_ids: &[String],
796 ) {
797 use super::primary_key::PK_BLOOM_FILE;
798
799 let writer = match self
800 .directory
801 .streaming_writer(std::path::Path::new(PK_BLOOM_FILE))
802 .await
803 {
804 Ok(writer) => writer,
805 Err(error) => {
806 log::warn!("[primary_key] failed to open bloom cache: {}", error);
807 return;
808 }
809 };
810 let result = crate::segment::block_in_place_if_multithread(|| {
811 write_pk_bloom_stream(pk_index, segment_ids, writer)
812 });
813 if let Err(e) = result {
814 log::warn!("[primary_key] failed to persist bloom cache: {}", e);
815 }
816 }
817
818 pub fn add_document(&self, doc: Document) -> Result<()> {
824 self.ensure_writer_lock()?;
825 if self.worker_state.shutdown.load(Ordering::Acquire) {
826 return Err(Error::IndexClosed);
827 }
828 if self.commit_finalization.in_progress.load(Ordering::Acquire) {
829 return Err(Error::CommitInProgress);
830 }
831 let sender = self.doc_sender.read().clone();
832 if sender.is_closed() {
836 return Err(Error::CommitInProgress);
837 }
838 let primary_key_index = self.primary_key_index.read();
839 if let Some(ref pk_index) = *primary_key_index {
840 pk_index.check_and_insert(&doc)?;
841 }
842 match sender.try_send(doc) {
843 Ok(()) => Ok(()),
844 Err(async_channel::TrySendError::Full(doc)) => {
845 if let Some(ref pk_index) = *primary_key_index {
847 pk_index.rollback_uncommitted_key(&doc);
848 }
849 Err(Error::QueueFull)
850 }
851 Err(async_channel::TrySendError::Closed(doc)) => {
852 if let Some(ref pk_index) = *primary_key_index {
854 pk_index.rollback_uncommitted_key(&doc);
855 }
856 Err(Error::CommitInProgress)
857 }
858 }
859 }
860
861 pub fn add_documents(&self, documents: Vec<Document>) -> Result<usize> {
866 let total = documents.len();
867 for (i, doc) in documents.into_iter().enumerate() {
868 match self.add_document(doc) {
869 Ok(()) => {}
870 Err(Error::QueueFull | Error::CommitInProgress) => return Ok(i),
871 Err(e) => return Err(e),
872 }
873 }
874 Ok(total)
875 }
876
877 fn worker_loop(
890 state: Arc<WorkerState<D>>,
891 initial_receiver: async_channel::Receiver<Document>,
892 handle: tokio::runtime::Handle,
893 ) {
894 let mut receiver = initial_receiver;
895 let mut my_epoch = 0usize;
896
897 loop {
898 let build_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
902 let mut builder: Option<SegmentBuilder> = None;
903
904 while let Ok(doc) = receiver.recv_blocking() {
905 if state.shutdown.load(Ordering::Acquire) {
906 break;
907 }
908 if state.cycle_failed.load(Ordering::Acquire) {
913 continue;
914 }
915 if builder.is_none() {
917 match SegmentBuilder::new(
918 Arc::clone(&state.schema),
919 state.builder_config.clone(),
920 ) {
921 Ok(mut b) => {
922 for (field, tokenizer) in state.tokenizers.read().iter() {
923 b.set_tokenizer(*field, tokenizer.clone_box());
924 }
925 builder = Some(b);
926 }
927 Err(e) => {
928 log::error!("Failed to create segment builder: {:?}", e);
929 state.record_cycle_error(format!(
930 "failed to create segment builder: {e}"
931 ));
932 continue;
933 }
934 }
935 }
936
937 let b = builder.as_mut().unwrap();
938 if let Err(e) = b.add_document(doc) {
939 log::error!("Failed to index document: {:?}", e);
940 state.record_cycle_error(format!("failed to index document: {e}"));
941 continue;
942 }
943
944 let builder_memory = b.estimated_memory_bytes();
945
946 if b.num_docs() & 0x3FFF == 0 {
947 log::debug!(
948 "[indexing] docs={}, memory={}, budget={}",
949 b.num_docs(),
950 crate::format_bytes(builder_memory as u64),
951 crate::format_bytes(state.memory_budget_per_worker as u64)
952 );
953 }
954
955 const MIN_DOCS_BEFORE_FLUSH: u32 = 100;
957
958 let effective_budget = state.memory_budget_per_worker * 4 / 5;
962
963 if builder_memory >= effective_budget && b.num_docs() >= MIN_DOCS_BEFORE_FLUSH {
964 log::info!(
965 "[indexing] memory budget reached, building segment: \
966 docs={}, memory={}, budget={}",
967 b.num_docs(),
968 crate::format_bytes(builder_memory as u64),
969 crate::format_bytes(state.memory_budget_per_worker as u64),
970 );
971 let full_builder = builder.take().unwrap();
972 Self::build_segment_inline(&state, full_builder, &handle);
973 }
974 }
975
976 if !state.cycle_failed.load(Ordering::Acquire)
978 && let Some(b) = builder.take()
979 && b.num_docs() > 0
980 {
981 Self::build_segment_inline(&state, b, &handle);
982 }
983 }));
984
985 if build_result.is_err() {
986 log::error!(
987 "[worker] panic during indexing cycle — documents in this cycle may be lost"
988 );
989 state.record_cycle_error("indexing worker panicked while building the batch");
990 }
991
992 let prev = state.flush_count.fetch_add(1, Ordering::Release);
995 if prev + 1 == state.num_workers {
996 let _lock = state.flush_mutex.lock();
1003 state.flush_cvar.notify_all();
1004 }
1005
1006 {
1010 let mut lock = state.resume_receiver.lock();
1011 loop {
1012 if state.shutdown.load(Ordering::Acquire) {
1013 return;
1014 }
1015 let current_epoch = state.resume_epoch.load(Ordering::Acquire);
1016 if current_epoch > my_epoch
1017 && let Some(rx) = lock.as_ref()
1018 {
1019 receiver = rx.clone();
1020 my_epoch = current_epoch;
1021 break;
1022 }
1023 state.resume_cvar.wait(&mut lock);
1024 }
1025 }
1026 }
1027 }
1028
1029 fn build_segment_inline(
1033 state: &WorkerState<D>,
1034 builder: SegmentBuilder,
1035 handle: &tokio::runtime::Handle,
1036 ) {
1037 let segment_id = SegmentId::new();
1038 let segment_hex = segment_id.to_hex();
1039 let operation = match state
1042 .segment_manager
1043 .protect_new_segment(segment_hex.clone())
1044 {
1045 Ok(operation) => operation,
1046 Err(e) => {
1047 log::error!(
1048 "[segment_build_failed] segment_id={} lifecycle_error={}",
1049 segment_hex,
1050 e,
1051 );
1052 state.record_cycle_error(format!(
1053 "failed to claim segment {segment_hex} for building: {e}"
1054 ));
1055 return;
1056 }
1057 };
1058 let trained = state.segment_manager.trained_for_segment_build();
1059 let doc_count = builder.num_docs();
1060 let build_start = std::time::Instant::now();
1061
1062 log::info!(
1063 "[segment_build] segment_id={} doc_count={} ann={}",
1064 segment_hex,
1065 doc_count,
1066 trained.is_some()
1067 );
1068
1069 let mut prepared = PreparedSegment {
1073 id: segment_hex.clone(),
1074 segment_id,
1075 num_docs: doc_count,
1076 segment_manager: Arc::clone(&state.segment_manager),
1077 operation: Some(operation),
1078 runtime: handle.clone(),
1079 needs_vector_upgrade: trained.is_none(),
1080 published: false,
1081 };
1082
1083 match handle.block_on(builder.build(
1084 state.directory.as_ref(),
1085 segment_id,
1086 trained.as_deref(),
1087 )) {
1088 Ok(meta) if meta.num_docs == doc_count && meta.num_docs > 0 => {
1089 let duration_ms = build_start.elapsed().as_millis() as u64;
1090 log::info!(
1091 "[segment_build_done] segment_id={} doc_count={} duration_ms={}",
1092 segment_hex,
1093 meta.num_docs,
1094 duration_ms,
1095 );
1096 prepared.num_docs = meta.num_docs;
1097 state.built_segments.lock().push(prepared);
1098 }
1099 Ok(meta) => {
1100 let error = format!(
1101 "segment {segment_hex} built {} docs from a {doc_count}-document builder",
1102 meta.num_docs
1103 );
1104 log::error!("[segment_build_failed] {error}");
1105 state.record_cycle_error(error);
1106 }
1107 Err(e) => {
1108 log::error!(
1109 "[segment_build_failed] segment_id={} error={:?}",
1110 segment_hex,
1111 e
1112 );
1113 state.record_cycle_error(format!("failed to build segment {segment_hex}: {e}"));
1116 }
1117 }
1118 }
1119
1120 pub async fn maybe_merge(&self) {
1126 self.segment_manager.maybe_merge().await;
1127 }
1128
1129 pub async fn abort_merges(&self) {
1132 self.segment_manager.abort_merges().await;
1133 }
1134
1135 pub async fn shutdown(&mut self) -> Result<()> {
1140 self.segment_manager.begin_shutdown();
1141 self.signal_worker_shutdown();
1142
1143 self.commit_finalization.wait_until_idle().await;
1148
1149 let workers = std::mem::take(&mut self.workers);
1150 let panicked = tokio::task::spawn_blocking(move || {
1151 workers
1152 .into_iter()
1153 .map(|worker| worker.join().is_err())
1154 .filter(|panicked| *panicked)
1155 .count()
1156 })
1157 .await
1158 .map_err(|error| Error::Internal(format!("failed to join index workers: {}", error)))?;
1159 if panicked > 0 {
1160 log::error!("[index_shutdown] {} indexing worker(s) panicked", panicked);
1161 }
1162
1163 self.flushed_segments.lock().clear();
1166 self.worker_state.built_segments.lock().clear();
1167 if let Some(pk_index) = self.primary_key_index.write().as_mut() {
1168 pk_index.clear_uncommitted();
1169 }
1170 Ok(())
1171 }
1172
1173 pub async fn wait_for_merging_thread(&self) {
1175 self.segment_manager.wait_for_merging_thread().await;
1176 }
1177
1178 pub async fn wait_for_all_merges(&self) {
1180 self.segment_manager.wait_for_all_merges().await;
1181 }
1182
1183 pub async fn wait_for_commit_finalization(&self) {
1188 self.commit_finalization.wait_until_idle().await;
1189 }
1190
1191 pub fn tracker(&self) -> std::sync::Arc<crate::segment::SegmentTracker> {
1193 self.segment_manager.tracker()
1194 }
1195
1196 pub async fn acquire_snapshot(&self) -> crate::segment::SegmentSnapshot {
1198 self.segment_manager.acquire_snapshot().await
1199 }
1200
1201 pub async fn cleanup_orphan_segments(&self) -> Result<usize> {
1206 self.ensure_writer_lock()?;
1207 self.segment_manager.cleanup_orphan_segments().await
1208 }
1209
1210 pub async fn prepare_commit(&mut self) -> Result<PreparedCommit<'_, D>> {
1221 self.ensure_writer_lock()?;
1222 if self.worker_state.shutdown.load(Ordering::Acquire) {
1223 return Err(Error::IndexClosed);
1224 }
1225 if self.commit_finalization.in_progress.load(Ordering::Acquire) {
1226 return Err(Error::CommitInProgress);
1227 }
1228 self.doc_sender.read().close();
1230
1231 self.worker_state.resume_cvar.notify_all();
1235
1236 let state = Arc::clone(&self.worker_state);
1239 let all_flushed = tokio::task::spawn_blocking(move || {
1240 let mut lock = state.flush_mutex.lock();
1241 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(300);
1242 while state.flush_count.load(Ordering::Acquire) < state.num_workers {
1243 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
1244 if remaining.is_zero() {
1245 log::error!(
1246 "[prepare_commit] timed out waiting for workers: {}/{} flushed",
1247 state.flush_count.load(Ordering::Acquire),
1248 state.num_workers
1249 );
1250 return false;
1251 }
1252 state.flush_cvar.wait_for(&mut lock, remaining);
1253 }
1254 true
1255 })
1256 .await
1257 .map_err(|e| Error::Internal(format!("Failed to wait for workers: {}", e)))?;
1258
1259 if !all_flushed {
1260 return Err(Error::Internal(format!(
1268 "prepare_commit timed out: {}/{} workers flushed; writer remains paused, retry commit",
1269 self.worker_state.flush_count.load(Ordering::Acquire),
1270 self.worker_state.num_workers
1271 )));
1272 }
1273
1274 let cycle_error = { self.worker_state.cycle_error.lock().take() };
1275 if let Some(error) = cycle_error {
1276 self.flushed_segments.lock().clear();
1281 self.worker_state.built_segments.lock().clear();
1282 self.clear_uncommitted_pk_reservations();
1283 self.resume_workers();
1284 return Err(Error::Internal(format!(
1285 "indexing generation failed; no documents from this batch were committed: {error}"
1286 )));
1287 }
1288
1289 let built = std::mem::take(&mut *self.worker_state.built_segments.lock());
1291 self.flushed_segments.lock().extend(built);
1292
1293 Ok(PreparedCommit {
1294 writer: self,
1295 is_resolved: false,
1296 })
1297 }
1298
1299 pub async fn commit(&mut self) -> Result<bool> {
1304 self.prepare_commit().await?.commit().await
1305 }
1306
1307 pub async fn force_merge(&mut self) -> Result<()> {
1309 self.force_merge_with_snapshot_refresh(|| std::future::ready(Ok(())))
1310 .await
1311 }
1312
1313 pub async fn force_merge_with_snapshot_refresh<F, Fut>(
1320 &mut self,
1321 refresh_external: F,
1322 ) -> Result<()>
1323 where
1324 F: FnMut() -> Fut,
1325 Fut: std::future::Future<Output = Result<()>>,
1326 {
1327 self.prepare_commit().await?.commit().await?;
1328
1329 self.segment_manager
1330 .force_merge_with_snapshot_refresh(refresh_external)
1331 .await?;
1332
1333 self.persist_replacement_snapshot().await
1337 }
1338
1339 pub async fn reorder(&mut self) -> Result<()> {
1344 self.reorder_with_snapshot_refresh(|| std::future::ready(Ok(())))
1345 .await
1346 }
1347
1348 pub async fn reorder_with_snapshot_refresh<F, Fut>(&mut self, refresh_external: F) -> Result<()>
1351 where
1352 F: FnMut() -> Fut,
1353 Fut: std::future::Future<Output = Result<()>>,
1354 {
1355 self.prepare_commit().await?.commit().await?;
1356
1357 self.segment_manager
1358 .reorder_segments_with_snapshot_refresh(refresh_external)
1359 .await?;
1360 self.persist_replacement_snapshot().await
1361 }
1362
1363 async fn persist_replacement_snapshot(&self) -> Result<()> {
1365 refresh_primary_key_snapshot(
1366 &self.directory,
1367 &self.schema,
1368 &self.segment_manager,
1369 &self.primary_key_index,
1370 &self.primary_key_refresh_lock,
1371 PrimaryKeyRefresh::FinalReplacement,
1372 )
1373 .await
1374 }
1375
1376 pub fn segment_manager(&self) -> &Arc<crate::merge::SegmentManager<D>> {
1378 &self.segment_manager
1379 }
1380
1381 fn resume_workers(&mut self) {
1386 Self::resume_workers_shared(&self.worker_state, &self.doc_sender);
1387 }
1388
1389 fn resume_workers_shared(
1390 worker_state: &Arc<WorkerState<D>>,
1391 doc_sender: &Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
1392 ) {
1393 if worker_state.shutdown.load(Ordering::Acquire) {
1394 return;
1395 }
1396 if tokio::runtime::Handle::try_current().is_err() {
1397 worker_state.shutdown.store(true, Ordering::Release);
1400 worker_state.resume_cvar.notify_all();
1401 return;
1402 }
1403
1404 worker_state.flush_count.store(0, Ordering::Release);
1406 *worker_state.cycle_error.lock() = None;
1407 worker_state.cycle_failed.store(false, Ordering::Release);
1408
1409 let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
1411 *doc_sender.write() = sender;
1412
1413 {
1415 let mut lock = worker_state.resume_receiver.lock();
1416 *lock = Some(receiver);
1417 }
1418 worker_state.resume_epoch.fetch_add(1, Ordering::Release);
1419 worker_state.resume_cvar.notify_all();
1420 }
1421
1422 fn signal_worker_shutdown(&self) {
1423 self.worker_state.shutdown.store(true, Ordering::Release);
1424 self.doc_sender.read().close();
1425 self.worker_state.resume_cvar.notify_all();
1426 }
1427
1428 }
1430
1431impl<D: DirectoryWriter + 'static> Drop for IndexWriter<D> {
1432 fn drop(&mut self) {
1433 self.signal_worker_shutdown();
1434 for w in std::mem::take(&mut self.workers) {
1435 let _ = w.join();
1436 }
1437 }
1438}
1439
1440pub struct PreparedCommit<'a, D: DirectoryWriter + 'static> {
1447 writer: &'a mut IndexWriter<D>,
1448 is_resolved: bool,
1449}
1450
1451struct PreparedSegmentsGuard<D: DirectoryWriter + 'static> {
1456 segments: Option<Vec<PreparedSegment<D>>>,
1457 retry_slot: Arc<parking_lot::Mutex<Vec<PreparedSegment<D>>>>,
1458}
1459
1460impl<D: DirectoryWriter + 'static> PreparedSegmentsGuard<D> {
1461 fn metadata_entries(&self) -> Vec<(String, u32)> {
1462 self.segments
1463 .as_deref()
1464 .unwrap_or_default()
1465 .iter()
1466 .map(PreparedSegment::metadata_entry)
1467 .collect()
1468 }
1469
1470 fn take_published(&mut self) -> Vec<PreparedSegment<D>> {
1471 self.segments.take().unwrap_or_default()
1472 }
1473
1474 fn vector_upgrade_segment_ids(&self) -> Vec<String> {
1475 self.segments
1476 .as_deref()
1477 .unwrap_or_default()
1478 .iter()
1479 .filter(|segment| segment.needs_vector_upgrade)
1480 .map(|segment| segment.id.clone())
1481 .collect()
1482 }
1483}
1484
1485impl<D: DirectoryWriter + 'static> Drop for PreparedSegmentsGuard<D> {
1486 fn drop(&mut self) {
1487 if let Some(segments) = self.segments.take() {
1488 self.retry_slot.lock().extend(segments);
1489 }
1490 }
1491}
1492
1493struct CommitFinalizationGuard<D: DirectoryWriter + 'static> {
1498 state: Arc<CommitFinalizationState>,
1499 worker_state: Arc<WorkerState<D>>,
1500 doc_sender: Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
1501 resume_workers: bool,
1502}
1503
1504impl<D: DirectoryWriter + 'static> CommitFinalizationGuard<D> {
1505 fn resume_on_drop(&mut self) {
1506 self.resume_workers = true;
1507 }
1508}
1509
1510impl<D: DirectoryWriter + 'static> Drop for CommitFinalizationGuard<D> {
1511 fn drop(&mut self) {
1512 if self.resume_workers {
1513 IndexWriter::<D>::resume_workers_shared(&self.worker_state, &self.doc_sender);
1514 }
1515 self.state.finish();
1516 }
1517}
1518
1519struct OwnedCommitFinalization<D: DirectoryWriter + 'static> {
1524 directory: Arc<D>,
1525 schema: Arc<Schema>,
1526 segment_manager: Arc<crate::merge::SegmentManager<D>>,
1527 primary_key_index: Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
1528 primary_key_refresh_lock: Arc<tokio::sync::Mutex<()>>,
1529 prepared: PreparedSegmentsGuard<D>,
1530 finalization: Option<CommitFinalizationGuard<D>>,
1531 publication_observed: Arc<AtomicBool>,
1532 pk_reservations_retained: Arc<AtomicBool>,
1533}
1534
1535#[derive(Clone, Copy)]
1536enum PrimaryKeyRefresh {
1537 Commit,
1539 Replacement,
1542 FinalReplacement,
1545}
1546
1547async fn refresh_primary_key_snapshot<D: DirectoryWriter + 'static>(
1548 directory: &Arc<D>,
1549 schema: &Arc<Schema>,
1550 segment_manager: &Arc<crate::merge::SegmentManager<D>>,
1551 primary_key_index: &Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
1552 primary_key_refresh_lock: &Arc<tokio::sync::Mutex<()>>,
1553 refresh: PrimaryKeyRefresh,
1554) -> Result<()> {
1555 let _refresh_guard = primary_key_refresh_lock.lock().await;
1556 let existing_ids: std::collections::HashSet<String> = {
1557 let guard = primary_key_index.read();
1558 let Some(pk_index) = guard.as_ref() else {
1559 return Ok(());
1560 };
1561 pk_index
1562 .committed_segment_ids()
1563 .map(ToOwned::to_owned)
1564 .collect()
1565 };
1566
1567 let snapshot = segment_manager.acquire_snapshot().await;
1568 let load_futures: Vec<_> = snapshot
1569 .segment_ids()
1570 .iter()
1571 .filter(|id| !existing_ids.contains(id.as_str()))
1572 .map(|seg_id_str| {
1573 let seg_id_str = seg_id_str.clone();
1574 let dir = directory.as_ref();
1575 let schema = Arc::clone(schema);
1576 async move { load_pk_segment_data(dir, &seg_id_str, &schema).await }
1577 })
1578 .collect();
1579 let new_data = futures::future::try_join_all(load_futures).await?;
1580 let seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
1581
1582 let persist_bloom = {
1583 let mut guard = primary_key_index.write();
1584 let Some(pk_index) = guard.as_mut() else {
1585 return Ok(());
1586 };
1587 match refresh {
1588 PrimaryKeyRefresh::Commit => pk_index.refresh_incremental(new_data, snapshot),
1589 PrimaryKeyRefresh::Replacement | PrimaryKeyRefresh::FinalReplacement => {
1590 pk_index.refresh_replacement(new_data, snapshot);
1591 }
1592 }
1593 matches!(
1594 refresh,
1595 PrimaryKeyRefresh::Commit | PrimaryKeyRefresh::FinalReplacement
1596 )
1597 };
1598
1599 if persist_bloom {
1600 let writer = match directory
1601 .streaming_writer(std::path::Path::new(super::primary_key::PK_BLOOM_FILE))
1602 .await
1603 {
1604 Ok(writer) => writer,
1605 Err(error) => {
1606 log::warn!("[primary_key] failed to open bloom cache: {}", error);
1607 return Ok(());
1608 }
1609 };
1610 let guard = primary_key_index.read();
1613 if let Some(pk_index) = guard.as_ref()
1614 && let Err(error) = crate::segment::block_in_place_if_multithread(|| {
1615 write_pk_bloom_stream(pk_index, &seg_ids, writer)
1616 })
1617 {
1618 log::warn!("[primary_key] failed to persist bloom cache: {}", error);
1619 }
1620 }
1621 Ok(())
1622}
1623
1624fn write_pk_bloom_stream(
1625 pk_index: &super::primary_key::PrimaryKeyIndex,
1626 segment_ids: &[String],
1627 mut writer: Box<dyn crate::directories::StreamingWriter>,
1628) -> std::io::Result<()> {
1629 pk_index.write_bloom_cache(segment_ids, writer.as_mut())?;
1630 writer.finish()
1631}
1632
1633async fn finalize_prepared_commit<D: DirectoryWriter + 'static>(
1634 mut commit: OwnedCommitFinalization<D>,
1635) -> Result<bool> {
1636 let metadata_entries = commit.prepared.metadata_entries();
1637 let published_segment_ids = commit.prepared.vector_upgrade_segment_ids();
1638
1639 commit.segment_manager.commit(&metadata_entries).await?;
1643 commit.publication_observed.store(true, Ordering::Release);
1644
1645 let mut published = commit.prepared.take_published();
1646 for segment in &mut published {
1647 segment.mark_published();
1648 }
1649 drop(published);
1650 commit
1651 .segment_manager
1652 .schedule_vector_segment_upgrades(published_segment_ids);
1653 if let Some(finalization) = commit.finalization.as_mut() {
1657 finalization.resume_on_drop();
1658 } else {
1659 log::error!("owned commit finalization guard was already released after publication");
1660 }
1661
1662 match refresh_primary_key_snapshot(
1667 &commit.directory,
1668 &commit.schema,
1669 &commit.segment_manager,
1670 &commit.primary_key_index,
1671 &commit.primary_key_refresh_lock,
1672 PrimaryKeyRefresh::Commit,
1673 )
1674 .await
1675 {
1676 Ok(()) => commit
1679 .pk_reservations_retained
1680 .store(false, Ordering::Release),
1681 Err(error) => {
1682 commit
1687 .pk_reservations_retained
1688 .store(true, Ordering::Release);
1689 log::error!(
1690 "[primary_key] committed metadata but failed to refresh dedup state; \
1691 retaining reservations until a later successful commit: {}",
1692 error,
1693 );
1694 }
1695 }
1696
1697 drop(commit.finalization.take());
1701 commit.segment_manager.maybe_merge().await;
1702 Ok(true)
1703}
1704
1705impl<'a, D: DirectoryWriter + 'static> PreparedCommit<'a, D> {
1706 pub async fn commit(mut self) -> Result<bool> {
1710 let segments = std::mem::take(&mut *self.writer.flushed_segments.lock());
1711
1712 if segments.is_empty() {
1714 log::debug!("[commit] no segments to commit, skipping");
1715 self.is_resolved = true;
1716 self.writer.resume_workers();
1717 return Ok(false);
1718 }
1719
1720 if !self.writer.commit_finalization.begin() {
1721 self.writer.flushed_segments.lock().extend(segments);
1722 self.is_resolved = true;
1726 return Err(Error::CommitInProgress);
1727 }
1728
1729 let publication_observed = Arc::new(AtomicBool::new(false));
1730 let owned = OwnedCommitFinalization {
1731 directory: Arc::clone(&self.writer.directory),
1732 schema: Arc::clone(&self.writer.schema),
1733 segment_manager: Arc::clone(&self.writer.segment_manager),
1734 primary_key_index: Arc::clone(&self.writer.primary_key_index),
1735 primary_key_refresh_lock: Arc::clone(&self.writer.primary_key_refresh_lock),
1736 prepared: PreparedSegmentsGuard {
1737 segments: Some(segments),
1738 retry_slot: Arc::clone(&self.writer.flushed_segments),
1739 },
1740 finalization: Some(CommitFinalizationGuard {
1741 state: Arc::clone(&self.writer.commit_finalization),
1742 worker_state: Arc::clone(&self.writer.worker_state),
1743 doc_sender: Arc::clone(&self.writer.doc_sender),
1744 resume_workers: false,
1745 }),
1746 publication_observed: Arc::clone(&publication_observed),
1747 pk_reservations_retained: Arc::clone(&self.writer.pk_reservations_retained),
1748 };
1749
1750 self.is_resolved = true;
1755 let task_publication = Arc::clone(&publication_observed);
1756 let task = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1757 tokio::spawn(async move {
1758 match std::panic::AssertUnwindSafe(finalize_prepared_commit(owned))
1759 .catch_unwind()
1760 .await
1761 {
1762 Ok(result) => result,
1763 Err(_) if task_publication.load(Ordering::Acquire) => {
1764 log::error!(
1765 "owned commit finalizer panicked after metadata publication; \
1766 treating the durable generation as committed"
1767 );
1768 Ok(true)
1769 }
1770 Err(_) => Err(Error::Internal(
1771 "owned commit finalizer panicked before metadata publication".into(),
1772 )),
1773 }
1774 })
1775 }))
1776 .map_err(|_| Error::Internal("runtime rejected owned commit finalizer".into()))?;
1777
1778 match task.await {
1779 Ok(result) => result,
1780 Err(error) if publication_observed.load(Ordering::Acquire) => {
1781 log::error!(
1782 "owned commit finalizer terminated after metadata publication: {}; \
1783 treating the durable generation as committed",
1784 error,
1785 );
1786 Ok(true)
1787 }
1788 Err(error) => Err(Error::Internal(format!(
1789 "owned commit finalizer terminated unexpectedly: {error}"
1790 ))),
1791 }
1792 }
1793
1794 pub fn abort(mut self) {
1797 self.is_resolved = true;
1798 self.writer.flushed_segments.lock().clear();
1799 self.writer.clear_uncommitted_pk_reservations();
1800 self.writer.resume_workers();
1801 }
1802}
1803
1804impl<D: DirectoryWriter + 'static> Drop for PreparedCommit<'_, D> {
1805 fn drop(&mut self) {
1806 if !self.is_resolved {
1807 log::warn!("PreparedCommit dropped without commit/abort — auto-aborting");
1808 self.writer.flushed_segments.lock().clear();
1809 self.writer.clear_uncommitted_pk_reservations();
1810 self.writer.resume_workers();
1811 }
1812 }
1813}
1814
1815async fn load_pk_segment_data<D: crate::directories::Directory>(
1817 dir: &D,
1818 seg_id_str: &str,
1819 schema: &Arc<crate::dsl::Schema>,
1820) -> Result<super::primary_key::PkSegmentData> {
1821 let seg_id = crate::segment::SegmentId::from_hex(seg_id_str)
1822 .ok_or_else(|| Error::Internal(format!("Invalid segment id: {}", seg_id_str)))?;
1823 let files = crate::segment::SegmentFiles::new(seg_id.0);
1824 let fast_fields =
1825 crate::segment::reader::loader::load_fast_fields_file(dir, &files, schema).await?;
1826 Ok(super::primary_key::PkSegmentData {
1827 segment_id: seg_id_str.to_string(),
1828 fast_fields,
1829 })
1830}