alopex_core/kv/
read_at.rs1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub struct ReadAtPoint {
13 pub data_epoch: u64,
15 pub metadata_version: u64,
17 pub schema_epoch: u64,
19 pub index_epoch: u64,
21}
22
23impl ReadAtPoint {
24 pub const fn new(
26 data_epoch: u64,
27 metadata_version: u64,
28 schema_epoch: u64,
29 index_epoch: u64,
30 ) -> Self {
31 Self {
32 data_epoch,
33 metadata_version,
34 schema_epoch,
35 index_epoch,
36 }
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub enum ReadAtCapability {
43 Available {
46 readable_from_epoch: u64,
48 readable_through_epoch: u64,
50 },
51 Unavailable {
53 reason: String,
55 },
56}
57
58impl ReadAtCapability {
59 pub fn unavailable(reason: impl Into<String>) -> Self {
61 Self::Unavailable {
62 reason: reason.into(),
63 }
64 }
65
66 pub fn validate(&self, point: &ReadAtPoint) -> ReadAtResult<()> {
68 match self {
69 Self::Available {
70 readable_from_epoch,
71 readable_through_epoch,
72 } if point.data_epoch < *readable_from_epoch => Err(ReadAtError::Expired {
73 requested_epoch: point.data_epoch,
74 readable_from_epoch: *readable_from_epoch,
75 }),
76 Self::Available {
77 readable_through_epoch,
78 ..
79 } if point.data_epoch > *readable_through_epoch => Err(ReadAtError::NotYetReadable {
80 requested_epoch: point.data_epoch,
81 readable_through_epoch: *readable_through_epoch,
82 }),
83 Self::Available { .. } => Ok(()),
84 Self::Unavailable { reason } => Err(ReadAtError::Unavailable {
85 requested_epoch: point.data_epoch,
86 reason: reason.clone(),
87 }),
88 }
89 }
90
91 pub fn unavailable_error(&self, point: &ReadAtPoint, fallback_reason: &str) -> ReadAtError {
93 match self {
94 Self::Unavailable { reason } => ReadAtError::Unavailable {
95 requested_epoch: point.data_epoch,
96 reason: reason.clone(),
97 },
98 Self::Available { .. } => ReadAtError::Unavailable {
99 requested_epoch: point.data_epoch,
100 reason: fallback_reason.to_string(),
101 },
102 }
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
108pub enum ReadAtError {
109 #[error(
111 "read point expired: requested={requested_epoch}, readable_from={readable_from_epoch}"
112 )]
113 Expired {
114 requested_epoch: u64,
116 readable_from_epoch: u64,
118 },
119 #[error("read point unavailable: requested={requested_epoch}, readable_through={readable_through_epoch}")]
121 NotYetReadable {
122 requested_epoch: u64,
124 readable_through_epoch: u64,
126 },
127 #[error("read point unavailable at epoch {requested_epoch}: {reason}")]
129 Unavailable {
130 requested_epoch: u64,
132 reason: String,
134 },
135}
136
137pub type ReadAtResult<T> = std::result::Result<T, ReadAtError>;
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::kv::{AnyKV, KVStore};
144
145 fn point(epoch: u64) -> ReadAtPoint {
146 ReadAtPoint::new(epoch, 4, 5, 6)
147 }
148
149 #[test]
150 fn retained_interval_classifies_expired_and_unapplied_epochs() {
151 let capability = ReadAtCapability::Available {
152 readable_from_epoch: 10,
153 readable_through_epoch: 20,
154 };
155 assert!(matches!(
156 capability.validate(&point(9)),
157 Err(ReadAtError::Expired { .. })
158 ));
159 assert!(capability.validate(&point(10)).is_ok());
160 assert!(capability.validate(&point(20)).is_ok());
161 assert!(matches!(
162 capability.validate(&point(21)),
163 Err(ReadAtError::NotYetReadable { .. })
164 ));
165 }
166
167 #[test]
168 fn any_kv_does_not_treat_a_local_snapshot_as_a_cluster_read_point() {
169 let store = AnyKV::Memory(crate::kv::memory::MemoryKV::new());
170 assert!(matches!(
171 store.read_at_capability(),
172 ReadAtCapability::Unavailable { .. }
173 ));
174 assert!(matches!(
175 store.begin_read_at(&point(1)),
176 Err(ReadAtError::Unavailable { .. })
177 ));
178 }
179}