#![cfg(not(any(feature = "blocking", feature = "wal")))]
use btree_ondisk::{MemoryBlockLoader, NullNodeCache};
use hyperfile::config::{HyperFileConfigBuilder, HyperFileMetaConfig, HyperFileRuntimeConfig};
use hyperfile::file::file::HyperFile;
use hyperfile::file::flags::{FileFlags, HyperFileFlags};
use hyperfile::file::mode::{FileMode, HyperFileMode};
use hyperfile::file::HyperTrait;
use hyperfile::inode::FlushInodeFlag;
use hyperfile::meta_format::BlockPtrFormat;
use hyperfile::staging::config::StagingConfig;
use hyperfile::staging::memory::MemoryStaging;
use hyperfile::staging::{Staging, StagingIntercept};
use hyperfile::SegmentId;
use std::io::Result;
type MemFile<'a> = HyperFile<'a, MemoryStaging, MemoryBlockLoader<hyperfile::BlockPtr>, NullNodeCache>;
const BS: usize = 4096;
const MIB: usize = 1 << 20;
const FMT: BlockPtrFormat = BlockPtrFormat::StreamSegment;
fn runtime(target: usize, piece: usize, limit: usize) -> HyperFileRuntimeConfig {
let mut rt = HyperFileRuntimeConfig::default();
rt.data_cache_dirty_max_bytes_threshold = 1 << 40;
rt.data_cache_dirty_max_blocks_threshold = (1 << 40) / BS;
rt.data_cache_dirty_max_flush_interval = u64::MAX / 2;
rt.segment_buffer_size = 1 << 40;
rt.segment_max_object_bytes = limit;
rt.data_cache_blocks = 0;
rt.stream_segment_bytes = target;
rt.segment_mpu_chunk_size = piece;
rt
}
fn config(s: &MemoryStaging, rt: &HyperFileRuntimeConfig) -> hyperfile::config::HyperFileConfig {
let mut meta = HyperFileMetaConfig::default();
meta.block_ptr_format = FMT;
HyperFileConfigBuilder::new().with_staging_config(s.config()).with_meta_config(&meta).with_runtime_config(rt).build()
}
async fn create(name: &str, rt: &HyperFileRuntimeConfig) -> (MemoryStaging, MemFile<'static>) {
let s = MemoryStaging::new(StagingConfig::new_memory(name), rt.clone());
let f = MemFile::new(s.clone(), s.to_block_loader(), NullNodeCache, config(&s, rt),
HyperFileFlags::from_flags(FileFlags::rdwr()), HyperFileMode::from_mode(FileMode::default_file()))
.await.expect("create");
(s, f)
}
async fn reopen(s: &MemoryStaging, rt: &HyperFileRuntimeConfig) -> MemFile<'static> {
MemFile::open(s.clone(), s.to_block_loader(), NullNodeCache, config(s, rt), HyperFileFlags::from_flags(FileFlags::rdonly()))
.await.expect("open")
}
fn block(b: usize, g: u8) -> Vec<u8> {
(0..BS).map(|i| (b as u8).wrapping_mul(29) ^ g ^ (i as u8)).collect()
}
fn largest_object(s: &MemoryStaging) -> (SegmentId, usize) {
s.segment_ids().into_iter().map(|id| (id, s.segment_bytes(id).unwrap().len()))
.max_by_key(|(_, n)| *n).unwrap()
}
async fn check_cold(s: &MemoryStaging, rt: &HyperFileRuntimeConfig, name: &str, blocks: impl Iterator<Item = usize>, want: impl Fn(usize) -> Vec<u8>) {
let mut g = reopen(s, rt).await;
let mut v = vec![0u8; BS];
for b in blocks {
g.read(b * BS, &mut v).await.unwrap();
assert!(v == want(b), "{}: block {} reads back wrong", name, b);
}
}
#[tokio::test]
async fn objects_sent_ahead_stay_within_the_limit() {
for (name, target, piece, limit) in [
("ol-pieces", 16 * MIB, 5 * MIB, 16 * MIB),
("ol-pieces-under", 16 * MIB, 5 * MIB, 18 * MIB),
("ol-whole", 5 * MIB, 5 * MIB, 5 * MIB),
] {
let rt = runtime(target, piece, limit);
let (s, mut f) = create(name, &rt).await;
let n = 64 * MIB / BS;
for b in 0..n {
f.write(b * BS, &block(b, 1)).await.unwrap();
if b % 256 == 0 { tokio::task::yield_now().await; }
}
f.flush().await.expect("flush");
assert_eq!(f.flush_timing().snapshot().stream_dead_bytes, 0, "{}: an object was given up", name);
assert!(s.segment_ids().len() > 3, "{}: nothing was sent ahead", name);
let (id, len) = largest_object(&s);
assert!(len <= limit, "{}: object {} is {} bytes, past the limit of {}", name, id, len, limit);
check_cold(&s, &rt, name, 0..n, |b| block(b, 1)).await;
}
}
#[tokio::test]
async fn scattered_blocks_and_their_leaves_stay_within_the_limit() {
for (name, target, piece, limit) in [
("ol-sparse-whole", 2 * MIB, 5 * MIB, 2 * MIB),
("ol-sparse-pieces", 8 * MIB, 5 * MIB, 8 * MIB),
] {
let rt = runtime(0, piece, limit);
let (s, mut f) = create(name, &rt).await;
let n = 256 * MIB / BS;
for b in 0..n {
f.write(b * BS, &block(b, 1)).await.unwrap();
}
f.flush().await.expect("publish the base");
drop(f);
let rt = runtime(target, piece, limit);
let mut f = MemFile::open(s.clone(), s.to_block_loader(), NullNodeCache, config(&s, &rt),
HyperFileFlags::from_flags(FileFlags::rdwr())).await.unwrap();
let before: std::collections::HashSet<SegmentId> = s.segment_ids().into_iter().collect();
for b in (0..n).step_by(24) {
f.write(b * BS, &block(b, 2)).await.unwrap();
tokio::task::yield_now().await;
}
f.flush().await.expect("flush");
assert_eq!(f.flush_timing().snapshot().stream_dead_bytes, 0, "{}: an object was given up", name);
let sent: Vec<(SegmentId, usize)> = s.segment_ids().into_iter().filter(|id| !before.contains(id))
.map(|id| (id, s.segment_bytes(id).unwrap().len())).collect();
assert!(sent.len() > 2, "{}: nothing was sent ahead: {:?}", name, sent);
for (id, len) in sent.iter() {
assert!(*len <= limit, "{}: object {} is {} bytes, past the limit of {}", name, id, len, limit);
}
check_cold(&s, &rt, name, 0..n, |b| block(b, if b % 24 == 0 { 2 } else { 1 })).await;
}
}
#[tokio::test]
async fn an_object_whose_leaves_outgrew_it_is_given_up() {
let name = "ol-give-up";
let rt = runtime(16 * MIB, 5 * MIB, 16 * MIB);
let (s, mut f) = create(name, &rt).await;
let placed = 3 * 5 * MIB / BS;
for k in 0..placed {
f.write(k * 16 * BS, &block(k * 16, 1)).await.unwrap();
tokio::task::yield_now().await;
}
for k in 0..placed {
f.write_zero((k * 16 + 1) * BS, 15 * BS).await.unwrap();
}
f.flush_timing().reset();
f.flush().await.expect("flush");
let dead = f.flush_timing().snapshot().stream_dead_bytes;
let (id, len) = largest_object(&s);
assert!(len <= 16 * MIB, "{}: object {} is {} bytes, past the limit", name, id, len);
assert_eq!(dead, (placed * BS) as u64, "{}: the open object was not given up", name);
check_cold(&s, &rt, name, 0..placed * 16, |b| if b % 16 == 0 { block(b, 1) } else { vec![0u8; BS] }).await;
}
#[derive(Clone)]
struct PanicOnPart;
impl StagingIntercept<MemoryStaging> for PanicOnPart {
fn after_flush_inode(&self, _: &MemoryStaging, _: &[u8], _: FlushInodeFlag)
-> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + '_ + Send>> {
Box::pin(async { Ok(()) })
}
fn after_remove_inode(&self, _: &MemoryStaging)
-> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + '_ + Send>> {
Box::pin(async { Ok(()) })
}
fn before_segment_done(&self, _: &MemoryStaging, segid: SegmentId, _: &[u8], _: usize)
-> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + '_ + Send>> {
let part = segid.part_id().is_some_and(|p| p > 0);
Box::pin(async move {
if part { panic!("a send-ahead task panicked (the test's)"); }
Ok(())
})
}
}
async fn a_panic_sending_ahead_reaches_the_caller(target: usize) {
let rt = runtime(target, 5 * MIB, 1 << 30);
let (_s, mut f) = create("ol-panic", &rt).await;
f.staging_interceptor(PanicOnPart);
for b in 0..(32 * MIB / BS) {
f.write(b * BS, &block(b, 1)).await.unwrap();
tokio::task::yield_now().await;
}
f.flush().await.unwrap();
}
#[tokio::test]
#[should_panic(expected = "a send-ahead task panicked (the test's)")]
async fn a_panic_sending_whole_objects_reaches_the_caller() {
a_panic_sending_ahead_reaches_the_caller(5 * MIB).await;
}
#[tokio::test]
#[should_panic(expected = "a send-ahead task panicked (the test's)")]
async fn a_panic_sending_pieces_reaches_the_caller() {
a_panic_sending_ahead_reaches_the_caller(16 * MIB).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_checkpoint_past_one_gib_with_the_target_at_the_limit() {
for (target, piece) in [(1024 * MIB, 16 * MIB), (1023 * MIB, 16 * MIB), (1000 * MIB, 16 * MIB), (1024 * MIB, 5 * MIB)] {
let name = format!("ol-gib-{}-{}", target / MIB, piece / MIB);
let rt = runtime(target, piece, 1 << 30);
let (s, mut f) = create(&name, &rt).await;
let chunk: Vec<u8> = (0..16 * MIB).map(|i| (i.wrapping_mul(2654435761) >> 13) as u8).collect();
let total = 1100 * MIB;
let mut off = 0;
while off < total {
f.write(off, &chunk).await.unwrap();
off += chunk.len();
}
f.flush().await.expect("flush");
drop(f);
let (id, len) = largest_object(&s);
assert!(len <= 1 << 30, "{}: object {} is {} bytes", name, id, len);
let mut g = reopen(&s, &rt).await;
let mut v = vec![0u8; chunk.len()];
let mut off = 0;
while off < total {
assert_eq!(g.read(off, &mut v).await.unwrap(), v.len());
assert!(v == chunk, "{}: 16 MiB at {} reads back wrong", name, off);
off += chunk.len();
}
}
}
#[tokio::test]
async fn an_object_sent_whole_fits_in_one_piece() {
for (name, step) in [("ol-whole-piece", 1usize), ("ol-whole-piece-sparse", 24)] {
let piece = 5 * MIB;
let rt = runtime(piece, piece, 1 << 30);
let (s, mut f) = create(name, &rt).await;
let n = 64 * MIB / BS * step;
for b in (0..n).step_by(step) {
f.write(b * BS, &block(b, 1)).await.unwrap();
if b % (256 * step) == 0 { tokio::task::yield_now().await; }
}
f.flush().await.expect("flush");
let dead = f.flush_timing().snapshot().stream_dead_bytes;
let parts: Vec<(SegmentId, usize)> = s.segment_ids().into_iter()
.filter(|id| id.part_id().is_some_and(|p| p > 0))
.map(|id| (id, s.segment_bytes(id).unwrap().len())).collect();
assert!(parts.len() > 3, "{}: nothing was sent ahead", name);
for (id, len) in parts.iter() {
assert!(*len <= piece, "{}: object {} is {} bytes, more than the {} byte piece", name, id, len, piece);
}
assert_eq!(dead, 0, "{}: an object was given up", name);
check_cold(&s, &rt, name, (0..n).step_by(step), |b| block(b, 1)).await;
}
}