use crate::index::helpers::flush_split_generation;
use crate::index::key_codec::key_types::SecondaryIndexKey;
use crate::index::manifest::{IndexManifest, IndexShard, ManifestCatalog, ManifestHandle};
use crate::index::shard_runtime::{
generation_from_maps_with_pool_capacity, GenerationRuntime, IndexMaps,
};
use crate::index::types::{IndexIdentity, IndexRecord};
use linkrs_core::types::{CommitLsn, IndexGeneration, Timestamp};
use linkrs_core::{StorageError, StorageResult};
use std::collections::{BTreeMap, HashMap};
use std::sync::atomic::Ordering;
use std::sync::Arc;
use super::super::remove_dir_if_empty;
use super::super::IndexDataManagerImpl;
impl IndexDataManagerImpl {
pub fn retired_generation_count(&self) -> usize {
self.manifest_catalogs
.read()
.values()
.map(|catalog| catalog.retired_reclaimable(|_| true).len())
.sum()
}
pub(crate) fn publish_native_index(
&self,
manifest: IndexManifest,
forward: BTreeMap<SecondaryIndexKey, IndexRecord>,
reverse: BTreeMap<SecondaryIndexKey, IndexRecord>,
barrier_lsn: CommitLsn,
) -> StorageResult<()> {
let runtime = self.runtime(manifest.space_id, manifest.index_id)?;
let identity = IndexIdentity {
space_id: manifest.space_id,
index_id: manifest.index_id,
};
let mut maps = HashMap::new();
let shard = manifest
.shards
.first()
.ok_or_else(|| StorageError::invalid_operation("Index manifest has no shards"))?;
maps.insert(shard.shard_id, (forward, reverse));
let pool_cap = self.pool_capacity.load(Ordering::Relaxed);
runtime.install_generation(generation_from_maps_with_pool_capacity(
&manifest,
maps,
None,
0,
Vec::new(),
Vec::new(),
pool_cap,
));
self.manifest_catalog(manifest.space_id, manifest.index_id)
.ok_or_else(|| StorageError::not_found("Index manifest catalog is unavailable"))?
.publish(manifest)?;
if let Some(stats) = &self.stats_manager {
stats.record_generation_publish();
}
runtime.establish_barrier_lsn(barrier_lsn);
self.record_barrier_lsn(identity, barrier_lsn);
runtime.wait_for_barrier_lsn(barrier_lsn);
if let Some(catalog) = self.manifest_catalog(identity.space_id, identity.index_id) {
self.record_manifest_state(&catalog);
}
self.sync_memory_usage();
Ok(())
}
pub(crate) fn retire_generations(&self, safe_ts: Timestamp) -> usize {
if safe_ts == 0 {
return 0;
}
let mut retired = 0;
let identities: Vec<IndexIdentity> = self.runtimes.read().keys().copied().collect();
for identity in identities {
let Some(catalog) = self.manifest_catalog(identity.space_id, identity.index_id) else {
continue;
};
let active_gen = catalog.acquire().manifest().generation;
let Some(runtime) = self.runtime(identity.space_id, identity.index_id).ok() else {
continue;
};
let mut to_remove = Vec::new();
for gen in runtime.generations() {
if gen.generation < active_gen && safe_ts > gen.max_ts {
to_remove.push(gen.generation);
}
}
for gen in to_remove {
if runtime.remove_generation(gen) {
retired += 1;
}
}
if retired > 0 {
self.record_manifest_state(&catalog);
if let Err(error) = self.reclaim_retired_generations(identity) {
log::warn!(
"Failed to reclaim retired generation files for index {} (space {}): {error}",
identity.index_id,
identity.space_id
);
}
self.sync_memory_usage();
self.resync_tombstone_count();
}
}
retired
}
pub(crate) fn pin_chain_manifests(
&self,
catalog: &ManifestCatalog,
chain: &[Arc<GenerationRuntime>],
) -> Vec<ManifestHandle> {
chain
.iter()
.filter_map(|gen| catalog.acquire_generation(gen.generation))
.collect()
}
pub(crate) fn reclaim_retired_generations(
&self,
identity: IndexIdentity,
) -> StorageResult<usize> {
let Some(catalog) = self.manifest_catalog(identity.space_id, identity.index_id) else {
return Ok(0);
};
let runtime = self.runtime(identity.space_id, identity.index_id)?;
let active_generation = catalog.acquire().manifest().generation;
let candidates = catalog.retired_reclaimable(|manifest| {
manifest.generation < active_generation
&& runtime.generation(manifest.generation).is_none()
});
let mut reclaimed = 0;
for manifest in candidates {
for shard in &manifest.shards {
if shard.checkpoint_file.is_dir() {
std::fs::remove_dir_all(&shard.checkpoint_file)?;
reclaimed += 1;
}
}
if let Some(parent) = manifest
.shards
.first()
.and_then(|s| s.checkpoint_file.parent())
{
remove_dir_if_empty(parent);
}
catalog.remove_retired(manifest.generation);
}
Ok(reclaimed)
}
pub(crate) fn compact_native_index(
&self,
identity: IndexIdentity,
safe_ts: Timestamp,
) -> StorageResult<bool> {
self.compact_native_index_impl(identity, safe_ts, false)
}
pub(crate) fn compact_native_index_impl(
&self,
identity: IndexIdentity,
safe_ts: Timestamp,
force: bool,
) -> StorageResult<bool> {
self.publish_pending_delta(identity)?;
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();
if !force {
let has_tombstones = current.shards.iter().any(|s| {
runtime
.generation(current.generation)
.and_then(|g| g.shard(s.shard_id))
.is_some_and(|shard| {
shard
.read_forward()
.snapshot()
.into_values()
.any(|e| e.deleted_ts.is_some())
|| shard
.read_reverse()
.snapshot()
.into_values()
.any(|e| e.deleted_ts.is_some())
})
});
if !has_tombstones {
return Ok(false);
}
}
let maps = {
let chain = runtime.generation_chain_until(current.generation)?;
let _chain_pins = self.pin_chain_manifests(&catalog, &chain);
let mut maps: HashMap<u32, IndexMaps> = HashMap::new();
for shard_def in ¤t.shards {
let mut forward = BTreeMap::new();
let mut reverse = BTreeMap::new();
for gen in &chain {
if let Some(shard) = gen.shard(shard_def.shard_id) {
let (f, r) = shard.snapshot();
for (key, entry) in f {
if entry.is_visible_at(safe_ts) {
forward.entry(key).or_insert(entry);
}
}
for (key, entry) in r {
if entry.is_visible_at(safe_ts) {
reverse.entry(key).or_insert(entry);
}
}
}
}
maps.insert(shard_def.shard_id, (forward, reverse));
}
maps
};
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 (fwd_prefix, rev_prefix) = self.compute_prefixes(identity);
let pool_cap = self.pool_capacity.load(Ordering::Relaxed);
let next_runtime = generation_from_maps_with_pool_capacity(
&next_manifest,
maps,
current_gen.as_ref(),
safe_ts,
fwd_prefix,
rev_prefix,
pool_cap,
);
let index_type = self.index_types.read().get(&identity).cloned();
if self.index_root.is_some() && index_type.is_some() {
flush_split_generation(&next_manifest, &next_runtime)?;
}
let active_gen = catalog.acquire().manifest().generation;
if active_gen != current.generation {
return Err(StorageError::invalid_operation(
"Index generation changed while compacting; retry",
));
}
runtime.install_generation(next_runtime);
catalog.publish(next_manifest)?;
if let Some(stats) = &self.stats_manager {
stats.record_generation_publish();
}
self.record_barrier_lsn(identity, CommitLsn::ZERO);
if let Some(gen) = current_gen {
if safe_ts > gen.max_ts {
runtime.remove_generation(current.generation);
}
}
self.reclaim_retired_generations(identity)?;
self.sync_memory_usage();
if let Some(catalog_ref) = self.manifest_catalog(identity.space_id, identity.index_id) {
self.record_manifest_state(&catalog_ref);
}
Ok(true)
}
}