reddb_server/
pager_zone_migration.rs1use std::fs::{self, File, OpenOptions};
28use std::io::{Read, Seek, SeekFrom, Write};
29use std::path::{Path, PathBuf};
30
31use reddb_file::layout::retired;
32
33const PAGE_SIZE: usize = reddb_file::PAGED_PAGE_SIZE;
34const SLOT_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_SLOT_SIZE;
35const ZONE_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_ZONE_SIZE;
36const SUPERBLOCK_TRAILER_OFFSET: usize = reddb_file::PAGED_SUPERBLOCK_TRAILER_OFFSET;
37
38const BACKUP_SUFFIX: &str = "pre-migration";
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ZoneMigrationReport {
44 pub data_path: PathBuf,
46 pub backup_path: PathBuf,
48 pub removed_sidecars: Vec<PathBuf>,
50 pub header_recovered_from_shadow: bool,
52 pub manifest_recovered_from_shadow: bool,
55}
56
57#[derive(Debug)]
58pub enum ZoneMigrationError {
59 MissingStore(PathBuf),
61 NotALegacyStore(PathBuf),
63 AlreadyZoned(PathBuf),
65 HeaderUnrecoverable(PathBuf),
67 Io(std::io::Error),
68}
69
70impl std::fmt::Display for ZoneMigrationError {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 match self {
73 Self::MissingStore(path) => write!(f, "no store at {}", path.display()),
74 Self::NotALegacyStore(path) => write!(
75 f,
76 "{} carries no retired rdb-hdr/rdb-meta sidecar, so there is nothing to \
77 migrate; a zoned store opens directly",
78 path.display()
79 ),
80 Self::AlreadyZoned(path) => write!(
81 f,
82 "{} already has a valid superblock zone; migrating again would discard it",
83 path.display()
84 ),
85 Self::HeaderUnrecoverable(path) => write!(
86 f,
87 "neither page 0 of {} nor its rdb-hdr shadow holds a readable database \
88 header, so no superblock can be seeded from this store. This is a damaged \
89 store, not a legacy one: reach for red salvage (ADR 0074 §4)",
90 path.display()
91 ),
92 Self::Io(err) => write!(f, "io error: {err}"),
93 }
94 }
95}
96
97impl std::error::Error for ZoneMigrationError {}
98
99impl From<std::io::Error> for ZoneMigrationError {
100 fn from(err: std::io::Error) -> Self {
101 Self::Io(err)
102 }
103}
104
105type Result<T> = std::result::Result<T, ZoneMigrationError>;
106
107pub fn backup_path_for(data_path: &Path) -> PathBuf {
109 let mut path = data_path.as_os_str().to_os_string();
110 path.push(".");
111 path.push(BACKUP_SUFFIX);
112 PathBuf::from(path)
113}
114
115pub fn migrate_to_zoned(data_path: &Path) -> Result<ZoneMigrationReport> {
122 if !data_path.exists() {
123 return Err(ZoneMigrationError::MissingStore(data_path.to_path_buf()));
124 }
125 let sidecars = present_sidecars(data_path);
126 if sidecars.is_empty() {
127 return Err(ZoneMigrationError::NotALegacyStore(data_path.to_path_buf()));
128 }
129 if has_valid_superblock_zone(data_path)? {
130 return Err(ZoneMigrationError::AlreadyZoned(data_path.to_path_buf()));
131 }
132
133 let backup_path = backup_path_for(data_path);
134 fs::copy(data_path, &backup_path)?;
135
136 let page_zero = read_page(data_path, 0)?;
137 let (header_image, header_recovered_from_shadow) =
140 if reddb_file::database_header_magic_matches(&page_zero) {
141 (page_zero, false)
142 } else {
143 let shadow = read_sidecar_page(&retired::pager_header_shadow_path_v0(data_path))?
144 .filter(|page| reddb_file::database_header_magic_matches(page))
145 .ok_or_else(|| ZoneMigrationError::HeaderUnrecoverable(data_path.to_path_buf()))?;
146 (shadow, true)
147 };
148
149 let manifest_recovered_from_shadow = restore_manifest_page_if_torn(data_path)?;
150
151 let mut slot = [0u8; SLOT_SIZE];
154 slot.copy_from_slice(&header_image[..SLOT_SIZE]);
155 let mut file = OpenOptions::new().read(true).write(true).open(data_path)?;
156 for (copy_index, generation) in [(0usize, 1u64), (1usize, 2u64)] {
157 reddb_file::seal_paged_superblock_slot(&mut slot, copy_index, generation)
158 .map_err(|err| std::io::Error::other(err.to_string()))?;
159 write_at(&mut file, superblock_offset(copy_index), &slot)?;
160 }
161 file.sync_all()?;
162 drop(file);
163
164 for sidecar in &sidecars {
167 fs::remove_file(sidecar)?;
168 }
169
170 Ok(ZoneMigrationReport {
171 data_path: data_path.to_path_buf(),
172 backup_path,
173 removed_sidecars: sidecars,
174 header_recovered_from_shadow,
175 manifest_recovered_from_shadow,
176 })
177}
178
179pub fn revert_to_sidecars(data_path: &Path) -> Result<ZoneMigrationReport> {
188 if !data_path.exists() {
189 return Err(ZoneMigrationError::MissingStore(data_path.to_path_buf()));
190 }
191
192 let image = newest_superblock_image(data_path)?
193 .ok_or_else(|| ZoneMigrationError::HeaderUnrecoverable(data_path.to_path_buf()))?;
194
195 let mut header_page = [0u8; PAGE_SIZE];
198 header_page[..SLOT_SIZE].copy_from_slice(&image);
199 header_page[SUPERBLOCK_TRAILER_OFFSET..SLOT_SIZE].fill(0);
200 reddb_file::clear_paged_page_checksum(&mut header_page);
201 let checksum = crate::storage::engine::crc32::crc32(&header_page);
202 reddb_file::set_paged_page_checksum(&mut header_page, checksum);
203
204 let manifest_page = read_page(data_path, 1)?;
205
206 let mut file = OpenOptions::new().read(true).write(true).open(data_path)?;
207 write_at(&mut file, 0, &header_page)?;
208 file.sync_all()?;
209 drop(file);
210
211 write_sidecar_page(
212 &retired::pager_header_shadow_path_v0(data_path),
213 &header_page,
214 )?;
215 write_sidecar_page(
216 &retired::pager_meta_shadow_path_v0(data_path),
217 &manifest_page,
218 )?;
219
220 let backup_path = backup_path_for(data_path);
221 if backup_path.exists() {
222 fs::remove_file(&backup_path)?;
223 }
224
225 Ok(ZoneMigrationReport {
226 data_path: data_path.to_path_buf(),
227 backup_path,
228 removed_sidecars: Vec::new(),
229 header_recovered_from_shadow: false,
230 manifest_recovered_from_shadow: false,
231 })
232}
233
234fn newest_superblock_image(data_path: &Path) -> Result<Option<[u8; SLOT_SIZE]>> {
237 let zone = read_superblock_zone(data_path)?;
238 let Some(selection) = reddb_file::select_paged_superblock(&zone) else {
239 return Ok(None);
240 };
241 let start = selection.copy_index * SLOT_SIZE;
242 let mut image = [0u8; SLOT_SIZE];
243 image.copy_from_slice(&zone[start..start + SLOT_SIZE]);
244 Ok(Some(image))
245}
246
247fn read_superblock_zone(data_path: &Path) -> Result<[u8; ZONE_SIZE]> {
248 let mut zone = [0u8; ZONE_SIZE];
249 let mut file = File::open(data_path)?;
250 let len = file.metadata()?.len().min(ZONE_SIZE as u64) as usize;
251 if len > 0 {
252 file.read_exact(&mut zone[..len])?;
253 }
254 Ok(zone)
255}
256
257fn present_sidecars(data_path: &Path) -> Vec<PathBuf> {
258 let mut seen: Vec<PathBuf> = Vec::new();
259 for candidate in retired::phase1_sidecar_paths(data_path) {
260 if candidate.exists() && !seen.contains(&candidate) {
261 seen.push(candidate);
262 }
263 }
264 seen
265}
266
267fn has_valid_superblock_zone(data_path: &Path) -> Result<bool> {
268 let zone = read_superblock_zone(data_path)?;
269 Ok(reddb_file::select_paged_superblock(&zone).is_some())
270}
271
272fn restore_manifest_page_if_torn(data_path: &Path) -> Result<bool> {
275 let page_one = read_page(data_path, 1)?;
276 if page_checksum_valid(&page_one) {
277 return Ok(false);
278 }
279 let Some(shadow) = read_sidecar_page(&retired::pager_meta_shadow_path_v0(data_path))? else {
280 return Ok(false);
281 };
282 if !page_checksum_valid(&shadow) {
283 return Ok(false);
284 }
285 let mut file = OpenOptions::new().read(true).write(true).open(data_path)?;
286 write_at(&mut file, PAGE_SIZE as u64, &shadow)?;
287 file.sync_all()?;
288 Ok(true)
289}
290
291fn page_checksum_valid(page: &[u8; PAGE_SIZE]) -> bool {
294 let stored = reddb_file::paged_page_checksum(page);
295 let mut scratch = *page;
296 reddb_file::clear_paged_page_checksum(&mut scratch);
297 stored == crate::storage::engine::crc32::crc32(&scratch)
298}
299
300fn superblock_offset(copy_index: usize) -> u64 {
301 reddb_file::paged_superblock_slot_offset(copy_index)
302}
303
304fn read_page(path: &Path, page_id: u64) -> Result<[u8; PAGE_SIZE]> {
305 let mut file = File::open(path)?;
306 let mut page = [0u8; PAGE_SIZE];
307 file.seek(SeekFrom::Start(page_id * PAGE_SIZE as u64))?;
308 file.read_exact(&mut page)?;
309 Ok(page)
310}
311
312fn read_sidecar_page(path: &Path) -> Result<Option<[u8; PAGE_SIZE]>> {
313 if !path.exists() {
314 return Ok(None);
315 }
316 let mut file = File::open(path)?;
317 let mut page = [0u8; PAGE_SIZE];
318 match file.read_exact(&mut page) {
319 Ok(()) => Ok(Some(page)),
320 Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None),
322 Err(err) => Err(err.into()),
323 }
324}
325
326fn write_sidecar_page(path: &Path, page: &[u8; PAGE_SIZE]) -> Result<()> {
327 let mut file = File::create(path)?;
328 file.write_all(page)?;
329 file.sync_all()?;
330 Ok(())
331}
332
333fn write_at(file: &mut File, offset: u64, bytes: &[u8]) -> Result<()> {
334 file.seek(SeekFrom::Start(offset))?;
335 file.write_all(bytes)?;
336 Ok(())
337}