1use std::collections::HashSet;
4use std::ops::ControlFlow;
5use std::sync::Arc;
6
7use crate::error::{Error, Result};
8use crate::objects::{HashAlgo, Object, ObjectId, ObjectInfo, ObjectKind};
9
10use super::{ObjectStore, ObjectStream};
11
12#[derive(Debug)]
21pub struct CompositeStore {
22 stores: Vec<Arc<dyn ObjectStore>>,
23 hash_algo: HashAlgo,
24}
25
26impl CompositeStore {
27 #[must_use]
31 pub fn new(stores: Vec<Arc<dyn ObjectStore>>, hash_algo: HashAlgo) -> Self {
32 Self { stores, hash_algo }
33 }
34
35 #[must_use]
37 pub fn stores(&self) -> &[Arc<dyn ObjectStore>] {
38 &self.stores
39 }
40
41 fn first_hit<'s, T>(
48 &'s self,
49 mut lookup: impl FnMut(&'s dyn ObjectStore) -> Result<Option<T>>,
50 ) -> Result<Option<T>> {
51 let mut damaged = None;
52 for store in &self.stores {
53 match lookup(store.as_ref()) {
54 Ok(Some(found)) => return Ok(Some(found)),
55 Ok(None) => {}
56 Err(err @ (Error::Zlib(_) | Error::CorruptObject(_))) => {
57 damaged.get_or_insert(err);
58 }
59 Err(err) => return Err(err),
60 }
61 }
62 damaged.map_or(Ok(None), Err)
63 }
64}
65
66impl ObjectStore for CompositeStore {
67 fn hash_algo(&self) -> HashAlgo {
68 self.stores
69 .first()
70 .map(|store| store.hash_algo())
71 .unwrap_or(self.hash_algo)
72 }
73
74 fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
75 self.first_hit(|store| store.read(oid))
76 }
77
78 fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
79 self.first_hit(|store| store.read_into(oid, buf))
80 }
81
82 fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
83 self.first_hit(|store| store.read_info(oid))
84 }
85
86 fn contains(&self, oid: &ObjectId) -> Result<bool> {
87 for store in &self.stores {
88 if store.contains(oid)? {
89 return Ok(true);
90 }
91 }
92 Ok(false)
93 }
94
95 fn contains_many(&self, oids: &[ObjectId]) -> Result<Vec<bool>> {
96 let mut present = vec![false; oids.len()];
97 let mut pending: Vec<usize> = (0..oids.len()).collect();
98 for store in &self.stores {
99 super::resolve_pending(oids, &mut present, &mut pending, |batch| {
100 store.contains_many(batch)
101 })?;
102 }
103 Ok(present)
104 }
105
106 fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'_>>> {
107 self.first_hit(|store| store.open_stream(oid))
108 }
109
110 fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
111 let mut seen = HashSet::new();
112 for store in &self.stores {
113 let stop = super::for_each_propagate_break(store.as_ref(), &mut |oid| {
114 if !seen.insert(*oid) {
115 return ControlFlow::Continue(());
116 }
117 f(oid)
118 })?;
119 if stop {
120 break;
121 }
122 }
123 Ok(())
124 }
125
126 fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
127 let mut seen: HashSet<ObjectId> = out.iter().copied().collect();
128 for store in &self.stores {
129 if limit != 0 && out.len() >= limit {
130 return Ok(());
131 }
132 let sub_limit = if limit == 0 {
133 0
134 } else {
135 limit.saturating_sub(out.len())
136 };
137 let mut scratch = Vec::new();
138 store.lookup_prefix(prefix, sub_limit, &mut scratch)?;
139 for oid in scratch {
140 if seen.insert(oid) {
141 out.push(oid);
142 if limit != 0 && out.len() >= limit {
143 return Ok(());
144 }
145 }
146 }
147 }
148 Ok(())
149 }
150
151 fn refresh(&self) -> Result<bool> {
152 let mut changed = false;
153 for store in &self.stores {
154 if store.refresh()? {
155 changed = true;
156 }
157 }
158 Ok(changed)
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::objects::ObjectKind;
166 use crate::odb::store::{MemoryStore, WritableObjectStore};
167 use crate::odb::WriteOptions;
168
169 #[test]
170 fn for_each_stops_after_break_across_layers() {
171 let store_a = MemoryStore::new(HashAlgo::Sha1);
172 let store_b = MemoryStore::new(HashAlgo::Sha1);
173 store_a
174 .write(ObjectKind::Blob, b"a", WriteOptions::default())
175 .unwrap();
176 store_b
177 .write(ObjectKind::Blob, b"b", WriteOptions::default())
178 .unwrap();
179 let composite = CompositeStore::new(
180 vec![
181 Arc::new(store_a) as Arc<dyn ObjectStore>,
182 Arc::new(store_b) as Arc<dyn ObjectStore>,
183 ],
184 HashAlgo::Sha1,
185 );
186 let mut calls = 0usize;
187 composite
188 .for_each_object(&mut |_| {
189 calls += 1;
190 ControlFlow::Break(())
191 })
192 .unwrap();
193 assert_eq!(calls, 1);
194 }
195
196 #[test]
197 fn first_hit_read_and_deduped_for_each() {
198 let store_a = MemoryStore::new(HashAlgo::Sha1);
199 let store_b = MemoryStore::new(HashAlgo::Sha1);
200 let oid_a = store_a
201 .write(ObjectKind::Blob, b"only-a", WriteOptions::default())
202 .unwrap();
203 let oid_b = store_b
204 .write(ObjectKind::Blob, b"only-b", WriteOptions::default())
205 .unwrap();
206 let a = Arc::new(store_a) as Arc<dyn ObjectStore>;
207 let b = Arc::new(store_b) as Arc<dyn ObjectStore>;
208 let composite = CompositeStore::new(vec![a, b], HashAlgo::Sha1);
209 assert_eq!(
210 composite.read(&oid_a).unwrap().expect("a").data,
211 b"only-a".as_slice()
212 );
213 assert_eq!(
214 composite.read(&oid_b).unwrap().expect("b").data,
215 b"only-b".as_slice()
216 );
217 let mut ids = Vec::new();
218 composite
219 .for_each_object(&mut |oid| {
220 ids.push(*oid);
221 ControlFlow::Continue(())
222 })
223 .unwrap();
224 ids.sort_by_key(|o| o.to_hex());
225 let mut expected = vec![oid_a, oid_b];
226 expected.sort_by_key(|o| o.to_hex());
227 assert_eq!(ids, expected);
228 }
229
230 #[derive(Debug)]
232 struct Rotten {
233 inner: MemoryStore,
234 make_err: fn() -> Error,
235 }
236
237 impl ObjectStore for Rotten {
238 fn hash_algo(&self) -> HashAlgo {
239 self.inner.hash_algo()
240 }
241
242 fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
243 match self.inner.read(oid)? {
244 Some(_) => Err((self.make_err)()),
245 None => Ok(None),
246 }
247 }
248
249 fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
250 buf.clear();
251 buf.extend_from_slice(b"garbage from a half-inflated copy");
252 self.read(oid).map(|obj| obj.map(|o| o.kind))
253 }
254
255 fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
256 self.read(oid).map(|obj| obj.map(|_| unreachable!()))
257 }
258
259 fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
260 self.inner.for_each_object(f)
261 }
262 }
263
264 fn rotten_with(data: &[u8], make_err: fn() -> Error) -> (Arc<dyn ObjectStore>, ObjectId) {
265 let inner = MemoryStore::new(HashAlgo::Sha1);
266 let oid = inner
267 .write(ObjectKind::Blob, data, WriteOptions::default())
268 .unwrap();
269 (Arc::new(Rotten { inner, make_err }), oid)
270 }
271
272 fn corrupt() -> Error {
273 Error::CorruptObject("checksum mismatch".to_owned())
274 }
275
276 fn zlib() -> Error {
277 Error::Zlib("invalid stored block lengths".to_owned())
278 }
279
280 fn assert_every_read_fails(
281 composite: &CompositeStore,
282 oid: &ObjectId,
283 is_expected: fn(&Error) -> bool,
284 ) {
285 let err = composite.read(oid).unwrap_err();
286 assert!(is_expected(&err), "read: {err:?}");
287 let err = composite.read_into(oid, &mut Vec::new()).unwrap_err();
288 assert!(is_expected(&err), "read_into: {err:?}");
289 let err = composite.read_info(oid).unwrap_err();
290 assert!(is_expected(&err), "read_info: {err:?}");
291 let Err(err) = composite.open_stream(oid) else {
292 panic!("open_stream returned Ok");
293 };
294 assert!(is_expected(&err), "open_stream: {err:?}");
295 }
296
297 #[test]
298 fn corrupt_copy_with_no_good_copy_is_reported_not_missing() {
299 let (rotten, oid) = rotten_with(b"precious data", corrupt);
300 let empty = Arc::new(MemoryStore::new(HashAlgo::Sha1)) as Arc<dyn ObjectStore>;
301 let composite = CompositeStore::new(vec![rotten, empty], HashAlgo::Sha1);
302 assert_every_read_fails(&composite, &oid, |e| matches!(e, Error::CorruptObject(_)));
303 }
304
305 #[test]
306 fn zlib_failure_with_no_good_copy_is_reported_not_missing() {
307 let empty = Arc::new(MemoryStore::new(HashAlgo::Sha1)) as Arc<dyn ObjectStore>;
308 let (rotten, oid) = rotten_with(b"precious data", zlib);
309 let composite = CompositeStore::new(vec![empty, rotten], HashAlgo::Sha1);
310 assert_every_read_fails(&composite, &oid, |e| matches!(e, Error::Zlib(_)));
311 }
312
313 #[test]
314 fn first_corruption_error_wins_when_every_copy_is_damaged() {
315 let (zlib_store, oid) = rotten_with(b"precious data", zlib);
316 let (corrupt_store, _) = rotten_with(b"precious data", corrupt);
317 let composite = CompositeStore::new(vec![zlib_store, corrupt_store], HashAlgo::Sha1);
318 assert_every_read_fails(&composite, &oid, |e| matches!(e, Error::Zlib(_)));
319 }
320
321 #[test]
322 fn good_copy_in_later_store_beats_corrupt_copy() {
323 for make_err in [corrupt as fn() -> Error, zlib] {
324 let (rotten, oid) = rotten_with(b"precious data", make_err);
325 let good = MemoryStore::new(HashAlgo::Sha1);
326 good.write(ObjectKind::Blob, b"precious data", WriteOptions::default())
327 .unwrap();
328 let composite = CompositeStore::new(
329 vec![rotten, Arc::new(good) as Arc<dyn ObjectStore>],
330 HashAlgo::Sha1,
331 );
332
333 assert_eq!(
334 composite.read(&oid).unwrap().expect("good copy").data,
335 b"precious data"
336 );
337 let mut buf = Vec::new();
338 assert_eq!(
339 composite.read_into(&oid, &mut buf).unwrap(),
340 Some(ObjectKind::Blob)
341 );
342 assert_eq!(buf, b"precious data");
343 let info = composite.read_info(&oid).unwrap().expect("good copy");
344 assert_eq!((info.kind, info.size), (ObjectKind::Blob, 13));
345 let mut stream = composite.open_stream(&oid).unwrap().expect("good copy");
346 let mut streamed = Vec::new();
347 std::io::Read::read_to_end(&mut stream.reader, &mut streamed).unwrap();
348 assert_eq!(streamed, b"precious data");
349 }
350 }
351
352 #[test]
353 fn other_errors_still_stop_the_lookup() {
354 let (rotten, oid) = rotten_with(b"precious data", || {
355 Error::Io(std::io::Error::other("backend down"))
356 });
357 let good = MemoryStore::new(HashAlgo::Sha1);
358 good.write(ObjectKind::Blob, b"precious data", WriteOptions::default())
359 .unwrap();
360 let composite = CompositeStore::new(
361 vec![rotten, Arc::new(good) as Arc<dyn ObjectStore>],
362 HashAlgo::Sha1,
363 );
364 assert!(matches!(composite.read(&oid), Err(Error::Io(_))));
365 }
366
367 #[test]
368 fn contains_delegates_without_reading_payload() {
369 let store_a = MemoryStore::new(HashAlgo::Sha1);
370 let oid = store_a
371 .write(ObjectKind::Blob, b"x", WriteOptions::default())
372 .unwrap();
373 let composite = CompositeStore::new(
374 vec![Arc::new(store_a) as Arc<dyn ObjectStore>],
375 HashAlgo::Sha1,
376 );
377 assert!(composite.contains(&oid).unwrap());
378 assert!(!composite
379 .contains(&ObjectId::from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap())
380 .unwrap());
381 }
382
383 #[derive(Debug)]
385 struct BatchLog {
386 inner: MemoryStore,
387 batches: std::sync::Mutex<Vec<Vec<ObjectId>>>,
388 }
389
390 impl ObjectStore for BatchLog {
391 fn hash_algo(&self) -> HashAlgo {
392 self.inner.hash_algo()
393 }
394 fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
395 self.inner.read(oid)
396 }
397 fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
398 self.inner.read_info(oid)
399 }
400 fn contains_many(&self, oids: &[ObjectId]) -> Result<Vec<bool>> {
401 self.batches
402 .lock()
403 .unwrap_or_else(|e| e.into_inner())
404 .push(oids.to_vec());
405 self.inner.contains_many(oids)
406 }
407 fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
408 self.inner.for_each_object(f)
409 }
410 }
411
412 #[test]
413 fn contains_many_asks_later_layers_only_about_misses() {
414 let first = MemoryStore::new(HashAlgo::Sha1);
415 let in_first = first
416 .write(ObjectKind::Blob, b"first", WriteOptions::default())
417 .unwrap();
418 let second = Arc::new(BatchLog {
419 inner: MemoryStore::new(HashAlgo::Sha1),
420 batches: std::sync::Mutex::new(Vec::new()),
421 });
422 let in_second = second
423 .inner
424 .write(ObjectKind::Blob, b"second", WriteOptions::default())
425 .unwrap();
426 let absent = ObjectId::from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap();
427 let composite = CompositeStore::new(
428 vec![
429 Arc::new(first) as Arc<dyn ObjectStore>,
430 second.clone() as Arc<dyn ObjectStore>,
431 ],
432 HashAlgo::Sha1,
433 );
434 assert_eq!(
435 composite
436 .contains_many(&[absent, in_first, in_second])
437 .unwrap(),
438 vec![false, true, true]
439 );
440 assert_eq!(
441 *second.batches.lock().unwrap(),
442 vec![vec![absent, in_second]],
443 "one batch for the second layer, without ids the first layer holds"
444 );
445
446 second.batches.lock().unwrap().clear();
447 assert_eq!(composite.contains_many(&[in_first]).unwrap(), vec![true]);
448 assert!(second.batches.lock().unwrap().is_empty());
449 }
450}