1use std::collections::HashSet;
4use std::ops::ControlFlow;
5use std::sync::Arc;
6
7use crate::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) if err.is_damaged_object() => {
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::error::Error;
166 use crate::objects::ObjectKind;
167 use crate::odb::store::{MemoryStore, WritableObjectStore};
168 use crate::odb::WriteOptions;
169
170 #[test]
171 fn for_each_stops_after_break_across_layers() {
172 let store_a = MemoryStore::new(HashAlgo::Sha1);
173 let store_b = MemoryStore::new(HashAlgo::Sha1);
174 store_a
175 .write(ObjectKind::Blob, b"a", WriteOptions::default())
176 .unwrap();
177 store_b
178 .write(ObjectKind::Blob, b"b", WriteOptions::default())
179 .unwrap();
180 let composite = CompositeStore::new(
181 vec![
182 Arc::new(store_a) as Arc<dyn ObjectStore>,
183 Arc::new(store_b) as Arc<dyn ObjectStore>,
184 ],
185 HashAlgo::Sha1,
186 );
187 let mut calls = 0usize;
188 composite
189 .for_each_object(&mut |_| {
190 calls += 1;
191 ControlFlow::Break(())
192 })
193 .unwrap();
194 assert_eq!(calls, 1);
195 }
196
197 #[test]
198 fn first_hit_read_and_deduped_for_each() {
199 let store_a = MemoryStore::new(HashAlgo::Sha1);
200 let store_b = MemoryStore::new(HashAlgo::Sha1);
201 let oid_a = store_a
202 .write(ObjectKind::Blob, b"only-a", WriteOptions::default())
203 .unwrap();
204 let oid_b = store_b
205 .write(ObjectKind::Blob, b"only-b", WriteOptions::default())
206 .unwrap();
207 let a = Arc::new(store_a) as Arc<dyn ObjectStore>;
208 let b = Arc::new(store_b) as Arc<dyn ObjectStore>;
209 let composite = CompositeStore::new(vec![a, b], HashAlgo::Sha1);
210 assert_eq!(
211 composite.read(&oid_a).unwrap().expect("a").data,
212 b"only-a".as_slice()
213 );
214 assert_eq!(
215 composite.read(&oid_b).unwrap().expect("b").data,
216 b"only-b".as_slice()
217 );
218 let mut ids = Vec::new();
219 composite
220 .for_each_object(&mut |oid| {
221 ids.push(*oid);
222 ControlFlow::Continue(())
223 })
224 .unwrap();
225 ids.sort_by_key(|o| o.to_hex());
226 let mut expected = vec![oid_a, oid_b];
227 expected.sort_by_key(|o| o.to_hex());
228 assert_eq!(ids, expected);
229 }
230
231 #[derive(Debug)]
233 struct Rotten {
234 inner: MemoryStore,
235 make_err: fn() -> Error,
236 }
237
238 impl ObjectStore for Rotten {
239 fn hash_algo(&self) -> HashAlgo {
240 self.inner.hash_algo()
241 }
242
243 fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
244 match self.inner.read(oid)? {
245 Some(_) => Err((self.make_err)()),
246 None => Ok(None),
247 }
248 }
249
250 fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
251 buf.clear();
252 buf.extend_from_slice(b"garbage from a half-inflated copy");
253 self.read(oid).map(|obj| obj.map(|o| o.kind))
254 }
255
256 fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
257 self.read(oid).map(|obj| obj.map(|_| unreachable!()))
258 }
259
260 fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
261 self.inner.for_each_object(f)
262 }
263 }
264
265 fn rotten_with(data: &[u8], make_err: fn() -> Error) -> (Arc<dyn ObjectStore>, ObjectId) {
266 let inner = MemoryStore::new(HashAlgo::Sha1);
267 let oid = inner
268 .write(ObjectKind::Blob, data, WriteOptions::default())
269 .unwrap();
270 (Arc::new(Rotten { inner, make_err }), oid)
271 }
272
273 fn corrupt() -> Error {
274 Error::CorruptObject("checksum mismatch".to_owned())
275 }
276
277 fn zlib() -> Error {
278 Error::Zlib("invalid stored block lengths".to_owned())
279 }
280
281 fn assert_every_read_fails(
282 composite: &CompositeStore,
283 oid: &ObjectId,
284 is_expected: fn(&Error) -> bool,
285 ) {
286 let err = composite.read(oid).unwrap_err();
287 assert!(is_expected(&err), "read: {err:?}");
288 let err = composite.read_into(oid, &mut Vec::new()).unwrap_err();
289 assert!(is_expected(&err), "read_into: {err:?}");
290 let err = composite.read_info(oid).unwrap_err();
291 assert!(is_expected(&err), "read_info: {err:?}");
292 let Err(err) = composite.open_stream(oid) else {
293 panic!("open_stream returned Ok");
294 };
295 assert!(is_expected(&err), "open_stream: {err:?}");
296 }
297
298 #[test]
299 fn corrupt_copy_with_no_good_copy_is_reported_not_missing() {
300 let (rotten, oid) = rotten_with(b"precious data", corrupt);
301 let empty = Arc::new(MemoryStore::new(HashAlgo::Sha1)) as Arc<dyn ObjectStore>;
302 let composite = CompositeStore::new(vec![rotten, empty], HashAlgo::Sha1);
303 assert_every_read_fails(&composite, &oid, |e| matches!(e, Error::CorruptObject(_)));
304 }
305
306 #[test]
307 fn zlib_failure_with_no_good_copy_is_reported_not_missing() {
308 let empty = Arc::new(MemoryStore::new(HashAlgo::Sha1)) as Arc<dyn ObjectStore>;
309 let (rotten, oid) = rotten_with(b"precious data", zlib);
310 let composite = CompositeStore::new(vec![empty, rotten], HashAlgo::Sha1);
311 assert_every_read_fails(&composite, &oid, |e| matches!(e, Error::Zlib(_)));
312 }
313
314 #[test]
315 fn first_corruption_error_wins_when_every_copy_is_damaged() {
316 let (zlib_store, oid) = rotten_with(b"precious data", zlib);
317 let (corrupt_store, _) = rotten_with(b"precious data", corrupt);
318 let composite = CompositeStore::new(vec![zlib_store, corrupt_store], HashAlgo::Sha1);
319 assert_every_read_fails(&composite, &oid, |e| matches!(e, Error::Zlib(_)));
320 }
321
322 #[test]
323 fn good_copy_in_later_store_beats_corrupt_copy() {
324 for make_err in [corrupt as fn() -> Error, zlib] {
325 let (rotten, oid) = rotten_with(b"precious data", make_err);
326 let good = MemoryStore::new(HashAlgo::Sha1);
327 good.write(ObjectKind::Blob, b"precious data", WriteOptions::default())
328 .unwrap();
329 let composite = CompositeStore::new(
330 vec![rotten, Arc::new(good) as Arc<dyn ObjectStore>],
331 HashAlgo::Sha1,
332 );
333
334 assert_eq!(
335 composite.read(&oid).unwrap().expect("good copy").data,
336 b"precious data"
337 );
338 let mut buf = Vec::new();
339 assert_eq!(
340 composite.read_into(&oid, &mut buf).unwrap(),
341 Some(ObjectKind::Blob)
342 );
343 assert_eq!(buf, b"precious data");
344 let info = composite.read_info(&oid).unwrap().expect("good copy");
345 assert_eq!((info.kind, info.size), (ObjectKind::Blob, 13));
346 let mut stream = composite.open_stream(&oid).unwrap().expect("good copy");
347 let mut streamed = Vec::new();
348 std::io::Read::read_to_end(&mut stream.reader, &mut streamed).unwrap();
349 assert_eq!(streamed, b"precious data");
350 }
351 }
352
353 #[test]
354 fn other_errors_still_stop_the_lookup() {
355 let (rotten, oid) = rotten_with(b"precious data", || {
356 Error::Io(std::io::Error::other("backend down"))
357 });
358 let good = MemoryStore::new(HashAlgo::Sha1);
359 good.write(ObjectKind::Blob, b"precious data", WriteOptions::default())
360 .unwrap();
361 let composite = CompositeStore::new(
362 vec![rotten, Arc::new(good) as Arc<dyn ObjectStore>],
363 HashAlgo::Sha1,
364 );
365 assert!(matches!(composite.read(&oid), Err(Error::Io(_))));
366 }
367
368 #[test]
369 fn contains_delegates_without_reading_payload() {
370 let store_a = MemoryStore::new(HashAlgo::Sha1);
371 let oid = store_a
372 .write(ObjectKind::Blob, b"x", WriteOptions::default())
373 .unwrap();
374 let composite = CompositeStore::new(
375 vec![Arc::new(store_a) as Arc<dyn ObjectStore>],
376 HashAlgo::Sha1,
377 );
378 assert!(composite.contains(&oid).unwrap());
379 assert!(!composite
380 .contains(&ObjectId::from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap())
381 .unwrap());
382 }
383
384 #[derive(Debug)]
386 struct BatchLog {
387 inner: MemoryStore,
388 batches: std::sync::Mutex<Vec<Vec<ObjectId>>>,
389 }
390
391 impl ObjectStore for BatchLog {
392 fn hash_algo(&self) -> HashAlgo {
393 self.inner.hash_algo()
394 }
395 fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
396 self.inner.read(oid)
397 }
398 fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
399 self.inner.read_info(oid)
400 }
401 fn contains_many(&self, oids: &[ObjectId]) -> Result<Vec<bool>> {
402 self.batches
403 .lock()
404 .unwrap_or_else(|e| e.into_inner())
405 .push(oids.to_vec());
406 self.inner.contains_many(oids)
407 }
408 fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
409 self.inner.for_each_object(f)
410 }
411 }
412
413 #[test]
414 fn contains_many_asks_later_layers_only_about_misses() {
415 let first = MemoryStore::new(HashAlgo::Sha1);
416 let in_first = first
417 .write(ObjectKind::Blob, b"first", WriteOptions::default())
418 .unwrap();
419 let second = Arc::new(BatchLog {
420 inner: MemoryStore::new(HashAlgo::Sha1),
421 batches: std::sync::Mutex::new(Vec::new()),
422 });
423 let in_second = second
424 .inner
425 .write(ObjectKind::Blob, b"second", WriteOptions::default())
426 .unwrap();
427 let absent = ObjectId::from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap();
428 let composite = CompositeStore::new(
429 vec![
430 Arc::new(first) as Arc<dyn ObjectStore>,
431 second.clone() as Arc<dyn ObjectStore>,
432 ],
433 HashAlgo::Sha1,
434 );
435 assert_eq!(
436 composite
437 .contains_many(&[absent, in_first, in_second])
438 .unwrap(),
439 vec![false, true, true]
440 );
441 assert_eq!(
442 *second.batches.lock().unwrap(),
443 vec![vec![absent, in_second]],
444 "one batch for the second layer, without ids the first layer holds"
445 );
446
447 second.batches.lock().unwrap().clear();
448 assert_eq!(composite.contains_many(&[in_first]).unwrap(), vec![true]);
449 assert!(second.batches.lock().unwrap().is_empty());
450 }
451}