use std::collections::HashMap;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
use crate::chunk_index_inplace::{Store, apply_ea_append, plan_ea_append};
use crate::edit::{
AppendBuilder, LocatedState, as_inplace_error, locate_dataset_state, validate_gathered_append,
};
use crate::error::{Error, FormatError};
use crate::file_lock::{self, FileLocking};
use crate::file_space_info::FileSpaceInfo;
use crate::message_type::MessageType;
use crate::object_header::ObjectHeader;
use crate::signature;
use crate::source::{MetadataCacheConfig, MetadataReadCache, Source};
use crate::superblock::Superblock;
const APPEND_BATCH_BYTES: u64 = 1 << 20;
pub(crate) struct AppendGeometry {
pub(crate) chunk_elems: u64,
pub(crate) element_size: usize,
pub(crate) current_dim: u64,
pub(crate) filtered: bool,
pub(crate) full_batch_elems: u64,
}
fn read_at_handle(
handle: &std::fs::File,
len: u64,
offset: u64,
buf: &mut [u8],
) -> Result<(), FormatError> {
let end = offset
.checked_add(buf.len() as u64)
.ok_or(FormatError::OffsetOverflow {
offset,
length: buf.len() as u64,
})?;
if end > len {
return Err(FormatError::UnexpectedEof {
expected: end.to_usize().unwrap_or(usize::MAX),
available: len.to_usize().unwrap_or(usize::MAX),
});
}
let mut h = handle;
h.seek(SeekFrom::Start(offset))
.map_err(|e| FormatError::Source(std::format!("{e}")))?;
h.read_exact(buf)
.map_err(|e| FormatError::Source(std::format!("{e}")))?;
Ok(())
}
use crate::convert::TryToUsize;
struct RawSource<'a> {
handle: &'a std::fs::File,
len: u64,
}
impl Source for RawSource<'_> {
fn len(&self) -> u64 {
self.len
}
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
read_at_handle(self.handle, self.len, offset, buf)
}
}
pub(crate) struct BoundedStore {
handle: std::fs::File,
len: u64,
sb_sig_off: u64,
superblock: Superblock,
metadata_cache: Option<(MetadataCacheConfig, std::sync::Mutex<MetadataReadCache>)>,
}
impl BoundedStore {
pub(crate) fn superblock(&self) -> &Superblock {
&self.superblock
}
fn write_at_raw(&mut self, offset: u64, bytes: &[u8]) -> Result<(), Error> {
let end = offset
.checked_add(bytes.len() as u64)
.filter(|&e| e <= self.len)
.ok_or(Error::Format(FormatError::UnexpectedEof {
expected: offset.to_usize().unwrap_or(usize::MAX),
available: self.len.to_usize().unwrap_or(usize::MAX),
}))?;
debug_assert!(end <= self.len);
if let Some((_, cache)) = &self.metadata_cache {
cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.invalidate_overlapping(offset, bytes.len());
}
self.handle
.seek(SeekFrom::Start(offset))
.map_err(Error::Io)?;
self.handle.write_all(bytes).map_err(Error::Io)?;
Ok(())
}
}
impl Source for BoundedStore {
fn len(&self) -> u64 {
self.len
}
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
read_at_handle(&self.handle, self.len, offset, buf)
}
fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
let Some((config, cache)) = &self.metadata_cache else {
return self.read_exact_at(offset, len);
};
if len == 0 || len > config.max_entry_bytes() || len > config.max_bytes() {
return self.read_exact_at(offset, len);
}
if let Some(bytes) = cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(offset, len)
{
return Ok(bytes);
}
let bytes = self.read_exact_at(offset, len)?;
cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(offset, len, bytes.clone(), config.max_bytes());
Ok(bytes)
}
}
impl Store for BoundedStore {
fn offset_size(&self) -> u8 {
self.superblock.offset_size
}
fn length_size(&self) -> u8 {
self.superblock.length_size
}
fn append_bytes(&mut self, bytes: &[u8]) -> Result<u64, Error> {
let addr = self.len;
self.handle.seek(SeekFrom::Start(addr)).map_err(Error::Io)?;
self.handle.write_all(bytes).map_err(Error::Io)?;
self.len += bytes.len() as u64;
Ok(addr)
}
fn write_at(&mut self, offset: u64, bytes: &[u8]) -> Result<(), Error> {
self.write_at_raw(offset, bytes)
}
fn patch_superblock_eof(&mut self) -> Result<(), Error> {
self.superblock.eof_address = self.len;
let bytes = self.superblock.serialize();
self.write_at_raw(self.sb_sig_off, &bytes)
}
fn sync(&mut self) -> Result<(), Error> {
self.handle.flush().map_err(Error::Io)?;
self.handle.sync_data().map_err(Error::Io)?;
Ok(())
}
}
pub(crate) struct BoundedEngine {
store: BoundedStore,
located: HashMap<u64, LocatedState>,
}
impl BoundedEngine {
pub(crate) fn open(path: &Path, metadata_cache: MetadataCacheConfig) -> Result<Self, Error> {
let handle = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(Error::Io)?;
file_lock::acquire_exclusive(&handle, FileLocking::Enabled, path)?;
let len = handle.metadata().map_err(Error::Io)?.len();
let raw = RawSource {
handle: &handle,
len,
};
let sb_sig_off = signature::find_signature_in(&raw)?;
let superblock = Superblock::parse_from_source(&raw, sb_sig_off)?;
if superblock.version < 2 {
return Err(Error::EditUnsupported(
"bounded read-write access requires a latest-format file (v2/v3 superblock); \
use File::open_rw",
));
}
if superblock.offset_size != 8 || superblock.length_size != 8 {
return Err(Error::EditUnsupported(
"bounded read-write access requires 8-byte offsets and lengths",
));
}
if superblock.base_address != 0 || sb_sig_off != 0 {
return Err(Error::EditUnsupported(
"bounded read-write access does not support a file with a userblock \
(non-zero base address); use File::open_rw",
));
}
if persisted_free_space_armed(&raw, &superblock) {
return Err(Error::EditUnsupported(
"bounded read-write access does not support a file that persists its \
free space (H5Pset_file_space_strategy persist=true); use File::open_rw",
));
}
Ok(Self {
store: BoundedStore {
handle,
len,
sb_sig_off,
superblock,
metadata_cache: metadata_cache.is_enabled().then(|| {
(
metadata_cache,
std::sync::Mutex::new(MetadataReadCache::new()),
)
}),
},
located: HashMap::new(),
})
}
pub(crate) fn store(&self) -> &BoundedStore {
&self.store
}
pub(crate) fn sync(&mut self) -> Result<(), Error> {
Store::sync(&mut self.store)
}
pub(crate) fn append_geometry(&mut self, oh_addr: u64) -> Result<AppendGeometry, Error> {
let Self { store, located } = self;
let st = match located.entry(oh_addr) {
std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(locate_dataset_state(&*store, oh_addr)?)
}
};
let chunk_elems = st.loc.chunk_elems.max(1);
let batch_chunks = (APPEND_BATCH_BYTES / (st.loc.chunk_bytes.max(1) as u64)).max(1);
Ok(AppendGeometry {
chunk_elems,
element_size: st.element_size,
current_dim: st.loc.current_dim,
filtered: st.pipeline.is_some(),
full_batch_elems: batch_chunks * chunk_elems,
})
}
pub(crate) fn append_gathered(
&mut self,
oh_addr: u64,
b: &AppendBuilder,
max_phase: u8,
) -> Result<(), Error> {
if b.dt_conflict() {
return Err(Error::AppendInPlaceUnsupported(
"append mixes element types in one call; use one element type per append",
));
}
let Self { store, located } = self;
let st = match located.entry(oh_addr) {
std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(locate_dataset_state(&*store, oh_addr)?)
}
};
let new_elems = validate_gathered_append(st, b)?;
if new_elems == 0 {
return Ok(());
}
let raw = b.raw();
let chunk_elems = st.loc.chunk_elems.max(1);
if st.pipeline.is_some()
&& (st.loc.current_dim % chunk_elems != 0 || new_elems % chunk_elems != 0)
{
return Err(Error::AppendInPlaceUnsupported(
"a filtered dataset can only be appended in place in whole chunks (the current \
length and the appended length must both be multiples of the chunk length); \
use Dataset::append_staged for a non-chunk-aligned filtered append",
));
}
let elem_bytes = st.element_size as u64;
let batch_chunks = (APPEND_BATCH_BYTES / (st.loc.chunk_bytes.max(1) as u64)).max(1);
let full_batch_elems = batch_chunks * chunk_elems;
let mut done = 0u64;
while done < new_elems {
let to_boundary = (chunk_elems - st.loc.current_dim % chunk_elems) % chunk_elems;
let take = (new_elems - done).min(to_boundary + full_batch_elems);
let start = (done * elem_bytes).to_usize()?;
let end = ((done + take) * elem_bytes).to_usize()?;
let batch = &raw[start..end];
let plan = plan_ea_append(
&*store,
&st.loc,
&st.datatype,
&st.spatial,
st.element_size,
st.pipeline.as_ref(),
batch,
take,
)
.map_err(as_inplace_error)?;
apply_ea_append(store, &mut st.loc, &plan, max_phase).map_err(as_inplace_error)?;
if max_phase < 4 {
return Ok(());
}
done += take;
}
Ok(())
}
}
fn persisted_free_space_armed(raw: &RawSource<'_>, superblock: &Superblock) -> bool {
let Some(rel) = superblock.superblock_extension_address else {
return false;
};
if rel == u64::MAX {
return false;
}
let Ok(header) = ObjectHeader::parse_from_source(
raw,
rel,
superblock.offset_size,
superblock.length_size,
0,
) else {
return false;
};
let Some(msg) = header
.messages
.iter()
.find(|m| m.msg_type == MessageType::FileSpaceInfo)
else {
return false;
};
matches!(
FileSpaceInfo::parse(&msg.data, superblock.offset_size, superblock.length_size),
Ok(info) if info.persist
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::group_v2;
use crate::writer::FileBuilder;
use tempfile::tempdir;
fn build(path: &Path, n: i32, chunk: u64) {
let data: Vec<i32> = (0..n).collect();
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&data)
.with_shape(&[n as u64])
.with_maxshape(&[u64::MAX])
.with_chunks(&[chunk]);
b.write(path).unwrap();
}
fn dataset_addr(engine: &BoundedEngine) -> u64 {
group_v2::resolve_path_any_from_source(engine.store(), &engine.store().superblock, "d")
.unwrap()
}
#[test]
fn append_crash_consistency_partial_tail_prefix() {
let dir = tempdir().unwrap();
for (case, (n, chunk, add)) in [(0usize, (6i32, 4u64, 5i32)), (1, (8, 2, 6))] {
let base = dir.path().join(std::format!("base_{case}.h5"));
build(&base, n, chunk);
for max_phase in 1u8..=4 {
let p = dir.path().join(std::format!("crash_{case}_{max_phase}.h5"));
std::fs::copy(&base, &p).unwrap();
{
let mut engine =
BoundedEngine::open(&p, MetadataCacheConfig::disabled()).unwrap();
let addr = dataset_addr(&engine);
let mut b = AppendBuilder::new();
b.append_i32(&(n..n + add).collect::<Vec<_>>());
engine.append_gathered(addr, &b, max_phase).unwrap();
}
let expected_len = if max_phase == 4 { n + add } else { n };
let got = crate::File::open(&p)
.unwrap()
.dataset("d")
.unwrap()
.read_i32()
.unwrap();
assert_eq!(
got,
(0..expected_len).collect::<Vec<_>>(),
"case {case} phase {max_phase}"
);
}
}
}
#[test]
fn multi_batch_append_commits_every_batch() {
let dir = tempdir().unwrap();
let p = dir.path().join("multibatch.h5");
build(&p, 5, 512);
let total = 700_000i32;
{
let mut engine = BoundedEngine::open(&p, MetadataCacheConfig::disabled()).unwrap();
let addr = dataset_addr(&engine);
let mut b = AppendBuilder::new();
b.append_i32(&(5..total).collect::<Vec<_>>());
engine.append_gathered(addr, &b, 4).unwrap();
}
let got = crate::File::open(&p)
.unwrap()
.dataset("d")
.unwrap()
.read_i32()
.unwrap();
assert_eq!(got.len(), total as usize);
assert!(got.iter().enumerate().all(|(i, &v)| v == i as i32));
}
}