use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReadAtPoint {
pub data_epoch: u64,
pub metadata_version: u64,
pub schema_epoch: u64,
pub index_epoch: u64,
}
impl ReadAtPoint {
pub const fn new(
data_epoch: u64,
metadata_version: u64,
schema_epoch: u64,
index_epoch: u64,
) -> Self {
Self {
data_epoch,
metadata_version,
schema_epoch,
index_epoch,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReadAtCapability {
Available {
readable_from_epoch: u64,
readable_through_epoch: u64,
},
Unavailable {
reason: String,
},
}
impl ReadAtCapability {
pub fn unavailable(reason: impl Into<String>) -> Self {
Self::Unavailable {
reason: reason.into(),
}
}
pub fn validate(&self, point: &ReadAtPoint) -> ReadAtResult<()> {
match self {
Self::Available {
readable_from_epoch,
readable_through_epoch,
} if point.data_epoch < *readable_from_epoch => Err(ReadAtError::Expired {
requested_epoch: point.data_epoch,
readable_from_epoch: *readable_from_epoch,
}),
Self::Available {
readable_through_epoch,
..
} if point.data_epoch > *readable_through_epoch => Err(ReadAtError::NotYetReadable {
requested_epoch: point.data_epoch,
readable_through_epoch: *readable_through_epoch,
}),
Self::Available { .. } => Ok(()),
Self::Unavailable { reason } => Err(ReadAtError::Unavailable {
requested_epoch: point.data_epoch,
reason: reason.clone(),
}),
}
}
pub fn unavailable_error(&self, point: &ReadAtPoint, fallback_reason: &str) -> ReadAtError {
match self {
Self::Unavailable { reason } => ReadAtError::Unavailable {
requested_epoch: point.data_epoch,
reason: reason.clone(),
},
Self::Available { .. } => ReadAtError::Unavailable {
requested_epoch: point.data_epoch,
reason: fallback_reason.to_string(),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ReadAtError {
#[error(
"read point expired: requested={requested_epoch}, readable_from={readable_from_epoch}"
)]
Expired {
requested_epoch: u64,
readable_from_epoch: u64,
},
#[error("read point unavailable: requested={requested_epoch}, readable_through={readable_through_epoch}")]
NotYetReadable {
requested_epoch: u64,
readable_through_epoch: u64,
},
#[error("read point unavailable at epoch {requested_epoch}: {reason}")]
Unavailable {
requested_epoch: u64,
reason: String,
},
}
pub type ReadAtResult<T> = std::result::Result<T, ReadAtError>;
#[cfg(test)]
mod tests {
use super::*;
use crate::kv::{AnyKV, KVStore};
fn point(epoch: u64) -> ReadAtPoint {
ReadAtPoint::new(epoch, 4, 5, 6)
}
#[test]
fn retained_interval_classifies_expired_and_unapplied_epochs() {
let capability = ReadAtCapability::Available {
readable_from_epoch: 10,
readable_through_epoch: 20,
};
assert!(matches!(
capability.validate(&point(9)),
Err(ReadAtError::Expired { .. })
));
assert!(capability.validate(&point(10)).is_ok());
assert!(capability.validate(&point(20)).is_ok());
assert!(matches!(
capability.validate(&point(21)),
Err(ReadAtError::NotYetReadable { .. })
));
}
#[test]
fn any_kv_does_not_treat_a_local_snapshot_as_a_cluster_read_point() {
let store = AnyKV::Memory(crate::kv::memory::MemoryKV::new());
assert!(matches!(
store.read_at_capability(),
ReadAtCapability::Unavailable { .. }
));
assert!(matches!(
store.begin_read_at(&point(1)),
Err(ReadAtError::Unavailable { .. })
));
}
}