Read a packed object from its pack before asking for a loose copy 092457020692
Store::get and get_many asked the backend for an object's loose key before looking in the packs. On a directory that is a cheap miss; on a bucket it is a request that finds nothing, and misses are never cached, so every read of a packed object cost one. The hub's new per-kind counts showed it: metalgear2 (in GCS) made ~470 uncached gets a minute while its cache never grew. The packs' indexes are in memory, so they are asked first now; loose keys only for objects no pack holds. Test: a counting backend sees no loose lookups for packed objects. Refs andymccall/oxygenhub#228
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
Claude Code <[email protected]> · delegate for changes, until 2026-11-13 19:00Z · key ea5f78160088
- When
- · 2026-10-17 02:42 UTC
- Signature
- ✔ signed by Claude Code
- Hash
092457020692260d9069e5011ea78a697c0caf07dec09f96a5d88484283e1ce8- Depends on
- 648189388f1a 75167672ebcd 8f10a07a1dbe a9f686edf60b afaba5f1306d ff0af1ee299d
- Depended on by
- 795b8b502ded 767ac6cd0545
- In bookmarks
- carried-not-removed doctor-trust identical-sides keyless-tags main merge-remote-bookmark paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state
- In tags
- v0.10.0 v0.11.0 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 12 atoms, 0 tree operations, 1 file
Browse the tree at this change
modified crates/oxy-core/src/store.rs +100 −30
| … | 1269 unchanged lines |
||
| 1270 | 1270 | self.backend.put_many_new(&items).map_err(backend_err) |
|
| 1271 | 1271 | } |
|
| 1272 | 1272 | ||
| 1273 | - | /// Read and verify an object, loose, packed, or pending in a batch. |
|
| 1273 | + | /// Read and verify an object, packed, loose, or pending in a batch. |
|
| 1274 | + | /// The packs' indexes are in memory, so they are asked first: asking |
|
| 1275 | + | /// for a loose copy of a packed object costs a request that finds |
|
| 1276 | + | /// nothing on a store in a bucket, every time (oxygenhub#228). |
|
| 1274 | 1277 | pub fn get(&self, r: &ObjectRef) -> Result<Object> { |
|
| 1278 | + | if let Some((i, e)) = self.find_packed(r) { |
|
| 1279 | + | return self.packs.borrow()[i].fetch(self.backend.as_ref(), r, e); |
|
| 1280 | + | } |
|
| 1275 | 1281 | match get_optional(self.backend.as_ref(), &loose_key(r))? { |
|
| 1276 | 1282 | Some(bytes) => parse_loose(&bytes, r), |
|
| 1277 | - | None => match self.find_packed(r) { |
|
| 1278 | - | Some((i, e)) => self.packs.borrow()[i].fetch(self.backend.as_ref(), r, e), |
|
| 1279 | - | None => self.pending_get(r).unwrap_or(Err(StoreError::NotFound(*r))), |
|
| 1280 | - | }, |
|
| 1283 | + | None => self.pending_get(r).unwrap_or(Err(StoreError::NotFound(*r))), |
|
| 1281 | 1284 | } |
|
| 1282 | 1285 | } |
|
| 1283 | 1286 | ||
| 1284 | - | /// Many objects at once: loose ones are read and verified in parallel, |
|
| 1285 | - | /// then packed ones in parallel, then what is pending in a batch. |
|
| 1286 | - | /// Results are in the order of `refs`. |
|
| 1287 | + | /// Many objects at once, read and verified in parallel: packed ones |
|
| 1288 | + | /// from their packs, the others loose (as [`Store::get`], packs first), |
|
| 1289 | + | /// then what is pending in a batch. Results are in the order of `refs`. |
|
| 1287 | 1290 | pub fn get_many(&self, refs: &[ObjectRef]) -> Vec<Result<Object>> { |
|
| 1288 | 1291 | use rayon::prelude::*; |
|
| 1289 | 1292 | let backend = self.backend.as_ref(); |
|
| 1290 | - | let mut found: Vec<Option<Result<Object>>> = refs |
|
| 1291 | - | .par_iter() |
|
| 1292 | - | .map(|r| match get_optional(backend, &loose_key(r)) { |
|
| 1293 | - | Ok(Some(bytes)) => Some(parse_loose(&bytes, r)), |
|
| 1294 | - | Ok(None) => None, |
|
| 1295 | - | Err(e) => Some(Err(e)), |
|
| 1296 | - | }) |
|
| 1297 | - | .collect(); |
|
| 1298 | - | // Packed ones: look them up in order, then read them in parallel. |
|
| 1299 | 1293 | let packs = self.packs.borrow(); |
|
| 1300 | - | let packed: Vec<(usize, usize, PackEntry)> = found |
|
| 1294 | + | let place: Vec<Option<(usize, PackEntry)>> = refs |
|
| 1301 | 1295 | .iter() |
|
| 1302 | - | .enumerate() |
|
| 1303 | - | .filter(|(_, f)| f.is_none()) |
|
| 1304 | - | .filter_map(|(i, _)| { |
|
| 1296 | + | .map(|r| { |
|
| 1305 | 1297 | packs |
|
| 1306 | 1298 | .iter() |
|
| 1307 | 1299 | .enumerate() |
|
| 1308 | - | .find_map(|(p, pack)| pack.find(&refs[i]).map(|e| (i, p, e))) |
|
| 1300 | + | .find_map(|(p, pack)| pack.find(r).map(|e| (p, e))) |
|
| 1309 | 1301 | }) |
|
| 1310 | 1302 | .collect(); |
|
| 1311 | 1303 | let indexes: &[PackIndex] = &packs; |
|
| 1312 | - | let read: Vec<(usize, Result<Object>)> = packed |
|
| 1304 | + | let found: Vec<Option<Result<Object>>> = refs |
|
| 1313 | 1305 | .par_iter() |
|
| 1314 | - | .map(|(i, p, e)| (*i, indexes[*p].fetch(backend, &refs[*i], *e))) |
|
| 1306 | + | .zip(place.par_iter()) |
|
| 1307 | + | .map(|(r, at)| match at { |
|
| 1308 | + | Some((p, e)) => Some(indexes[*p].fetch(backend, r, *e)), |
|
| 1309 | + | None => match get_optional(backend, &loose_key(r)) { |
|
| 1310 | + | Ok(Some(bytes)) => Some(parse_loose(&bytes, r)), |
|
| 1311 | + | Ok(None) => None, |
|
| 1312 | + | Err(e) => Some(Err(e)), |
|
| 1313 | + | }, |
|
| 1314 | + | }) |
|
| 1315 | 1315 | .collect(); |
|
| 1316 | 1316 | drop(packs); |
|
| 1317 | - | for (i, r) in read { |
|
| 1318 | - | found[i] = Some(r); |
|
| 1319 | - | } |
|
| 1320 | 1317 | found |
|
| 1321 | 1318 | .into_iter() |
|
| 1322 | 1319 | .zip(refs) |
|
| 1323 | - | .map(|(f, r)| match f { |
|
| 1324 | - | Some(f) => f, |
|
| 1325 | - | None => self.get(r), |
|
| 1320 | + | .map(|(f, r)| { |
|
| 1321 | + | f.unwrap_or_else(|| self.pending_get(r).unwrap_or(Err(StoreError::NotFound(*r)))) |
|
| 1326 | 1322 | }) |
|
| 1327 | 1323 | .collect() |
|
| 1328 | 1324 | } |
|
| … | 1000 unchanged lines |
||
| 2329 | 2325 | } |
|
| 2330 | 2326 | assert!(matches!(got[40], Err(StoreError::NotFound(_)))); |
|
| 2331 | 2327 | }); |
|
| 2328 | + | } |
|
| 2329 | + | ||
| 2330 | + | /// A packed object is read from its pack without first asking the |
|
| 2331 | + | /// backend for a loose copy (oxygenhub#228: on a bucket each such ask |
|
| 2332 | + | /// was a request that found nothing, every time). |
|
| 2333 | + | #[test] |
|
| 2334 | + | fn packed_objects_cost_no_loose_lookups() { |
|
| 2335 | + | use std::sync::atomic::{AtomicUsize, Ordering}; |
|
| 2336 | + | struct Counting(MemoryStore, AtomicUsize); |
|
| 2337 | + | impl crate::strata::Store for Counting { |
|
| 2338 | + | fn describe(&self) -> String { |
|
| 2339 | + | "counting".into() |
|
| 2340 | + | } |
|
| 2341 | + | fn has(&self, k: &str) -> crate::strata::Result<bool> { |
|
| 2342 | + | self.0.has(k) |
|
| 2343 | + | } |
|
| 2344 | + | fn get(&self, k: &str) -> crate::strata::Result<Vec<u8>> { |
|
| 2345 | + | if k.starts_with("loose/") { |
|
| 2346 | + | self.1.fetch_add(1, Ordering::Relaxed); |
|
| 2347 | + | } |
|
| 2348 | + | self.0.get(k) |
|
| 2349 | + | } |
|
| 2350 | + | fn get_range( |
|
| 2351 | + | &self, |
|
| 2352 | + | k: &str, |
|
| 2353 | + | r: std::ops::Range<u64>, |
|
| 2354 | + | ) -> crate::strata::Result<Vec<u8>> { |
|
| 2355 | + | self.0.get_range(k, r) |
|
| 2356 | + | } |
|
| 2357 | + | fn put_new(&self, k: &str, b: &[u8]) -> crate::strata::Result<()> { |
|
| 2358 | + | self.0.put_new(k, b) |
|
| 2359 | + | } |
|
| 2360 | + | fn list(&self, p: &str) -> crate::strata::Result<Vec<crate::strata::Entry>> { |
|
| 2361 | + | self.0.list(p) |
|
| 2362 | + | } |
|
| 2363 | + | fn delete(&self, k: &str) -> crate::strata::Result<()> { |
|
| 2364 | + | self.0.delete(k) |
|
| 2365 | + | } |
|
| 2366 | + | fn read_ref( |
|
| 2367 | + | &self, |
|
| 2368 | + | k: &str, |
|
| 2369 | + | ) -> crate::strata::Result<Option<(Vec<u8>, crate::strata::Version)>> { |
|
| 2370 | + | self.0.read_ref(k) |
|
| 2371 | + | } |
|
| 2372 | + | fn write_ref( |
|
| 2373 | + | &self, |
|
| 2374 | + | k: &str, |
|
| 2375 | + | b: &[u8], |
|
| 2376 | + | e: Option<&crate::strata::Version>, |
|
| 2377 | + | ) -> crate::strata::Result<crate::strata::Version> { |
|
| 2378 | + | self.0.write_ref(k, b, e) |
|
| 2379 | + | } |
|
| 2380 | + | } |
|
| 2381 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 2382 | + | let backend = Arc::new(Counting(MemoryStore::new(), AtomicUsize::new(0))); |
|
| 2383 | + | let s = Store::open_on(backend.clone(), dir.path().join("scratch")).unwrap(); |
|
| 2384 | + | let refs: Vec<ObjectRef> = (0..10) |
|
| 2385 | + | .map(|i| { |
|
| 2386 | + | s.put(ObjectType::Chunk, format!("p {i}").as_bytes()) |
|
| 2387 | + | .unwrap() |
|
| 2388 | + | }) |
|
| 2389 | + | .collect(); |
|
| 2390 | + | s.pack_loose().unwrap(); |
|
| 2391 | + | let s = Store::open_on(backend.clone(), dir.path().join("scratch")).unwrap(); |
|
| 2392 | + | backend.1.store(0, Ordering::Relaxed); |
|
| 2393 | + | for r in &refs { |
|
| 2394 | + | assert!(s.get(r).is_ok()); |
|
| 2395 | + | } |
|
| 2396 | + | assert!(s.get_many(&refs).iter().all(|g| g.is_ok())); |
|
| 2397 | + | assert_eq!(backend.1.load(Ordering::Relaxed), 0, "no loose lookups"); |
|
| 2398 | + | // An object no pack holds is still looked for loose. |
|
| 2399 | + | let missing = hash_object(ObjectType::Chunk, b"never stored"); |
|
| 2400 | + | assert!(matches!(s.get(&missing), Err(StoreError::NotFound(_)))); |
|
| 2401 | + | assert!(backend.1.load(Ordering::Relaxed) > 0); |
|
| 2332 | 2402 | } |
|
| 2333 | 2403 | } |
|
| 2334 | 2404 | ||
| … | 149 unchanged lines |