diff --git a/arborist/cold_clone.py b/arborist/cold_clone.py index 7aa018f..081b243 100644 --- a/arborist/cold_clone.py +++ b/arborist/cold_clone.py @@ -414,26 +414,31 @@ def hydrate_doc_jit( if not rows: return 0 - def _fetch(pair: tuple[int, str]) -> tuple[int, bytes]: + def _fetch(pair: tuple[int, str]): chunk_id, leaf_hash = pair key = f"{BLOB_PREFIX}/{leaf_hash[:2]}/{leaf_hash[2:]}" - body = backend.get(key) + try: + body = backend.get(key) + except Exception: + # Blob missing or unreadable — skip this chunk. The query + # path will see content still NULL and behave as it would + # for a cold shard (graceful degradation, not a crash). + return None if hash_leaf(body).hex() != leaf_hash: - raise ValueError( - f"chunk {chunk_id}: bucket blob hash mismatch (leaf_hash {leaf_hash[:12]})" - ) + return None return chunk_id, body pairs = [(r[0], r[1]) for r in rows] n_workers = max(1, min(fetch_workers, len(pairs))) with ThreadPoolExecutor(max_workers=n_workers) as ex: - fetched = list(ex.map(_fetch, pairs)) - with conn: - conn.executemany( - "UPDATE chunks SET content = ? WHERE chunk_id = ?", - [(body, cid) for cid, body in fetched], - ) - return len(fetched) + results = [r for r in ex.map(_fetch, pairs) if r is not None] + if results: + with conn: + conn.executemany( + "UPDATE chunks SET content = ? WHERE chunk_id = ?", + [(body, cid) for cid, body in results], + ) + return len(results) def fetch_chunk_jit(