mod cache;
mod cursor;
mod entry;
mod filter;
#[cfg(test)]
pub(in crate::mvcc) mod read_counts;
mod reader;
#[cfg(test)]
mod tests;
#[cfg(test)]
pub(in crate::mvcc) mod write_counts;
mod writer;
pub(in crate::mvcc) use cache::RunCacheReader;
pub(in crate::mvcc) use cursor::RunCursor;
pub(in crate::mvcc) use entry::RunEntry;
pub(in crate::mvcc) use writer::SpilledRunWriter;
use std::io::{Read, Seek, SeekFrom};
use std::ops::Bound;
use std::sync::Arc;
use uqa_core::memory::{BudgetedVec, MemoryBudget};
use crate::read_control::StorageReadControl;
use crate::temporary_file::TemporaryFile;
use crate::mvcc::key::RecordKey;
use crate::mvcc::{VersionError, VersionResult};
use cache::RunCache;
use entry::ValueLocation;
use filter::KeyFilter;
const VALUE_CHUNK: u64 = 16 * 1024;
struct RunBlock {
first: RecordKey,
last: RecordKey,
offset: u64,
end: u64,
}
pub(in crate::mvcc) struct SpilledRun {
entries: TemporaryFile,
values: TemporaryFile,
blocks: BudgetedVec<RunBlock>,
filter: Option<KeyFilter>,
prefix_filter: Option<KeyFilter>,
last: RecordKey,
len: u64,
key_bytes: u64,
entry_bytes: u64,
bytes: u64,
kinds: u16,
cache: Arc<RunCache>,
}
impl SpilledRun {
#[cfg(test)]
pub(in crate::mvcc) fn read_io_counts(&self) -> (usize, usize) {
(
self.entries.block_io_counts().0,
self.values.block_io_counts().0,
)
}
#[cfg(test)]
pub(in crate::mvcc) fn file_blocks(&self) -> usize {
(self.entry_bytes.div_ceil(VALUE_CHUNK)
+ (self.bytes - self.entry_bytes).div_ceil(VALUE_CHUNK)) as usize
}
pub(in crate::mvcc) fn shares_allowance(&self, memory: &MemoryBudget) -> bool {
self.cache.memory().shares_allowance(memory)
}
pub(in crate::mvcc) fn cache_reader(&self) -> RunCacheReader {
self.cache.reader()
}
pub(in crate::mvcc) fn has_kind(&self, kind: crate::mvcc::commit::RecordWriteKind) -> bool {
self.kinds & (1 << kind.code()) != 0
}
pub(in crate::mvcc) fn typed(&self) -> bool {
self.kinds & !(1 << crate::mvcc::commit::RecordWriteKind::Canonical.code()) != 0
}
pub(in crate::mvcc) fn len(&self) -> u64 {
self.len
}
pub(in crate::mvcc) fn key_bytes(&self) -> u64 {
self.key_bytes
}
pub(super) fn bytes(&self) -> u64 {
self.bytes
}
pub(in crate::mvcc) fn get(
&self,
key: &[u8],
control: &StorageReadControl,
) -> VersionResult<Option<RunEntry>> {
control.cancellation().check()?;
if key > self.last.bytes()
|| self
.filter
.as_ref()
.is_some_and(|filter| !filter.may_contain(key))
{
return Ok(None);
}
let Some(index) = self.block_at_or_before(key) else {
return Ok(None);
};
if key > self.blocks[index].last.bytes() {
return Ok(None);
}
let mut block = reader::EntryReader::new(self, index, control)?;
block.select(control, |candidate| match candidate.cmp(key) {
std::cmp::Ordering::Less => reader::Selection::Skip,
std::cmp::Ordering::Equal => reader::Selection::Take,
std::cmp::Ordering::Greater => reader::Selection::End,
})
}
pub(super) fn last_before(
&self,
end: Bound<&[u8]>,
control: &StorageReadControl,
) -> VersionResult<Option<RunEntry>> {
control.cancellation().check()?;
let before = |key: &[u8]| match end {
Bound::Included(end) => key <= end,
Bound::Excluded(end) => key < end,
Bound::Unbounded => true,
};
let mut index = self
.blocks
.partition_point(|block| before(block.first.bytes()));
while index > 0 {
index -= 1;
let mut block = reader::EntryReader::new(self, index, control)?;
let mut found = None;
while let Some(entry) = block.select(control, |key| {
if before(key) {
reader::Selection::Take
} else {
reader::Selection::End
}
})? {
found = Some(entry);
}
if found.is_some() {
return Ok(found);
}
}
Ok(None)
}
pub(in crate::mvcc) fn cursor(self: &Arc<Self>, start: Bound<&[u8]>) -> RunCursor {
RunCursor::new(Arc::clone(self), start)
}
pub(super) fn intersects_prefix(&self, prefix: &[u8]) -> bool {
let index = self
.blocks
.partition_point(|block| block.last.bytes() < prefix);
self.blocks.get(index).is_some_and(|block| {
(block.first.bytes() <= prefix || block.first.bytes().starts_with(prefix))
&& (prefix.is_empty()
|| self
.prefix_filter
.as_ref()
.is_none_or(|filter| filter.may_contain(prefix)))
})
}
pub(in crate::mvcc) fn copy_value(
&self,
location: ValueLocation,
sink: &mut dyn FnMut(&[u8]) -> VersionResult<()>,
control: &StorageReadControl,
) -> VersionResult<()> {
if let Some((chunk, begin)) = self.value_chunk(location, control)? {
return sink(&chunk[begin..begin + location.len as usize]);
}
let mut reader = self.values.reopen().map_err(spill_error)?;
reader
.seek(SeekFrom::Start(location.offset))
.map_err(spill_error)?;
let mut chunk = [0_u8; VALUE_CHUNK as usize];
let mut remaining = location.len;
while remaining > 0 {
let count = remaining.min(chunk.len() as u64) as usize;
let count = read_chunk(&mut reader, &mut chunk[..count], control)?;
sink(&chunk[..count])?;
remaining -= count as u64;
}
Ok(())
}
pub(in crate::mvcc) fn load_value(
&self,
location: ValueLocation,
control: &StorageReadControl,
) -> VersionResult<Arc<BudgetedVec<u8>>> {
self.load_value_cached(location, None, control)
}
fn load_value_cached(
&self,
location: ValueLocation,
cache: Option<&mut cache::ValueCache>,
control: &StorageReadControl,
) -> VersionResult<Arc<BudgetedVec<u8>>> {
#[cfg(test)]
read_counts::value();
let mut value = BudgetedVec::new(control.memory());
if let Some((chunk, begin)) = self.value_chunk_cached(location, cache, control)? {
value.extend_from_slice(&chunk[begin..begin + location.len as usize])?;
return Ok(Arc::new(value));
}
read_range(
&self.values,
location.offset,
location.len,
control.memory(),
control,
)
.map(Arc::new)
}
fn value_chunk(
&self,
location: ValueLocation,
control: &StorageReadControl,
) -> VersionResult<Option<(Arc<BudgetedVec<u8>>, usize)>> {
self.value_chunk_cached(location, None, control)
}
fn value_chunk_cached(
&self,
location: ValueLocation,
mut cache: Option<&mut cache::ValueCache>,
control: &StorageReadControl,
) -> VersionResult<Option<(Arc<BudgetedVec<u8>>, usize)>> {
control.check()?;
let start = location.offset - location.offset % VALUE_CHUNK;
let cached = match &mut cache {
Some(cache) => cache.get(start),
None => self.cache.chunk(start),
};
if location.offset + location.len > start + VALUE_CHUNK {
return Ok(None);
}
let begin = (location.offset - start) as usize;
if let Some(chunk) = cached {
return Ok(Some((chunk, begin)));
}
let end = (start + VALUE_CHUNK).min(self.bytes - self.entry_bytes);
let available = self
.cache
.memory()
.available()
.min(control.memory().available());
if end - start > (available / 2) as u64 {
return Ok(None);
}
match read_range(
&self.values,
start,
end - start,
self.cache.memory(),
control,
) {
Ok(bytes) => {
let bytes = Arc::new(bytes);
match cache {
Some(cache) => cache.keep(start, &bytes),
None => self.cache.keep_chunk(start, &bytes),
}
Ok(Some((bytes, begin)))
}
Err(VersionError::Memory(_)) => Ok(None),
Err(error) => Err(error),
}
}
fn block_at_or_before(&self, key: &[u8]) -> Option<usize> {
self.blocks
.partition_point(|block| block.first.bytes() <= key)
.checked_sub(1)
}
fn first_block_from(&self, start: Bound<&[u8]>) -> usize {
match start {
Bound::Unbounded => 0,
Bound::Included(key) => self
.blocks
.partition_point(|block| block.last.bytes() < key),
Bound::Excluded(key) => self
.blocks
.partition_point(|block| block.last.bytes() <= key),
}
}
fn read_block(
&self,
index: usize,
control: &StorageReadControl,
) -> VersionResult<Arc<BudgetedVec<u8>>> {
if let Some(block) = self.cache.block(index) {
return Ok(block);
}
#[cfg(test)]
read_counts::block();
let block = &self.blocks[index];
let len = block.end - block.offset;
match read_range(
&self.entries,
block.offset,
len,
self.cache.memory(),
control,
) {
Ok(bytes) => {
let bytes = Arc::new(bytes);
self.cache.keep_block(index, &bytes);
Ok(bytes)
}
Err(VersionError::Memory(_)) => {
read_range(&self.entries, block.offset, len, control.memory(), control)
.map(Arc::new)
}
Err(error) => Err(error),
}
}
}
fn read_range(
file: &TemporaryFile,
offset: u64,
len: u64,
memory: &MemoryBudget,
control: &StorageReadControl,
) -> VersionResult<BudgetedVec<u8>> {
control.cancellation().check()?;
let len = usize::try_from(len)
.map_err(|_| VersionError::InvalidEncoding("spilled range exceeds the address space"))?;
let mut bytes = BudgetedVec::new(memory);
bytes.reserve(len)?;
let mut reader = file.reopen().map_err(spill_error)?;
reader.seek(SeekFrom::Start(offset)).map_err(spill_error)?;
let mut chunk = [0_u8; 16 * 1024];
while bytes.len() < len {
let count = (len - bytes.len()).min(chunk.len());
let count = read_chunk(&mut reader, &mut chunk[..count], control)?;
bytes.extend_from_slice(&chunk[..count])?;
}
Ok(bytes)
}
fn read_chunk(
reader: &mut TemporaryFile,
output: &mut [u8],
control: &StorageReadControl,
) -> VersionResult<usize> {
loop {
control.cancellation().check()?;
match reader.read(output) {
Ok(0) => {
return Err(spill_error(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"failed to fill whole buffer",
)))
}
Ok(count) => return Ok(count),
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => (),
Err(error) => return Err(spill_error(error)),
}
}
}
pub(super) fn spill_error(error: std::io::Error) -> VersionError {
crate::StorageBackendError::backend("transaction spill file", error).into()
}