1use {
9 super::ControllerHandle,
10 crate::{
11 error::RaftError,
12 kraft::{
13 controller::checkpoint::{
14 CheckpointChunk, latest_checkpoint_id, load_checkpoint_by_id, open_checkpoint,
15 },
16 snapshot_fetch::SnapshotId,
17 transport::Inbound,
18 },
19 },
20 std::path::Path,
21 tokio::sync::oneshot,
22};
23
24#[derive(Debug, PartialEq, Eq)]
27pub struct SnapshotSlice {
28 pub end_offset: i64,
29 pub epoch: i32,
30 pub total_size: i64,
31 pub bytes: bytes::Bytes,
32}
33
34pub enum SnapshotRange {
36 NoSnapshot,
38 OutOfRange,
41 Slice(SnapshotSlice),
43}
44
45impl ControllerHandle {
46 #[must_use]
55 pub fn read_snapshot_range(&self, position: i64, max_bytes: i32) -> SnapshotRange {
56 let Some(id) = latest_checkpoint_id(&self.data_dir) else {
57 return SnapshotRange::NoSnapshot;
58 };
59 let Some(mut checkpoint) = open_checkpoint(&self.data_dir, id.0, id.1) else {
62 return SnapshotRange::NoSnapshot;
63 };
64 match checkpoint.read_chunk(position, max_bytes) {
65 Ok(CheckpointChunk::Bytes(bytes)) => SnapshotRange::Slice(SnapshotSlice {
66 end_offset: id.0,
67 epoch: id.1,
68 total_size: i64::try_from(checkpoint.len()).unwrap_or(i64::MAX),
69 bytes: bytes::Bytes::from(bytes),
70 }),
71 Ok(CheckpointChunk::OutOfRange) => SnapshotRange::OutOfRange,
72 Err(_) => SnapshotRange::NoSnapshot,
73 }
74 }
75
76 pub async fn fetch_snapshot(
92 &self,
93 version: i16,
94 body: bytes::Bytes,
95 ) -> Result<bytes::Bytes, RaftError> {
96 let (reply, answer) = oneshot::channel();
97 self.engine
98 .deliver(Inbound::FetchSnapshot {
99 req: body,
100 version,
101 reply,
102 })
103 .await?;
104 answer.await.map_err(|_| RaftError::Shutdown)
105 }
106
107 pub async fn trigger_snapshot(&self) -> Result<(), RaftError> {
112 self.engine.trigger_snapshot().await
113 }
114}
115
116pub(super) fn load_latest_checkpoint(dir: &Path) -> Option<(SnapshotId, Vec<u8>)> {
120 let id = latest_checkpoint_id(dir)?;
121 let bytes = load_checkpoint_by_id(dir, id.0, id.1)?;
122 Some((id, bytes))
123}
124
125#[cfg(test)]
126mod tests {
127 use {
128 super::*,
129 crate::{controller::test_support::joining_controller, metadata_partition_dir},
130 std::fs,
131 tempfile::TempDir,
132 };
133
134 #[test]
135 fn load_latest_checkpoint_picks_highest_offset_then_epoch() {
136 let dir = TempDir::new().unwrap();
137 fs::write(
138 dir.path()
139 .join("00000000000000000001-0000000009.checkpoint"),
140 b"old-offset",
141 )
142 .unwrap();
143 fs::write(
144 dir.path()
145 .join("00000000000000000002-0000000001.checkpoint"),
146 b"old-epoch",
147 )
148 .unwrap();
149 fs::write(
150 dir.path()
151 .join("00000000000000000002-0000000003.checkpoint"),
152 b"best",
153 )
154 .unwrap();
155 fs::write(dir.path().join("9-9.txt"), b"ignored suffix").unwrap();
156 fs::write(
157 dir.path().join("not-a-checkpoint.checkpoint"),
158 b"ignored name",
159 )
160 .unwrap();
161
162 let latest = load_latest_checkpoint(dir.path()).expect("checkpoint");
163 assert2::assert!(latest == ((2, 3), b"best".to_vec()));
164 }
165
166 #[yare::parameterized(
167 zero = { 0, Some(b"abc".as_slice()) },
168 last_byte = { 2, Some(b"c".as_slice()) },
169 exact_end = { 3, None },
170 past_end = { 4, None },
171 negative = { -1, None },
172 )]
173 #[test_macro(tokio::test)]
174 async fn read_snapshot_range_rejects_the_end_and_everything_past_it(
175 position: i64,
176 expected: Option<&'static [u8]>,
177 ) {
178 let (dir, ctrl) = joining_controller("join start").await;
179 let checkpoint_dir = metadata_partition_dir(dir.path());
180 fs::create_dir_all(&checkpoint_dir).unwrap();
181 fs::write(
182 checkpoint_dir.join("00000000000000000010-0000000004.checkpoint"),
183 b"abc",
184 )
185 .unwrap();
186
187 let read = |position| match ctrl.read_snapshot_range(position, 10) {
190 SnapshotRange::Slice(slice) => Some(slice),
191 SnapshotRange::OutOfRange => None,
192 SnapshotRange::NoSnapshot => panic!("the checkpoint was written"),
193 };
194 let window = |bytes: &'static [u8]| SnapshotSlice {
195 end_offset: 10,
196 epoch: 4,
197 total_size: 3,
198 bytes: bytes::Bytes::from_static(bytes),
199 };
200 assert2::assert!(
201 read(position) == expected.map(window),
202 "position {position}"
203 );
204 ctrl.shutdown().await;
205 }
206}