use std::collections::HashMap;
use std::fs::OpenOptions;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
use crate::chunk_index_inplace::{ElemRecord, InPlaceFile, Located};
use crate::error::{Error, FormatError};
use crate::file_lock::{self, FileLocking};
use crate::group_v2;
use crate::signature;
use crate::superblock::Superblock;
#[deprecated(
since = "0.22.0",
note = "use File::open_swmr_writer + Dataset::append; see the SwmrWriter type docs for migration"
)]
pub struct SwmrWriter {
file: InPlaceFile,
located: HashMap<String, Located>,
flag_set: bool,
}
const SWMR_WRITE_FLAGS: u32 = 0x05;
#[allow(deprecated)] impl SwmrWriter {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let file = InPlaceFile::open(path, None, Error::SwmrAppendUnsupported)?;
let mut w = Self {
file,
located: HashMap::new(),
flag_set: false,
};
w.set_swmr_flag(true)?;
Ok(w)
}
fn set_swmr_flag(&mut self, active: bool) -> Result<(), Error> {
self.file
.set_consistency_flags(if active { SWMR_WRITE_FLAGS } else { 0 })?;
self.flag_set = active;
Ok(())
}
pub fn close(mut self) -> Result<(), Error> {
self.set_swmr_flag(false)
}
pub fn clear_swmr_flag<P: AsRef<Path>>(path: P) -> Result<(), Error> {
clear_swmr_flag_at(path.as_ref())
}
pub fn append_i32(&mut self, dataset: &str, values: &[i32]) -> Result<(), Error> {
let mut bytes = Vec::with_capacity(values.len() * 4);
for &v in values {
bytes.extend_from_slice(&v.to_le_bytes());
}
self.append_raw(dataset, &bytes)
}
pub fn append_f64(&mut self, dataset: &str, values: &[f64]) -> Result<(), Error> {
let mut bytes = Vec::with_capacity(values.len() * 8);
for &v in values {
bytes.extend_from_slice(&v.to_le_bytes());
}
self.append_raw(dataset, &bytes)
}
pub fn append_raw(&mut self, dataset: &str, bytes: &[u8]) -> Result<(), Error> {
self.append_phased(dataset, bytes, 4)
}
fn append_phased(&mut self, dataset: &str, bytes: &[u8], max_phase: u8) -> Result<(), Error> {
if !self.located.contains_key(dataset) {
let oh_addr =
group_v2::resolve_path_any(self.file.data(), &self.file.superblock, dataset)?;
let result = Located::locate_at(&self.file, oh_addr, Error::SwmrAppendUnsupported)?;
if result.has_filters {
return Err(Error::SwmrAppendUnsupported(
"filtered datasets are not supported",
));
}
self.located.insert(dataset.to_string(), result.located);
}
let (chunk_bytes, chunk_elems, elem_bytes, current_dim, num_chunks) = {
let loc = &self.located[dataset];
(
loc.chunk_bytes,
loc.chunk_elems,
loc.elem_bytes,
loc.current_dim,
loc.num_chunks,
)
};
if elem_bytes == 0 || chunk_bytes == 0 {
return Err(Error::SwmrAppendUnsupported(
"dataset has zero-sized elements or chunks",
));
}
if bytes.len() % elem_bytes != 0 {
return Err(Error::Format(FormatError::ChunkedReadError(
"append byte length is not a whole number of elements".into(),
)));
}
let new_elems = (bytes.len() / elem_bytes) as u64;
if new_elems == 0 {
return Ok(());
}
if current_dim % chunk_elems != 0 || new_elems % chunk_elems != 0 {
return Err(Error::Format(FormatError::ChunkedReadError(
"SWMR append must be chunk-aligned (current length and appended length \
must be multiples of the chunk length)"
.into(),
)));
}
let n_new_chunks = bytes.len() / chunk_bytes;
for c in 0..n_new_chunks {
let chunk_data = &bytes[c * chunk_bytes..(c + 1) * chunk_bytes];
let chunk_addr = self.file.append_bytes(chunk_data)?;
let e = num_chunks + c as u64;
self.located[dataset].ea_insert(
&mut self.file,
e,
ElemRecord::addr_only(chunk_addr),
)?;
}
self.file.sync()?;
if max_phase < 2 {
return Ok(());
}
self.file.patch_superblock_eof()?;
self.file.sync()?;
if max_phase < 3 {
return Ok(());
}
let new_num_chunks = num_chunks + n_new_chunks as u64;
self.located[dataset].update_ea_header(&mut self.file, new_num_chunks)?;
self.file.sync()?;
if max_phase < 4 {
return Ok(());
}
let new_dim = current_dim + new_elems;
self.located[dataset].patch_dimension(&mut self.file, new_dim)?;
self.file.sync()?;
if let Some(loc) = self.located.get_mut(dataset) {
loc.current_dim = new_dim;
loc.num_chunks = new_num_chunks;
}
Ok(())
}
}
pub(crate) fn clear_swmr_flag_at(path: &Path) -> Result<(), Error> {
let mut w = OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(Error::Io)?;
file_lock::acquire_exclusive(&w, FileLocking::Enabled, path)?;
let mut data = Vec::new();
w.read_to_end(&mut data).map_err(Error::Io)?;
let sig = signature::find_signature(&data)?;
let mut sb = Superblock::parse(&data, sig)?;
if sb.version < 2 {
return Ok(());
}
if sb.consistency_flags == 0 {
return Ok(());
}
sb.consistency_flags = 0;
let bytes = sb.serialize();
w.seek(SeekFrom::Start(sig as u64)).map_err(Error::Io)?;
w.write_all(&bytes).map_err(Error::Io)?;
w.sync_data().map_err(Error::Io)?;
Ok(())
}
#[allow(deprecated)] impl Drop for SwmrWriter {
fn drop(&mut self) {
if self.flag_set {
let _ = self.set_swmr_flag(false);
}
}
}
#[cfg(test)]
#[allow(deprecated)] mod tests {
use super::*;
use crate::reader::File as PureFile;
use crate::writer::FileBuilder;
use tempfile::tempdir;
fn i32_bytes(range: std::ops::Range<i32>) -> Vec<u8> {
let mut b = Vec::new();
for v in range {
b.extend_from_slice(&v.to_le_bytes());
}
b
}
#[test]
fn crash_consistency_consistent_prefix() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.h5");
let n = 50i32;
let target = 250i32; {
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(&[1]);
b.write(&base).unwrap();
}
for max_phase in 1u8..=4 {
let p = dir.path().join(format!("crash_{max_phase}.h5"));
std::fs::copy(&base, &p).unwrap();
{
let mut w = SwmrWriter::open(&p).unwrap();
w.append_phased("d", &i32_bytes(n..target), max_phase)
.unwrap();
}
let expected_len = if max_phase == 4 { target } else { n };
let f = PureFile::from_bytes(std::fs::read(&p).unwrap()).unwrap();
let v = f.dataset("d").unwrap().read_i32().unwrap();
assert_eq!(
v,
(0..expected_len).collect::<Vec<_>>(),
"inconsistent view after crash at phase {max_phase}"
);
}
}
#[test]
fn crash_consistency_paged_prefix() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.h5");
let start = 131_000i32; let target = 132_000i32; {
let data: Vec<i32> = (0..start).collect();
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&data)
.with_shape(&[start as u64])
.with_maxshape(&[u64::MAX])
.with_chunks(&[1]);
b.write(&base).unwrap();
}
for max_phase in 1u8..=4 {
let p = dir.path().join(format!("crash_paged_{max_phase}.h5"));
std::fs::copy(&base, &p).unwrap();
{
let mut w = SwmrWriter::open(&p).unwrap();
w.append_phased("d", &i32_bytes(start..target), max_phase)
.unwrap();
}
let expected_len = if max_phase == 4 { target } else { start };
let f = PureFile::from_bytes(std::fs::read(&p).unwrap()).unwrap();
let v = f.dataset("d").unwrap().read_i32().unwrap();
assert_eq!(
v,
(0..expected_len).collect::<Vec<_>>(),
"inconsistent paged view after crash at phase {max_phase}"
);
}
}
#[test]
#[cfg(not(target_pointer_width = "32"))]
fn crash_consistency_c_library_reads_prefix() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.h5");
let n = 50i32;
let target = 250i32; {
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(&[1]);
b.write(&base).unwrap();
}
for max_phase in 1u8..=4 {
let p = dir.path().join(format!("crash_c_{max_phase}.h5"));
std::fs::copy(&base, &p).unwrap();
{
let mut w = SwmrWriter::open(&p).unwrap();
w.append_phased("d", &i32_bytes(n..target), max_phase)
.unwrap();
}
let expected_len = if max_phase == 4 { target } else { n };
let f = hdf5::File::open(&p).unwrap();
let v = f.dataset("d").unwrap().read_raw::<i32>().unwrap();
assert_eq!(
v,
(0..expected_len).collect::<Vec<_>>(),
"C library saw an inconsistent view after crash at phase {max_phase}"
);
f.close().unwrap();
}
}
#[test]
#[cfg(not(target_pointer_width = "32"))]
fn recover_and_reappend_after_phase3_crash() {
let dir = tempdir().unwrap();
let path = dir.path().join("phase3_recover.h5");
let n = 50i32;
{
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(&[1]);
b.write(&path).unwrap();
}
{
let mut w = SwmrWriter::open(&path).unwrap();
w.append_phased("d", &i32_bytes(1000..1200), 3).unwrap();
}
let pf = PureFile::from_bytes(std::fs::read(&path).unwrap()).unwrap();
assert_eq!(
pf.dataset("d").unwrap().read_i32().unwrap(),
(0..n).collect::<Vec<_>>(),
"phase-3 crash exposed uncommitted data to the pure reader"
);
{
let f = hdf5::File::open(&path).unwrap();
assert_eq!(
f.dataset("d").unwrap().read_raw::<i32>().unwrap(),
(0..n).collect::<Vec<_>>(),
"phase-3 crash exposed uncommitted data to the C library"
);
f.close().unwrap();
}
{
let mut w = SwmrWriter::open(&path).unwrap();
w.append_i32("d", &(n..150).collect::<Vec<_>>()).unwrap();
w.close().unwrap();
}
let expected: Vec<i32> = (0..150).collect();
let pf = PureFile::from_bytes(std::fs::read(&path).unwrap()).unwrap();
assert_eq!(
pf.dataset("d").unwrap().read_i32().unwrap(),
expected,
"recovery did not roll forward correctly (pure reader)"
);
let f = hdf5::File::open(&path).unwrap();
assert_eq!(
f.dataset("d").unwrap().read_raw::<i32>().unwrap(),
expected,
"recovery did not roll forward correctly (C library)"
);
f.close().unwrap();
}
}