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
16pub 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 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 assert!(result.is_ok());
199 }
200
201 #[test]
202 fn fsck_re_exports_are_accessible() {
203 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 #[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}