edit: delete a heap's huge-object B-tree with its last huge object
libhdf5 deletes a fractal heap's huge-object B-tree when the heap is
closed with no huge object left (H5HF__huge_term) and starts huge IDs
over. The editor left the empty tree, and a read-only libhdf5 then
failed to list the object's attributes: closing the heap tried to delete
the tree ("no write intent on file"), and h5dump failed the same way.
Replacing an object's last huge attribute (one above the heap's 4 KiB
managed limit) now deletes the tree (header and nodes freed), resets the
next huge ID and the wrapped flag, as libhdf5 does; a later huge
attribute creates a new tree. Bt2::delete frees a whole tree.
Found by the extended random-operation test; regression:
last_huge_attribute_replaced (fails before: h5dump, h5py listing), which
also compares the heap with libhdf5's after the same replacement.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -1396,3 +1396,57 @@ fn freed_space_is_reused_within_a_session() {
|
|||||||
);
|
);
|
||||||
assert!(fresh.1 > fresh.0, "without reuse the file grows");
|
assert!(fresh.1 > fresh.0, "without reuse the file grows");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replacing the last huge attribute (larger than the heap's managed
|
||||||
|
/// limit) of an object with a small one deletes the heap's huge-object
|
||||||
|
/// B-tree, as libhdf5 does when it closes the heap (`H5HF__huge_term`). A
|
||||||
|
/// heap left with an empty huge-object B-tree made read-only libhdf5 fail
|
||||||
|
/// to list the attributes ("no write intent on file").
|
||||||
|
#[test]
|
||||||
|
fn last_huge_attribute_replaced() {
|
||||||
|
if !tools_ok() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dir = tmpdir();
|
||||||
|
let a = dir.path().join("huge_h5py.h5");
|
||||||
|
let b = dir.path().join("huge_edit.h5");
|
||||||
|
for p in [&a, &b] {
|
||||||
|
py(&format!(
|
||||||
|
"import h5py, numpy as np\n\
|
||||||
|
with h5py.File({p:?}, 'w', libver='v110') as f:\n\
|
||||||
|
\x20 g = f.create_group('g')\n\
|
||||||
|
\x20 for i in range(10): g.attrs.create(f'k{{i}}', np.array([i], dtype='<i8'))\n\
|
||||||
|
\x20 g.attrs.create('big', np.bytes_('h' * 6000))\n",
|
||||||
|
p = p.to_str().unwrap()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
py(&format!(
|
||||||
|
"import h5py, numpy as np\n\
|
||||||
|
with h5py.File({p:?}, 'r+') as f:\n\
|
||||||
|
\x20 f['g'].attrs.create('big', np.array([1, 2], dtype='<i8'))\n",
|
||||||
|
p = a.to_str().unwrap()
|
||||||
|
));
|
||||||
|
let mut ed = FileEditor::open(&b).unwrap();
|
||||||
|
ed.set_attr("g", "big", &clawhdf5::AttrValue::I64Array(vec![1, 2]))
|
||||||
|
.unwrap();
|
||||||
|
drop(ed);
|
||||||
|
check_tools(&b, true);
|
||||||
|
let mut want: Vec<AttrOp> = (0..10i64)
|
||||||
|
.map(|i| ("g".to_string(), format!("k{i}"), AV::Ints(vec![i])))
|
||||||
|
.collect();
|
||||||
|
want.push(("g".into(), "big".into(), AV::Ints(vec![1, 2])));
|
||||||
|
check_attr_values(&b, &want);
|
||||||
|
let info = |p: &Path| {
|
||||||
|
let s = dense_info(p, "g");
|
||||||
|
s[..s.find(" fs ").or(s.find(" no-fs")).unwrap()].to_string()
|
||||||
|
};
|
||||||
|
assert_eq!(info(&b), info(&a), "heap after the replacement");
|
||||||
|
// A huge attribute again starts the huge-object B-tree over.
|
||||||
|
let mut ed = FileEditor::open(&b).unwrap();
|
||||||
|
ed.set_attr("g", "big2", &clawhdf5::AttrValue::String("x".repeat(7000)))
|
||||||
|
.unwrap();
|
||||||
|
drop(ed);
|
||||||
|
want.push(("g".into(), "big2".into(), AV::Str("x".repeat(7000))));
|
||||||
|
check_tools(&b, true);
|
||||||
|
check_attr_values(&b, &want);
|
||||||
|
}
|
||||||
|
|||||||
@@ -384,6 +384,32 @@ impl Bt2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete the whole tree (`H5B2_delete`): every node and the header go
|
||||||
|
/// to the image's free list.
|
||||||
|
pub(crate) fn delete(mut self, img: &mut Image<'_>) -> Result<(), Error> {
|
||||||
|
if self.root.addr != undef(img.os) && self.root.nrec > 0 {
|
||||||
|
let mut level = vec![self.root];
|
||||||
|
let mut depth = self.depth;
|
||||||
|
loop {
|
||||||
|
let mut next = Vec::new();
|
||||||
|
for p in level {
|
||||||
|
self.load(img, p, depth)?;
|
||||||
|
if depth > 0 {
|
||||||
|
next.extend(self.peek(p.addr).ptrs.iter().copied());
|
||||||
|
}
|
||||||
|
img.free(p.addr, u64::from(self.node_size));
|
||||||
|
}
|
||||||
|
if depth == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
depth -= 1;
|
||||||
|
level = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img.free(self.addr, Self::header_len(img.os, img.ls) as u64);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Insert `rec`, or replace the record `cmp` matches (`H5B2_update`).
|
/// Insert `rec`, or replace the record `cmp` matches (`H5B2_update`).
|
||||||
pub(crate) fn update(
|
pub(crate) fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|||||||
@@ -948,6 +948,15 @@ impl Heap {
|
|||||||
img.free(get_uint(&rec, os), len);
|
img.free(get_uint(&rec, os), len);
|
||||||
self.huge_size = self.huge_size.saturating_sub(len);
|
self.huge_size = self.huge_size.saturating_sub(len);
|
||||||
self.huge_nobjs = self.huge_nobjs.saturating_sub(1);
|
self.huge_nobjs = self.huge_nobjs.saturating_sub(1);
|
||||||
|
// H5HF__huge_term: with no huge object left, the huge-object
|
||||||
|
// B-tree is deleted and IDs start over (libhdf5 does it when
|
||||||
|
// it closes the heap, and a read-only libhdf5 fails to).
|
||||||
|
if self.huge_nobjs == 0 {
|
||||||
|
t.delete(img)?;
|
||||||
|
self.huge_bt2 = undef(os);
|
||||||
|
self.next_huge_id = 0;
|
||||||
|
self.flags &= !0x01;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(unsupported("tiny objects")),
|
_ => Err(unsupported("tiny objects")),
|
||||||
|
|||||||
Reference in New Issue
Block a user