facade: retry every read path of a live file (strings, vlen, attributes)
read_string, read_string_bytes, read_string_selection, read_vlen and read_vlen_selection now retry as a whole, the global-heap decoding after the read included; so do File::decode_strings / decode_string_bytes / decode_vlen, Group::datasets / groups / attrs / attr, Dataset::attrs / attr, the typed full reads (read_f64 ...), the header messages behind shape() and dtype() (a shared message is read from another header), and verify_provenance. Before, a transient failure on those reached the caller. Attribute reads leave out an attribute they cannot read (or return a variable-length string one as AttrValue::Raw) instead of failing, which hid a transient error as a missing or raw attribute: on a live file such an error of a retried kind now runs the read again too, and after the last attempt the last result is returned as before. The format crate gains find_attribute_reporting_in, which returns the errors find_attribute_in skips (dense name-index lookups dropped them). The zero-copy reads need the file in memory, which a live file never is, so they have nothing to retry. Test: a storage that fails one read with a checksum mismatch; for 14 read paths over a new fixture (tests/fixtures/swmr_strings_attrs.h5, an h5py copy with the SWMR-write flag: vlen strings, vlen int32, dense attributes), each read the path makes fails once in turn and the path must return the same result with exactly one retry. It fails on the previous commit (root.attrs, read 0). Dataset::attr on dense attributes returned None before find_attribute_reporting_in. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -576,7 +576,14 @@ pub fn find_attribute_in_file(
|
||||
offset_size: u8,
|
||||
length_size: u8,
|
||||
) -> Result<Option<AttributeMessage>, FormatError> {
|
||||
find_attribute_core(file_data, header, name, offset_size, length_size)
|
||||
find_attribute_core(
|
||||
file_data,
|
||||
header,
|
||||
name,
|
||||
offset_size,
|
||||
length_size,
|
||||
&mut Vec::new(),
|
||||
)
|
||||
}
|
||||
|
||||
/// [`find_attribute_in_file`] over any [`Storage`] (see
|
||||
@@ -594,16 +601,48 @@ pub fn find_attribute_in<S: Storage + ?Sized>(
|
||||
) -> Result<Option<AttributeMessage>, FormatError> {
|
||||
match file_data.as_contiguous() {
|
||||
Some(all) => find_attribute_in_file(all, header, name, offset_size, length_size),
|
||||
None => find_attribute_core(file_data, header, name, offset_size, length_size),
|
||||
None => find_attribute_core(
|
||||
file_data,
|
||||
header,
|
||||
name,
|
||||
offset_size,
|
||||
length_size,
|
||||
&mut Vec::new(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`find_attribute_in`], also returning the errors of the attributes it
|
||||
/// could not read on the way (which it leaves out rather than failing
|
||||
/// the call): the attribute asked for may be one of them. A reader of a
|
||||
/// file that is being written uses them to tell a read that raced the
|
||||
/// writer from an absent attribute.
|
||||
pub fn find_attribute_reporting_in<S: Storage + ?Sized>(
|
||||
file_data: &S,
|
||||
header: &ObjectHeader,
|
||||
name: &str,
|
||||
offset_size: u8,
|
||||
length_size: u8,
|
||||
) -> Result<(Option<AttributeMessage>, Vec<FormatError>), FormatError> {
|
||||
let mut errors = Vec::new();
|
||||
let found = find_attribute_core(
|
||||
file_data,
|
||||
header,
|
||||
name,
|
||||
offset_size,
|
||||
length_size,
|
||||
&mut errors,
|
||||
)?;
|
||||
Ok((found, errors))
|
||||
}
|
||||
|
||||
fn find_attribute_core<S: Storage + ?Sized>(
|
||||
file_data: &S,
|
||||
header: &ObjectHeader,
|
||||
name: &str,
|
||||
offset_size: u8,
|
||||
length_size: u8,
|
||||
errors: &mut Vec<FormatError>,
|
||||
) -> Result<Option<AttributeMessage>, FormatError> {
|
||||
let attr_info = find_attribute_info(header, offset_size)?;
|
||||
let dense = attr_info
|
||||
@@ -612,12 +651,10 @@ fn find_attribute_core<S: Storage + ?Sized>(
|
||||
let Some((fh_addr, btree_addr)) = dense else {
|
||||
// Compact only (or dense storage without a name index, which a
|
||||
// listing reports): as a listing finds it.
|
||||
return Ok(
|
||||
extract_attributes_tolerant_in(file_data, header, offset_size, length_size)?
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|a| a.name == name),
|
||||
);
|
||||
let (attrs, errs) =
|
||||
extract_attributes_tolerant_in(file_data, header, offset_size, length_size)?;
|
||||
errors.extend(errs);
|
||||
return Ok(attrs.into_iter().find(|a| a.name == name));
|
||||
};
|
||||
let btree_hdr = BTreeV2Header::parse_in(
|
||||
file_data,
|
||||
@@ -627,12 +664,10 @@ fn find_attribute_core<S: Storage + ?Sized>(
|
||||
)?;
|
||||
let fh = FractalHeapHeader::parse_in(file_data, fh_addr, offset_size, length_size)?;
|
||||
if btree_hdr.tree_type != ATTRIBUTE_NAME_INDEX || btree_hdr.record_size < 4 {
|
||||
return Ok(
|
||||
extract_attributes_tolerant_in(file_data, header, offset_size, length_size)?
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|a| a.name == name),
|
||||
);
|
||||
let (attrs, errs) =
|
||||
extract_attributes_tolerant_in(file_data, header, offset_size, length_size)?;
|
||||
errors.extend(errs);
|
||||
return Ok(attrs.into_iter().find(|a| a.name == name));
|
||||
}
|
||||
|
||||
// A listing has the compact attributes first.
|
||||
@@ -671,10 +706,10 @@ fn find_attribute_core<S: Storage + ?Sized>(
|
||||
AttributeMessage::parse_in_storage(&d, file_data, offset_size, length_size)
|
||||
});
|
||||
// One that cannot be read is left out, as from a listing.
|
||||
if let Ok(attr) = attr
|
||||
&& attr.name == name
|
||||
{
|
||||
return Ok(Some(attr));
|
||||
match attr {
|
||||
Ok(attr) if attr.name == name => return Ok(Some(attr)),
|
||||
Ok(_) => {}
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
|
||||
Reference in New Issue
Block a user