OXYGENHUB

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