From c047d01850bffd4c4fffbd9ca717f3718d6a0794 Mon Sep 17 00:00:00 2001 From: Christian Schwarz Date: Tue, 20 Aug 2024 13:27:19 +0000 Subject: [PATCH] add blackbox test --- .../inmemory_layer/vectored_dio_read.rs | 102 +++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/pageserver/src/tenant/storage_layer/inmemory_layer/vectored_dio_read.rs b/pageserver/src/tenant/storage_layer/inmemory_layer/vectored_dio_read.rs index 5b7708f3cc..128328ecc3 100644 --- a/pageserver/src/tenant/storage_layer/inmemory_layer/vectored_dio_read.rs +++ b/pageserver/src/tenant/storage_layer/inmemory_layer/vectored_dio_read.rs @@ -23,6 +23,7 @@ pub trait File: Send { } /// A logical read that our user wants to do. +#[derive(Debug)] pub struct ValueRead { pos: u32, state: MutexRefCell>>, @@ -52,6 +53,9 @@ pub trait Buffer: sealed::Sealed + std::ops::Deref { fn extend_from_slice(&mut self, src: &[u8]); } +/// The minimum alignment and size requirement for disk offsets and memory buffer size for direct IO. +const DIO_CHUNK_SIZE: usize = 512; + /// Execute the given `reads` against `file`. /// The results are placed in the buffers of the [`ValueRead`]s. /// Retrieve the results by calling [`ValueRead::into_result`] on each [`ValueRead`]. @@ -65,8 +69,6 @@ where F: File + Send, B: Buffer + IoBufMut + Send, { - const DIO_CHUNK_SIZE: usize = 512; - // Plan which parts of which chunks need to be appended to which buffer struct ChunkReadDestination<'a, B: Buffer> { value_read: &'a ValueRead, @@ -230,6 +232,7 @@ where } } +#[derive(Debug)] struct MutexRefCell(Mutex); impl MutexRefCell { fn new(value: T) -> Self { @@ -266,6 +269,8 @@ impl Buffer for Vec { #[cfg(test)] mod tests { + use rand::Rng; + use crate::{ context::DownloadBehavior, task_mgr::TaskKind, virtual_file::owned_buffers_io::slice::SliceMutExt, @@ -372,4 +377,97 @@ mod tests { .unwrap(); assert_eq!(err.to_string(), "foo"); } + + struct InMemoryFile { + content: Vec, + } + + impl InMemoryFile { + fn new_random(len: usize) -> Self { + Self { + content: rand::thread_rng() + .sample_iter(rand::distributions::Standard) + .take(len) + .collect(), + } + } + fn test_value_read(&self, pos: u32, len: usize) -> TestValueRead { + let expected_result = self.content[pos as usize..pos as usize + len].to_vec(); + TestValueRead { + pos, + expected_result, + } + } + } + + impl File for InMemoryFile { + async fn read_at_to_end<'a, 'b, B: IoBufMut + Send>( + &'b self, + start: u32, + mut dst: Slice, + _ctx: &'a RequestContext, + ) -> std::io::Result<(Slice, usize)> { + let len = std::cmp::min( + dst.bytes_total(), + self.content.len().saturating_sub(start as usize), + ); + let dst_slice: &mut [u8] = dst.as_mut_rust_slice_full_zeroed(); + dst_slice[..len].copy_from_slice(&self.content[start as usize..start as usize + len]); + rand::Rng::fill(&mut rand::thread_rng(), &mut dst_slice[len..]); // to discover bugs + Ok((dst, len)) + } + } + + #[derive(Debug)] + struct TestValueRead { + pos: u32, + expected_result: Vec, + } + + impl TestValueRead { + fn make_value_read(&self) -> ValueRead> { + ValueRead::new(self.pos, Vec::with_capacity(self.expected_result.len())) + } + } + + #[tokio::test] + async fn test_blackbox() { + let ctx = RequestContext::new(TaskKind::UnitTest, DownloadBehavior::Error); + let cs = DIO_CHUNK_SIZE; + let cs_u32 = u32::try_from(cs).unwrap(); + + let file = InMemoryFile::new_random(10 * cs); + + let test_value_reads = vec![ + file.test_value_read(0, 1), + // adjacent to value_read0 + file.test_value_read(1, 2), + // gap + // spans adjacent chunks + file.test_value_read(cs_u32 - 1, 2), + // gap + // tail of chunk 3, all of chunk 4, and 2 bytes of chunk 5 + file.test_value_read(3 * cs_u32 - 1, cs + 2), + // gap + file.test_value_read(5 * cs_u32, 1), + ]; + let test_value_reads_perms = test_value_reads.iter().permutations(test_value_reads.len()); + + // test all orderings of ValueReads, the order shouldn't matter for the results + for test_value_reads in test_value_reads_perms { + let value_reads: Vec> = test_value_reads + .iter() + .map(|tr| tr.make_value_read()) + .collect(); + execute(&file, value_reads.iter(), &ctx).await; + for (value_read, test_value_read) in + value_reads.into_iter().zip(test_value_reads.iter()) + { + let res = value_read + .into_result() + .expect("InMemoryFile is infallible"); + assert_eq!(res, test_value_read.expected_result); + } + } + } }