use std::collections::HashMap;
use std::sync::Arc;
use crate::common::save_on_disk::SaveOnDisk;
use crate::segment::common::operation_error::{OperationError, OperationResult};
pub use crate::segment::segment_constructor::NewSegmentToken;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::shard::segment_holder::SegmentHolder;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SegmentManifestState {
Active,
UnderConstruction,
Optimizing {
holder: String,
lease_until: u64,
},
Retiring,
}
impl SegmentManifestState {
pub fn is_usable(&self) -> bool {
match self {
SegmentManifestState::Active
| SegmentManifestState::Optimizing {
holder: _,
lease_until: _,
} => true,
SegmentManifestState::UnderConstruction | SegmentManifestState::Retiring => false,
}
}
pub fn is_optimizer_mark(&self) -> bool {
match self {
SegmentManifestState::Optimizing {
holder: _,
lease_until: _,
}
| SegmentManifestState::Retiring => true,
SegmentManifestState::Active | SegmentManifestState::UnderConstruction => false,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SegmentsManifest {
segments: HashMap<Uuid, SegmentManifestState>,
}
impl SegmentsManifest {
pub fn from_segment_holder(holder: &SegmentHolder) -> Self {
let segments = holder
.iter()
.map(|(_, locked_segment)| {
let uuid = locked_segment.get_read().read().segment_uuid();
(uuid, SegmentManifestState::Active)
})
.collect();
Self { segments }
}
pub fn set(&mut self, uuid: Uuid, state: SegmentManifestState) -> Option<SegmentManifestState> {
self.segments.insert(uuid, state)
}
pub fn remove(&mut self, uuid: &Uuid) -> Option<SegmentManifestState> {
self.segments.remove(uuid)
}
#[must_use]
pub fn preserving(mut self, previous: &SegmentsManifest) -> Self {
for (uuid, state) in previous.iter() {
if state.is_optimizer_mark() && self.segments.contains_key(uuid) {
self.segments.insert(*uuid, state.clone());
}
}
self
}
pub fn sync(
manifest: Option<&Arc<SaveOnDisk<SegmentsManifest>>>,
holder: &SegmentHolder,
extra_segment: Option<Uuid>,
) -> OperationResult<()> {
let Some(manifest) = manifest else {
return Ok(());
};
let mut rebuilt = Self::from_segment_holder(holder);
if let Some(uuid) = extra_segment {
rebuilt.set(uuid, SegmentManifestState::Active);
}
manifest
.write_optional(|previous| {
let current = rebuilt.preserving(previous);
(*previous != current).then_some(current)
})
.map_err(|err| {
OperationError::service_error(format!("failed to persist segment manifest: {err}"))
})?;
Ok(())
}
pub fn get(&self, uuid: &Uuid) -> Option<SegmentManifestState> {
self.segments.get(uuid).cloned()
}
pub fn iter(&self) -> impl Iterator<Item = (&Uuid, &SegmentManifestState)> {
self.segments.iter()
}
pub fn len(&self) -> usize {
self.segments.len()
}
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
}
impl FromIterator<(Uuid, SegmentManifestState)> for SegmentsManifest {
fn from_iter<I: IntoIterator<Item = (Uuid, SegmentManifestState)>>(iter: I) -> Self {
Self {
segments: iter.into_iter().collect(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serializes_active_as_snake_case_uuid_map() {
let uuid = Uuid::parse_str("1b4e28ba-2fa1-11d2-883f-0016d3cca427").unwrap();
let manifest = SegmentsManifest {
segments: HashMap::from([(uuid, SegmentManifestState::Active)]),
};
let json = serde_json::to_string(&manifest).unwrap();
assert_eq!(json, r#"{"1b4e28ba-2fa1-11d2-883f-0016d3cca427":"active"}"#);
let parsed: SegmentsManifest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, manifest);
assert_eq!(parsed.get(&uuid), Some(SegmentManifestState::Active));
}
#[test]
fn deserializes_future_states_for_forward_compat() {
let active = Uuid::parse_str("1b4e28ba-2fa1-11d2-883f-0016d3cca427").unwrap();
let building = Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap();
let retiring = Uuid::parse_str("6ba7b811-9dad-11d1-80b4-00c04fd430c8").unwrap();
let json = format!(
r#"{{"{active}":"active","{building}":"under_construction","{retiring}":"retiring"}}"#,
);
let parsed: SegmentsManifest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.get(&active), Some(SegmentManifestState::Active));
assert_eq!(
parsed.get(&building),
Some(SegmentManifestState::UnderConstruction),
);
assert_eq!(parsed.get(&retiring), Some(SegmentManifestState::Retiring));
}
#[test]
fn optimizing_roundtrips_as_tagged_object() {
let uuid = Uuid::parse_str("1b4e28ba-2fa1-11d2-883f-0016d3cca427").unwrap();
let manifest: SegmentsManifest = [(
uuid,
SegmentManifestState::Optimizing {
holder: "indexer-1".to_string(),
lease_until: 1_752_000_000,
},
)]
.into_iter()
.collect();
let json = serde_json::to_string(&manifest).unwrap();
assert_eq!(
json,
r#"{"1b4e28ba-2fa1-11d2-883f-0016d3cca427":{"optimizing":{"holder":"indexer-1","lease_until":1752000000}}}"#,
);
let parsed: SegmentsManifest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, manifest);
}
#[test]
fn unit_states_keep_their_bare_string_form() {
let uuid = Uuid::parse_str("6ba7b811-9dad-11d1-80b4-00c04fd430c8").unwrap();
let manifest: SegmentsManifest = [(uuid, SegmentManifestState::Retiring)]
.into_iter()
.collect();
assert_eq!(
serde_json::to_string(&manifest).unwrap(),
r#"{"6ba7b811-9dad-11d1-80b4-00c04fd430c8":"retiring"}"#,
);
}
#[test]
fn preserving_keeps_in_progress_marks_for_listed_segments_only() {
let kept = Uuid::parse_str("1b4e28ba-2fa1-11d2-883f-0016d3cca427").unwrap();
let gone = Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap();
let fresh = Uuid::parse_str("6ba7b811-9dad-11d1-80b4-00c04fd430c8").unwrap();
let optimizing = SegmentManifestState::Optimizing {
holder: "indexer-1".to_string(),
lease_until: 42,
};
let previous: SegmentsManifest = [
(kept, optimizing.clone()),
(gone, SegmentManifestState::Retiring),
]
.into_iter()
.collect();
let rebuilt: SegmentsManifest = [
(kept, SegmentManifestState::Active),
(fresh, SegmentManifestState::Active),
]
.into_iter()
.collect();
let merged = rebuilt.preserving(&previous);
assert_eq!(merged.get(&kept), Some(optimizing));
assert_eq!(merged.get(&fresh), Some(SegmentManifestState::Active));
assert_eq!(merged.get(&gone), None, "marks drop with the segment");
}
}