format: the chunk cache keeps the index's chunk order
ChunkCache::chunks_for returned the chunk index's values in hash-map order, which differs between File instances (a new HashMap per open). Readers that stop at the first failing chunk therefore named different chunks on different opens of the same damaged file: whole-dataset selections here, and the indexed reader behind the storage harness's intermittent cve-2025-2310.h5 failure. The cache now also keeps the chunks in the order the index lists them (fetched or built under one lock) and returns that order, as the uncached readers use. Regression: several_damaged_chunks_report_the_same_chunk_every_time (four chunks inflating to different short lengths; before the fix two opens reported chunk [40, 0] and chunk [320, 0]). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -262,6 +262,11 @@ struct CachedChunk {
|
||||
struct DatasetEntry {
|
||||
/// Chunk coordinate -> ChunkInfo (offset + size in file).
|
||||
index: Option<Arc<HashMap<ChunkCoord, ChunkInfo>>>,
|
||||
/// The same chunks in the order the chunk index lists them: what
|
||||
/// [`ChunkCache::chunks_for`] returns, so a cached read walks (and, on a
|
||||
/// damaged file, fails at) the chunks in the same order as an uncached
|
||||
/// one, rather than in hash-map order.
|
||||
ordered: Option<Arc<Vec<ChunkInfo>>>,
|
||||
/// Pre-built chunk index for O(1) coordinate lookups.
|
||||
chunk_index: Option<Arc<ChunkIndex>>,
|
||||
/// Pre-computed chunk layout for fast assembly.
|
||||
@@ -274,6 +279,7 @@ struct DatasetEntry {
|
||||
impl DatasetEntry {
|
||||
fn weight(&self) -> usize {
|
||||
self.index.as_ref().map_or(0, |m| m.len())
|
||||
+ self.ordered.as_ref().map_or(0, |o| o.len())
|
||||
+ self.chunk_index.as_ref().map_or(0, |c| c.num_chunks())
|
||||
}
|
||||
}
|
||||
@@ -562,11 +568,22 @@ impl ChunkCache {
|
||||
rank: usize,
|
||||
build: impl FnOnce() -> Result<Vec<ChunkInfo>, E>,
|
||||
) -> Result<Vec<ChunkInfo>, E> {
|
||||
Ok(self
|
||||
.index_for(addr, rank, build)?
|
||||
.values()
|
||||
.cloned()
|
||||
.collect())
|
||||
if let Some(ordered) = self.lock().touch(addr).ordered.clone() {
|
||||
return Ok(ordered.as_ref().clone());
|
||||
}
|
||||
let chunks = build()?;
|
||||
let map: HashMap<ChunkCoord, ChunkInfo> = chunks
|
||||
.iter()
|
||||
.map(|ci| (ci.offsets.iter().take(rank).copied().collect(), ci.clone()))
|
||||
.collect();
|
||||
let mut inner = self.lock();
|
||||
let entry = inner.touch(addr);
|
||||
// Another thread may have built this dataset's index meanwhile: keep
|
||||
// the first one, so every reader sees the same order.
|
||||
let ordered = Arc::clone(entry.ordered.get_or_insert_with(|| Arc::new(chunks)));
|
||||
entry.index.get_or_insert_with(|| Arc::new(map));
|
||||
inner.trim_datasets(addr);
|
||||
Ok(ordered.as_ref().clone())
|
||||
}
|
||||
|
||||
fn index_for<E>(
|
||||
@@ -580,11 +597,14 @@ impl ChunkCache {
|
||||
}
|
||||
let chunks = build()?;
|
||||
let map: HashMap<ChunkCoord, ChunkInfo> = chunks
|
||||
.into_iter()
|
||||
.map(|ci| (ci.offsets.iter().take(rank).copied().collect(), ci))
|
||||
.iter()
|
||||
.map(|ci| (ci.offsets.iter().take(rank).copied().collect(), ci.clone()))
|
||||
.collect();
|
||||
let mut inner = self.lock();
|
||||
let entry = inner.touch(addr);
|
||||
if entry.index.is_none() {
|
||||
entry.ordered = Some(Arc::new(chunks));
|
||||
}
|
||||
let index = Arc::clone(entry.index.get_or_insert_with(|| Arc::new(map)));
|
||||
inner.trim_datasets(addr);
|
||||
Ok(index)
|
||||
|
||||
Reference in New Issue
Block a user