use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use futures::FutureExt;
use rustc_hash::FxHashMap;
use crate::directories::DirectoryWriter;
use crate::dsl::{Document, Field, Schema};
use crate::error::{Error, Result};
use crate::segment::{SegmentBuilder, SegmentBuilderConfig, SegmentId};
use crate::tokenizer::BoxedTokenizer;
use super::IndexConfig;
const PIPELINE_MAX_SIZE_IN_DOCS: usize = 10_000;
pub struct IndexWriter<D: DirectoryWriter + 'static> {
pub(super) directory: Arc<D>,
pub(super) schema: Arc<Schema>,
pub(super) config: IndexConfig,
doc_sender: Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
workers: Vec<std::thread::JoinHandle<()>>,
worker_state: Arc<WorkerState<D>>,
pub(super) segment_manager: Arc<crate::merge::SegmentManager<D>>,
flushed_segments: Arc<parking_lot::Mutex<Vec<PreparedSegment<D>>>>,
primary_key_index: Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
commit_finalization: Arc<CommitFinalizationState>,
}
#[derive(Default)]
struct CommitFinalizationState {
in_progress: AtomicBool,
idle: tokio::sync::Notify,
}
impl CommitFinalizationState {
fn begin(&self) -> bool {
self.in_progress
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
}
fn finish(&self) {
self.in_progress.store(false, Ordering::Release);
self.idle.notify_waiters();
}
async fn wait_until_idle(&self) {
while self.in_progress.load(Ordering::Acquire) {
let notified = self.idle.notified();
if !self.in_progress.load(Ordering::Acquire) {
break;
}
notified.await;
}
}
}
struct WorkerState<D: DirectoryWriter + 'static> {
directory: Arc<D>,
schema: Arc<Schema>,
builder_config: SegmentBuilderConfig,
tokenizers: parking_lot::RwLock<FxHashMap<Field, BoxedTokenizer>>,
memory_budget_per_worker: usize,
segment_manager: Arc<crate::merge::SegmentManager<D>>,
built_segments: parking_lot::Mutex<Vec<PreparedSegment<D>>>,
cycle_error: parking_lot::Mutex<Option<String>>,
cycle_failed: AtomicBool,
flush_count: AtomicUsize,
flush_mutex: parking_lot::Mutex<()>,
flush_cvar: parking_lot::Condvar,
resume_receiver: parking_lot::Mutex<Option<async_channel::Receiver<Document>>>,
resume_epoch: AtomicUsize,
resume_cvar: parking_lot::Condvar,
shutdown: AtomicBool,
num_workers: usize,
}
struct PreparedSegment<D: DirectoryWriter + 'static> {
id: String,
segment_id: SegmentId,
num_docs: u32,
segment_manager: Arc<crate::merge::SegmentManager<D>>,
operation: Option<crate::merge::SegmentOperationGuard>,
runtime: tokio::runtime::Handle,
published: bool,
}
impl<D: DirectoryWriter + 'static> PreparedSegment<D> {
fn metadata_entry(&self) -> (String, u32) {
(self.id.clone(), self.num_docs)
}
fn mark_published(&mut self) {
self.published = true;
drop(self.operation.take());
}
}
impl<D: DirectoryWriter + 'static> WorkerState<D> {
fn record_cycle_error(&self, error: impl Into<String>) {
let mut first_error = self.cycle_error.lock();
if first_error.is_none() {
*first_error = Some(error.into());
}
drop(first_error);
self.cycle_failed.store(true, Ordering::Release);
}
}
impl<D: DirectoryWriter + 'static> Drop for PreparedSegment<D> {
fn drop(&mut self) {
if self.published {
return;
}
let Some(operation) = self.operation.take() else {
return;
};
self.segment_manager.schedule_unpublished_segment_cleanup(
self.segment_id,
operation,
self.runtime.clone(),
);
}
}
impl<D: DirectoryWriter + 'static> IndexWriter<D> {
pub async fn create(directory: D, schema: Schema, config: IndexConfig) -> Result<Self> {
Self::create_with_config(directory, schema, config, SegmentBuilderConfig::default()).await
}
pub async fn create_with_config(
directory: D,
schema: Schema,
config: IndexConfig,
builder_config: SegmentBuilderConfig,
) -> Result<Self> {
let directory = Arc::new(directory);
let schema = Arc::new(schema);
directory.set_index_label(schema.index_label());
let metadata = super::IndexMetadata::new((*schema).clone());
let segment_manager = Arc::new(crate::merge::SegmentManager::new(
Arc::clone(&directory),
Arc::clone(&schema),
metadata,
config.merge_policy.clone_box(),
config.term_cache_blocks,
config.max_concurrent_merges,
Arc::clone(&config.background_merge_permits),
config.merge_bp_time_budget,
config.bp_memory_budget_bytes,
Arc::clone(&config.background_reorder_permits),
config.background_reorder_pool.clone(),
));
segment_manager.update_metadata(|_| {}).await?;
Ok(Self::new_with_parts(
directory,
schema,
config,
builder_config,
segment_manager,
))
}
pub async fn open(directory: D, config: IndexConfig) -> Result<Self> {
Self::open_with_config(directory, config, SegmentBuilderConfig::default()).await
}
pub async fn open_with_config(
directory: D,
config: IndexConfig,
builder_config: SegmentBuilderConfig,
) -> Result<Self> {
let directory = Arc::new(directory);
let metadata = super::IndexMetadata::load(directory.as_ref()).await?;
let schema = Arc::new(metadata.schema.clone());
directory.set_index_label(schema.index_label());
let segment_manager = Arc::new(crate::merge::SegmentManager::new(
Arc::clone(&directory),
Arc::clone(&schema),
metadata,
config.merge_policy.clone_box(),
config.term_cache_blocks,
config.max_concurrent_merges,
Arc::clone(&config.background_merge_permits),
config.merge_bp_time_budget,
config.bp_memory_budget_bytes,
Arc::clone(&config.background_reorder_permits),
config.background_reorder_pool.clone(),
));
let swept = segment_manager.cleanup_orphan_segments().await?;
if swept > 0 {
log::warn!(
"[segment_cleanup] swept {} orphan segment(s) while opening writer",
swept
);
}
segment_manager.try_load_and_publish_trained().await?;
Ok(Self::new_with_parts(
directory,
schema,
config,
builder_config,
segment_manager,
))
}
pub fn from_index(index: &super::Index<D>) -> Self {
Self::new_with_parts(
Arc::clone(&index.directory),
Arc::clone(&index.schema),
index.config.clone(),
SegmentBuilderConfig::default(),
Arc::clone(&index.segment_manager),
)
}
fn new_with_parts(
directory: Arc<D>,
schema: Arc<Schema>,
config: IndexConfig,
builder_config: SegmentBuilderConfig,
segment_manager: Arc<crate::merge::SegmentManager<D>>,
) -> Self {
let registry = crate::tokenizer::TokenizerRegistry::new();
let mut tokenizers = FxHashMap::default();
for (field, entry) in schema.fields() {
if matches!(entry.field_type, crate::dsl::FieldType::Text)
&& let Some(ref tok_name) = entry.tokenizer
&& let Some(tok) = registry.get(tok_name)
{
tokenizers.insert(field, tok);
}
}
let num_workers = config.num_indexing_threads.max(1);
let worker_state = Arc::new(WorkerState {
directory: Arc::clone(&directory),
schema: Arc::clone(&schema),
builder_config,
tokenizers: parking_lot::RwLock::new(tokenizers),
memory_budget_per_worker: config.max_indexing_memory_bytes / num_workers,
segment_manager: Arc::clone(&segment_manager),
built_segments: parking_lot::Mutex::new(Vec::new()),
cycle_error: parking_lot::Mutex::new(None),
cycle_failed: AtomicBool::new(false),
flush_count: AtomicUsize::new(0),
flush_mutex: parking_lot::Mutex::new(()),
flush_cvar: parking_lot::Condvar::new(),
resume_receiver: parking_lot::Mutex::new(None),
resume_epoch: AtomicUsize::new(0),
resume_cvar: parking_lot::Condvar::new(),
shutdown: AtomicBool::new(false),
num_workers,
});
let (doc_sender, workers) = Self::spawn_workers(&worker_state, num_workers);
Self {
directory,
schema,
config,
doc_sender: Arc::new(parking_lot::RwLock::new(doc_sender)),
workers,
worker_state,
segment_manager,
flushed_segments: Arc::new(parking_lot::Mutex::new(Vec::new())),
primary_key_index: Arc::new(parking_lot::RwLock::new(None)),
commit_finalization: Arc::new(CommitFinalizationState::default()),
}
}
fn spawn_workers(
worker_state: &Arc<WorkerState<D>>,
num_workers: usize,
) -> (
async_channel::Sender<Document>,
Vec<std::thread::JoinHandle<()>>,
) {
let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
let handle = tokio::runtime::Handle::current();
let mut workers = Vec::with_capacity(num_workers);
for i in 0..num_workers {
let state = Arc::clone(worker_state);
let rx = receiver.clone();
let rt = handle.clone();
workers.push(
std::thread::Builder::new()
.name(format!("index-worker-{}", i))
.spawn(move || Self::worker_loop(state, rx, rt))
.expect("failed to spawn index worker thread"),
);
}
(sender, workers)
}
pub fn schema(&self) -> &Schema {
&self.schema
}
pub fn set_tokenizer<T: crate::tokenizer::Tokenizer>(&mut self, field: Field, tokenizer: T) {
self.worker_state
.tokenizers
.write()
.insert(field, Box::new(tokenizer));
}
pub async fn init_primary_key_dedup(&mut self) -> Result<()> {
use super::primary_key::{PK_BLOOM_FILE, deserialize_pk_bloom};
self.commit_finalization.wait_until_idle().await;
let field = match self.schema.primary_field() {
Some(f) => f,
None => return Ok(()),
};
let snapshot = self.segment_manager.acquire_snapshot().await;
let current_seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
let cached = match self
.directory
.open_read(std::path::Path::new(PK_BLOOM_FILE))
.await
{
Ok(handle) => {
let data = handle.read_bytes_range(0..handle.len()).await;
match data {
Ok(bytes) => deserialize_pk_bloom(bytes.as_slice()),
Err(_) => None,
}
}
Err(_) => None,
};
let load_futures: Vec<_> = current_seg_ids
.iter()
.map(|seg_id_str| {
let seg_id_str = seg_id_str.clone();
let dir = self.directory.as_ref();
let schema = Arc::clone(&self.schema);
async move { load_pk_segment_data(dir, &seg_id_str, &schema).await }
})
.collect();
let all_data = futures::future::try_join_all(load_futures).await?;
if let Some((persisted_seg_ids, bloom)) = cached {
let mut pk_data = Vec::with_capacity(all_data.len());
let mut new_data = Vec::new();
for d in all_data {
if persisted_seg_ids.contains(&d.segment_id) {
pk_data.push(d);
} else {
new_data.push(d);
}
}
let needs_persist = !new_data.is_empty();
let new_start = pk_data.len();
pk_data.extend(new_data);
let pk_index = if new_start == pk_data.len() {
super::primary_key::PrimaryKeyIndex::from_persisted(
field,
bloom,
pk_data,
&[],
snapshot,
)
} else {
tokio::task::spawn_blocking(move || {
let mut bloom = bloom;
let mut added = 0usize;
let num_new = pk_data.len() - new_start;
for data in &pk_data[new_start..] {
if let Some(ff) = data.fast_fields.get(&field.0)
&& let Some(dict) = ff.text_dict()
{
for key in dict.iter() {
bloom.insert(key.as_bytes());
added += 1;
}
}
}
if added > 0 {
log::info!(
"[primary_key] bloom: added {} keys from {} new segment(s)",
added,
num_new,
);
}
super::primary_key::PrimaryKeyIndex::from_persisted(
field,
bloom,
pk_data,
&[],
snapshot,
)
})
.await
.map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?
};
if needs_persist {
self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
}
*self.primary_key_index.write() = Some(pk_index);
} else {
let pk_index = tokio::task::spawn_blocking(move || {
super::primary_key::PrimaryKeyIndex::new(field, all_data, snapshot)
})
.await
.map_err(|e| Error::Internal(format!("spawn_blocking failed: {}", e)))?;
self.persist_pk_bloom(&pk_index, ¤t_seg_ids).await;
*self.primary_key_index.write() = Some(pk_index);
}
Ok(())
}
async fn persist_pk_bloom(
&self,
pk_index: &super::primary_key::PrimaryKeyIndex,
segment_ids: &[String],
) {
use super::primary_key::{PK_BLOOM_FILE, serialize_pk_bloom};
let bloom_bytes = pk_index.bloom_to_bytes();
let data = serialize_pk_bloom(segment_ids, &bloom_bytes);
if let Err(e) = self
.directory
.write(std::path::Path::new(PK_BLOOM_FILE), &data)
.await
{
log::warn!("[primary_key] failed to persist bloom cache: {}", e);
}
}
pub fn add_document(&self, doc: Document) -> Result<()> {
if self.worker_state.shutdown.load(Ordering::Acquire) {
return Err(Error::IndexClosed);
}
if self.commit_finalization.in_progress.load(Ordering::Acquire) {
return Err(Error::CommitInProgress);
}
let sender = self.doc_sender.read().clone();
if sender.is_closed() {
return Err(Error::CommitInProgress);
}
let primary_key_index = self.primary_key_index.read();
if let Some(ref pk_index) = *primary_key_index {
pk_index.check_and_insert(&doc)?;
}
match sender.try_send(doc) {
Ok(()) => Ok(()),
Err(async_channel::TrySendError::Full(doc)) => {
if let Some(ref pk_index) = *primary_key_index {
pk_index.rollback_uncommitted_key(&doc);
}
Err(Error::QueueFull)
}
Err(async_channel::TrySendError::Closed(doc)) => {
if let Some(ref pk_index) = *primary_key_index {
pk_index.rollback_uncommitted_key(&doc);
}
Err(Error::CommitInProgress)
}
}
}
pub fn add_documents(&self, documents: Vec<Document>) -> Result<usize> {
let total = documents.len();
for (i, doc) in documents.into_iter().enumerate() {
match self.add_document(doc) {
Ok(()) => {}
Err(Error::QueueFull | Error::CommitInProgress) => return Ok(i),
Err(e) => return Err(e),
}
}
Ok(total)
}
fn worker_loop(
state: Arc<WorkerState<D>>,
initial_receiver: async_channel::Receiver<Document>,
handle: tokio::runtime::Handle,
) {
let mut receiver = initial_receiver;
let mut my_epoch = 0usize;
loop {
let build_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut builder: Option<SegmentBuilder> = None;
while let Ok(doc) = receiver.recv_blocking() {
if state.shutdown.load(Ordering::Acquire) {
break;
}
if state.cycle_failed.load(Ordering::Acquire) {
continue;
}
if builder.is_none() {
match SegmentBuilder::new(
Arc::clone(&state.schema),
state.builder_config.clone(),
) {
Ok(mut b) => {
for (field, tokenizer) in state.tokenizers.read().iter() {
b.set_tokenizer(*field, tokenizer.clone_box());
}
builder = Some(b);
}
Err(e) => {
log::error!("Failed to create segment builder: {:?}", e);
state.record_cycle_error(format!(
"failed to create segment builder: {e}"
));
continue;
}
}
}
let b = builder.as_mut().unwrap();
if let Err(e) = b.add_document(doc) {
log::error!("Failed to index document: {:?}", e);
state.record_cycle_error(format!("failed to index document: {e}"));
continue;
}
let builder_memory = b.estimated_memory_bytes();
if b.num_docs() & 0x3FFF == 0 {
log::debug!(
"[indexing] docs={}, memory={:.2} MB, budget={:.2} MB",
b.num_docs(),
builder_memory as f64 / (1024.0 * 1024.0),
state.memory_budget_per_worker as f64 / (1024.0 * 1024.0)
);
}
const MIN_DOCS_BEFORE_FLUSH: u32 = 100;
let effective_budget = state.memory_budget_per_worker * 4 / 5;
if builder_memory >= effective_budget && b.num_docs() >= MIN_DOCS_BEFORE_FLUSH {
log::info!(
"[indexing] memory budget reached, building segment: \
docs={}, memory={:.2} MB, budget={:.2} MB",
b.num_docs(),
builder_memory as f64 / (1024.0 * 1024.0),
state.memory_budget_per_worker as f64 / (1024.0 * 1024.0),
);
let full_builder = builder.take().unwrap();
Self::build_segment_inline(&state, full_builder, &handle);
}
}
if !state.cycle_failed.load(Ordering::Acquire)
&& let Some(b) = builder.take()
&& b.num_docs() > 0
{
Self::build_segment_inline(&state, b, &handle);
}
}));
if build_result.is_err() {
log::error!(
"[worker] panic during indexing cycle — documents in this cycle may be lost"
);
state.record_cycle_error("indexing worker panicked while building the batch");
}
let prev = state.flush_count.fetch_add(1, Ordering::Release);
if prev + 1 == state.num_workers {
let _lock = state.flush_mutex.lock();
state.flush_cvar.notify_one();
}
{
let mut lock = state.resume_receiver.lock();
loop {
if state.shutdown.load(Ordering::Acquire) {
return;
}
let current_epoch = state.resume_epoch.load(Ordering::Acquire);
if current_epoch > my_epoch
&& let Some(rx) = lock.as_ref()
{
receiver = rx.clone();
my_epoch = current_epoch;
break;
}
state.resume_cvar.wait(&mut lock);
}
}
}
}
fn build_segment_inline(
state: &WorkerState<D>,
builder: SegmentBuilder,
handle: &tokio::runtime::Handle,
) {
let segment_id = SegmentId::new();
let segment_hex = segment_id.to_hex();
let operation = match state
.segment_manager
.protect_new_segment(segment_hex.clone())
{
Ok(operation) => operation,
Err(e) => {
log::error!(
"[segment_build_failed] segment_id={} lifecycle_error={}",
segment_hex,
e,
);
state.record_cycle_error(format!(
"failed to claim segment {segment_hex} for building: {e}"
));
return;
}
};
let trained = state.segment_manager.trained_for_segment_build();
let doc_count = builder.num_docs();
let build_start = std::time::Instant::now();
log::info!(
"[segment_build] segment_id={} doc_count={} ann={}",
segment_hex,
doc_count,
trained.is_some()
);
let mut prepared = PreparedSegment {
id: segment_hex.clone(),
segment_id,
num_docs: doc_count,
segment_manager: Arc::clone(&state.segment_manager),
operation: Some(operation),
runtime: handle.clone(),
published: false,
};
match handle.block_on(builder.build(
state.directory.as_ref(),
segment_id,
trained.as_deref(),
)) {
Ok(meta) if meta.num_docs == doc_count && meta.num_docs > 0 => {
let duration_ms = build_start.elapsed().as_millis() as u64;
log::info!(
"[segment_build_done] segment_id={} doc_count={} duration_ms={}",
segment_hex,
meta.num_docs,
duration_ms,
);
prepared.num_docs = meta.num_docs;
state.built_segments.lock().push(prepared);
}
Ok(meta) => {
let error = format!(
"segment {segment_hex} built {} docs from a {doc_count}-document builder",
meta.num_docs
);
log::error!("[segment_build_failed] {error}");
state.record_cycle_error(error);
}
Err(e) => {
log::error!(
"[segment_build_failed] segment_id={} error={:?}",
segment_hex,
e
);
state.record_cycle_error(format!("failed to build segment {segment_hex}: {e}"));
}
}
}
pub async fn maybe_merge(&self) {
self.segment_manager.maybe_merge().await;
}
pub async fn abort_merges(&self) {
self.segment_manager.abort_merges().await;
}
pub async fn shutdown(&mut self) -> Result<()> {
self.segment_manager.begin_shutdown();
self.signal_worker_shutdown();
self.commit_finalization.wait_until_idle().await;
let workers = std::mem::take(&mut self.workers);
let panicked = tokio::task::spawn_blocking(move || {
workers
.into_iter()
.map(|worker| worker.join().is_err())
.filter(|panicked| *panicked)
.count()
})
.await
.map_err(|error| Error::Internal(format!("failed to join index workers: {}", error)))?;
if panicked > 0 {
log::error!("[index_shutdown] {} indexing worker(s) panicked", panicked);
}
self.flushed_segments.lock().clear();
self.worker_state.built_segments.lock().clear();
if let Some(pk_index) = self.primary_key_index.write().as_mut() {
pk_index.clear_uncommitted();
}
Ok(())
}
pub async fn wait_for_merging_thread(&self) {
self.segment_manager.wait_for_merging_thread().await;
}
pub async fn wait_for_all_merges(&self) {
self.segment_manager.wait_for_all_merges().await;
}
pub async fn wait_for_commit_finalization(&self) {
self.commit_finalization.wait_until_idle().await;
}
pub fn tracker(&self) -> std::sync::Arc<crate::segment::SegmentTracker> {
self.segment_manager.tracker()
}
pub async fn acquire_snapshot(&self) -> crate::segment::SegmentSnapshot {
self.segment_manager.acquire_snapshot().await
}
pub async fn cleanup_orphan_segments(&self) -> Result<usize> {
self.segment_manager.cleanup_orphan_segments().await
}
pub async fn prepare_commit(&mut self) -> Result<PreparedCommit<'_, D>> {
if self.worker_state.shutdown.load(Ordering::Acquire) {
return Err(Error::IndexClosed);
}
if self.commit_finalization.in_progress.load(Ordering::Acquire) {
return Err(Error::CommitInProgress);
}
self.doc_sender.read().close();
self.worker_state.resume_cvar.notify_all();
let state = Arc::clone(&self.worker_state);
let all_flushed = tokio::task::spawn_blocking(move || {
let mut lock = state.flush_mutex.lock();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(300);
while state.flush_count.load(Ordering::Acquire) < state.num_workers {
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
if remaining.is_zero() {
log::error!(
"[prepare_commit] timed out waiting for workers: {}/{} flushed",
state.flush_count.load(Ordering::Acquire),
state.num_workers
);
return false;
}
state.flush_cvar.wait_for(&mut lock, remaining);
}
true
})
.await
.map_err(|e| Error::Internal(format!("Failed to wait for workers: {}", e)))?;
if !all_flushed {
return Err(Error::Internal(format!(
"prepare_commit timed out: {}/{} workers flushed; writer remains paused, retry commit",
self.worker_state.flush_count.load(Ordering::Acquire),
self.worker_state.num_workers
)));
}
let cycle_error = { self.worker_state.cycle_error.lock().take() };
if let Some(error) = cycle_error {
self.flushed_segments.lock().clear();
self.worker_state.built_segments.lock().clear();
if let Some(pk_index) = self.primary_key_index.write().as_mut() {
pk_index.clear_uncommitted();
}
self.resume_workers();
return Err(Error::Internal(format!(
"indexing generation failed; no documents from this batch were committed: {error}"
)));
}
let built = std::mem::take(&mut *self.worker_state.built_segments.lock());
self.flushed_segments.lock().extend(built);
Ok(PreparedCommit {
writer: self,
is_resolved: false,
})
}
pub async fn commit(&mut self) -> Result<bool> {
self.prepare_commit().await?.commit().await
}
pub async fn force_merge(&mut self) -> Result<()> {
self.prepare_commit().await?.commit().await?;
self.segment_manager.force_merge().await
}
pub async fn reorder(&mut self) -> Result<()> {
self.prepare_commit().await?.commit().await?;
self.segment_manager.reorder_segments().await
}
pub fn segment_manager(&self) -> &Arc<crate::merge::SegmentManager<D>> {
&self.segment_manager
}
fn resume_workers(&mut self) {
Self::resume_workers_shared(&self.worker_state, &self.doc_sender);
}
fn resume_workers_shared(
worker_state: &Arc<WorkerState<D>>,
doc_sender: &Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
) {
if worker_state.shutdown.load(Ordering::Acquire) {
return;
}
if tokio::runtime::Handle::try_current().is_err() {
worker_state.shutdown.store(true, Ordering::Release);
worker_state.resume_cvar.notify_all();
return;
}
worker_state.flush_count.store(0, Ordering::Release);
*worker_state.cycle_error.lock() = None;
worker_state.cycle_failed.store(false, Ordering::Release);
let (sender, receiver) = async_channel::bounded(PIPELINE_MAX_SIZE_IN_DOCS);
*doc_sender.write() = sender;
{
let mut lock = worker_state.resume_receiver.lock();
*lock = Some(receiver);
}
worker_state.resume_epoch.fetch_add(1, Ordering::Release);
worker_state.resume_cvar.notify_all();
}
fn signal_worker_shutdown(&self) {
self.worker_state.shutdown.store(true, Ordering::Release);
self.doc_sender.read().close();
self.worker_state.resume_cvar.notify_all();
}
}
impl<D: DirectoryWriter + 'static> Drop for IndexWriter<D> {
fn drop(&mut self) {
self.signal_worker_shutdown();
for w in std::mem::take(&mut self.workers) {
let _ = w.join();
}
}
}
pub struct PreparedCommit<'a, D: DirectoryWriter + 'static> {
writer: &'a mut IndexWriter<D>,
is_resolved: bool,
}
struct PreparedSegmentsGuard<D: DirectoryWriter + 'static> {
segments: Option<Vec<PreparedSegment<D>>>,
retry_slot: Arc<parking_lot::Mutex<Vec<PreparedSegment<D>>>>,
}
impl<D: DirectoryWriter + 'static> PreparedSegmentsGuard<D> {
fn metadata_entries(&self) -> Vec<(String, u32)> {
self.segments
.as_deref()
.unwrap_or_default()
.iter()
.map(PreparedSegment::metadata_entry)
.collect()
}
fn take_published(&mut self) -> Vec<PreparedSegment<D>> {
self.segments.take().unwrap_or_default()
}
}
impl<D: DirectoryWriter + 'static> Drop for PreparedSegmentsGuard<D> {
fn drop(&mut self) {
if let Some(segments) = self.segments.take() {
self.retry_slot.lock().extend(segments);
}
}
}
struct CommitFinalizationGuard<D: DirectoryWriter + 'static> {
state: Arc<CommitFinalizationState>,
worker_state: Arc<WorkerState<D>>,
doc_sender: Arc<parking_lot::RwLock<async_channel::Sender<Document>>>,
resume_workers: bool,
}
impl<D: DirectoryWriter + 'static> CommitFinalizationGuard<D> {
fn resume_on_drop(&mut self) {
self.resume_workers = true;
}
}
impl<D: DirectoryWriter + 'static> Drop for CommitFinalizationGuard<D> {
fn drop(&mut self) {
if self.resume_workers {
IndexWriter::<D>::resume_workers_shared(&self.worker_state, &self.doc_sender);
}
self.state.finish();
}
}
struct OwnedCommitFinalization<D: DirectoryWriter + 'static> {
directory: Arc<D>,
schema: Arc<Schema>,
segment_manager: Arc<crate::merge::SegmentManager<D>>,
primary_key_index: Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
prepared: PreparedSegmentsGuard<D>,
finalization: Option<CommitFinalizationGuard<D>>,
publication_observed: Arc<AtomicBool>,
}
async fn refresh_primary_key_after_commit<D: DirectoryWriter + 'static>(
directory: &Arc<D>,
schema: &Arc<Schema>,
segment_manager: &Arc<crate::merge::SegmentManager<D>>,
primary_key_index: &Arc<parking_lot::RwLock<Option<super::primary_key::PrimaryKeyIndex>>>,
) -> Result<()> {
let existing_ids: std::collections::HashSet<String> = {
let guard = primary_key_index.read();
let Some(pk_index) = guard.as_ref() else {
return Ok(());
};
pk_index
.committed_segment_ids()
.map(ToOwned::to_owned)
.collect()
};
let snapshot = segment_manager.acquire_snapshot().await;
let load_futures: Vec<_> = snapshot
.segment_ids()
.iter()
.filter(|id| !existing_ids.contains(id.as_str()))
.map(|seg_id_str| {
let seg_id_str = seg_id_str.clone();
let dir = directory.as_ref();
let schema = Arc::clone(schema);
async move { load_pk_segment_data(dir, &seg_id_str, &schema).await }
})
.collect();
let new_data = futures::future::try_join_all(load_futures).await?;
let seg_ids: Vec<String> = snapshot.segment_ids().to_vec();
let bloom_file = {
let mut guard = primary_key_index.write();
let Some(pk_index) = guard.as_mut() else {
return Ok(());
};
pk_index.refresh_incremental(new_data, snapshot);
let bloom_bytes = pk_index.bloom_to_bytes();
super::primary_key::serialize_pk_bloom(&seg_ids, &bloom_bytes)
};
if let Err(error) = directory
.write(
std::path::Path::new(super::primary_key::PK_BLOOM_FILE),
&bloom_file,
)
.await
{
log::warn!("[primary_key] failed to persist bloom cache: {}", error);
}
Ok(())
}
async fn finalize_prepared_commit<D: DirectoryWriter + 'static>(
mut commit: OwnedCommitFinalization<D>,
) -> Result<bool> {
let metadata_entries = commit.prepared.metadata_entries();
commit.segment_manager.commit(&metadata_entries).await?;
commit.publication_observed.store(true, Ordering::Release);
let mut published = commit.prepared.take_published();
for segment in &mut published {
segment.mark_published();
}
drop(published);
if let Some(finalization) = commit.finalization.as_mut() {
finalization.resume_on_drop();
} else {
log::error!("owned commit finalization guard was already released after publication");
}
if let Err(error) = refresh_primary_key_after_commit(
&commit.directory,
&commit.schema,
&commit.segment_manager,
&commit.primary_key_index,
)
.await
{
log::error!(
"[primary_key] committed metadata but failed to refresh dedup state; \
retaining reservations until a later successful commit: {}",
error,
);
}
drop(commit.finalization.take());
commit.segment_manager.maybe_merge().await;
Ok(true)
}
impl<'a, D: DirectoryWriter + 'static> PreparedCommit<'a, D> {
pub async fn commit(mut self) -> Result<bool> {
let segments = std::mem::take(&mut *self.writer.flushed_segments.lock());
if segments.is_empty() {
log::debug!("[commit] no segments to commit, skipping");
self.is_resolved = true;
self.writer.resume_workers();
return Ok(false);
}
if !self.writer.commit_finalization.begin() {
self.writer.flushed_segments.lock().extend(segments);
self.is_resolved = true;
return Err(Error::CommitInProgress);
}
let publication_observed = Arc::new(AtomicBool::new(false));
let owned = OwnedCommitFinalization {
directory: Arc::clone(&self.writer.directory),
schema: Arc::clone(&self.writer.schema),
segment_manager: Arc::clone(&self.writer.segment_manager),
primary_key_index: Arc::clone(&self.writer.primary_key_index),
prepared: PreparedSegmentsGuard {
segments: Some(segments),
retry_slot: Arc::clone(&self.writer.flushed_segments),
},
finalization: Some(CommitFinalizationGuard {
state: Arc::clone(&self.writer.commit_finalization),
worker_state: Arc::clone(&self.writer.worker_state),
doc_sender: Arc::clone(&self.writer.doc_sender),
resume_workers: false,
}),
publication_observed: Arc::clone(&publication_observed),
};
self.is_resolved = true;
let task_publication = Arc::clone(&publication_observed);
let task = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
tokio::spawn(async move {
match std::panic::AssertUnwindSafe(finalize_prepared_commit(owned))
.catch_unwind()
.await
{
Ok(result) => result,
Err(_) if task_publication.load(Ordering::Acquire) => {
log::error!(
"owned commit finalizer panicked after metadata publication; \
treating the durable generation as committed"
);
Ok(true)
}
Err(_) => Err(Error::Internal(
"owned commit finalizer panicked before metadata publication".into(),
)),
}
})
}))
.map_err(|_| Error::Internal("runtime rejected owned commit finalizer".into()))?;
match task.await {
Ok(result) => result,
Err(error) if publication_observed.load(Ordering::Acquire) => {
log::error!(
"owned commit finalizer terminated after metadata publication: {}; \
treating the durable generation as committed",
error,
);
Ok(true)
}
Err(error) => Err(Error::Internal(format!(
"owned commit finalizer terminated unexpectedly: {error}"
))),
}
}
pub fn abort(mut self) {
self.is_resolved = true;
self.writer.flushed_segments.lock().clear();
if let Some(pk_index) = self.writer.primary_key_index.write().as_mut() {
pk_index.clear_uncommitted();
}
self.writer.resume_workers();
}
}
impl<D: DirectoryWriter + 'static> Drop for PreparedCommit<'_, D> {
fn drop(&mut self) {
if !self.is_resolved {
log::warn!("PreparedCommit dropped without commit/abort — auto-aborting");
self.writer.flushed_segments.lock().clear();
if let Some(pk_index) = self.writer.primary_key_index.write().as_mut() {
pk_index.clear_uncommitted();
}
self.writer.resume_workers();
}
}
}
async fn load_pk_segment_data<D: crate::directories::Directory>(
dir: &D,
seg_id_str: &str,
schema: &Arc<crate::dsl::Schema>,
) -> Result<super::primary_key::PkSegmentData> {
let seg_id = crate::segment::SegmentId::from_hex(seg_id_str)
.ok_or_else(|| Error::Internal(format!("Invalid segment id: {}", seg_id_str)))?;
let files = crate::segment::SegmentFiles::new(seg_id.0);
let fast_fields =
crate::segment::reader::loader::load_fast_fields_file(dir, &files, schema).await?;
Ok(super::primary_key::PkSegmentData {
segment_id: seg_id_str.to_string(),
fast_fields,
})
}