use diskann::ANNResult;
use serde::{Deserialize, Serialize};
use tracing::info;
use super::WorkStage;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CheckpointRecord {
stage: WorkStage,
is_valid: bool,
progress: usize,
}
impl Default for CheckpointRecord {
fn default() -> Self {
CheckpointRecord::new()
}
}
impl CheckpointRecord {
pub fn new() -> CheckpointRecord {
CheckpointRecord {
stage: WorkStage::Start,
is_valid: true,
progress: 0,
}
}
pub fn is_valid(&self) -> bool {
self.is_valid
}
pub fn get_resumption_point(&self, stage: WorkStage) -> Option<usize> {
if self.stage == stage {
info!(
"The resumption point is at {} for stage {:?}",
self.progress, stage
);
Some(if self.is_valid { self.progress } else { 0 })
} else {
info!(
"Failed to get resumption point for {:?} since the current stage is {:?}.",
stage, self.stage
);
None
}
}
pub fn advance_work_type(&self, next_stage: WorkStage) -> ANNResult<CheckpointRecord> {
info!(
"Advancing work type from {:?} to {:?}.",
self.stage, next_stage
);
Ok(CheckpointRecord {
stage: next_stage,
is_valid: true,
progress: 0,
})
}
pub fn mark_as_invalid(&self) -> CheckpointRecord {
CheckpointRecord {
stage: self.stage,
is_valid: false,
progress: self.progress,
}
}
pub fn update_progress(&self, progress: usize) -> CheckpointRecord {
info!("Updating progress to {:?}={}", self.stage, progress);
CheckpointRecord {
stage: self.stage,
is_valid: true,
progress,
}
}
#[allow(unused)]
pub(crate) fn get_work_stage(&self) -> WorkStage {
self.stage
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Copy)]
enum LegacyWorkStage {
QuantizeFPV,
InMemIndexBuild,
WriteDiskLayout,
End,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct LegacyCheckpointRecord {
stage: LegacyWorkStage,
is_valid: bool,
progress: usize,
}
#[rstest]
#[case(LegacyWorkStage::QuantizeFPV, WorkStage::QuantizeFPV, true, 0)]
#[case(
LegacyWorkStage::InMemIndexBuild,
WorkStage::InMemIndexBuild,
false,
42
)]
#[case(
LegacyWorkStage::WriteDiskLayout,
WorkStage::WriteDiskLayout,
true,
100
)]
#[case(LegacyWorkStage::End, WorkStage::End, false, 0)]
fn test_backward_compatibility(
#[case] legacy_stage: LegacyWorkStage,
#[case] stage: WorkStage,
#[case] is_valid: bool,
#[case] progress: usize,
) {
let legacy_record = LegacyCheckpointRecord {
stage: legacy_stage,
is_valid,
progress,
};
let serialized = bincode::serialize(&legacy_record).unwrap();
let deserialized: CheckpointRecord = bincode::deserialize(&serialized).unwrap();
assert_eq!(deserialized.stage, stage);
assert_eq!(deserialized.is_valid, is_valid);
assert_eq!(deserialized.progress, progress);
}
#[rstest]
#[case(WorkStage::QuantizeFPV, LegacyWorkStage::QuantizeFPV, true, 10)]
#[case(
WorkStage::InMemIndexBuild,
LegacyWorkStage::InMemIndexBuild,
false,
30
)]
#[case(WorkStage::WriteDiskLayout, LegacyWorkStage::WriteDiskLayout, true, 80)]
#[case(WorkStage::End, LegacyWorkStage::End, false, 0)]
fn test_forward_compatibility(
#[case] current_stage: WorkStage,
#[case] expected_legacy_stage: LegacyWorkStage,
#[case] is_valid: bool,
#[case] progress: usize,
) {
let current_record = CheckpointRecord {
stage: current_stage,
is_valid,
progress,
};
let serialized = bincode::serialize(¤t_record).unwrap();
let deserialized: LegacyCheckpointRecord = bincode::deserialize(&serialized).unwrap();
assert_eq!(deserialized.stage, expected_legacy_stage);
assert_eq!(deserialized.is_valid, is_valid);
assert_eq!(deserialized.progress, progress);
}
#[rstest]
#[case(WorkStage::PartitionData, true, 25)]
#[case(WorkStage::BuildIndicesOnShards(0), true, 75)]
#[case(WorkStage::BuildIndicesOnShards(10), true, 75)]
#[case(WorkStage::MergeIndices, false, 75)]
fn test_rolling_back_with_new_variants(
#[case] stage: WorkStage,
#[case] is_valid: bool,
#[case] progress: usize,
) {
let current_record = CheckpointRecord {
stage,
is_valid,
progress,
};
let serialized = bincode::serialize(¤t_record).unwrap();
let result: Result<LegacyCheckpointRecord, bincode::Error> =
bincode::deserialize(&serialized);
assert!(
result.is_err(),
"Legacy code should not be able to deserialize newer enum variants"
);
}
#[test]
fn test_checkpoint_record_default() {
let record = CheckpointRecord::default();
assert!(record.is_valid());
assert_eq!(record.get_work_stage(), WorkStage::Start);
}
#[test]
fn test_checkpoint_record_is_valid() {
let record = CheckpointRecord::new();
assert!(record.is_valid());
let invalid_record = record.mark_as_invalid();
assert!(!invalid_record.is_valid());
}
#[test]
fn test_get_resumption_point_with_matching_stage() {
let record = CheckpointRecord::new().update_progress(42);
let resumption = record.get_resumption_point(WorkStage::Start);
assert_eq!(resumption, Some(42));
}
#[test]
fn test_get_resumption_point_with_different_stage() {
let record = CheckpointRecord::new();
let resumption = record.get_resumption_point(WorkStage::QuantizeFPV);
assert_eq!(resumption, None);
}
#[test]
fn test_get_resumption_point_when_invalid() {
let record = CheckpointRecord::new()
.update_progress(100)
.mark_as_invalid();
let resumption = record.get_resumption_point(WorkStage::Start);
assert_eq!(resumption, Some(0)); }
#[test]
fn test_advance_work_type() {
let record = CheckpointRecord::new();
let advanced = record.advance_work_type(WorkStage::QuantizeFPV).unwrap();
assert_eq!(advanced.get_work_stage(), WorkStage::QuantizeFPV);
assert!(advanced.is_valid());
}
}