1use std::path::{Path, PathBuf};
20use std::sync::Arc;
21
22use crate::fts::tokenizer::{Analyzer, TokenizerConfig};
23use crate::ingest::{PURGE_INTENT_FILE, read_intent};
24use crate::lifecycle::stats::Accounting;
25use crate::manifest::io::{DurableLog, MANIFEST_FILE};
26use crate::manifest::{Manifest, decode_manifest};
27use crate::meta::Schema;
28use crate::segment::SegmentError;
29use crate::segment::SegmentMeta;
30use crate::segment::layout::RegionKind;
31use crate::segment::reader::SegmentReader;
32use crate::vfs::{StdVfs, Vfs};
33use crate::wal::replay::ReplayTerminator;
34use crate::wal::{WalReadError, WalReader};
35
36const WAL_FILE: &str = "wal.ze";
38
39#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
41#[non_exhaustive]
42pub enum FindingKind {
43 ManifestMissing,
46 ManifestCorrupt,
48 ManifestAheadOfWal,
50 SegmentMissing,
52 SegmentCorrupt,
54 SegmentMismatch,
56 SegmentRegionCorrupt,
58 SegmentIndexInvalid,
60 WalMissing,
62 WalHeaderCorrupt,
64 WalRecordCorrupt,
66 WalRecordInvalid,
68 Unreadable,
70 PurgeIntentCorrupt,
72}
73
74impl FindingKind {
75 #[must_use]
77 pub const fn as_str(self) -> &'static str {
78 match self {
79 Self::ManifestMissing => "manifestMissing",
80 Self::ManifestCorrupt => "manifestCorrupt",
81 Self::ManifestAheadOfWal => "manifestAheadOfWal",
82 Self::SegmentMissing => "segmentMissing",
83 Self::SegmentCorrupt => "segmentCorrupt",
84 Self::SegmentMismatch => "segmentMismatch",
85 Self::SegmentRegionCorrupt => "segmentRegionCorrupt",
86 Self::SegmentIndexInvalid => "segmentIndexInvalid",
87 Self::WalMissing => "walMissing",
88 Self::WalHeaderCorrupt => "walHeaderCorrupt",
89 Self::WalRecordCorrupt => "walRecordCorrupt",
90 Self::WalRecordInvalid => "walRecordInvalid",
91 Self::Unreadable => "unreadable",
92 Self::PurgeIntentCorrupt => "purgeIntentCorrupt",
93 }
94 }
95}
96
97#[derive(Clone, Debug, Eq, PartialEq)]
99pub struct Finding {
100 pub kind: FindingKind,
102 pub file: String,
104 pub offset: Option<u64>,
106 pub detail: String,
108}
109
110#[derive(Clone, Debug, Default, Eq, PartialEq)]
112pub struct VerifyReport {
113 pub findings: Vec<Finding>,
115 pub generation: u64,
117 pub segments_checked: u64,
119 pub wal_records_checked: u64,
121}
122
123impl VerifyReport {
124 #[must_use]
126 pub fn is_clean(&self) -> bool {
127 self.findings.is_empty()
128 }
129}
130
131#[derive(Debug)]
134pub enum VerifyError {
135 NotFound {
137 path: PathBuf,
139 },
140 NotDirectory {
142 path: PathBuf,
144 },
145 Io {
147 path: PathBuf,
149 source: std::io::Error,
151 },
152 Tokenizer(String),
154}
155
156impl std::fmt::Display for VerifyError {
157 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 match self {
159 Self::NotFound { path } => {
160 write!(formatter, "store path {} does not exist", path.display())
161 }
162 Self::NotDirectory { path } => {
163 write!(
164 formatter,
165 "store path {} is not a directory",
166 path.display()
167 )
168 }
169 Self::Io { path, source } => {
170 write!(formatter, "store I/O {}: {source}", path.display())
171 }
172 Self::Tokenizer(detail) => write!(formatter, "verify tokenizer: {detail}"),
173 }
174 }
175}
176
177impl std::error::Error for VerifyError {
178 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
179 match self {
180 Self::Io { source, .. } => Some(source),
181 Self::NotFound { .. } | Self::NotDirectory { .. } | Self::Tokenizer(_) => None,
182 }
183 }
184}
185
186pub fn verify_store(path: impl AsRef<Path>) -> Result<VerifyReport, VerifyError> {
188 verify_store_on_vfs(&StdVfs, path.as_ref())
189}
190
191pub fn verify_store_on_vfs(vfs: &dyn Vfs, path: &Path) -> Result<VerifyReport, VerifyError> {
194 match vfs.ensure_directory(path, false) {
195 Ok(true) => {}
196 Ok(false) => {
197 return Err(VerifyError::NotDirectory {
198 path: path.to_path_buf(),
199 });
200 }
201 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
202 return Err(VerifyError::NotFound {
203 path: path.to_path_buf(),
204 });
205 }
206 Err(source) => {
207 return Err(VerifyError::Io {
208 path: path.to_path_buf(),
209 source,
210 });
211 }
212 }
213 let entries = vfs.list(path).map_err(|source| VerifyError::Io {
214 path: path.to_path_buf(),
215 source,
216 })?;
217 let has_segment_files = entries.iter().any(|entry| is_segment_file(entry));
218 let mut walk = Walk {
219 vfs,
220 directory: path,
221 report: VerifyReport::default(),
222 };
223 let manifest = walk.manifest();
224 if let Some(decoded) = manifest.decoded() {
225 walk.report.generation = decoded.generation;
226 walk.segments(decoded);
227 }
228 walk.wal(&manifest, has_segment_files)?;
229 walk.purge_intent();
230 Ok(walk.report)
231}
232
233struct Walk<'a> {
234 vfs: &'a dyn Vfs,
235 directory: &'a Path,
236 report: VerifyReport,
237}
238
239enum ManifestState {
241 Absent,
243 Damaged,
245 Decoded(Manifest),
247}
248
249impl ManifestState {
250 fn decoded(&self) -> Option<&Manifest> {
251 match self {
252 Self::Decoded(manifest) => Some(manifest),
253 Self::Absent | Self::Damaged => None,
254 }
255 }
256}
257
258impl Walk<'_> {
259 fn record(&mut self, kind: FindingKind, file: &str, offset: Option<u64>, detail: String) {
260 self.report.findings.push(Finding {
261 kind,
262 file: file.to_owned(),
263 offset,
264 detail,
265 });
266 }
267
268 fn manifest(&mut self) -> ManifestState {
269 let bytes = match self.vfs.read(&self.directory.join(MANIFEST_FILE)) {
270 Ok(bytes) => bytes,
271 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
272 return ManifestState::Absent;
273 }
274 Err(error) => {
275 self.record(
276 FindingKind::Unreadable,
277 MANIFEST_FILE,
278 None,
279 error.to_string(),
280 );
281 return ManifestState::Damaged;
282 }
283 };
284 match decode_manifest(MANIFEST_FILE, &bytes) {
285 Ok(manifest) => ManifestState::Decoded(manifest),
286 Err(error) => {
287 self.record(
288 FindingKind::ManifestCorrupt,
289 MANIFEST_FILE,
290 None,
291 error.to_string(),
292 );
293 ManifestState::Damaged
294 }
295 }
296 }
297
298 fn segments(&mut self, manifest: &Manifest) {
299 let schema = Arc::new(manifest.schema.clone());
303 for expected in &manifest.segments {
304 self.report.segments_checked = self.report.segments_checked.saturating_add(1);
305 self.segment(expected, &schema);
306 }
307 }
308
309 fn purge_intent(&mut self) {
312 let path = self.directory.join(PURGE_INTENT_FILE);
313 match self.vfs.open(&path) {
314 Ok(_) => {}
315 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return,
316 Err(error) => {
317 self.record(
318 FindingKind::Unreadable,
319 PURGE_INTENT_FILE,
320 None,
321 error.to_string(),
322 );
323 return;
324 }
325 }
326 if let Err(error) = read_intent(self.vfs, self.directory) {
327 self.record(
328 FindingKind::PurgeIntentCorrupt,
329 PURGE_INTENT_FILE,
330 None,
331 error.to_string(),
332 );
333 }
334 }
335
336 fn segment(&mut self, expected: &SegmentMeta, schema: &Arc<Schema>) {
337 let file = expected.id.file_name();
338 let path = self.directory.join(&file);
339 let reader = match SegmentReader::open(self.vfs, &path, expected.id) {
340 Ok(reader) => reader.with_collection_schema(Arc::clone(schema)),
341 Err(SegmentError::Io { source, .. })
342 if source.kind() == std::io::ErrorKind::NotFound =>
343 {
344 self.record(
345 FindingKind::SegmentMissing,
346 &file,
347 None,
348 format!(
349 "the manifest references segment {} with {} rows",
350 expected.id, expected.row_count
351 ),
352 );
353 return;
354 }
355 Err(SegmentError::Io { source, .. }) => {
356 self.record(FindingKind::Unreadable, &file, None, source.to_string());
357 return;
358 }
359 Err(error) => {
360 self.record(FindingKind::SegmentCorrupt, &file, None, error.to_string());
361 return;
362 }
363 };
364 if !reader.meta().same_segment_file(expected) {
365 self.record(
366 FindingKind::SegmentMismatch,
367 &file,
368 None,
369 format!(
370 "manifest records {expected:?}, segment header records {:?}",
371 reader.meta()
372 ),
373 );
374 return;
375 }
376 let mut regions_valid = true;
377 for entry in reader.directory() {
378 if let Err(error) = reader.region_by_id(entry.kind) {
379 regions_valid = false;
380 self.record(
381 FindingKind::SegmentRegionCorrupt,
382 &file,
383 Some(entry.offset),
384 format!("{}: {error}", region_name(entry.kind)),
385 );
386 }
387 }
388 if !regions_valid {
389 return;
390 }
391 if let Err(error) = reader.validate_all() {
392 self.record(FindingKind::SegmentCorrupt, &file, None, error.to_string());
393 return;
394 }
395 if let Err(error) = decode_regions(&reader) {
396 self.record(
397 FindingKind::SegmentIndexInvalid,
398 &file,
399 None,
400 error.to_string(),
401 );
402 }
403 }
404
405 fn wal(
406 &mut self,
407 manifest: &ManifestState,
408 has_segment_files: bool,
409 ) -> Result<(), VerifyError> {
410 let decoded = manifest.decoded();
411 let absent = matches!(manifest, ManifestState::Absent);
412 let absorbed_through = decoded.map_or(0, |manifest| manifest.log_seq);
413 let wal_path = self.directory.join(WAL_FILE);
414 match self.vfs.open(&wal_path) {
415 Ok(length) if length > 0 => {}
416 Err(error) if error.kind() != std::io::ErrorKind::NotFound => {
417 self.record(FindingKind::Unreadable, WAL_FILE, None, error.to_string());
418 return Ok(());
419 }
420 Ok(_) | Err(_) => {
421 if absorbed_through > 0 {
422 self.record(
423 FindingKind::WalMissing,
424 WAL_FILE,
425 None,
426 format!("the manifest covers WAL sequences through {absorbed_through}"),
427 );
428 } else if absent && has_segment_files {
429 self.record(
430 FindingKind::ManifestMissing,
431 MANIFEST_FILE,
432 None,
433 "segment files exist but neither a manifest nor a WAL holds their rows"
434 .to_owned(),
435 );
436 }
437 return Ok(());
438 }
439 }
440 let reader = match WalReader::open(self.vfs, &wal_path) {
441 Ok(reader) => reader,
442 Err(WalReadError::Io(error)) => {
443 self.record(FindingKind::Unreadable, WAL_FILE, None, error.to_string());
444 return Ok(());
445 }
446 Err(error) => {
447 self.record(
448 FindingKind::WalHeaderCorrupt,
449 WAL_FILE,
450 Some(0),
451 error.to_string(),
452 );
453 return Ok(());
454 }
455 };
456 self.report.wal_records_checked = u64::try_from(reader.records().len()).unwrap_or(u64::MAX);
457 match reader.terminator() {
458 Some(ReplayTerminator::CleanEnd) | None => {}
459 Some(ReplayTerminator::InvalidHeader(error)) => {
460 self.record(
461 FindingKind::WalHeaderCorrupt,
462 WAL_FILE,
463 Some(0),
464 error.to_string(),
465 );
466 return Ok(());
467 }
468 Some(ReplayTerminator::CorruptAt { offset, reason }) => {
469 self.record(
470 FindingKind::WalRecordCorrupt,
471 WAL_FILE,
472 Some(u64::try_from(offset).unwrap_or(u64::MAX)),
473 format!("{reason:?}"),
474 );
475 return Ok(());
476 }
477 }
478 let durable_end = reader.durable_end();
479 let first_seq = reader
480 .records()
481 .first()
482 .map_or(durable_end.saturating_add(1), |record| record.seq.get());
483 drop(reader);
484 if durable_end < absorbed_through {
485 self.record(
486 FindingKind::ManifestAheadOfWal,
487 MANIFEST_FILE,
488 None,
489 format!(
490 "the manifest covers WAL sequences through {absorbed_through}, \
491 the WAL ends at {durable_end}"
492 ),
493 );
494 return Ok(());
495 }
496 if absent && first_seq > 1 {
497 self.record(
498 FindingKind::ManifestMissing,
499 MANIFEST_FILE,
500 None,
501 format!(
502 "the WAL starts at sequence {first_seq}; earlier sequences were \
503 absorbed by a snapshot that is gone"
504 ),
505 );
506 return Ok(());
507 }
508 if matches!(manifest, ManifestState::Damaged) {
509 return Ok(());
511 }
512 let schema =
513 decoded.map_or_else(Schema::timestamp_only, |manifest| manifest.schema.clone());
514 let analyzer = Analyzer::new(TokenizerConfig::text_default())
515 .map_err(|error| VerifyError::Tokenizer(error.to_string()))?;
516 let accounting = Arc::new(Accounting::new(u64::MAX, u64::MAX));
517 let generation = decoded.map_or(0, |manifest| manifest.generation);
518 if let Err(error) = crate::ingest::ActiveState::recover(
519 self.vfs,
520 &wal_path,
521 generation,
522 absorbed_through,
523 &accounting,
524 &schema,
525 &analyzer,
526 ) {
527 if absent && has_segment_files {
528 self.record(
529 FindingKind::ManifestMissing,
530 MANIFEST_FILE,
531 None,
532 format!("the WAL cannot be replayed without the manifest's schema: {error}"),
533 );
534 } else {
535 self.record(
536 FindingKind::WalRecordInvalid,
537 WAL_FILE,
538 None,
539 error.to_string(),
540 );
541 }
542 }
543 Ok(())
544 }
545}
546
547fn decode_regions(reader: &SegmentReader) -> Result<(), SegmentError> {
551 reader.columns()?;
552 reader.alive()?;
553 reader.postings()?;
554 reader.stored_text()?;
555 reader.stored_metadata()?;
556 if reader
557 .directory()
558 .iter()
559 .any(|entry| entry.kind == RegionKind::GraphNodeBlocks.id())
560 {
561 reader.graph_node_blocks()?;
562 }
563 Ok(())
564}
565
566fn region_name(kind: u16) -> String {
567 RegionKind::from_id(kind).map_or_else(|| format!("region {kind}"), |kind| format!("{kind:?}"))
568}
569
570fn is_segment_file(path: &Path) -> bool {
571 path.file_name()
572 .and_then(|name| name.to_str())
573 .is_some_and(|name| name.starts_with("segment-") && name.ends_with(".zseg"))
574}