use crate::index::key_codec::key_types::{KEY_TYPE_EDGE_REVERSE, KEY_TYPE_VERTEX_REVERSE};
use crate::index::key_codec::KeyBuilder;
use crate::index::manifest::{IndexManifest, IndexShard};
use crate::index::shard_runtime::{GenerationRuntime, IndexMaps};
use crate::index::types::IndexIdentity;
use crate::index::types::IndexRecord;
use linkrs_core::types::{IndexGeneration, IndexType, Timestamp};
use linkrs_core::{StorageError, StorageResult};
use std::collections::{BTreeMap, HashMap};
use std::sync::atomic::Ordering;
use super::IndexDataManagerImpl;
impl IndexDataManagerImpl {
pub(crate) fn compute_prefixes(&self, identity: IndexIdentity) -> (Vec<u8>, Vec<u8>) {
let index_type = self.index_types.read().get(&identity).cloned();
let index_def = self.index_definitions.read().get(&identity).cloned();
match (index_type, index_def.as_ref()) {
(Some(IndexType::TagIndex), Some(def)) => {
let fwd = KeyBuilder::build_vertex_index_prefix(identity.space_id, &def.name).0;
let mut rev = Vec::with_capacity(9);
rev.extend_from_slice(&identity.space_id.to_le_bytes());
rev.push(KEY_TYPE_VERTEX_REVERSE);
(fwd, rev)
}
(Some(IndexType::EdgeIndex), Some(def)) => {
let fwd = KeyBuilder::build_edge_index_prefix(identity.space_id, &def.name).0;
let mut rev = Vec::with_capacity(9);
rev.extend_from_slice(&identity.space_id.to_le_bytes());
rev.push(KEY_TYPE_EDGE_REVERSE);
(fwd, rev)
}
_ => (Vec::new(), Vec::new()),
}
}
pub(crate) fn accumulate_delta(
&self,
identity: IndexIdentity,
delta: HashMap<u32, IndexMaps>,
write_ts: Timestamp,
) -> StorageResult<()> {
let threshold = self.delta_publish_threshold.load(Ordering::Relaxed).max(1);
if threshold <= 1 {
let tombstones = delta
.values()
.flat_map(|(forward, reverse)| forward.values().chain(reverse.values()))
.filter(|record| record.deleted_ts.is_some())
.count() as u64;
if tombstones > 0 {
self.cached_tombstone_count
.fetch_add(tombstones, Ordering::Relaxed);
}
return self.publish_delta_generation(identity, delta, write_ts);
}
let mut pending = self.pending_deltas.lock();
let entry = pending.entry(identity).or_default();
let mut added = 0usize;
let merge_record = |map: &mut BTreeMap<Vec<u8>, IndexRecord>,
key: Vec<u8>,
record: IndexRecord,
counter: &std::sync::atomic::AtomicU64| {
let record_is_tombstone = record.deleted_ts.is_some();
match map.insert(key, record) {
Some(old) => match (old.deleted_ts.is_some(), record_is_tombstone) {
(false, true) => {
counter.fetch_add(1, Ordering::Relaxed);
}
(true, false) => {
let _ =
counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |count| {
Some(count.saturating_sub(1))
});
}
_ => {}
},
None => {
if record_is_tombstone {
counter.fetch_add(1, Ordering::Relaxed);
}
}
}
};
for (shard_id, (forward, reverse)) in delta {
let (pending_fwd, pending_rev) = entry.per_shard.entry(shard_id).or_default();
added += forward.len() + reverse.len();
for (key, record) in forward {
merge_record(pending_fwd, key, record, &self.cached_tombstone_count);
}
for (key, record) in reverse {
merge_record(pending_rev, key, record, &self.cached_tombstone_count);
}
}
entry.entries += added;
entry.write_ts = entry.write_ts.max(write_ts);
if entry.entries >= threshold {
let to_publish = pending.remove(&identity).expect("entry exists");
drop(pending);
return self.publish_delta_generation(
identity,
to_publish.per_shard,
to_publish.write_ts,
);
}
Ok(())
}
pub(crate) fn publish_pending_delta(&self, identity: IndexIdentity) -> StorageResult<()> {
let to_publish = {
let mut pending = self.pending_deltas.lock();
let Some(entry) = pending.remove(&identity) else {
return Ok(());
};
if entry.per_shard.is_empty() {
return Ok(());
}
Some((entry.per_shard, entry.write_ts))
};
if let Some((per_shard, write_ts)) = to_publish {
self.publish_delta_generation(identity, per_shard, write_ts)?;
}
Ok(())
}
#[cfg(test)]
pub fn set_delta_publish_threshold(&self, threshold: usize) {
self.delta_publish_threshold
.store(threshold.max(1), Ordering::Relaxed);
}
#[cfg(test)]
pub(crate) fn pending_delta_entries(&self, identity: IndexIdentity) -> usize {
self.pending_deltas
.lock()
.get(&identity)
.map(|entry| entry.entries)
.unwrap_or(0)
}
pub(crate) fn publish_delta_generation(
&self,
identity: IndexIdentity,
delta: HashMap<u32, IndexMaps>,
write_ts: Timestamp,
) -> StorageResult<()> {
let catalog = self
.manifest_catalog(identity.space_id, identity.index_id)
.ok_or_else(|| {
StorageError::not_found(format!("Index {} has no manifest", identity.index_id))
})?;
let runtime = self.runtime(identity.space_id, identity.index_id)?;
let current = catalog.acquire().manifest().clone();
let next_gen = IndexGeneration::new(current.generation.get().saturating_add(1));
let new_shards: Vec<IndexShard> = current
.shards
.iter()
.map(|s| {
let path = self.generation_checkpoint_path(
identity.space_id,
identity.index_id,
next_gen,
s.shard_id,
);
IndexShard {
shard_id: s.shard_id,
lower: s.lower.clone(),
upper: s.upper.clone(),
checkpoint_file: path,
checksum: None,
}
})
.collect();
let next_manifest =
IndexManifest::new(identity.space_id, identity.index_id, next_gen, new_shards)?;
let current_gen = runtime.generation(current.generation);
let (forward_prefix, reverse_prefix) = self.compute_prefixes(identity);
let generation = GenerationRuntime::empty_with_maps(
&next_manifest,
forward_prefix,
reverse_prefix,
delta,
current_gen.as_ref(),
write_ts,
);
let active_gen = catalog.acquire().manifest().generation;
if active_gen != current.generation {
return Err(StorageError::invalid_operation(
"Index generation changed while publishing delta; retry",
));
}
let generation_bytes = generation.memory_usage_bytes();
runtime.install_generation(generation);
catalog.publish(next_manifest)?;
if let Some(stats) = &self.stats_manager {
stats.record_generation_publish();
}
self.record_manifest_state(&catalog);
self.total_memory_usage
.fetch_add(generation_bytes, Ordering::Relaxed);
let _ = self.check_memory_limit();
Ok(())
}
}