use std::collections::HashSet;
use std::debug_assert_matches;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use ahash::AHashSet;
use crate::common::budget::{ResourceBudget, ResourcePermit};
use crate::common::bytes::bytes_to_human;
use crate::common::counter::hardware_counter::HardwareCounterCell;
use crate::common::disk::dir_disk_size;
use crate::common::fs::safe_delete_with_suffix;
use crate::common::progress_tracker::ProgressTracker;
use crate::common::storage_version::StorageVersion;
use crate::common::types::PointOffsetType;
use fs_err as fs;
use itertools::Itertools;
use parking_lot::lock_api::RwLockWriteGuard;
use parking_lot::{Mutex, RwLockUpgradableReadGuard};
use crate::segment::common::operation_error::{OperationError, OperationResult, check_process_stopped};
use crate::segment::common::operation_time_statistics::{
OperationDurationsAggregator, ScopeDurationMeasurer,
};
use crate::segment::entry::{
NonAppendableSegmentEntry as _, ReadSegmentEntry as _, StorageSegmentEntry as _,
};
use crate::segment::segment::{Segment, SegmentVersion};
use crate::segment::segment_constructor::segment_builder::SegmentBuilder;
use crate::segment::types::{PointIdType, VectorNameBuf};
use uuid::Uuid;
use crate::shard::locked_segment::LockedSegment;
use crate::shard::proxy_segment::{
DeletedPoints, IntendedVector, ProxyIndexChange, ProxyIndexChanges, ProxyVectorNameChanges,
UnsyncedProxySegment,
};
use crate::shard::quota::{self, DiskFit};
use crate::shard::segment_holder::SegmentId;
use crate::shard::segment_holder::locked::LockedSegmentHolder;
use crate::shard::segment_manifest::NewSegmentToken;
#[derive(Debug)]
pub struct OptimizationResult {
pub points_count: usize,
}
#[derive(Debug, Clone)]
pub struct OptimizationPaths {
pub segments_path: PathBuf,
pub temp_path: PathBuf,
}
pub trait OptimizationStrategy: Send {
fn create_segment_builder(
&self,
input_segments: &[LockedSegment],
) -> OperationResult<SegmentBuilder>;
fn create_temp_segment(&self) -> OperationResult<(LockedSegment, NewSegmentToken)>;
fn live_vector_names(&self) -> Option<HashSet<VectorNameBuf>>;
}
pub fn unwrap_proxy(
segments: &LockedSegmentHolder,
proxy_ids: &[SegmentId],
) -> OperationResult<()> {
let mut segments_lock = segments.write();
for &proxy_id in proxy_ids {
if let Some(proxy_segment_ref) = segments_lock.get(proxy_id) {
let locked_proxy_segment = proxy_segment_ref.clone();
match locked_proxy_segment {
LockedSegment::Original(_) => {
log::warn!("Attempt to unwrap raw segment! Should not happen.");
}
LockedSegment::Proxy(proxy_segment) => {
let wrapped_segment = proxy_segment.read().wrapped_segment.clone();
segments_lock.replace(proxy_id, wrapped_segment)?;
}
}
}
}
Ok(())
}
fn cleanup_cancelled_optimized_segment(segments_path: &Path, output_segment_uuid: Uuid) {
let orphan_path = segments_path.join(output_segment_uuid.to_string());
if !orphan_path.exists() {
return;
}
if let Err(err) = safe_delete_with_suffix(&orphan_path) {
log::warn!(
"Failed to remove cancelled optimized segment at {}: {err}",
orphan_path.display(),
);
}
}
pub fn proxy_deleted_points(proxies: &[LockedSegment]) -> DeletedPoints {
let mut deleted_points = DeletedPoints::new();
for proxy_segment in proxies {
match proxy_segment {
LockedSegment::Original(_) => {
log::error!("Reading raw segment, while proxy expected");
debug_assert!(false, "Reading raw segment, while proxy expected");
}
LockedSegment::Proxy(proxy) => {
let proxy_read = proxy.read();
for (point_id, versions) in proxy_read.get_deleted_points() {
let entry = deleted_points.entry(*point_id).or_insert(*versions);
entry.operation_version =
entry.operation_version.max(versions.operation_version);
entry.local_version = entry.local_version.max(versions.local_version);
}
}
}
}
deleted_points
}
pub fn proxy_index_changes(proxies: &[LockedSegment]) -> ProxyIndexChanges {
let mut index_changes = ProxyIndexChanges::default();
for proxy_segment in proxies {
match proxy_segment {
LockedSegment::Original(_) => {
log::error!("Reading raw segment, while proxy expected");
debug_assert!(false, "Reading raw segment, while proxy expected");
}
LockedSegment::Proxy(proxy) => {
let proxy_read = proxy.read();
index_changes.merge(proxy_read.get_index_changes())
}
}
}
index_changes
}
pub fn proxy_vector_name_changes(proxies: &[LockedSegment]) -> ProxyVectorNameChanges {
let mut changes = ProxyVectorNameChanges::default();
for proxy_segment in proxies {
match proxy_segment {
LockedSegment::Original(_) => {
log::error!("Reading raw segment, while proxy expected");
debug_assert!(false, "Reading raw segment, while proxy expected");
}
LockedSegment::Proxy(proxy) => {
let proxy_read = proxy.read();
changes.merge(proxy_read.get_vector_name_changes())
}
}
}
changes
}
#[allow(clippy::too_many_arguments)]
fn build_new_segment<F: ?Sized + OptimizationStrategy>(
factory: &F,
input_segments: &[LockedSegment], output_segment_uuid: Uuid, deferred_internal_id: Option<PointOffsetType>,
proxies: &[LockedSegment],
permit: ResourcePermit, resource_budget: ResourceBudget,
stopped: &AtomicBool,
hw_counter: &HardwareCounterCell,
progress: ProgressTracker,
segments_path: &Path,
) -> OperationResult<Segment> {
let mut segment_builder = factory.create_segment_builder(input_segments)?;
check_process_stopped(stopped)?;
let progress_copy_data = progress.subtask("copy_data");
let progress_populate_storages = progress.subtask("populate_vector_storages");
let progress_wait_permit = progress.subtask("wait_cpu_permit");
let segments: Vec<_> = input_segments
.iter()
.map(|i| match i {
LockedSegment::Original(o) => o.clone(),
LockedSegment::Proxy(_) => {
panic!("Trying to optimize a segment that is already being optimized!")
}
})
.collect();
let mut defragmentation_keys = HashSet::new();
for segment in &segments {
let payload_index = &segment.read().payload_index;
let payload_index = payload_index.borrow();
let keys = payload_index
.config()
.indices
.iter()
.filter(|(_, schema)| schema.schema.is_tenant())
.map(|(key, _)| key.clone());
defragmentation_keys.extend(keys);
}
if !defragmentation_keys.is_empty() {
segment_builder.set_defragment_keys(defragmentation_keys.into_iter().collect());
}
if let Some(live_vector_names) = factory.live_vector_names() {
segment_builder.set_live_vector_names(live_vector_names);
}
{
progress_copy_data.start();
let segment_guards = segments.iter().map(|segment| segment.read()).collect_vec();
segment_builder.update(
&segment_guards.iter().map(Deref::deref).collect_vec(),
stopped,
hw_counter,
)?;
drop(progress_copy_data);
}
let index_changes = proxy_index_changes(proxies);
for (field_name, change) in index_changes.iter_unordered() {
match change {
ProxyIndexChange::Create(schema, _) => {
segment_builder.add_indexed_field(field_name.to_owned(), schema.to_owned());
}
ProxyIndexChange::Delete(_) => {
segment_builder.remove_indexed_field(field_name);
}
ProxyIndexChange::DeleteIfIncompatible(_, schema) => {
segment_builder.remove_index_field_if_incompatible(field_name, schema);
}
}
}
progress_populate_storages.start();
segment_builder.populate_vector_storages()?;
drop(progress_populate_storages);
progress_wait_permit.start();
let desired_cpus = permit.num_io as usize;
let indexing_permit = resource_budget
.replace_with(permit, desired_cpus, 0, stopped)
.map_err(|_| {
crate::segment::common::operation_error::OperationError::cancelled(
"optimization cancelled while waiting for budget",
)
})?;
drop(progress_wait_permit);
let mut rng = rand::rng();
let mut optimized_segment = segment_builder.build(
segments_path,
output_segment_uuid,
deferred_internal_id,
indexing_permit,
stopped,
&mut rng,
hw_counter,
progress,
)?;
let deleted_points_snapshot = proxy_deleted_points(proxies);
let index_changes = proxy_index_changes(proxies);
let old_optimized_segment_version = optimized_segment.version();
for (field_name, change) in index_changes.iter_ordered() {
debug_assert!(
change.version() >= old_optimized_segment_version,
"proxied index change should have newer version than segment",
);
match change {
ProxyIndexChange::Create(schema, version) => {
optimized_segment.create_field_index(
*version,
field_name,
Some(schema),
hw_counter,
)?;
}
ProxyIndexChange::Delete(version) => {
optimized_segment.delete_field_index(*version, field_name)?;
}
ProxyIndexChange::DeleteIfIncompatible(version, schema) => {
optimized_segment
.delete_field_index_if_incompatible(*version, field_name, schema)?;
}
}
check_process_stopped(stopped)?;
}
for (point_id, versions) in deleted_points_snapshot {
optimized_segment
.delete_point(versions.operation_version, point_id, hw_counter)
.unwrap();
}
Ok(optimized_segment)
}
#[allow(clippy::too_many_arguments)]
fn optimize_segment_propagate_changes<F: ?Sized + OptimizationStrategy>(
factory: &F,
optimizing_segments: Vec<LockedSegment>,
output_segment_uuid: Uuid,
deferred_internal_id: Option<PointOffsetType>,
proxies: &[LockedSegment],
permit: ResourcePermit, resource_budget: ResourceBudget,
stopped: &AtomicBool,
hw_counter: &HardwareCounterCell,
progress: ProgressTracker,
segments_path: &Path,
) -> OperationResult<(Segment, DeletedPoints)> {
check_process_stopped(stopped)?;
let optimized_segment = build_new_segment(
factory,
&optimizing_segments,
output_segment_uuid,
deferred_internal_id,
proxies,
permit,
resource_budget,
stopped,
hw_counter,
progress,
segments_path,
)?;
let already_remove_points = {
let mut all_removed_points = proxy_deleted_points(proxies);
for existing_point in optimized_segment.iter_points() {
all_removed_points.remove(&existing_point);
}
all_removed_points
};
check_process_stopped(stopped)?;
Ok((optimized_segment, already_remove_points))
}
#[allow(clippy::too_many_arguments)]
fn finish_optimization(
segment_holder: &LockedSegmentHolder,
locked_proxies: Vec<LockedSegment>,
mut optimized_segment: Segment,
already_remove_points: &DeletedPoints,
proxy_ids: &[SegmentId],
cow_segment_id_opt: Option<SegmentId>,
stopped: &AtomicBool,
hw_counter: &HardwareCounterCell,
) -> OperationResult<usize> {
let upgradable_segment_holder = segment_holder.upgradable_read();
let update_guard = segment_holder.acquire_updates_lock();
let old_optimized_segment_version = optimized_segment.version();
let vector_name_changes = proxy_vector_name_changes(&locked_proxies);
for (vector_name, intent) in vector_name_changes.iter_ordered() {
debug_assert!(
intent.version() >= old_optimized_segment_version,
"proxied vector name change should have newer version than segment",
);
match intent {
IntendedVector::Absent { version } => {
optimized_segment.delete_vector_name(*version, vector_name)?;
}
IntendedVector::Present {
config,
version,
supersedes_wrapped,
} => {
if *supersedes_wrapped {
optimized_segment.delete_vector_name(*version, vector_name)?;
}
optimized_segment.create_vector_name(*version, vector_name, config)?;
}
}
check_process_stopped(stopped)?;
}
let index_changes = proxy_index_changes(&locked_proxies);
for (field_name, change) in index_changes.iter_ordered() {
match change {
ProxyIndexChange::Create(schema, version) => {
optimized_segment.create_field_index(
*version,
field_name,
Some(schema),
hw_counter,
)?;
}
ProxyIndexChange::Delete(version) => {
optimized_segment.delete_field_index(*version, field_name)?;
}
ProxyIndexChange::DeleteIfIncompatible(version, schema) => {
optimized_segment
.delete_field_index_if_incompatible(*version, field_name, schema)?;
}
}
check_process_stopped(stopped)?;
}
let deleted_points = proxy_deleted_points(&locked_proxies);
let points_diff = deleted_points
.iter()
.filter(|&(point_id, _)| !already_remove_points.contains_key(point_id));
for (&point_id, &versions) in points_diff {
optimized_segment
.delete_point(versions.operation_version, point_id, hw_counter)
.unwrap();
}
let point_count = optimized_segment.available_point_count();
let optimized_segment_version = optimized_segment.version();
let mut writable_segment_holder = RwLockUpgradableReadGuard::upgrade(upgradable_segment_holder);
let (_, proxies) = writable_segment_holder.swap_new(optimized_segment, proxy_ids);
debug_assert_eq!(
proxies.len(),
proxy_ids.len(),
"swapped different number of proxies"
);
if let Some(cow_segment_id) = cow_segment_id_opt {
writable_segment_holder.remove_segment_if_not_needed(cow_segment_id)?;
}
let read_segment_holder = RwLockWriteGuard::downgrade(writable_segment_holder);
let mut deferred_points_set = AHashSet::new();
for proxy in &proxies {
deferred_points_set.extend(proxy.get().read().deferred_point_ids());
}
let deferred_points: Vec<PointIdType> = deferred_points_set.into_iter().collect();
if !deferred_points.is_empty() {
const CHUNK_SIZE: usize = 100;
deferred_points
.chunks(CHUNK_SIZE)
.try_for_each(|chunk| read_segment_holder.deduplicate_points(chunk, hw_counter))?;
}
read_segment_holder.sync_segment_manifest(None)?;
for proxy in proxies {
let ack_pin = proxy.get().read().persistent_version();
read_segment_holder.register_segment_drop(optimized_segment_version, ack_pin, proxy);
}
drop(read_segment_holder);
drop(update_guard);
drop(locked_proxies);
Ok(point_count)
}
fn check_segments_size(
optimizer_name: &str,
optimizing_segments: &[LockedSegment],
temp_path: &Path,
) -> OperationResult<()> {
let mut space_occupied = Some(0u64);
for segment in optimizing_segments {
match segment {
LockedSegment::Original(segment) => {
let locked_segment = segment.read();
space_occupied = space_occupied.and_then(|acc| {
match dir_disk_size(locked_segment.data_path()) {
Ok(size) => Some(size + acc),
Err(err) => {
log::debug!(
"Could not estimate size of segment `{}`: {}",
locked_segment.data_path().display(),
err
);
None
}
}
});
}
LockedSegment::Proxy(_) => {
return Err(
crate::segment::common::operation_error::OperationError::service_error(
"Proxy segment is not expected here",
),
);
}
}
}
let space_needed = space_occupied.map(|x| 2 * x);
if !temp_path.exists() {
fs::create_dir_all(temp_path).map_err(|err| {
crate::segment::common::operation_error::OperationError::service_error(format!(
"Could not create temp directory `{}`: {}",
temp_path.display(),
err
))
})?;
}
let Some(space_needed) = space_needed else {
log::warn!(
"Could not estimate the space needed by `{optimizer_name}`; will try optimizing anyway",
);
return Ok(());
};
match quota::global().fits_on_disk(temp_path, space_needed) {
DiskFit::Fits { available } => {
if space_needed > 0 {
log::debug!(
"Available space: {}, needed for optimization: {}",
bytes_to_human(available as usize),
bytes_to_human(space_needed as usize),
);
}
Ok(())
}
DiskFit::TooLarge {
available,
required,
} => Err(
crate::segment::common::operation_error::OperationError::service_error(format!(
"Not enough space available for optimization, needed: {}, available: {}",
bytes_to_human(required as usize),
bytes_to_human(available as usize),
)),
),
DiskFit::Unknown => {
log::warn!(
"Could not estimate available storage space in `{optimizer_name}`; will try optimizing anyway",
);
Ok(())
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn execute_optimization<F: ?Sized + OptimizationStrategy>(
optimizer_name: &'static str,
segment_holder: LockedSegmentHolder,
input_segment_ids: Vec<SegmentId>,
output_segment_uuid: Uuid,
deferred_internal_id: Option<PointOffsetType>,
paths: &OptimizationPaths,
permit: ResourcePermit,
resource_budget: ResourceBudget,
stopped: &AtomicBool,
progress: ProgressTracker,
telemetry_counter: &Mutex<OperationDurationsAggregator>,
factory: &F,
on_successful_start: Box<dyn FnOnce()>,
) -> OperationResult<OptimizationResult> {
check_process_stopped(stopped)?;
let mut timer = ScopeDurationMeasurer::new(telemetry_counter);
timer.set_success(false);
let segment_holder_read = segment_holder.upgradable_read();
let appendable_segments_ids = segment_holder_read.appendable_segments_ids();
let has_appendable_segments_except_optimized = appendable_segments_ids
.iter()
.any(|id| !input_segment_ids.contains(id));
let need_extra_cow_segment = !has_appendable_segments_except_optimized;
let input_segments: Vec<_> = input_segment_ids
.iter()
.cloned()
.map(|id| segment_holder_read.get(id))
.filter_map(|x| x.cloned())
.collect();
let all_segments_ok = input_segments.len() == input_segment_ids.len()
&& input_segments
.iter()
.all(|s| matches!(s, LockedSegment::Original(_)));
if !all_segments_ok {
return Ok(OptimizationResult { points_count: 0 });
}
check_segments_size(optimizer_name, &input_segments, &paths.temp_path)?;
check_process_stopped(stopped)?;
on_successful_start();
let hw_counter = HardwareCounterCell::disposable();
let (extra_cow_segment_opt, extra_cow_token_opt) = if need_extra_cow_segment {
let (segment, token) = factory.create_temp_segment()?;
(Some(segment), Some(token))
} else {
(None, None)
};
let mut proxies = Vec::new();
for sg in input_segments.iter() {
let proxy = UnsyncedProxySegment::new(sg.clone());
if let Some(extra_cow_segment) = &extra_cow_segment_opt {
proxy.replicate_field_indexes(0, &hw_counter, extra_cow_segment)?;
}
proxies.push(proxy);
}
match &extra_cow_segment_opt {
Some(LockedSegment::Original(segment)) => {
segment_holder_read.sync_segment_manifest(extra_cow_token_opt)?;
let segment_path = &segment.read().segment_path;
SegmentVersion::save(segment_path)?;
}
Some(LockedSegment::Proxy(_)) => unreachable!(),
None => {}
}
let mut locked_proxies: Vec<LockedSegment> = Vec::with_capacity(proxies.len());
let (proxy_ids, cow_segment_id_opt, counter_handler): (Vec<_>, _, _) = {
let mut segment_holder_write = RwLockUpgradableReadGuard::upgrade(segment_holder_read);
let mut proxy_ids = Vec::new();
for (proxy, idx) in proxies.into_iter().zip(input_segment_ids.iter().cloned()) {
debug_assert_matches!(
proxy.wrapped_segment(),
LockedSegment::Original(_),
"during optimization, wrapped segment must not be another proxy segment",
);
let proxy = proxy.finalize();
if let Some(extra_cow_segment) = &extra_cow_segment_opt {
proxy.replicate_field_indexes(0, &hw_counter, extra_cow_segment)?;
}
let locked_proxy = LockedSegment::from(proxy);
segment_holder_write.replace(idx, locked_proxy.clone())?;
proxy_ids.push(idx);
locked_proxies.push(locked_proxy);
}
let cow_segment_id_opt = extra_cow_segment_opt
.map(|extra_cow_segment| segment_holder_write.add_new_locked(extra_cow_segment));
let counter_handler = segment_holder_write.running_optimizations.inc();
(proxy_ids, cow_segment_id_opt, counter_handler)
};
let build_result = optimize_segment_propagate_changes(
factory,
input_segments,
output_segment_uuid,
deferred_internal_id,
&locked_proxies,
permit,
resource_budget,
stopped,
&hw_counter,
progress,
&paths.segments_path,
);
let (optimized_segment, already_remove_points) = match build_result {
Ok(result) => result,
Err(err) => {
unwrap_proxy(&segment_holder, &proxy_ids)?;
if matches!(err, OperationError::Cancelled { .. }) {
cleanup_cancelled_optimized_segment(&paths.segments_path, output_segment_uuid);
}
return Err(err);
}
};
let points_count = match finish_optimization(
&segment_holder,
locked_proxies,
optimized_segment,
&already_remove_points,
&proxy_ids,
cow_segment_id_opt,
stopped,
&hw_counter,
) {
Ok(points_count) => points_count,
Err(err) => {
unwrap_proxy(&segment_holder, &proxy_ids)?;
if matches!(err, OperationError::Cancelled { .. }) {
cleanup_cancelled_optimized_segment(&paths.segments_path, output_segment_uuid);
}
return Err(err);
}
};
drop(counter_handler);
timer.set_success(true);
Ok(OptimizationResult { points_count })
}