mod cache;
mod cursor;
mod entry;
mod filter;
#[cfg(test)]
mod tests;
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,
offset: u64,
end: u64,
}
pub(in crate::mvcc) struct SpilledRun {
entries: TemporaryFile,
values: TemporaryFile,
blocks: BudgetedVec<RunBlock>,
filter: Option<KeyFilter>,
last: RecordKey,
len: u64,
entry_bytes: u64,
bytes: u64,
kinds: u16,
cache: Arc<RunCache>,
}
impl SpilledRun {
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(super) fn bytes(&self) -> u64 {
self.bytes
}
pub(in crate::mvcc) fn entry_bytes(&self) -> u64 {
self.entry_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);
};
let block = self.read_block(index, control)?;
let mut position = 0;
while position < block.len() {
let raw = entry::decode(&block, &mut position)?;
match raw.key.cmp(key) {
std::cmp::Ordering::Less => {}
std::cmp::Ordering::Equal => return raw.owned(control.memory()).map(Some),
std::cmp::Ordering::Greater => break,
}
}
Ok(None)
}
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 block = self.read_block(index, control)?;
let mut position = 0;
let mut found = None;
while position < block.len() {
let raw = entry::decode(&block, &mut position)?;
if !before(raw.key) {
break;
}
found = Some(raw.owned(control.memory())?);
}
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(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 {
control.cancellation().check()?;
let count = remaining.min(chunk.len() as u64) as usize;
reader
.read_exact(&mut chunk[..count])
.map_err(spill_error)?;
sink(&chunk[..count])?;
remaining -= count as u64;
}
Ok(())
}
pub(in crate::mvcc) fn load_value(
&self,
location: ValueLocation,
control: &StorageReadControl,
) -> VersionResult<Arc<BudgetedVec<u8>>> {
let mut value = BudgetedVec::new(control.memory());
if let Some((chunk, begin)) = self.value_chunk(location, 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)>> {
let start = location.offset - location.offset % VALUE_CHUNK;
if location.offset + location.len > start + VALUE_CHUNK {
return Ok(None);
}
let begin = (location.offset - start) as usize;
if let Some(chunk) = self.cache.chunk(start) {
return Ok(Some((chunk, begin)));
}
let end = (start + VALUE_CHUNK).min(self.bytes - self.entry_bytes);
match read_range(
&self.values,
start,
end - start,
self.cache.memory(),
control,
) {
Ok(bytes) => {
let bytes = Arc::new(bytes);
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) | Bound::Excluded(key) => {
self.block_at_or_before(key).unwrap_or(0)
}
}
}
fn read_block(
&self,
index: usize,
control: &StorageReadControl,
) -> VersionResult<Arc<BudgetedVec<u8>>> {
if let Some(block) = self.cache.block(index) {
return Ok(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 {
control.cancellation().check()?;
let count = (len - bytes.len()).min(chunk.len());
reader
.read_exact(&mut chunk[..count])
.map_err(spill_error)?;
bytes.extend_from_slice(&chunk[..count])?;
}
Ok(bytes)
}
pub(super) fn spill_error(error: std::io::Error) -> VersionError {
crate::StorageBackendError::backend("transaction spill file", error).into()
}