1#![deny(unsafe_code)]
2#![cfg_attr(
3 test,
4 allow(
5 clippy::unwrap_used,
6 clippy::expect_used,
7 clippy::indexing_slicing,
8 clippy::arithmetic_side_effects,
9 clippy::shadow_unrelated,
10 clippy::let_underscore_must_use,
11 clippy::format_push_string
12 )
13)]
14
15mod lifecycle_checks;
21mod record_checks;
22
23use std::{
24 collections::HashSet,
25 fmt::{Display, Formatter, Result as FmtResult},
26 io::Error as IoError,
27 num::TryFromIntError,
28 path::{Path, PathBuf},
29};
30
31use shardline_index::{
32 AsyncIndexStore, FileRecord, FileRecordInvariantError, LocalIndexStoreError,
33 MemoryIndexStoreError, MemoryRecordStoreError, PostgresMetadataStoreError, xet_hash_hex_string,
34};
35use shardline_protocol::HashParseError;
36use shardline_server_core::{
37 InvalidSerializedShardError, OpsRecordKind, OpsRecordStore, ServerObjectStore,
38 ServerObjectStoreError, ShardMetadataLimits, checked_increment, read_full_object,
39};
40use shardline_storage::{ObjectKey, ObjectStore};
41use shardline_xet_adapter::{XetAdapterError, XorbParseError, retained_shard_chunk_hashes};
42use thiserror::Error;
43
44use lifecycle_checks::inspect_lifecycle_metadata;
45use record_checks::scan_record_tree;
46
47#[derive(Debug, Error)]
49pub enum FsckError {
50 #[error("local storage operation failed")]
52 Io(#[from] IoError),
53 #[error("json operation failed")]
55 Json(#[from] serde_json::Error),
56 #[error("numeric conversion exceeded supported bounds")]
58 NumericConversion(#[from] TryFromIntError),
59 #[error("arithmetic overflow")]
61 Overflow,
62 #[error("local storage adapter operation failed")]
64 LocalObjectStore(#[from] shardline_storage::LocalObjectStoreError),
65 #[error("s3 object storage adapter operation failed")]
67 S3ObjectStore(#[from] shardline_storage::S3ObjectStoreError),
68 #[error("object store operation failed")]
70 ObjectStore(#[from] ServerObjectStoreError),
71 #[error("xet adapter operation failed")]
73 XetAdapter(#[from] XetAdapterError),
74 #[error("local index adapter operation failed")]
76 LocalIndexStore(#[from] LocalIndexStoreError),
77 #[error("memory index adapter operation failed")]
79 MemoryIndexStore(#[from] MemoryIndexStoreError),
80 #[error("memory record adapter operation failed")]
82 MemoryRecordStore(#[from] MemoryRecordStoreError),
83 #[error("postgres metadata adapter operation failed")]
85 PostgresMetadata(#[from] PostgresMetadataStoreError),
86 #[error("stored file metadata exceeded the bounded parser ceiling")]
88 StoredFileMetadataTooLarge {
89 observed_bytes: u64,
91 maximum_bytes: u64,
93 },
94}
95
96impl From<shardline_server_core::ParseStoredFileRecordError> for FsckError {
97 fn from(value: shardline_server_core::ParseStoredFileRecordError) -> Self {
98 match value {
99 shardline_server_core::ParseStoredFileRecordError::StoredFileMetadataTooLarge {
100 observed_bytes,
101 maximum_bytes,
102 } => Self::StoredFileMetadataTooLarge {
103 observed_bytes,
104 maximum_bytes,
105 },
106 shardline_server_core::ParseStoredFileRecordError::Json(e) => Self::Json(e),
107 }
108 }
109}
110
111impl From<shardline_server_core::ValidateIdentifierError> for FsckError {
112 fn from(_: shardline_server_core::ValidateIdentifierError) -> Self {
113 Self::Overflow
114 }
115}
116
117impl From<shardline_server_core::ValidateContentHashError> for FsckError {
118 fn from(_: shardline_server_core::ValidateContentHashError) -> Self {
119 Self::Overflow
120 }
121}
122
123impl From<shardline_server_core::RebuildOverflowError> for FsckError {
124 fn from(_: shardline_server_core::RebuildOverflowError) -> Self {
125 Self::Overflow
126 }
127}
128
129impl From<XorbParseError> for FsckError {
130 fn from(_: XorbParseError) -> Self {
131 Self::Overflow
132 }
133}
134
135impl From<HashParseError> for FsckError {
136 fn from(_: HashParseError) -> Self {
137 Self::Overflow
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct FsckReport {
144 pub latest_records: u64,
146 pub version_records: u64,
148 pub inspected_chunk_references: u64,
150 pub inspected_dedupe_shard_mappings: u64,
152 pub inspected_reconstructions: u64,
154 pub inspected_webhook_deliveries: u64,
156 pub inspected_provider_repository_states: u64,
158 pub issues: Vec<FsckIssue>,
160}
161
162impl FsckReport {
163 #[must_use]
165 pub const fn issue_count(&self) -> usize {
166 self.issues.len()
167 }
168
169 #[must_use]
171 pub const fn is_clean(&self) -> bool {
172 self.issues.is_empty()
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct FsckIssue {
179 pub kind: FsckIssueKind,
181 pub location: String,
183 pub detail: FsckIssueDetail,
185}
186
187#[derive(Debug, Clone, Error, PartialEq, Eq)]
189pub enum FsckIssueDetail {
190 #[error("missing version record {version_locator}")]
192 MissingVersionRecord {
193 version_locator: String,
195 },
196 #[error("record metadata exceeded the bounded parser ceiling")]
198 OversizedRecordMetadata,
199 #[error("record json was invalid")]
201 RecordJsonInvalid,
202 #[error("record file_id `{file_id}` is invalid")]
204 InvalidFileId {
205 file_id: String,
207 },
208 #[error("record content hash `{content_hash}` is invalid")]
210 InvalidContentHash {
211 content_hash: String,
213 },
214 #[error("expected record at {expected_locator}")]
216 RecordPathMismatch {
217 expected_locator: String,
219 },
220 #[error("record file_id does not match path")]
222 RecordFileIdPathMismatch,
223 #[error("record content hash does not match path")]
225 RecordContentHashPathMismatch,
226 #[error("{0}")]
228 InvalidReconstructionPlan(FsckReconstructionPlanDetail),
229 #[error("chunk hash `{chunk_hash}` is invalid")]
231 InvalidChunkHash {
232 chunk_hash: String,
234 },
235 #[error("xorb hash `{xorb_hash}` is invalid")]
237 InvalidXorbHash {
238 xorb_hash: String,
240 },
241 #[error("referenced by record {record_location}")]
243 ReferencedByRecord {
244 record_location: String,
246 },
247 #[error("referenced by native xet record {record_location}")]
249 ReferencedByNativeXetRecord {
250 record_location: String,
252 },
253 #[error("referenced by native xet xorb {xorb_location}")]
255 ReferencedByNativeXetXorb {
256 xorb_location: String,
258 },
259 #[error("expected {expected_hash}, got {observed_hash}")]
261 HashMismatch {
262 expected_hash: String,
264 observed_hash: String,
266 },
267 #[error("expected {expected_length}, got {observed_length}")]
269 LengthMismatch {
270 expected_length: u64,
272 observed_length: u64,
274 },
275 #[error("xorb range {range_start}..{range_end} exceeded {chunk_count} chunks")]
277 XorbRangeExceededChunkCount {
278 range_start: u32,
280 range_end: u32,
282 chunk_count: usize,
284 },
285 #[error("latest record differed from version record {version_locator}")]
287 MismatchedVersionRecord {
288 version_locator: String,
290 },
291 #[error("mapped chunk hash {chunk_hash}")]
293 MappedChunkHash {
294 chunk_hash: String,
296 },
297 #[error("mapped chunk hash {chunk_hash} was absent from retained shard")]
299 MappedChunkHashAbsentFromRetainedShard {
300 chunk_hash: String,
302 },
303 #[error("{0}")]
305 InvalidRetainedShard(InvalidSerializedShardError),
306 #[error("reconstruction index listed a file id without a readable row")]
308 ReconstructionListedUnreadableRow,
309 #[error("reconstruction contained no terms")]
311 ReconstructionContainedNoTerms,
312 #[error("reconstruction referenced unregistered xorb {xorb_hash}")]
314 MissingReconstructionXorb {
315 xorb_hash: String,
317 },
318 #[error(
320 "delete-after {delete_after_unix_seconds} preceded first-seen {first_seen_unreachable_at_unix_seconds}"
321 )]
322 InvalidQuarantineTimeline {
323 delete_after_unix_seconds: u64,
325 first_seen_unreachable_at_unix_seconds: u64,
327 },
328 #[error("quarantine metadata referenced a missing object")]
330 QuarantineReferencedMissingObject,
331 #[error("quarantine metadata still targeted a reachable live object")]
333 QuarantineTargetedReachableObject,
334 #[error("release-after {release_after_unix_seconds} preceded held-at {held_at_unix_seconds}")]
336 InvalidRetentionTimeline {
337 release_after_unix_seconds: u64,
339 held_at_unix_seconds: u64,
341 },
342 #[error("active retention hold reason: {reason}")]
344 ActiveRetentionHoldReason {
345 reason: String,
347 },
348 #[error("active retention hold still coexisted with quarantine state")]
350 ActiveRetentionHoldQuarantined,
351 #[error(
353 "processed-at {processed_at_unix_seconds} exceeded max allowed {max_allowed_unix_seconds}"
354 )]
355 WebhookDeliveryTimestampExceeded {
356 processed_at_unix_seconds: u64,
358 max_allowed_unix_seconds: u64,
360 },
361 #[error("provider repository state identity failed repository-scope validation")]
363 ProviderRepositoryIdentityInvalid,
364 #[error("{field} {timestamp} exceeded max allowed {max_allowed_unix_seconds}")]
366 ProviderRepositoryStateTimestampExceeded {
367 field: ProviderRepositoryStateTimestampField,
369 timestamp: u64,
371 max_allowed_unix_seconds: u64,
373 },
374}
375
376#[derive(Debug, Clone, Copy, Error, PartialEq, Eq)]
378pub enum FsckReconstructionPlanDetail {
379 #[error("record chunk hash is invalid")]
381 ChunkHashInvalid,
382 #[error("record contains an empty chunk")]
384 EmptyChunk,
385 #[error("record chunks are not contiguous")]
387 NonContiguousChunkOffsets,
388 #[error("record chunk range is invalid")]
390 InvalidChunkRange,
391 #[error("record packed range is invalid")]
393 InvalidPackedRange,
394 #[error("record length overflowed")]
396 LengthOverflow,
397 #[error("record total byte count did not match chunks")]
399 TotalBytesMismatch,
400}
401
402#[derive(Debug, Clone, Copy, PartialEq, Eq)]
404pub enum ProviderRepositoryStateTimestampField {
405 LastAccessChangedAtUnixSeconds,
407 LastRevisionPushedAtUnixSeconds,
409 LastCacheInvalidatedAtUnixSeconds,
411 LastAuthorizationRecheckedAtUnixSeconds,
413 LastDriftCheckedAtUnixSeconds,
415}
416
417impl ProviderRepositoryStateTimestampField {
418 const fn as_str(self) -> &'static str {
419 match self {
420 Self::LastAccessChangedAtUnixSeconds => "last_access_changed_at_unix_seconds",
421 Self::LastRevisionPushedAtUnixSeconds => "last_revision_pushed_at_unix_seconds",
422 Self::LastCacheInvalidatedAtUnixSeconds => "last_cache_invalidated_at_unix_seconds",
423 Self::LastAuthorizationRecheckedAtUnixSeconds => {
424 "last_authorization_rechecked_at_unix_seconds"
425 }
426 Self::LastDriftCheckedAtUnixSeconds => "last_drift_checked_at_unix_seconds",
427 }
428 }
429}
430
431impl Display for ProviderRepositoryStateTimestampField {
432 fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
433 formatter.write_str(self.as_str())
434 }
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq)]
439pub enum FsckIssueKind {
440 OversizedRecordMetadata,
442 InvalidRecordJson,
444 InvalidFileId,
446 InvalidContentHash,
448 RecordPathMismatch,
450 NonContiguousChunks,
452 EmptyChunk,
454 TotalBytesMismatch,
456 InvalidChunkRange,
458 InvalidPackedRange,
460 RecordHashMismatch,
462 MissingChunk,
464 ChunkHashMismatch,
466 ChunkLengthMismatch,
468 MissingVersionRecord,
470 MismatchedVersionRecord,
472 MissingDedupeShardObject,
474 InvalidRetainedShard,
476 InvalidDedupeShardMapping,
478 EmptyReconstruction,
480 MissingReconstructionXorb,
482 InvalidQuarantineCandidate,
484 MissingQuarantinedObject,
486 QuarantineLengthMismatch,
488 ReachableQuarantinedObject,
490 InvalidRetentionHold,
492 MissingHeldObject,
494 HeldQuarantinedObject,
496 InvalidWebhookDeliveryTimestamp,
498 InvalidProviderRepositoryState,
500 InvalidProviderRepositoryStateTimestamp,
502}
503
504impl FsckIssueKind {
505 #[must_use]
507 pub const fn as_str(self) -> &'static str {
508 match self {
509 Self::OversizedRecordMetadata => "oversized_record_metadata",
510 Self::InvalidRecordJson => "invalid_record_json",
511 Self::InvalidFileId => "invalid_file_id",
512 Self::InvalidContentHash => "invalid_content_hash",
513 Self::RecordPathMismatch => "record_path_mismatch",
514 Self::NonContiguousChunks => "non_contiguous_chunks",
515 Self::EmptyChunk => "empty_chunk",
516 Self::TotalBytesMismatch => "total_bytes_mismatch",
517 Self::InvalidChunkRange => "invalid_chunk_range",
518 Self::InvalidPackedRange => "invalid_packed_range",
519 Self::RecordHashMismatch => "record_hash_mismatch",
520 Self::MissingChunk => "missing_chunk",
521 Self::ChunkHashMismatch => "chunk_hash_mismatch",
522 Self::ChunkLengthMismatch => "chunk_length_mismatch",
523 Self::MissingVersionRecord => "missing_version_record",
524 Self::MismatchedVersionRecord => "mismatched_version_record",
525 Self::MissingDedupeShardObject => "missing_dedupe_shard_object",
526 Self::InvalidRetainedShard => "invalid_retained_shard",
527 Self::InvalidDedupeShardMapping => "invalid_dedupe_shard_mapping",
528 Self::EmptyReconstruction => "empty_reconstruction",
529 Self::MissingReconstructionXorb => "missing_reconstruction_xorb",
530 Self::InvalidQuarantineCandidate => "invalid_quarantine_candidate",
531 Self::MissingQuarantinedObject => "missing_quarantined_object",
532 Self::QuarantineLengthMismatch => "quarantine_length_mismatch",
533 Self::ReachableQuarantinedObject => "reachable_quarantined_object",
534 Self::InvalidRetentionHold => "invalid_retention_hold",
535 Self::MissingHeldObject => "missing_held_object",
536 Self::HeldQuarantinedObject => "held_quarantined_object",
537 Self::InvalidWebhookDeliveryTimestamp => "invalid_webhook_delivery_timestamp",
538 Self::InvalidProviderRepositoryState => "invalid_provider_repository_state",
539 Self::InvalidProviderRepositoryStateTimestamp => {
540 "invalid_provider_repository_state_timestamp"
541 }
542 }
543 }
544}
545
546pub type LocalFsckReport = FsckReport;
548
549pub type LocalFsckIssue = FsckIssue;
551
552pub type LocalFsckIssueKind = FsckIssueKind;
554
555pub const WEBHOOK_DELIVERY_FUTURE_SKEW_SECONDS: u64 = 300;
556
557pub async fn run_local_fsck(root: PathBuf) -> Result<LocalFsckReport, FsckError> {
564 let object_root = root.join("chunks");
565 let object_store = ServerObjectStore::local(object_root.clone())?;
566 let index_store = shardline_index::LocalIndexStore::open(root.clone());
567 let record_store = shardline_index::LocalRecordStore::open(root);
568 run_fsck_with_stores(
569 &record_store,
570 &index_store,
571 &object_root,
572 &object_store,
573 shardline_server_core::DEFAULT_SHARD_METADATA_LIMITS,
574 )
575 .await
576}
577
578pub async fn run_fsck_with_stores<RecordAdapter, IndexAdapter>(
586 record_store: &RecordAdapter,
587 index_store: &IndexAdapter,
588 object_root: &Path,
589 object_store: &ServerObjectStore,
590 shard_metadata_limits: ShardMetadataLimits,
591) -> Result<FsckReport, FsckError>
592where
593 RecordAdapter: OpsRecordStore + Sync,
594 RecordAdapter::Error: Into<FsckError>,
595 IndexAdapter: AsyncIndexStore + Sync,
596 IndexAdapter::Error: Into<FsckError>,
597{
598 let start = std::time::Instant::now();
599 let mut report = FsckReport {
600 latest_records: 0,
601 version_records: 0,
602 inspected_chunk_references: 0,
603 inspected_dedupe_shard_mappings: 0,
604 inspected_reconstructions: 0,
605 inspected_webhook_deliveries: 0,
606 inspected_provider_repository_states: 0,
607 issues: Vec::new(),
608 };
609 let mut reachability = FsckReachability::default();
610
611 scan_record_tree(
612 record_store,
613 RecordKind::Latest,
614 object_root,
615 object_store,
616 &mut reachability,
617 &mut report,
618 )
619 .await?;
620 scan_record_tree(
621 record_store,
622 RecordKind::Version,
623 object_root,
624 object_store,
625 &mut reachability,
626 &mut report,
627 )
628 .await?;
629 inspect_dedupe_shard_mappings(
630 index_store,
631 object_root,
632 object_store,
633 shard_metadata_limits,
634 &mut reachability,
635 &mut report,
636 )
637 .await?;
638 inspect_reconstruction_index(index_store, &mut report).await?;
639 inspect_lifecycle_metadata(
640 index_store,
641 object_root,
642 object_store,
643 &reachability,
644 &mut report,
645 )
646 .await?;
647
648 let elapsed = start.elapsed();
649 shardline_metrics::record_fsck_run(elapsed, report.issue_count() as u64);
650
651 Ok(report)
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657
658 fn clean_report() -> FsckReport {
659 FsckReport {
660 latest_records: 0,
661 version_records: 0,
662 inspected_chunk_references: 0,
663 inspected_dedupe_shard_mappings: 0,
664 inspected_reconstructions: 0,
665 inspected_webhook_deliveries: 0,
666 inspected_provider_repository_states: 0,
667 issues: Vec::new(),
668 }
669 }
670
671 #[test]
672 fn is_clean_returns_true_for_empty_issues() {
673 assert!(clean_report().is_clean());
674 }
675
676 #[test]
677 fn is_clean_returns_false_when_issues_present() {
678 let mut report = clean_report();
679 report.issues.push(FsckIssue {
680 kind: FsckIssueKind::MissingChunk,
681 location: "test".to_owned(),
682 detail: FsckIssueDetail::HashMismatch {
683 expected_hash: "a".repeat(64),
684 observed_hash: "b".repeat(64),
685 },
686 });
687 assert!(!report.is_clean());
688 }
689
690 #[test]
691 fn issue_count_zero_for_clean_report() {
692 assert_eq!(clean_report().issue_count(), 0);
693 }
694
695 #[test]
696 fn issue_count_matches_issues_length() {
697 let mut report = clean_report();
698 report.issues.push(FsckIssue {
699 kind: FsckIssueKind::MissingChunk,
700 location: "a".to_owned(),
701 detail: FsckIssueDetail::HashMismatch {
702 expected_hash: "a".repeat(64),
703 observed_hash: "b".repeat(64),
704 },
705 });
706 report.issues.push(FsckIssue {
707 kind: FsckIssueKind::ChunkHashMismatch,
708 location: "b".to_owned(),
709 detail: FsckIssueDetail::InvalidChunkHash {
710 chunk_hash: "c".repeat(64),
711 },
712 });
713 assert_eq!(report.issue_count(), 2);
714 }
715}
716
717#[derive(Debug, Clone, Copy, PartialEq, Eq)]
718pub(crate) enum RecordKind {
719 Latest,
720 Version,
721}
722
723pub(crate) struct FsckObjectContext<'operation> {
724 pub(crate) object_root: &'operation Path,
725 pub(crate) object_store: &'operation ServerObjectStore,
726}
727
728#[derive(Debug, Default)]
729pub(crate) struct FsckReachability {
730 pub(crate) referenced_object_keys: HashSet<String>,
731 pub(crate) live_dedupe_chunk_hashes: HashSet<String>,
732}
733
734pub(crate) struct PendingVersionRecordCheck<Locator> {
735 pub(crate) latest_locator: Locator,
736 pub(crate) version_locator: Locator,
737 pub(crate) latest_record: FileRecord,
738}
739
740impl RecordKind {
741 pub(crate) const fn ops(self) -> OpsRecordKind {
742 match self {
743 Self::Latest => OpsRecordKind::Latest,
744 Self::Version => OpsRecordKind::Version,
745 }
746 }
747}
748
749pub(crate) fn push_reconstruction_plan_issue(
750 report: &mut FsckReport,
751 location: String,
752 error: &FileRecordInvariantError,
753) -> Result<(), FsckError> {
754 let kind = match error {
755 FileRecordInvariantError::ChunkHash(_) => FsckIssueKind::InvalidContentHash,
756 FileRecordInvariantError::EmptyChunk => FsckIssueKind::EmptyChunk,
757 FileRecordInvariantError::NonContiguousChunkOffsets => FsckIssueKind::NonContiguousChunks,
758 FileRecordInvariantError::InvalidChunkRange => FsckIssueKind::InvalidChunkRange,
759 FileRecordInvariantError::InvalidPackedRange => FsckIssueKind::InvalidPackedRange,
760 FileRecordInvariantError::LengthOverflow | FileRecordInvariantError::TotalBytesMismatch => {
761 FsckIssueKind::TotalBytesMismatch
762 }
763 };
764 push_issue(
765 report,
766 kind,
767 location,
768 FsckIssueDetail::InvalidReconstructionPlan(reconstruction_plan_error_detail(error)),
769 )
770}
771
772pub(crate) fn push_issue(
773 report: &mut FsckReport,
774 kind: FsckIssueKind,
775 location: String,
776 detail: FsckIssueDetail,
777) -> Result<(), FsckError> {
778 let _count = u64::try_from(report.issues.len())?;
779 report.issues.push(FsckIssue {
780 kind,
781 location,
782 detail,
783 });
784 Ok(())
785}
786
787const fn reconstruction_plan_error_detail(
788 error: &FileRecordInvariantError,
789) -> FsckReconstructionPlanDetail {
790 match error {
791 FileRecordInvariantError::ChunkHash(_) => FsckReconstructionPlanDetail::ChunkHashInvalid,
792 FileRecordInvariantError::EmptyChunk => FsckReconstructionPlanDetail::EmptyChunk,
793 FileRecordInvariantError::NonContiguousChunkOffsets => {
794 FsckReconstructionPlanDetail::NonContiguousChunkOffsets
795 }
796 FileRecordInvariantError::InvalidChunkRange => {
797 FsckReconstructionPlanDetail::InvalidChunkRange
798 }
799 FileRecordInvariantError::InvalidPackedRange => {
800 FsckReconstructionPlanDetail::InvalidPackedRange
801 }
802 FileRecordInvariantError::LengthOverflow => FsckReconstructionPlanDetail::LengthOverflow,
803 FileRecordInvariantError::TotalBytesMismatch => {
804 FsckReconstructionPlanDetail::TotalBytesMismatch
805 }
806 }
807}
808
809pub(crate) fn record_path<RecordAdapter>(
810 record_store: &RecordAdapter,
811 record_kind: RecordKind,
812 record: &FileRecord,
813) -> RecordAdapter::Locator
814where
815 RecordAdapter: OpsRecordStore,
816{
817 match record_kind {
818 RecordKind::Latest => record_store.latest_record_locator(record),
819 RecordKind::Version => record_store.version_record_locator(record),
820 }
821}
822
823fn object_key_storage_path(object_root: &Path, object_key: &ObjectKey) -> PathBuf {
824 object_root.join(object_key.as_str())
825}
826
827pub(crate) fn object_location_display(
828 object_root: &Path,
829 object_store: &ServerObjectStore,
830 object_key: &ObjectKey,
831) -> String {
832 object_store
833 .local_path_for_key(object_key)
834 .unwrap_or_else(|| object_key_storage_path(object_root, object_key))
835 .display()
836 .to_string()
837}
838
839pub(crate) fn unix_now_seconds_checked() -> Result<u64, FsckError> {
840 shardline_server_core::unix_now_seconds_checked().map_err(|_e| FsckError::Overflow)
841}
842
843async fn inspect_dedupe_shard_mappings<IndexAdapter>(
844 index_store: &IndexAdapter,
845 object_root: &Path,
846 object_store: &ServerObjectStore,
847 shard_metadata_limits: ShardMetadataLimits,
848 reachability: &mut FsckReachability,
849 report: &mut FsckReport,
850) -> Result<(), FsckError>
851where
852 IndexAdapter: AsyncIndexStore + Sync,
853 IndexAdapter::Error: Into<FsckError>,
854{
855 index_store
856 .visit_dedupe_shard_mappings(|mapping| {
857 report.inspected_dedupe_shard_mappings =
858 checked_increment(report.inspected_dedupe_shard_mappings)?;
859 let chunk_hash_hex = xet_hash_hex_string(mapping.chunk_hash());
860 let shard_location =
861 object_location_display(object_root, object_store, mapping.shard_object_key());
862 let metadata = match object_store.metadata(mapping.shard_object_key())? {
863 Some(metadata) => metadata,
864 None => {
865 push_issue(
866 report,
867 FsckIssueKind::MissingDedupeShardObject,
868 shard_location,
869 FsckIssueDetail::MappedChunkHash {
870 chunk_hash: chunk_hash_hex,
871 },
872 )?;
873 return Ok::<(), FsckError>(());
874 }
875 };
876 let shard_bytes =
877 read_full_object(object_store, mapping.shard_object_key(), metadata.length())?;
878 let chunk_hashes =
879 match retained_shard_chunk_hashes(&shard_bytes, shard_metadata_limits) {
880 Ok(chunk_hashes) => chunk_hashes,
881 Err(XetAdapterError::InvalidSerializedShard(detail)) => {
882 push_issue(
883 report,
884 FsckIssueKind::InvalidRetainedShard,
885 shard_location,
886 FsckIssueDetail::InvalidRetainedShard(detail),
887 )?;
888 return Ok::<(), FsckError>(());
889 }
890 Err(error) => return Err(error.into()),
891 };
892 if !chunk_hashes
893 .iter()
894 .any(|candidate| candidate == &chunk_hash_hex)
895 {
896 push_issue(
897 report,
898 FsckIssueKind::InvalidDedupeShardMapping,
899 shard_location,
900 FsckIssueDetail::MappedChunkHashAbsentFromRetainedShard {
901 chunk_hash: chunk_hash_hex.clone(),
902 },
903 )?;
904 }
905 if reachability
906 .live_dedupe_chunk_hashes
907 .contains(&chunk_hash_hex)
908 {
909 reachability
910 .referenced_object_keys
911 .insert(mapping.shard_object_key().as_str().to_owned());
912 }
913 Ok::<(), FsckError>(())
914 })
915 .await?;
916
917 Ok(())
918}
919
920async fn inspect_reconstruction_index<IndexAdapter>(
921 index_store: &IndexAdapter,
922 report: &mut FsckReport,
923) -> Result<(), FsckError>
924where
925 IndexAdapter: AsyncIndexStore + Sync,
926 IndexAdapter::Error: Into<FsckError>,
927{
928 let file_ids = index_store
929 .list_reconstruction_file_ids()
930 .await
931 .map_err(Into::into)?;
932 for file_id in file_ids {
933 report.inspected_reconstructions = checked_increment(report.inspected_reconstructions)?;
934 let file_id_hex = xet_hash_hex_string(file_id.hash());
935 let Some(reconstruction) = index_store
936 .reconstruction(&file_id)
937 .await
938 .map_err(Into::into)?
939 else {
940 push_issue(
941 report,
942 FsckIssueKind::EmptyReconstruction,
943 file_id_hex,
944 FsckIssueDetail::ReconstructionListedUnreadableRow,
945 )?;
946 continue;
947 };
948
949 if reconstruction.terms().is_empty() {
950 push_issue(
951 report,
952 FsckIssueKind::EmptyReconstruction,
953 file_id_hex.clone(),
954 FsckIssueDetail::ReconstructionContainedNoTerms,
955 )?;
956 }
957
958 for term in reconstruction.terms() {
959 let object_id = term.object_id();
960 if !index_store
961 .contains_object(&object_id)
962 .await
963 .map_err(Into::into)?
964 {
965 push_issue(
966 report,
967 FsckIssueKind::MissingReconstructionXorb,
968 file_id_hex.clone(),
969 FsckIssueDetail::MissingReconstructionXorb {
970 xorb_hash: xet_hash_hex_string(object_id.hash()),
971 },
972 )?;
973 }
974 }
975 }
976
977 Ok(())
978}