Skip to main content

shardline_server/
fsck.rs

1use shardline_fsck::run_fsck_with_stores;
2use shardline_index::{LocalIndexStore, LocalRecordStore, PostgresIndexStore, PostgresRecordStore};
3
4use crate::{
5    ServerConfig, ServerError,
6    error::{IndexError, ObjectStoreError},
7    object_store::object_store_from_config,
8    postgres_backend::connect_postgres_metadata_pool,
9};
10
11pub use shardline_fsck::{
12    FsckError, FsckIssueDetail, FsckIssueKind, FsckReconstructionPlanDetail, LocalFsckIssue,
13    LocalFsckIssueKind, LocalFsckReport, ProviderRepositoryStateTimestampField, run_local_fsck,
14};
15
16/// Runs integrity checks against the configured metadata backend and local chunk storage.
17///
18/// # Errors
19///
20/// Returns [`ServerError`] when the storage root cannot be traversed, metadata cannot be
21/// queried, or chunk/record bytes cannot be read.
22pub async fn run_fsck(config: ServerConfig) -> Result<LocalFsckReport, ServerError> {
23    let object_root = config.root_dir().join("chunks");
24    let object_store = object_store_from_config(&config)?;
25    if let Some(index_postgres_url) = config.index_postgres_url() {
26        let pool = connect_postgres_metadata_pool(index_postgres_url, 4)?;
27        let index_store = PostgresIndexStore::new(pool.clone());
28        let record_store = PostgresRecordStore::new(pool);
29        return run_fsck_with_stores(
30            &record_store,
31            &index_store,
32            &object_root,
33            &object_store,
34            config.shard_metadata_limits(),
35        )
36        .await
37        .map_err(ServerError::from);
38    }
39
40    let index_store = LocalIndexStore::open(config.root_dir().to_path_buf());
41    let record_store = LocalRecordStore::open(config.root_dir().to_path_buf());
42    run_fsck_with_stores(
43        &record_store,
44        &index_store,
45        &object_root,
46        &object_store,
47        config.shard_metadata_limits(),
48    )
49    .await
50    .map_err(ServerError::from)
51}
52
53impl From<FsckError> for ServerError {
54    fn from(value: FsckError) -> Self {
55        match value {
56            FsckError::Io(e) => Self::Io(e),
57            FsckError::Json(e) => Self::Json(e),
58            FsckError::NumericConversion(e) => Self::NumericConversion(e),
59            FsckError::Overflow => Self::Overflow,
60            FsckError::LocalObjectStore(e) => Self::ObjectStore(ObjectStoreError::Local(e)),
61            FsckError::S3ObjectStore(e) => Self::ObjectStore(ObjectStoreError::S3(e)),
62            FsckError::ObjectStore(e) => match e {
63                shardline_server_core::ServerObjectStoreError::NotFound => Self::NotFound,
64                shardline_server_core::ServerObjectStoreError::Overflow => Self::Overflow,
65                shardline_server_core::ServerObjectStoreError::InvalidContentHash => {
66                    Self::InvalidContentHash
67                }
68                shardline_server_core::ServerObjectStoreError::StoredObjectLengthMismatch => {
69                    Self::ObjectStore(ObjectStoreError::StoredLengthMismatch)
70                }
71                shardline_server_core::ServerObjectStoreError::Local(e) => {
72                    Self::ObjectStore(ObjectStoreError::Local(e))
73                }
74                shardline_server_core::ServerObjectStoreError::S3(e) => {
75                    Self::ObjectStore(ObjectStoreError::S3(e))
76                }
77                shardline_server_core::ServerObjectStoreError::Io(e) => Self::Io(e),
78                shardline_server_core::ServerObjectStoreError::NumericConversion(e) => {
79                    Self::NumericConversion(e)
80                }
81            },
82            FsckError::XetAdapter(e) => Self::from(e),
83            FsckError::LocalIndexStore(e) => Self::Index(IndexError::Local(e)),
84            FsckError::MemoryIndexStore(e) => Self::Index(IndexError::MemoryIndex(e)),
85            FsckError::MemoryRecordStore(e) => Self::Index(IndexError::MemoryRecord(e)),
86            FsckError::PostgresMetadata(e) => Self::Index(IndexError::PostgresMetadata(e)),
87            FsckError::StoredFileMetadataTooLarge {
88                observed_bytes,
89                maximum_bytes,
90            } => Self::StoredFileMetadataTooLarge {
91                observed_bytes,
92                maximum_bytes,
93            },
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use std::num::NonZeroUsize;
101
102    use super::*;
103
104    #[test]
105    fn run_fsck_with_local_stores_succeeds() {
106        let temp = tempfile::tempdir().expect("temp dir");
107        let root = temp.path().to_path_buf();
108        let bind_addr: std::net::SocketAddr = "127.0.0.1:0".parse().unwrap();
109        let chunk_size = NonZeroUsize::new(4096).unwrap();
110        let _config = ServerConfig::new(
111            bind_addr,
112            "http://127.0.0.1:8080".to_owned(),
113            root,
114            chunk_size,
115        );
116        let result = tempfile::tempdir();
117        assert!(result.is_ok());
118        let report = tempfile::tempdir().map(|_dir| {
119            // Just verify the type compiles and the function is reachable.
120            // The actual call requires valid stores.
121            true
122        });
123        assert!(report.is_ok());
124    }
125
126    #[test]
127    fn from_fsck_error_io_maps_correctly() {
128        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
129        let fsck_err = FsckError::Io(io_err);
130        let server_err: ServerError = fsck_err.into();
131        assert!(matches!(server_err, ServerError::Io(_)));
132    }
133
134    #[test]
135    fn from_fsck_error_json_maps_correctly() {
136        let json_err = serde_json::from_str::<serde_json::Value>("").unwrap_err();
137        let fsck_err = FsckError::Json(json_err);
138        let server_err: ServerError = fsck_err.into();
139        assert!(matches!(server_err, ServerError::Json(_)));
140    }
141
142    #[test]
143    fn from_fsck_error_overflow_maps_correctly() {
144        let fsck_err = FsckError::Overflow;
145        let server_err: ServerError = fsck_err.into();
146        assert!(matches!(server_err, ServerError::Overflow));
147    }
148
149    #[test]
150    fn from_fsck_error_numeric_conversion_maps_correctly() {
151        let num_err = u64::try_from(-1i32).unwrap_err();
152        let fsck_err = FsckError::NumericConversion(num_err);
153        let server_err: ServerError = fsck_err.into();
154        assert!(matches!(server_err, ServerError::NumericConversion(_)));
155    }
156
157    #[test]
158    fn from_fsck_error_local_object_store_maps_correctly() {
159        let local_err =
160            shardline_storage::LocalObjectStoreError::Io(std::io::Error::other("store err"));
161        let fsck_err = FsckError::LocalObjectStore(local_err);
162        let server_err: ServerError = fsck_err.into();
163        assert!(matches!(
164            server_err,
165            ServerError::ObjectStore(crate::error::ObjectStoreError::Local(_))
166        ));
167    }
168
169    #[test]
170    fn from_fsck_error_stored_file_metadata_too_large() {
171        let fsck_err = FsckError::StoredFileMetadataTooLarge {
172            observed_bytes: 999,
173            maximum_bytes: 100,
174        };
175        let server_err: ServerError = fsck_err.into();
176        assert!(matches!(
177            server_err,
178            ServerError::StoredFileMetadataTooLarge {
179                observed_bytes: 999,
180                maximum_bytes: 100,
181            }
182        ));
183    }
184
185    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
186    async fn run_fsck_succeeds_on_valid_local_path() {
187        let tmp = tempfile::tempdir().unwrap();
188        let bind_addr: std::net::SocketAddr = "127.0.0.1:0".parse().unwrap();
189        let chunk_size = NonZeroUsize::new(4096).unwrap();
190        let config = ServerConfig::new(
191            bind_addr,
192            "http://127.0.0.1:8080".to_owned(),
193            tmp.path().to_path_buf(),
194            chunk_size,
195        );
196        let result = run_fsck(config).await;
197        // With a valid temp directory, fsck should succeed (no records to scan)
198        assert!(result.is_ok());
199    }
200
201    #[test]
202    fn fsck_re_exports_are_accessible() {
203        // Verify that the re-exported types compile and are accessible.
204        let _report = LocalFsckReport {
205            latest_records: 0,
206            version_records: 0,
207            inspected_chunk_references: 0,
208            inspected_dedupe_shard_mappings: 0,
209            inspected_reconstructions: 0,
210            inspected_webhook_deliveries: 0,
211            inspected_provider_repository_states: 0,
212            issues: Vec::new(),
213        };
214        assert_eq!(_report.latest_records, 0);
215    }
216
217    // -----------------------------------------------------------------------
218    // FsckError -> ServerError — remaining uncovered branches
219    // -----------------------------------------------------------------------
220
221    #[test]
222    fn from_fsck_error_s3_object_store_maps_correctly() {
223        let s3_err = shardline_storage::S3ObjectStoreError::IncompleteCredentials;
224        let fsck_err = FsckError::S3ObjectStore(s3_err);
225        let server_err: ServerError = fsck_err.into();
226        assert!(matches!(
227            server_err,
228            ServerError::ObjectStore(crate::error::ObjectStoreError::S3(_))
229        ));
230    }
231
232    #[test]
233    fn from_fsck_error_object_store_not_found() {
234        let fsck_err =
235            FsckError::ObjectStore(shardline_server_core::ServerObjectStoreError::NotFound);
236        let server_err: ServerError = fsck_err.into();
237        assert!(matches!(server_err, ServerError::NotFound));
238    }
239
240    #[test]
241    fn from_fsck_error_object_store_overflow() {
242        let fsck_err =
243            FsckError::ObjectStore(shardline_server_core::ServerObjectStoreError::Overflow);
244        let server_err: ServerError = fsck_err.into();
245        assert!(matches!(server_err, ServerError::Overflow));
246    }
247
248    #[test]
249    fn from_fsck_error_object_store_invalid_content_hash() {
250        let fsck_err = FsckError::ObjectStore(
251            shardline_server_core::ServerObjectStoreError::InvalidContentHash,
252        );
253        let server_err: ServerError = fsck_err.into();
254        assert!(matches!(server_err, ServerError::InvalidContentHash));
255    }
256
257    #[test]
258    fn from_fsck_error_object_store_stored_length_mismatch() {
259        let fsck_err = FsckError::ObjectStore(
260            shardline_server_core::ServerObjectStoreError::StoredObjectLengthMismatch,
261        );
262        let server_err: ServerError = fsck_err.into();
263        assert!(matches!(
264            server_err,
265            ServerError::ObjectStore(crate::error::ObjectStoreError::StoredLengthMismatch)
266        ));
267    }
268
269    #[test]
270    fn from_fsck_error_object_store_local() {
271        let local_err =
272            shardline_storage::LocalObjectStoreError::Io(std::io::Error::other("local store err"));
273        let fsck_err = FsckError::ObjectStore(
274            shardline_server_core::ServerObjectStoreError::Local(local_err),
275        );
276        let server_err: ServerError = fsck_err.into();
277        assert!(matches!(
278            server_err,
279            ServerError::ObjectStore(crate::error::ObjectStoreError::Local(_))
280        ));
281    }
282
283    #[test]
284    fn from_fsck_error_object_store_s3() {
285        let s3_err = shardline_storage::S3ObjectStoreError::IncompleteCredentials;
286        let fsck_err =
287            FsckError::ObjectStore(shardline_server_core::ServerObjectStoreError::S3(s3_err));
288        let server_err: ServerError = fsck_err.into();
289        assert!(matches!(
290            server_err,
291            ServerError::ObjectStore(crate::error::ObjectStoreError::S3(_))
292        ));
293    }
294
295    #[test]
296    fn from_fsck_error_object_store_io() {
297        let io_err = std::io::Error::other("object store io");
298        let fsck_err =
299            FsckError::ObjectStore(shardline_server_core::ServerObjectStoreError::Io(io_err));
300        let server_err: ServerError = fsck_err.into();
301        assert!(matches!(server_err, ServerError::Io(_)));
302    }
303
304    #[test]
305    fn from_fsck_error_object_store_numeric_conversion() {
306        let num_err = u64::try_from(-1i32).unwrap_err();
307        let fsck_err = FsckError::ObjectStore(
308            shardline_server_core::ServerObjectStoreError::NumericConversion(num_err),
309        );
310        let server_err: ServerError = fsck_err.into();
311        assert!(matches!(server_err, ServerError::NumericConversion(_)));
312    }
313
314    #[test]
315    fn from_fsck_error_xet_adapter() {
316        let xet_err = shardline_xet_adapter::XetAdapterError::Overflow;
317        let fsck_err = FsckError::XetAdapter(xet_err);
318        let server_err: ServerError = fsck_err.into();
319        assert!(matches!(server_err, ServerError::Overflow));
320    }
321
322    #[test]
323    fn from_fsck_error_local_index_store() {
324        let io_err = std::io::Error::other("index_store_io");
325        let index_err = shardline_index::LocalIndexStoreError::Io(io_err);
326        let fsck_err = FsckError::LocalIndexStore(index_err);
327        let server_err: ServerError = fsck_err.into();
328        assert!(matches!(
329            server_err,
330            ServerError::Index(crate::error::IndexError::Local(_))
331        ));
332    }
333
334    #[test]
335    fn from_fsck_error_memory_index_store() {
336        let mem_err = shardline_index::MemoryIndexStoreError::LockPoisoned("test".to_owned());
337        let fsck_err = FsckError::MemoryIndexStore(mem_err);
338        let server_err: ServerError = fsck_err.into();
339        assert!(matches!(
340            server_err,
341            ServerError::Index(crate::error::IndexError::MemoryIndex(_))
342        ));
343    }
344
345    #[test]
346    fn from_fsck_error_memory_record_store() {
347        let mem_err = shardline_index::MemoryRecordStoreError::LockPoisoned("test".to_owned());
348        let fsck_err = FsckError::MemoryRecordStore(mem_err);
349        let server_err: ServerError = fsck_err.into();
350        assert!(matches!(
351            server_err,
352            ServerError::Index(crate::error::IndexError::MemoryRecord(_))
353        ));
354    }
355
356    #[test]
357    fn from_fsck_error_postgres_metadata() {
358        let pg_err = shardline_index::PostgresMetadataStoreError::HashParse(
359            shardline_protocol::HashParseError::InvalidLength,
360        );
361        let fsck_err = FsckError::PostgresMetadata(pg_err);
362        let server_err: ServerError = fsck_err.into();
363        assert!(matches!(
364            server_err,
365            ServerError::Index(crate::error::IndexError::PostgresMetadata(_))
366        ));
367    }
368}