use std::borrow::Cow;
use std::cmp::Ordering;
use std::fmt;
use std::fs::{self, File};
use std::io;
use std::mem;
use std::os::unix::fs::FileExt;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, RwLock};
use thiserror::Error;
use seglog::parse::parse_record_ref;
use seglog::read::{ReadError, ReadHint, Reader, RecordKind};
use seglog::tail::{Tail, TailError};
use seglog::write::{WriteError, Writer};
use seglog::{COMMIT_MARKER_PAYLOAD, FlushedOffset, RECORD_HEAD_SIZE};
use crate::Position;
use crate::log::header::{HeaderError, SEGMENT_HEADER_SIZE, SegmentHeader};
use crate::log::lock::{self, DirLock, LockFailure};
const NAME_DIGITS: usize = 20;
const FIRST_POSITION: u64 = 1;
pub const RECORD_OVERHEAD: usize = RECORD_HEAD_SIZE;
pub const BATCH_OVERHEAD: usize = RECORD_HEAD_SIZE + COMMIT_MARKER_PAYLOAD;
#[derive(Clone, Copy, Debug)]
pub struct SegmentConfig {
pub segment_size: usize,
pub max_record_len: usize,
pub header_size: usize,
}
impl SegmentConfig {
pub fn new(segment_size: usize) -> Self {
SegmentConfig {
segment_size,
max_record_len: segment_size / 4,
header_size: SEGMENT_HEADER_SIZE,
}
}
pub fn validate(&self) -> Result<(), LogError> {
let invalid = |reason: String| Err(LogError::InvalidConfig { reason });
if self.segment_size <= self.header_size {
return invalid(format!(
"segment_size {} must exceed header_size {}",
self.segment_size, self.header_size
));
}
if self.segment_size > u32::MAX as usize {
return invalid(format!(
"segment_size {} exceeds u32::MAX; byte offsets are stored as u32",
self.segment_size
));
}
if self.max_record_len < RECORD_HEAD_SIZE {
return invalid(format!(
"max_record_len {} is smaller than a record header ({RECORD_HEAD_SIZE} bytes)",
self.max_record_len
));
}
let usable = self.segment_size - self.header_size;
let need = self.max_record_len + RECORD_HEAD_SIZE + COMMIT_MARKER_PAYLOAD;
if need > usable {
return invalid(format!(
"a max-size record plus its commit marker ({need} bytes) does not fit a \
segment's usable space ({usable} bytes)"
));
}
Ok(())
}
}
pub struct Segment {
base_position: Position,
path: PathBuf,
flushed_offset: FlushedOffset,
offsets: RwLock<Vec<u32>>,
reader: Mutex<Option<Reader<0>>>,
}
impl Segment {
pub fn base_position(&self) -> Position {
self.base_position
}
pub fn event_count(&self) -> u64 {
self.offsets.read().unwrap().len() as u64
}
pub(crate) fn data_offset(&self, local: usize) -> Option<u32> {
self.offsets.read().unwrap().get(local).copied()
}
pub(crate) fn open_reader(&self) -> Result<Reader<0>, LogError> {
Reader::<0>::open_read_only(&self.path, Some(self.flushed_offset.clone()))
.map_err(|source| LogError::read(&self.path, source))
}
pub(crate) fn read_at_local(
&self,
reader: &mut Reader<0>,
local: usize,
) -> Result<Option<Record>, LogError> {
let Some(offset) = self.data_offset(local) else {
return Ok(None);
};
let record = reader
.read_record(offset as u64, ReadHint::Random)
.map_err(|source| LogError::read(&self.path, source))?;
Ok(Some(Record {
position: Position::new(self.base_position.get() + local as u64),
data: record.data.into_owned(),
}))
}
}
pub trait SegmentSource {
fn header_size(&self) -> u64;
fn segment_count(&self) -> usize;
fn segment_at(&self, idx: usize) -> Option<&Arc<Segment>>;
fn locate(&self, pos: Position) -> Option<(usize, &Arc<Segment>)> {
if pos == Position::ZERO {
return None;
}
let active_idx = self.segment_count();
if let Some(active) = self.segment_at(active_idx)
&& pos >= active.base_position
{
return Some((active_idx, active));
}
let mut lo = 0usize;
let mut hi = active_idx; let mut found = None;
while lo < hi {
let mid = lo + (hi - lo) / 2;
let base = self.segment_at(mid)?.base_position;
if base <= pos {
found = Some(mid);
lo = mid + 1;
} else {
hi = mid;
}
}
found.and_then(|idx| self.segment_at(idx).map(|seg| (idx, seg)))
}
}
impl fmt::Debug for Segment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Segment")
.field("base_position", &self.base_position)
.field("path", &self.path)
.field("event_count", &self.offsets.read().unwrap().len())
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Record {
pub position: Position,
pub data: Vec<u8>,
}
#[derive(Clone, Copy, Debug)]
pub struct RecordRef<'a> {
pub position: Position,
pub data: &'a [u8],
}
impl RecordRef<'_> {
pub fn to_owned(&self) -> Record {
Record {
position: self.position,
data: self.data.to_vec(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PositionRange {
pub first: Position,
pub last: Position,
}
impl PositionRange {
pub fn count(&self) -> u64 {
(self.last - self.first) + 1
}
}
#[derive(Debug)]
enum ActiveEnd {
Writer(Writer<0>),
Follower(Tail),
}
#[derive(Debug)]
pub struct SegmentSet {
dir: PathBuf,
config: SegmentConfig,
sealed: Vec<Arc<Segment>>,
active: Arc<Segment>,
active_end: ActiveEnd,
next_position: Position,
_lock: Option<DirLock>,
}
impl SegmentSet {
pub fn open(dir: impl AsRef<Path>, config: SegmentConfig) -> Result<Self, LogError> {
config.validate()?;
let dir = dir.as_ref().to_path_buf();
if !dir.exists() {
fs::create_dir_all(&dir).map_err(|source| LogError::io(&dir, source))?;
if let Some(parent) = dir.parent()
&& !parent.as_os_str().is_empty()
{
sync_dir(parent).map_err(|source| LogError::io(parent, source))?;
}
}
let lock = Some(acquire_lock(&dir)?);
let SegmentSplit { valid, unready } = split_trailing_unready(list_segments(&dir)?, true)?;
for path in &unready {
#[cfg(feature = "tracing")]
tracing::warn!(
"deleting unwritten trailing segment {path:?} (creation did not finish)"
);
fs::remove_file(path).map_err(|source| LogError::io(path, source))?;
sync_dir(&dir).map_err(|source| LogError::io(&dir, source))?;
}
if valid.is_empty() {
let (writer, active) =
Self::create_segment(&dir, &config, Position::new(FIRST_POSITION))?;
#[cfg(feature = "tracing")]
tracing::trace!("initialized empty segment set at {dir:?}");
return Ok(SegmentSet {
dir,
config,
sealed: Vec::new(),
active,
active_end: ActiveEnd::Writer(writer),
next_position: Position::new(FIRST_POSITION),
_lock: lock,
});
}
let (active_entry, sealed_entries) = valid.split_last().unwrap();
let (sealed, expected_base) = open_sealed_chain(sealed_entries, config.header_size as u64)?;
let (active_base, active_path) = active_entry;
if *active_base != expected_base {
return Err(LogError::NonContiguous {
path: active_path.clone(),
found: *active_base,
expected: expected_base,
});
}
let mut writer =
Writer::<0>::open(active_path, config.segment_size, config.header_size as u64)
.map_err(|source| LogError::write(active_path, source))?;
configure_writer(&mut writer, &config);
#[cfg(feature = "tracing")]
{
let committed = writer.write_offset();
if trailing_bytes_present(&writer, committed, config.segment_size) {
tracing::warn!(
"segment {active_path:?} recovered with rollback, discarding bytes from offset {committed}"
);
} else {
tracing::trace!("segment {active_path:?} opened cleanly at offset {committed}");
}
}
let flushed = writer.flushed_offset();
let offsets = scan_offsets(active_path, flushed.clone(), config.header_size as u64)?;
let count = offsets.len() as u64;
if let Some(highest) = writer.last_committed_position()
&& highest + 1 != *active_base + count
{
return Err(LogError::PositionMismatch {
path: active_path.clone(),
found: Position::new(highest),
expected: Position::new(*active_base + count - 1),
});
}
let next_position = Position::new(*active_base + count);
let active = Arc::new(Segment {
base_position: *active_base,
path: active_path.clone(),
flushed_offset: flushed,
offsets: RwLock::new(offsets),
reader: Mutex::new(None),
});
Ok(SegmentSet {
dir,
config,
sealed,
active,
active_end: ActiveEnd::Writer(writer),
next_position,
_lock: lock,
})
}
pub fn open_read_only(dir: impl AsRef<Path>, config: SegmentConfig) -> Result<Self, LogError> {
config.validate()?;
let dir = dir.as_ref().to_path_buf();
let SegmentSplit { valid, unready } = split_trailing_unready(list_segments(&dir)?, false)?;
if valid.is_empty() {
return Err(LogError::Uninitialized { dir });
}
#[cfg(feature = "tracing")]
for path in &unready {
tracing::debug!("segment {path:?} has no readable header yet; skipping for now");
}
let _ = unready;
let (active_entry, sealed_entries) = valid.split_last().unwrap();
let (sealed, expected_base) = open_sealed_chain(sealed_entries, config.header_size as u64)?;
let (active_base, active_path) = active_entry;
if *active_base != expected_base {
return Err(LogError::NonContiguous {
path: active_path.clone(),
found: *active_base,
expected: expected_base,
});
}
let (tail, active, next_position) =
Self::follow_segment(active_path, *active_base, &config)?;
#[cfg(feature = "tracing")]
tracing::debug!("following segment set at {dir:?} from position {next_position}");
Ok(SegmentSet {
dir,
config,
sealed,
active,
active_end: ActiveEnd::Follower(tail),
next_position,
_lock: None,
})
}
fn follow_segment(
path: &Path,
base: Position,
config: &SegmentConfig,
) -> Result<(Tail, Arc<Segment>, Position), LogError> {
let mut tail = Tail::open(path, config.header_size as u64)
.map_err(|source| LogError::tail(path, source))?;
let mut offsets = Vec::new();
let progress = tail
.poll(|offset| {
offsets.push(
u32::try_from(offset)
.expect("segment_size <= u32::MAX enforced by SegmentConfig::validate"),
);
})
.map_err(|source| LogError::tail(path, source))?;
let count = offsets.len() as u64;
if let Some(highest) = progress.last_position
&& highest + 1 != base.get() + count
{
return Err(LogError::PositionMismatch {
path: path.to_path_buf(),
found: Position::new(highest),
expected: Position::new(base.get() + count - 1),
});
}
let segment = Arc::new(Segment {
base_position: base,
path: path.to_path_buf(),
flushed_offset: tail.flushed_offset(),
offsets: RwLock::new(offsets),
reader: Mutex::new(None),
});
Ok((tail, segment, Position::new(base.get() + count)))
}
pub fn refresh(&mut self) -> Result<Refreshed, LogError> {
if !self.is_read_only() {
return Err(LogError::ReadOnly {
dir: self.dir.clone(),
op: "refresh",
});
}
let mut sealed_added = 0;
self.drain_active()?;
let SegmentSplit { valid, unready } =
split_trailing_unready(list_segments(&self.dir)?, false)?;
let pending_segment = unready.into_iter().next_back();
for (base, path) in valid {
if base <= self.active.base_position {
continue;
}
self.drain_active()?;
let expected =
Position::new(self.active.base_position.get() + self.active.event_count());
if base != expected {
return Err(LogError::NonContiguous {
path,
found: base,
expected,
});
}
let (tail, new_active, next_position) =
Self::follow_segment(&path, base, &self.config)?;
let old_active = mem::replace(&mut self.active, new_active);
self.sealed.push(old_active);
self.active_end = ActiveEnd::Follower(tail);
self.next_position = next_position;
sealed_added += 1;
#[cfg(feature = "tracing")]
tracing::debug!("follower adopted segment with base_position {base}");
}
Ok(Refreshed {
tip: self.last_position(),
sealed_added,
pending_segment,
})
}
fn drain_active(&mut self) -> Result<u64, LogError> {
let ActiveEnd::Follower(tail) = &mut self.active_end else {
return Err(LogError::ReadOnly {
dir: self.dir.clone(),
op: "refresh",
});
};
let mut fresh = Vec::new();
let progress = tail
.poll(|offset| {
fresh.push(
u32::try_from(offset)
.expect("segment_size <= u32::MAX enforced by SegmentConfig::validate"),
);
})
.map_err(|source| LogError::tail(&self.active.path, source))?;
let added = progress.committed_records;
if added > 0 {
self.active
.offsets
.write()
.unwrap()
.extend_from_slice(&fresh);
self.next_position = Position::new(self.next_position.get() + added);
}
let base = self.active.base_position.get();
let count = self.active.event_count();
if let Some(highest) = progress.last_position
&& highest + 1 != base + count
{
return Err(LogError::PositionMismatch {
path: self.active.path.clone(),
found: Position::new(highest),
expected: Position::new(base + count - 1),
});
}
Ok(added)
}
fn writer_mut(&mut self) -> Result<&mut Writer<0>, LogError> {
match &mut self.active_end {
ActiveEnd::Writer(writer) => Ok(writer),
ActiveEnd::Follower(_) => Err(LogError::ReadOnly {
dir: self.dir.clone(),
op: "append",
}),
}
}
pub fn is_read_only(&self) -> bool {
matches!(self.active_end, ActiveEnd::Follower(_))
}
pub fn append_batch(&mut self, records: &[&[u8]]) -> Result<PositionRange, LogError> {
if records.is_empty() {
return Err(LogError::EmptyBatch);
}
for record in records {
if record.is_empty() {
return Err(LogError::EmptyRecord);
}
let record_len = RECORD_HEAD_SIZE + record.len();
if record_len > self.config.max_record_len {
return Err(LogError::RecordTooLarge {
size: record_len,
max: self.config.max_record_len,
});
}
}
let records_len: usize = records.iter().map(|r| RECORD_HEAD_SIZE + r.len()).sum();
let total_size = records_len + RECORD_HEAD_SIZE + COMMIT_MARKER_PAYLOAD;
let capacity = self.config.segment_size - self.config.header_size;
if total_size > capacity {
return Err(LogError::BatchTooLarge {
size: total_size,
capacity,
});
}
if total_size as u64 > self.writer_mut()?.remaining_bytes() {
self.rollover()?;
}
let first = self.next_position;
let last = Position::new(first + records.len() as u64 - 1);
let writer = self.writer_mut()?;
let rewind_to = writer.write_offset();
let mut new_offsets = Vec::with_capacity(records.len());
let outcome = (|| -> Result<(), WriteError> {
for record in records {
let (offset, _len) = writer.append_data(record)?;
new_offsets.push(
u32::try_from(offset)
.expect("segment_size <= u32::MAX enforced by SegmentConfig::validate"),
);
}
writer.commit(last.get()).map(|_| ())
})();
if let Err(err) = outcome {
let rewound = writer.rewind_to(rewind_to);
let path = &self.active.path;
rewound.map_err(|source| LogError::write(path, source))?;
return Err(LogError::write(path, err));
}
self.active
.offsets
.write()
.unwrap()
.extend_from_slice(&new_offsets);
self.next_position = last.next();
Ok(PositionRange { first, last })
}
fn rollover(&mut self) -> Result<(), LogError> {
if let Err(source) = self.writer_mut()?.sync() {
return Err(LogError::write(&self.active.path, source));
}
let (writer, new_active) =
Self::create_segment(&self.dir, &self.config, self.next_position)?;
seglog::crash_point!("segment_created_before_commit");
let old_active = mem::replace(&mut self.active, new_active);
self.sealed.push(old_active);
self.active_end = ActiveEnd::Writer(writer);
#[cfg(feature = "tracing")]
tracing::trace!(
"rolled over to segment with base_position {}",
self.next_position
);
Ok(())
}
fn create_segment(
dir: &Path,
config: &SegmentConfig,
base: Position,
) -> Result<(Writer<0>, Arc<Segment>), LogError> {
let path = dir.join(segment_file_name(base));
let mut writer = Writer::<0>::create(&path, config.segment_size, config.header_size as u64)
.map_err(|source| LogError::write(&path, source))?;
configure_writer(&mut writer, config);
let header = SegmentHeader::new(base);
writer
.file()
.write_all_at(&header.to_bytes(), 0)
.map_err(|source| LogError::io(&path, source))?;
writer
.file()
.sync_all()
.map_err(|source| LogError::io(&path, source))?;
let segment = Arc::new(Segment {
base_position: base,
path,
flushed_offset: writer.flushed_offset(),
offsets: RwLock::new(Vec::new()),
reader: Mutex::new(None),
});
Ok((writer, segment))
}
pub fn read_at(&self, pos: Position) -> Result<Record, LogError> {
let segment = self
.segment_for(pos)
.ok_or(LogError::NotFound { position: pos })?;
let local = pos.offset_from(segment.base_position) as usize;
let offset = {
let offsets = segment.offsets.read().unwrap();
match offsets.get(local) {
Some(offset) => *offset as u64,
None => return Err(LogError::NotFound { position: pos }),
}
};
let mut guard = segment.reader.lock().unwrap();
if guard.is_none() {
*guard = Some(segment.open_reader()?);
}
let record = guard
.as_mut()
.unwrap()
.read_record(offset, ReadHint::Random)
.map_err(|source| LogError::read(&segment.path, source))?;
Ok(Record {
position: pos,
data: record.data.into_owned(),
})
}
pub fn scan_from(&self, pos: Position) -> Scan<&SegmentSet> {
self.scan_at(pos.max(Position::new(FIRST_POSITION)))
}
pub fn scan_after(&self, pos: Position) -> Scan<&SegmentSet> {
self.scan_at(Position::new(pos.get().saturating_add(1)))
}
pub fn scan_back(&self, first: Position, upto: Position) -> ScanBack<&SegmentSet> {
ScanBack::start(self, first, upto.min(self.last_position()))
}
fn scan_at(&self, first: Position) -> Scan<&SegmentSet> {
Scan::start(self, first, self.last_position())
}
pub fn last_position(&self) -> Position {
Position::new(self.next_position - 1)
}
pub fn next_position(&self) -> Position {
self.next_position
}
pub fn segment_capacity(&self) -> usize {
self.config.segment_size - self.config.header_size
}
pub fn max_record_len(&self) -> usize {
self.config.max_record_len
}
pub fn sealed_len(&self) -> usize {
self.sealed.len()
}
pub fn sealed_arcs(&self) -> &[Arc<Segment>] {
&self.sealed
}
pub fn active_arc(&self) -> Arc<Segment> {
Arc::clone(&self.active)
}
pub fn dir(&self) -> &Path {
&self.dir
}
pub fn active_base(&self) -> Position {
self.active.base_position
}
pub fn sealed_segments(&self) -> impl Iterator<Item = (Position, u64)> + '_ {
self.sealed
.iter()
.map(|s| (s.base_position(), s.event_count()))
}
pub fn segment_for(&self, pos: Position) -> Option<&Arc<Segment>> {
if pos >= self.next_position {
return None;
}
self.locate(pos).map(|(_, segment)| segment)
}
}
impl SegmentSource for SegmentSet {
fn header_size(&self) -> u64 {
self.config.header_size as u64
}
fn segment_count(&self) -> usize {
self.sealed.len()
}
fn segment_at(&self, idx: usize) -> Option<&Arc<Segment>> {
match idx.cmp(&self.sealed.len()) {
Ordering::Less => Some(&self.sealed[idx]),
Ordering::Equal => Some(&self.active),
Ordering::Greater => None,
}
}
}
macro_rules! forward_segment_source {
($wrapper:ty) => {
impl<T: SegmentSource + ?Sized> SegmentSource for $wrapper {
fn header_size(&self) -> u64 {
(**self).header_size()
}
fn segment_count(&self) -> usize {
(**self).segment_count()
}
fn segment_at(&self, idx: usize) -> Option<&Arc<Segment>> {
(**self).segment_at(idx)
}
}
};
}
forward_segment_source!(&T);
forward_segment_source!(Arc<T>);
pub struct Scan<S: SegmentSource> {
source: S,
seg_idx: usize,
offset: u64,
position: Position,
upto: Position,
reader: Option<Reader<0>>,
pending_err: Option<LogError>,
done: bool,
}
impl<S: SegmentSource> Scan<S> {
pub(crate) fn start(source: S, first: Position, upto: Position) -> Self {
if first == Position::ZERO || first > upto {
return Scan::empty(source);
}
let (seg_idx, offset) = match source.locate(first) {
Some((seg_idx, segment)) => {
let local = first.offset_from(segment.base_position) as usize;
match segment.data_offset(local) {
Some(offset) => (seg_idx, offset),
None => return Scan::failed(source, LogError::NotFound { position: first }),
}
}
None => return Scan::failed(source, LogError::NotFound { position: first }),
};
let reader = match source.segment_at(seg_idx).unwrap().open_reader() {
Ok(reader) => reader,
Err(err) => return Scan::failed(source, err),
};
Scan {
source,
seg_idx,
offset: offset as u64,
position: first,
upto,
reader: Some(reader),
pending_err: None,
done: false,
}
}
fn empty(source: S) -> Self {
Scan {
source,
seg_idx: 0,
offset: 0,
position: Position::ZERO,
upto: Position::ZERO,
reader: None,
pending_err: None,
done: true,
}
}
fn failed(source: S, err: LogError) -> Self {
Scan {
pending_err: Some(err),
done: false,
..Scan::empty(source)
}
}
fn advance_segment(&mut self) -> Result<bool, LogError> {
let next_idx = self.seg_idx + 1;
let Some(segment) = self.source.segment_at(next_idx) else {
return Ok(false);
};
self.reader = Some(segment.open_reader()?);
self.offset = self.source.header_size();
self.seg_idx = next_idx;
Ok(true)
}
}
impl<S: SegmentSource> Scan<S> {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<Result<RecordRef<'_>, LogError>> {
if let Some(err) = self.pending_err.take() {
self.done = true;
return Some(Err(err));
}
if self.done {
return None;
}
if self.position > self.upto {
self.done = true;
return None;
}
let total_len = match self.position_at_data() {
Ok(Some(total_len)) => total_len,
Ok(None) => {
self.done = true;
return None;
}
Err(err) => {
self.done = true;
return Some(Err(err));
}
};
let position = self.position;
let offset = self.offset;
self.offset = offset + total_len as u64;
self.position = position.next();
let source = &self.source;
let seg_idx = self.seg_idx;
let reader = self.reader.as_mut().unwrap();
match reader.read_record(offset, ReadHint::Sequential) {
Ok(record) => {
let data = match record.data {
Cow::Borrowed(bytes) => bytes,
Cow::Owned(_) => {
unreachable!(
"sequential reads borrow the read-ahead buffer \
(seglog::test_sequential_read_borrows_even_large_records)"
)
}
};
Some(Ok(RecordRef { position, data }))
}
Err(err) => {
self.done = true;
Some(Err(LogError::read(scan_segment_path(source, seg_idx), err)))
}
}
}
fn position_at_data(&mut self) -> Result<Option<usize>, LogError> {
loop {
if self.reader.is_none() && !self.advance_segment()? {
return Ok(None);
}
let seg_idx = self.seg_idx;
let path = scan_segment_path(&self.source, seg_idx);
let reader = self.reader.as_mut().unwrap();
let kind = reader
.peek(self.offset)
.map_err(|err| LogError::read(path, err))?;
match kind {
RecordKind::Data { total_len } => return Ok(Some(total_len)),
RecordKind::Control { total_len } => self.offset += total_len as u64,
RecordKind::End => self.reader = None, }
}
}
}
fn scan_segment_path<S: SegmentSource>(source: &S, idx: usize) -> PathBuf {
source
.segment_at(idx)
.map(|segment| segment.path.clone())
.unwrap_or_default()
}
const REVERSE_WINDOW_BYTES: usize = 64 * 1024;
pub struct ScanBack<S: SegmentSource> {
source: S,
first: Position,
position: Position,
window_bytes: usize,
segment: Option<Arc<Segment>>,
seg_idx: usize,
seg_base: Position,
seg_count: u64,
reader: Option<Reader<0>>,
flushed_len: u64,
window: Vec<u8>,
win_base_byte: u64,
win_lo_local: Option<u64>,
pending_err: Option<LogError>,
done: bool,
}
impl<S: SegmentSource> ScanBack<S> {
pub(crate) fn start(source: S, first: Position, upto: Position) -> Self {
Self::start_with_window(source, first, upto, REVERSE_WINDOW_BYTES)
}
pub(crate) fn start_with_window(
source: S,
first: Position,
upto: Position,
window_bytes: usize,
) -> Self {
let first = Position::new(first.get().max(FIRST_POSITION));
let done = upto == Position::ZERO || first > upto;
ScanBack {
source,
first,
position: upto,
window_bytes: window_bytes.max(1),
segment: None,
seg_idx: 0,
seg_base: Position::ZERO,
seg_count: 0,
reader: None,
flushed_len: 0,
window: Vec::new(),
win_base_byte: 0,
win_lo_local: None,
pending_err: None,
done,
}
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<Result<RecordRef<'_>, LogError>> {
if let Some(err) = self.pending_err.take() {
self.done = true;
return Some(Err(err));
}
if self.done || self.position.get() < self.first.get() {
self.done = true;
return None;
}
if let Err(err) = self.ensure_segment() {
self.done = true;
return Some(Err(err));
}
if self.done {
return None; }
if let Err(err) = self.ensure_window() {
self.done = true;
return Some(Err(err));
}
let position = self.position;
let local = (position.get() - self.seg_base.get()) as usize;
let byte = match self.segment.as_ref().unwrap().data_offset(local) {
Some(byte) => byte as u64,
None => {
self.done = true;
return Some(Err(LogError::NotFound { position }));
}
};
let rel = (byte - self.win_base_byte) as usize;
let seg_idx = self.seg_idx;
self.position = Position::new(position.get() - 1);
match parse_record_ref::<0>(&self.window, rel) {
Ok((data, _)) => Some(Ok(RecordRef { position, data })),
Err(err) => {
self.done = true;
Some(Err(LogError::read(
scan_segment_path(&self.source, seg_idx),
err,
)))
}
}
}
fn ensure_segment(&mut self) -> Result<(), LogError> {
if self.reader.is_some() && self.position.get() >= self.seg_base.get() {
return Ok(());
}
let located = self
.source
.locate(self.position)
.map(|(idx, seg)| (idx, Arc::clone(seg)));
match located {
Some((idx, seg)) => {
let reader = seg.open_reader()?;
self.flushed_len = reader.flushed_offset().load();
self.seg_base = seg.base_position();
self.seg_count = seg.event_count();
self.seg_idx = idx;
self.reader = Some(reader);
self.segment = Some(seg);
self.win_lo_local = None; Ok(())
}
None => {
self.done = true;
Ok(())
}
}
}
fn ensure_window(&mut self) -> Result<(), LogError> {
let h = self.position.get() - self.seg_base.get();
if let Some(lo) = self.win_lo_local
&& h >= lo
{
return Ok(());
}
let seg = self.segment.clone().unwrap();
let head_off = seg.data_offset(h as usize).ok_or(LogError::NotFound {
position: self.position,
})? as u64;
let end = if h + 1 < self.seg_count {
seg.data_offset((h + 1) as usize)
.map(|off| off as u64)
.unwrap_or(self.flushed_len)
.min(self.flushed_len)
} else {
let reader = self.reader.as_mut().unwrap();
match reader
.peek(head_off)
.map_err(|err| LogError::read(scan_segment_path(&self.source, self.seg_idx), err))?
{
RecordKind::Data { total_len } => {
(head_off + total_len as u64).min(self.flushed_len)
}
_ => {
return Err(LogError::NotFound {
position: self.position,
});
}
}
};
let stop_local = if self.first.get() > self.seg_base.get() {
self.first.get() - self.seg_base.get()
} else {
0
};
let mut lo = h;
let mut lo_byte = head_off;
while lo > stop_local {
let Some(cand) = seg.data_offset((lo - 1) as usize) else {
break;
};
let cand = cand as u64;
if end - cand > self.window_bytes as u64 {
break;
}
lo -= 1;
lo_byte = cand;
}
let read_len = (end - lo_byte) as usize;
self.window.resize(read_len, 0);
{
let reader = self.reader.as_ref().unwrap();
reader
.read_bytes(lo_byte, &mut self.window)
.map_err(|err| {
LogError::read(scan_segment_path(&self.source, self.seg_idx), err)
})?;
if lo_byte > 0 {
reader.prefetch(lo_byte - 1);
}
}
self.win_base_byte = lo_byte;
self.win_lo_local = Some(lo);
Ok(())
}
}
#[derive(Debug, Error)]
pub enum LogError {
#[error("invalid segment config: {reason}")]
InvalidConfig { reason: String },
#[error("i/o error at {path:?}: {source}")]
Io {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("segment header error in {path:?}: {source}")]
Header {
path: PathBuf,
#[source]
source: HeaderError,
},
#[error(
"segment {path:?}: header base_position {header} disagrees with filename position {name}"
)]
BasePositionMismatch {
path: PathBuf,
header: Position,
name: Position,
},
#[error("unwritten segment {path:?} is not the last segment; refusing to open")]
UnwrittenNonLast { path: PathBuf },
#[error("non-contiguous segments: {path:?} has base_position {found}, expected {expected}")]
NonContiguous {
path: PathBuf,
found: Position,
expected: Position,
},
#[error(
"recovered commit position {found} disagrees with event count (expected highest {expected}) in {path:?}"
)]
PositionMismatch {
path: PathBuf,
found: Position,
expected: Position,
},
#[error("record of {size} bytes exceeds the maximum record length of {max} bytes")]
RecordTooLarge { size: usize, max: usize },
#[error("batch of {size} bytes cannot fit in a segment (capacity {capacity} bytes)")]
BatchTooLarge { size: usize, capacity: usize },
#[error("empty batch")]
EmptyBatch,
#[error("empty record")]
EmptyRecord,
#[error("position {position} not found")]
NotFound { position: Position },
#[error("write error at {path:?}: {source}")]
Write {
path: PathBuf,
#[source]
source: WriteError,
},
#[error("read error at {path:?}: {source}")]
Read {
path: PathBuf,
#[source]
source: ReadError,
},
#[error("tail error at {path:?}: {source}")]
Tail {
path: PathBuf,
#[source]
source: TailError,
},
#[error(
"another handle already holds the write lock on {path:?}; a data directory takes one \
writer at a time"
)]
Locked {
path: PathBuf,
holder: Option<u32>,
},
#[error("the segment set at {dir:?} is open read-only; {op} is not permitted")]
ReadOnly { dir: PathBuf, op: &'static str },
#[error("no readable segment in {dir:?}; a read-only open needs an initialized store")]
Uninitialized { dir: PathBuf },
}
impl LogError {
fn io(path: impl AsRef<Path>, source: io::Error) -> Self {
LogError::Io {
path: path.as_ref().to_path_buf(),
source,
}
}
fn write(path: impl AsRef<Path>, source: WriteError) -> Self {
LogError::Write {
path: path.as_ref().to_path_buf(),
source,
}
}
fn read(path: impl AsRef<Path>, source: ReadError) -> Self {
LogError::Read {
path: path.as_ref().to_path_buf(),
source,
}
}
fn tail(path: impl AsRef<Path>, source: TailError) -> Self {
LogError::Tail {
path: path.as_ref().to_path_buf(),
source,
}
}
}
#[derive(Clone, Debug)]
pub struct Refreshed {
pub tip: Position,
pub sealed_added: usize,
pub pending_segment: Option<PathBuf>,
}
enum Headerness {
Ready,
Unwritten,
Unusable(LogError),
}
fn classify_header(path: &Path, name_base: Position) -> Result<Headerness, LogError> {
let Some(buf) = read_header(path)? else {
return Ok(Headerness::Unwritten);
};
match SegmentHeader::from_bytes(&buf) {
Ok(header) if header.base_position == name_base => Ok(Headerness::Ready),
Ok(header) => Ok(Headerness::Unusable(LogError::BasePositionMismatch {
path: path.to_path_buf(),
header: header.base_position,
name: name_base,
})),
Err(HeaderError::Unwritten) => Ok(Headerness::Unwritten),
Err(source) => Ok(Headerness::Unusable(LogError::Header {
path: path.to_path_buf(),
source,
})),
}
}
fn list_segments(dir: &Path) -> Result<Vec<(Position, PathBuf)>, LogError> {
let mut entries: Vec<(Position, PathBuf)> = Vec::new();
for entry in fs::read_dir(dir).map_err(|source| LogError::io(dir, source))? {
let entry = entry.map_err(|source| LogError::io(dir, source))?;
if let Some(base) = parse_base_position(&entry.file_name().to_string_lossy()) {
entries.push((base, entry.path()));
}
}
entries.sort_by_key(|(base, _)| *base);
Ok(entries)
}
struct SegmentSplit {
valid: Vec<(Position, PathBuf)>,
unready: Vec<PathBuf>,
}
fn split_trailing_unready(
entries: Vec<(Position, PathBuf)>,
strict_headers: bool,
) -> Result<SegmentSplit, LogError> {
let mut state = Vec::with_capacity(entries.len());
for (base, path) in &entries {
let headerness = classify_header(path, *base)?;
if strict_headers && let Headerness::Unusable(err) = headerness {
return Err(err);
}
state.push(headerness);
}
let last_ready = (0..state.len())
.rev()
.find(|&i| matches!(state[i], Headerness::Ready));
let mut valid = Vec::new();
let mut unready = Vec::new();
for (i, ((base, path), headerness)) in entries.into_iter().zip(state).enumerate() {
if matches!(headerness, Headerness::Ready) {
valid.push((base, path));
continue;
}
if last_ready.is_none_or(|lr| i > lr) {
unready.push(path);
continue;
}
return Err(match headerness {
Headerness::Unwritten => LogError::UnwrittenNonLast { path },
Headerness::Unusable(err) => err,
Headerness::Ready => unreachable!("handled above"),
});
}
Ok(SegmentSplit { valid, unready })
}
fn segment_file_name(base: Position) -> String {
format!("{:0width$}.log", base.get(), width = NAME_DIGITS)
}
fn parse_base_position(name: &str) -> Option<Position> {
let stem = name.strip_suffix(".log")?;
if stem.len() != NAME_DIGITS || !stem.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
stem.parse::<u64>().ok().map(Position::new)
}
fn read_header(path: &Path) -> Result<Option<[u8; SEGMENT_HEADER_SIZE]>, LogError> {
let file = File::open(path).map_err(|source| LogError::io(path, source))?;
let mut buf = [0u8; SEGMENT_HEADER_SIZE];
match file.read_exact_at(&mut buf, 0) {
Ok(()) => Ok(Some(buf)),
Err(source) if source.kind() == io::ErrorKind::UnexpectedEof => Ok(None),
Err(source) => Err(LogError::io(path, source)),
}
}
fn scan_offsets(
path: &Path,
flushed: FlushedOffset,
header_size: u64,
) -> Result<Vec<u32>, LogError> {
let mut reader = Reader::<0>::open_read_only(path, Some(flushed))
.map_err(|source| LogError::read(path, source))?;
let mut offsets = Vec::new();
let mut iter = reader.iter(header_size);
while let Some(record) = iter
.next_record()
.map_err(|source| LogError::read(path, source))?
{
offsets.push(
u32::try_from(record.offset)
.expect("segment_size <= u32::MAX enforced by SegmentConfig::validate"),
);
}
Ok(offsets)
}
fn open_sealed_chain(
sealed_entries: &[(Position, PathBuf)],
header_size: u64,
) -> Result<(Vec<Arc<Segment>>, Position), LogError> {
let mut sealed = Vec::with_capacity(sealed_entries.len());
let mut expected_base = Position::new(FIRST_POSITION);
for (base, path) in sealed_entries {
if *base != expected_base {
return Err(LogError::NonContiguous {
path: path.clone(),
found: *base,
expected: expected_base,
});
}
let scanned = scan_committed(path, header_size)?;
let count = scanned.offsets.len() as u64;
if let Some(highest) = scanned.last_position
&& highest + 1 != *base + count
{
return Err(LogError::PositionMismatch {
path: path.clone(),
found: Position::new(highest),
expected: Position::new(*base + count - 1),
});
}
expected_base = Position::new(*base + count);
sealed.push(Arc::new(Segment {
base_position: *base,
path: path.clone(),
flushed_offset: scanned.flushed,
offsets: RwLock::new(scanned.offsets),
reader: Mutex::new(None),
}));
}
Ok((sealed, expected_base))
}
struct Scanned {
offsets: Vec<u32>,
flushed: FlushedOffset,
last_position: Option<u64>,
}
fn scan_committed(path: &Path, header_size: u64) -> Result<Scanned, LogError> {
let mut tail = Tail::open(path, header_size).map_err(|source| LogError::tail(path, source))?;
let mut offsets = Vec::new();
let progress = tail
.poll(|offset| {
offsets.push(
u32::try_from(offset)
.expect("segment_size <= u32::MAX enforced by SegmentConfig::validate"),
);
})
.map_err(|source| LogError::tail(path, source))?;
Ok(Scanned {
offsets,
flushed: tail.flushed_offset(),
last_position: progress.last_position,
})
}
fn configure_writer(writer: &mut Writer<0>, config: &SegmentConfig) {
writer.set_max_record(config.max_record_len);
}
#[cfg(feature = "tracing")]
fn trailing_bytes_present(writer: &Writer<0>, offset: u64, segment_size: usize) -> bool {
if offset + RECORD_HEAD_SIZE as u64 > segment_size as u64 {
return false;
}
let mut head = [0u8; RECORD_HEAD_SIZE];
writer.file().read_exact_at(&mut head, offset).is_ok() && head.iter().any(|&b| b != 0)
}
fn sync_dir(dir: &Path) -> io::Result<()> {
File::open(dir)?.sync_all()
}
fn acquire_lock(dir: &Path) -> Result<DirLock, LogError> {
match lock::acquire(dir) {
Ok(acquired) => {
if let Some(_source) = acquired.unsupported {
#[cfg(feature = "tracing")]
tracing::warn!(
"{dir:?}: this filesystem does not implement record locking ({_source}); \
opening without cross-process single-writer protection"
);
}
Ok(acquired.lock)
}
Err(LockFailure::Contended { path, holder }) => Err(LogError::Locked { path, holder }),
Err(LockFailure::Io { path, source }) => Err(LogError::io(path, source)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::log::lock::LOCK_FILE;
use std::fs::OpenOptions;
use std::process;
use tempfile::TempDir;
const HEADER: usize = SEGMENT_HEADER_SIZE;
const REC_OVERHEAD: usize = RECORD_HEAD_SIZE;
const MARKER: usize = RECORD_HEAD_SIZE + COMMIT_MARKER_PAYLOAD;
fn open(dir: &Path, segment_size: usize) -> SegmentSet {
SegmentSet::open(dir, SegmentConfig::new(segment_size)).unwrap()
}
fn append_one(set: &mut SegmentSet, data: &[u8]) -> Position {
let range = set.append_batch(&[data]).unwrap();
assert_eq!(range.first, range.last);
range.first
}
fn drain<S: SegmentSource>(mut scan: Scan<S>) -> Vec<Record> {
let mut out = Vec::new();
while let Some(item) = scan.next() {
out.push(item.unwrap().to_owned());
}
out
}
#[test]
fn open_empty_creates_first_segment() {
let dir = TempDir::new().unwrap();
let set = open(dir.path(), 4096);
assert_eq!(set.next_position(), Position::new(1));
assert_eq!(set.last_position(), Position::new(0));
assert_eq!(set.sealed_len(), 0);
assert!(dir.path().join("00000000000000000001.log").exists());
}
#[test]
fn tiny_config_rejected() {
let dir = TempDir::new().unwrap();
let err = SegmentSet::open(dir.path(), SegmentConfig::new(64)).unwrap_err();
assert!(matches!(err, LogError::InvalidConfig { .. }), "got {err:?}");
}
#[test]
fn reopen_after_clean_shutdown_preserves_state() {
let dir = TempDir::new().unwrap();
{
let mut set = open(dir.path(), 4096);
for i in 1..=5u64 {
append_one(&mut set, format!("event-{i}").as_bytes());
}
assert_eq!(set.next_position(), Position::new(6));
}
let set = open(dir.path(), 4096);
assert_eq!(set.next_position(), Position::new(6));
assert_eq!(set.last_position(), Position::new(5));
for i in 1..=5u64 {
let record = set.read_at(Position::new(i)).unwrap();
assert_eq!(record.position, Position::new(i));
assert_eq!(record.data, format!("event-{i}").into_bytes());
}
}
#[test]
fn open_deletes_short_trailing_segment_and_recovers() {
let dir = TempDir::new().unwrap();
{
let mut set = open(dir.path(), 4096);
for i in 1..=5u64 {
append_one(&mut set, format!("event-{i}").as_bytes());
}
}
let stray1 = dir.path().join(segment_file_name(Position::new(6)));
let stray2 = dir.path().join(segment_file_name(Position::new(7)));
File::create(&stray1).unwrap();
File::create(&stray2).unwrap();
assert_eq!(fs::metadata(&stray1).unwrap().len(), 0);
let set = open(dir.path(), 4096);
assert!(
!stray1.exists(),
"trailing 0-byte segment 6 should be deleted on open"
);
assert!(
!stray2.exists(),
"trailing 0-byte segment 7 should be deleted on open"
);
assert_eq!(set.next_position(), Position::new(6));
assert_eq!(set.last_position(), Position::new(5));
for i in 1..=5u64 {
assert_eq!(
set.read_at(Position::new(i)).unwrap().data,
format!("event-{i}").into_bytes()
);
}
}
#[test]
fn rollover_keeps_positions_contiguous() {
let dir = TempDir::new().unwrap();
let mut set = open(dir.path(), 256);
let n = 20u64;
for i in 1..=n {
let pos = append_one(&mut set, format!("evt{i:03}").as_bytes());
assert_eq!(pos, Position::new(i));
}
assert_eq!(set.next_position(), Position::new(n + 1));
assert!(set.sealed_len() >= 1, "expected at least one rollover");
for i in 1..=n {
let record = set.read_at(Position::new(i)).unwrap();
assert_eq!(record.data, format!("evt{i:03}").into_bytes());
}
}
#[test]
fn read_at_across_boundary_for_every_position() {
let dir = TempDir::new().unwrap();
let mut set = open(dir.path(), 200);
let n = 30u64;
for i in 1..=n {
append_one(&mut set, format!("r{i:04}").as_bytes());
}
assert!(set.sealed_len() >= 2);
for i in 1..=n {
assert_eq!(
set.read_at(Position::new(i)).unwrap().data,
format!("r{i:04}").into_bytes()
);
}
assert!(matches!(
set.read_at(Position::new(0)),
Err(LogError::NotFound { .. })
));
assert!(matches!(
set.read_at(Position::new(n + 1)),
Err(LogError::NotFound { .. })
));
}
#[test]
fn scan_from_mid_segment_yields_expected_order() {
let dir = TempDir::new().unwrap();
let mut set = open(dir.path(), 4096);
let n = 12u64;
for i in 1..=n {
append_one(&mut set, format!("s{i}").as_bytes());
}
let start = 5u64;
let got = drain(set.scan_from(Position::new(start)));
assert_eq!(got.len() as u64, n - start + 1);
for (idx, record) in got.iter().enumerate() {
let pos = start + idx as u64;
assert_eq!(record.position, Position::new(pos));
assert_eq!(record.data, format!("s{pos}").into_bytes());
}
}
#[test]
fn scan_across_segments_is_contiguous_and_ordered() {
let dir = TempDir::new().unwrap();
let mut set = open(dir.path(), 200);
let n = 25u64;
for i in 1..=n {
append_one(&mut set, format!("x{i:04}").as_bytes());
}
assert!(set.sealed_len() >= 2);
let got = drain(set.scan_from(Position::new(1)));
assert_eq!(got.len() as u64, n);
for (idx, record) in got.iter().enumerate() {
let pos = idx as u64 + 1;
assert_eq!(record.position, Position::new(pos));
assert_eq!(record.data, format!("x{pos:04}").into_bytes());
}
}
#[test]
fn scan_from_zero_clamps_to_whole_log() {
let dir = TempDir::new().unwrap();
let mut set = open(dir.path(), 4096);
for i in 1..=3u64 {
append_one(&mut set, format!("e{i}").as_bytes());
}
let positions: Vec<Position> = drain(set.scan_from(Position::new(0)))
.iter()
.map(|r| r.position)
.collect();
assert_eq!(
positions,
vec![Position::new(1), Position::new(2), Position::new(3)]
);
assert!(set.scan_from(Position::new(5)).next().is_none());
}
#[test]
fn scan_after_is_exclusive() {
let dir = TempDir::new().unwrap();
let mut set = open(dir.path(), 4096);
for i in 1..=3u64 {
append_one(&mut set, format!("e{i}").as_bytes());
}
let all: Vec<Position> = drain(set.scan_after(Position::new(0)))
.iter()
.map(|r| r.position)
.collect();
assert_eq!(
all,
vec![Position::new(1), Position::new(2), Position::new(3)]
);
let resumed: Vec<Position> = drain(set.scan_after(Position::new(1)))
.iter()
.map(|r| r.position)
.collect();
assert_eq!(resumed, vec![Position::new(2), Position::new(3)]);
assert!(set.scan_after(set.last_position()).next().is_none());
let from2: Vec<Position> = drain(set.scan_from(Position::new(2)))
.iter()
.map(|r| r.position)
.collect();
assert_eq!(from2, resumed);
}
#[test]
fn oversized_record_rejected() {
let dir = TempDir::new().unwrap();
let mut config = SegmentConfig::new(4096);
config.max_record_len = 100;
let mut set = SegmentSet::open(dir.path(), config).unwrap();
let big = vec![0u8; 200];
let err = set.append_batch(&[&big]).unwrap_err();
assert!(
matches!(err, LogError::RecordTooLarge { .. }),
"got {err:?}"
);
}
#[test]
fn batch_larger_than_segment_rejected() {
let dir = TempDir::new().unwrap();
let mut set = open(dir.path(), 200);
let records: Vec<Vec<u8>> = (0..5).map(|_| vec![0u8; 30]).collect();
let refs: Vec<&[u8]> = records.iter().map(|r| r.as_slice()).collect();
let err = set.append_batch(&refs).unwrap_err();
assert!(matches!(err, LogError::BatchTooLarge { .. }), "got {err:?}");
}
#[test]
fn empty_batch_and_empty_record_rejected() {
let dir = TempDir::new().unwrap();
let mut set = open(dir.path(), 4096);
assert!(matches!(set.append_batch(&[]), Err(LogError::EmptyBatch)));
assert!(matches!(
set.append_batch(&[b""]),
Err(LogError::EmptyRecord)
));
assert_eq!(set.next_position(), Position::new(1));
}
#[test]
fn crash_after_create_before_header_write_is_deleted() {
let dir = TempDir::new().unwrap();
let segment_size = 4096;
{
let mut set = open(dir.path(), segment_size);
for i in 1..=3u64 {
append_one(&mut set, format!("e{i}").as_bytes());
}
assert_eq!(set.next_position(), Position::new(4));
}
let ghost = dir.path().join(segment_file_name(Position::new(4)));
fs::write(&ghost, vec![0u8; segment_size]).unwrap();
let set = open(dir.path(), segment_size);
assert!(
!ghost.exists(),
"zero-filled trailing segment should be deleted"
);
assert_eq!(set.next_position(), Position::new(4));
for i in 1..=3u64 {
assert_eq!(
set.read_at(Position::new(i)).unwrap().data,
format!("e{i}").into_bytes()
);
}
}
#[test]
fn missing_middle_segment_fails_open() {
let dir = TempDir::new().unwrap();
let segment_size = 200;
{
let mut set = open(dir.path(), segment_size);
for i in 1..=15u64 {
append_one(&mut set, format!("m{i:03}").as_bytes());
}
assert!(set.sealed_len() >= 3, "need several sealed segments");
}
let mut files: Vec<PathBuf> = fs::read_dir(dir.path())
.unwrap()
.map(|e| e.unwrap().path())
.filter(|p| p.extension().is_some_and(|e| e == "log"))
.collect();
files.sort();
assert!(files.len() >= 3);
fs::remove_file(&files[1]).unwrap();
let err = SegmentSet::open(dir.path(), SegmentConfig::new(segment_size)).unwrap_err();
assert!(matches!(err, LogError::NonContiguous { .. }), "got {err:?}");
}
#[test]
fn header_base_position_disagreeing_with_filename_fails() {
let dir = TempDir::new().unwrap();
let segment_size = 4096;
{
let mut set = open(dir.path(), segment_size);
append_one(&mut set, b"only");
}
let path = dir.path().join(segment_file_name(Position::new(1)));
let bogus = SegmentHeader::new(Position::new(7));
let file = File::options().write(true).open(&path).unwrap();
file.write_all_at(&bogus.to_bytes(), 0).unwrap();
file.sync_all().unwrap();
drop(file);
let err = SegmentSet::open(dir.path(), SegmentConfig::new(segment_size)).unwrap_err();
assert!(
matches!(err, LogError::BasePositionMismatch { .. }),
"got {err:?}"
);
}
#[test]
fn multi_record_batch_shares_positions() {
let dir = TempDir::new().unwrap();
let mut set = open(dir.path(), 4096);
let range = set.append_batch(&[b"a", b"bb", b"ccc"]).unwrap();
assert_eq!(range.first, Position::new(1));
assert_eq!(range.last, Position::new(3));
assert_eq!(range.count(), 3);
assert_eq!(set.next_position(), Position::new(4));
assert_eq!(set.read_at(Position::new(1)).unwrap().data, b"a");
assert_eq!(set.read_at(Position::new(2)).unwrap().data, b"bb");
assert_eq!(set.read_at(Position::new(3)).unwrap().data, b"ccc");
}
#[test]
fn append_continues_after_recovery() {
let dir = TempDir::new().unwrap();
let segment_size = 4096;
{
let mut set = open(dir.path(), segment_size);
append_one(&mut set, b"before"); }
let mut set = open(dir.path(), segment_size);
let pos = append_one(&mut set, b"after"); assert_eq!(pos, Position::new(2));
assert_eq!(set.read_at(Position::new(1)).unwrap().data, b"before");
assert_eq!(set.read_at(Position::new(2)).unwrap().data, b"after");
}
fn payload_for(position: u64, record_len: usize) -> Vec<u8> {
vec![((position - 1) as u8).wrapping_add(1); record_len]
}
fn build_single_segment(
dir: &Path,
segment_size: usize,
batches: usize,
record_len: usize,
) -> (Vec<u8>, Vec<usize>) {
let mut set = open(dir, segment_size);
for p in 1..=batches as u64 {
append_one(&mut set, &payload_for(p, record_len));
}
assert_eq!(set.sealed_len(), 0, "test assumes a single segment");
drop(set);
let path = dir.join(segment_file_name(Position::new(FIRST_POSITION)));
let bytes = fs::read(&path).unwrap();
let batch_size = REC_OVERHEAD + record_len + MARKER;
let commit_ends: Vec<usize> = (0..batches)
.map(|i| HEADER + (i + 1) * batch_size)
.collect();
(bytes, commit_ends)
}
#[test]
fn truncation_mid_batch_rolls_back_to_previous_commit() {
let segment_size = 4096;
let batches = 8;
let record_len = 10;
let source = TempDir::new().unwrap();
let (good_bytes, commit_ends) =
build_single_segment(source.path(), segment_size, batches, record_len);
let total_end = *commit_ends.last().unwrap();
for cutoff in HEADER..=total_end {
let dir = TempDir::new().unwrap();
let mut corrupt = good_bytes.clone();
for byte in corrupt.iter_mut().skip(cutoff) {
*byte = 0xFF;
}
let path = dir
.path()
.join(segment_file_name(Position::new(FIRST_POSITION)));
fs::write(&path, &corrupt).unwrap();
let set = open(dir.path(), segment_size);
let survived = commit_ends.iter().filter(|&&end| end <= cutoff).count() as u64;
assert_eq!(
set.next_position(),
Position::new(survived + 1),
"cutoff {cutoff}: expected {survived} surviving events"
);
for p in 1..=survived {
let record = set.read_at(Position::new(p)).unwrap();
assert_eq!(
record.data,
payload_for(p, record_len),
"cutoff {cutoff}, position {p}"
);
}
}
}
#[test]
fn corrupt_record_with_intact_marker_rejects_whole_batch() {
let dir = TempDir::new().unwrap();
let segment_size = 4096;
let rec_len = 6;
{
let mut set = open(dir.path(), segment_size);
append_one(&mut set, b"aaaa"); let recs: Vec<Vec<u8>> = (0..5).map(|i| vec![b'B' + i as u8; rec_len]).collect();
let refs: Vec<&[u8]> = recs.iter().map(|r| r.as_slice()).collect();
set.append_batch(&refs).unwrap(); assert_eq!(set.next_position(), Position::new(7));
}
let batch_a = REC_OVERHEAD + 4 + MARKER;
let rec_stride = REC_OVERHEAD + rec_len;
let rec2_data = HEADER + batch_a + rec_stride + REC_OVERHEAD;
let path = dir
.path()
.join(segment_file_name(Position::new(FIRST_POSITION)));
let file = File::options().read(true).write(true).open(&path).unwrap();
let mut byte = [0u8; 1];
file.read_exact_at(&mut byte, rec2_data as u64).unwrap();
byte[0] ^= 0xFF;
file.write_all_at(&byte, rec2_data as u64).unwrap();
file.sync_all().unwrap();
drop(file);
let set = open(dir.path(), segment_size);
assert_eq!(
set.next_position(),
Position::new(2),
"whole batch B must roll back"
);
assert_eq!(set.read_at(Position::new(1)).unwrap().data, b"aaaa");
assert!(matches!(
set.read_at(Position::new(2)),
Err(LogError::NotFound { .. })
));
}
#[test]
fn physical_truncation_mid_batch_rolls_back() {
let dir = TempDir::new().unwrap();
let segment_size = 4096;
let record_len = 10;
let batches = 6;
{
let mut set = open(dir.path(), segment_size);
for p in 1..=batches as u64 {
append_one(&mut set, &payload_for(p, record_len));
}
}
let batch_size = REC_OVERHEAD + record_len + MARKER;
let survive = 3usize;
let cut = HEADER + survive * batch_size + 5;
let path = dir
.path()
.join(segment_file_name(Position::new(FIRST_POSITION)));
let file = File::options().write(true).open(&path).unwrap();
file.set_len(cut as u64).unwrap();
file.sync_all().unwrap();
drop(file);
let set = open(dir.path(), segment_size);
assert_eq!(set.next_position(), Position::new(survive as u64 + 1));
for p in 1..=survive as u64 {
assert_eq!(
set.read_at(Position::new(p)).unwrap().data,
payload_for(p, record_len)
);
}
}
fn drain_back<S: SegmentSource>(mut scan: ScanBack<S>) -> Vec<Record> {
let mut out = Vec::new();
while let Some(item) = scan.next() {
out.push(item.unwrap().to_owned());
}
out
}
#[test]
fn scan_back_is_the_reverse_of_scan_forward() {
let dir = TempDir::new().unwrap();
let mut set = open(dir.path(), 1024);
for i in 1..=80u64 {
let size = 8 + (i as usize % 17) * 5;
append_one(&mut set, &vec![(i % 251) as u8; size]);
}
assert!(set.sealed_len() >= 2, "need several sealed segments");
let last = set.last_position();
let forward = drain(set.scan_from(Position::new(1)));
assert_eq!(forward.len(), 80);
for window in [1usize, 32, 200, REVERSE_WINDOW_BYTES] {
let full = drain_back(ScanBack::start_with_window(
&set,
Position::new(1),
last,
window,
));
let want: Vec<Record> = forward.iter().rev().cloned().collect();
assert_eq!(full, want, "window {window}, full range");
let (lo, hi) = (Position::new(20), Position::new(60));
let sub = drain_back(ScanBack::start_with_window(&set, lo, hi, window));
let want_sub: Vec<Record> = forward
.iter()
.filter(|record| record.position >= lo && record.position <= hi)
.rev()
.cloned()
.collect();
assert_eq!(sub, want_sub, "window {window}, sub range");
}
}
#[test]
fn scan_back_bounds_and_empty_cases() {
let dir = TempDir::new().unwrap();
let mut set = open(dir.path(), 4096);
for i in 1..=5u64 {
append_one(&mut set, format!("e{i}").as_bytes());
}
let all = drain_back(set.scan_back(Position::new(1), Position::MAX));
let positions: Vec<u64> = all.iter().map(|record| record.position.get()).collect();
assert_eq!(positions, vec![5, 4, 3, 2, 1]);
assert!(drain_back(set.scan_back(Position::new(4), Position::new(3))).is_empty());
assert!(drain_back(set.scan_back(Position::new(1), Position::ZERO)).is_empty());
let empty_dir = TempDir::new().unwrap();
let empty = open(empty_dir.path(), 4096);
assert!(drain_back(empty.scan_back(Position::new(1), Position::MAX)).is_empty());
}
#[test]
fn scan_back_record_larger_than_window() {
let dir = TempDir::new().unwrap();
let mut set = open(dir.path(), 1 << 16);
let big = vec![7u8; 4096];
append_one(&mut set, b"small-before");
let big_pos = append_one(&mut set, &big);
append_one(&mut set, b"small-after");
let got = drain_back(ScanBack::start_with_window(
&set,
Position::new(1),
set.last_position(),
1,
));
assert_eq!(got[0].position, Position::new(3));
assert_eq!(got[1].position, big_pos);
assert_eq!(got[1].data, big);
assert_eq!(got[2].position, Position::new(1));
}
#[test]
fn sealed_segment_orphans_are_not_counted() {
let dir = TempDir::new().unwrap();
let segment_size = 4096;
let config = SegmentConfig::new(segment_size);
{
let mut set = open(dir.path(), segment_size);
for i in 1..=5u64 {
append_one(&mut set, format!("event-{i}").as_bytes());
}
}
let first = dir.path().join(segment_file_name(Position::new(1)));
{
let mut writer =
Writer::<0>::open(&first, segment_size, config.header_size as u64).unwrap();
let rewind = writer.write_offset();
writer.append_data(b"orphan-a").unwrap();
writer.append_data(b"orphan-b").unwrap();
writer.flush_writer().unwrap();
writer.rewind_to(rewind).unwrap();
}
SegmentSet::create_segment(dir.path(), &config, Position::new(6)).unwrap();
let set = open(dir.path(), segment_size);
assert_eq!(
set.sealed_len(),
1,
"the orphan-bearing segment should be sealed"
);
assert_eq!(
set.sealed_arcs()[0].event_count(),
5,
"orphans past the last commit marker are not events"
);
assert_eq!(set.last_position(), Position::new(5));
assert_eq!(set.next_position(), Position::new(6));
for i in 1..=5u64 {
assert_eq!(
set.read_at(Position::new(i)).unwrap().data,
format!("event-{i}").into_bytes()
);
}
assert!(
set.read_at(Position::new(6)).is_err(),
"an orphan must not be readable as an event"
);
}
#[test]
fn sealed_segment_flushed_offset_bounds_reads_at_the_commit_point() {
let dir = TempDir::new().unwrap();
let segment_size = 4096;
let config = SegmentConfig::new(segment_size);
{
let mut set = open(dir.path(), segment_size);
append_one(&mut set, b"only");
}
let first = dir.path().join(segment_file_name(Position::new(1)));
let committed_end = {
let writer =
Writer::<0>::open(&first, segment_size, config.header_size as u64).unwrap();
writer.write_offset()
};
{
let mut writer =
Writer::<0>::open(&first, segment_size, config.header_size as u64).unwrap();
let rewind = writer.write_offset();
writer.append_data(b"orphan").unwrap();
writer.flush_writer().unwrap();
writer.rewind_to(rewind).unwrap();
}
SegmentSet::create_segment(dir.path(), &config, Position::new(2)).unwrap();
let set = open(dir.path(), segment_size);
let sealed = &set.sealed_arcs()[0];
assert_eq!(
sealed.flushed_offset.load(),
committed_end,
"a sealed segment is bounded by its commit point"
);
}
#[test]
fn a_second_open_in_the_same_process_is_locked_out() {
let dir = TempDir::new().unwrap();
let _held = open(dir.path(), 4096);
let err = SegmentSet::open(dir.path(), SegmentConfig::new(4096)).unwrap_err();
let LogError::Locked { path, holder } = &err else {
panic!("expected Locked, got {err:?}");
};
assert_eq!(path.file_name().unwrap(), LOCK_FILE);
assert_eq!(
*holder,
Some(process::id()),
"the holder hint should name the holding pid"
);
}
#[test]
fn the_lock_is_released_on_drop() {
let dir = TempDir::new().unwrap();
{
let mut set = open(dir.path(), 4096);
append_one(&mut set, b"event");
}
let set = open(dir.path(), 4096);
assert_eq!(set.last_position(), Position::new(1));
}
#[test]
fn a_stale_lock_file_does_not_block() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join(LOCK_FILE), b"999999\n").unwrap();
let mut set = open(dir.path(), 4096);
append_one(&mut set, b"event");
assert_eq!(set.last_position(), Position::new(1));
}
#[test]
fn the_lock_file_is_not_mistaken_for_a_segment() {
let dir = TempDir::new().unwrap();
{
let mut set = open(dir.path(), 4096);
append_one(&mut set, b"event");
}
assert!(
dir.path().join(LOCK_FILE).exists(),
"the lock file is left on disk"
);
assert!(parse_base_position(LOCK_FILE).is_none());
let set = open(dir.path(), 4096);
assert_eq!(set.sealed_len(), 0);
assert_eq!(set.last_position(), Position::new(1));
}
fn follow(dir: &Path, segment_size: usize) -> SegmentSet {
SegmentSet::open_read_only(dir, SegmentConfig::new(segment_size)).unwrap()
}
fn tree(dir: &Path) -> Vec<(PathBuf, u64)> {
let mut out = Vec::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(next) = stack.pop() {
for entry in fs::read_dir(&next).unwrap() {
let entry = entry.unwrap();
let meta = entry.metadata().unwrap();
if meta.is_dir() {
stack.push(entry.path());
} else {
out.push((entry.path(), meta.len()));
}
}
}
out.sort();
out
}
#[test]
fn open_read_only_on_an_empty_dir_errors() {
let dir = TempDir::new().unwrap();
let err = SegmentSet::open_read_only(dir.path(), SegmentConfig::new(4096)).unwrap_err();
assert!(matches!(err, LogError::Uninitialized { .. }), "got {err:?}");
}
#[test]
fn open_read_only_on_a_missing_dir_errors() {
let dir = TempDir::new().unwrap();
let missing = dir.path().join("nope");
assert!(SegmentSet::open_read_only(&missing, SegmentConfig::new(4096)).is_err());
assert!(
!missing.exists(),
"a read-only open must not create the directory"
);
}
#[test]
fn open_read_only_mutates_nothing() {
let dir = TempDir::new().unwrap();
{
let mut set = open(dir.path(), 4096);
for i in 1..=5u64 {
append_one(&mut set, format!("event-{i}").as_bytes());
}
}
let stray = dir.path().join(segment_file_name(Position::new(6)));
File::create(&stray).unwrap();
let before = tree(dir.path());
let mut follower = follow(dir.path(), 4096);
follower.refresh().unwrap();
drop(follower);
assert_eq!(
before,
tree(dir.path()),
"a follower must not touch the directory"
);
assert!(
stray.exists(),
"a follower must not delete an unfinished segment"
);
}
#[test]
fn open_read_only_takes_no_lock() {
let dir = TempDir::new().unwrap();
let mut writer = open(dir.path(), 4096);
append_one(&mut writer, b"event");
let follower = follow(dir.path(), 4096);
assert_eq!(follower.last_position(), Position::new(1));
assert!(follower.is_read_only());
assert!(!writer.is_read_only());
}
#[test]
fn open_read_only_skips_a_trailing_unwritten_segment() {
let dir = TempDir::new().unwrap();
{
let mut set = open(dir.path(), 4096);
append_one(&mut set, b"event");
}
let stray = dir.path().join(segment_file_name(Position::new(2)));
File::create(&stray).unwrap();
let mut follower = follow(dir.path(), 4096);
assert_eq!(follower.active_base(), Position::new(1));
let refreshed = follower.refresh().unwrap();
assert_eq!(refreshed.pending_segment.as_deref(), Some(stray.as_path()));
assert_eq!(refreshed.sealed_added, 0);
assert_eq!(refreshed.tip, Position::new(1));
}
#[test]
fn open_read_only_skips_a_trailing_torn_header() {
let dir = TempDir::new().unwrap();
let config = SegmentConfig::new(4096);
{
let mut set = open(dir.path(), 4096);
append_one(&mut set, b"event");
}
SegmentSet::create_segment(dir.path(), &config, Position::new(2)).unwrap();
let second = dir.path().join(segment_file_name(Position::new(2)));
let file = OpenOptions::new().write(true).open(&second).unwrap();
file.write_all_at(&[0xAB; 4], (SEGMENT_HEADER_SIZE - 4) as u64)
.unwrap();
file.sync_all().unwrap();
let mut follower = follow(dir.path(), 4096);
assert_eq!(follower.active_base(), Position::new(1));
let refreshed = follower.refresh().unwrap();
assert_eq!(refreshed.pending_segment.as_deref(), Some(second.as_path()));
SegmentSet::create_segment(dir.path(), &config, Position::new(3)).unwrap();
let err = SegmentSet::open_read_only(dir.path(), config).unwrap_err();
assert!(matches!(err, LogError::Header { .. }), "got {err:?}");
}
#[test]
fn open_read_only_rejects_append_and_a_writer_rejects_refresh() {
let dir = TempDir::new().unwrap();
{
let mut set = open(dir.path(), 4096);
append_one(&mut set, b"event");
}
let mut follower = follow(dir.path(), 4096);
let err = follower.append_batch(&[b"nope"]).unwrap_err();
assert!(
matches!(err, LogError::ReadOnly { op: "append", .. }),
"got {err:?}"
);
let mut writer = open(dir.path(), 4096);
let err = writer.refresh().unwrap_err();
assert!(
matches!(err, LogError::ReadOnly { op: "refresh", .. }),
"got {err:?}"
);
}
#[test]
fn refresh_picks_up_committed_batches() {
let dir = TempDir::new().unwrap();
let mut writer = open(dir.path(), 4096);
append_one(&mut writer, b"first");
let mut follower = follow(dir.path(), 4096);
assert_eq!(follower.last_position(), Position::new(1));
for i in 2..=6u64 {
append_one(&mut writer, format!("event-{i}").as_bytes());
let refreshed = follower.refresh().unwrap();
assert_eq!(refreshed.tip, Position::new(i));
assert_eq!(
follower.read_at(Position::new(i)).unwrap().data,
format!("event-{i}").into_bytes()
);
}
}
#[test]
fn refresh_is_idempotent_when_nothing_changed() {
let dir = TempDir::new().unwrap();
let mut writer = open(dir.path(), 4096);
append_one(&mut writer, b"only");
let mut follower = follow(dir.path(), 4096);
let first = follower.refresh().unwrap();
let second = follower.refresh().unwrap();
assert_eq!(first.tip, second.tip);
assert_eq!(second.sealed_added, 0);
assert_eq!(follower.active.event_count(), 1, "no double counting");
}
#[test]
fn refresh_ignores_an_uncommitted_partial_batch() {
let dir = TempDir::new().unwrap();
let segment_size = 4096;
let config = SegmentConfig::new(segment_size);
{
let mut set = open(dir.path(), segment_size);
append_one(&mut set, b"committed");
}
let first = dir.path().join(segment_file_name(Position::new(1)));
let mut follower = follow(dir.path(), segment_size);
assert_eq!(follower.last_position(), Position::new(1));
let mut writer =
Writer::<0>::open(&first, segment_size, config.header_size as u64).unwrap();
writer.append_data(b"pending-a").unwrap();
writer.append_data(b"pending-b").unwrap();
writer.flush_writer().unwrap();
let refreshed = follower.refresh().unwrap();
assert_eq!(
refreshed.tip,
Position::new(1),
"an uncommitted batch is invisible"
);
writer.commit(3).unwrap();
let refreshed = follower.refresh().unwrap();
assert_eq!(
refreshed.tip,
Position::new(3),
"and appears whole once committed"
);
assert_eq!(
follower.read_at(Position::new(3)).unwrap().data,
b"pending-b".to_vec()
);
}
#[test]
fn refresh_adopts_a_new_segment_and_seals_the_old() {
let dir = TempDir::new().unwrap();
let segment_size = HEADER + (REC_OVERHEAD + 8 + MARKER) * 2;
let mut writer = open(dir.path(), segment_size);
append_one(&mut writer, b"aaaaaaaa");
let mut follower = follow(dir.path(), segment_size);
assert_eq!(follower.sealed_len(), 0);
append_one(&mut writer, b"bbbbbbbb");
append_one(&mut writer, b"cccccccc");
assert!(
writer.sealed_len() >= 1,
"the writer should have rolled over"
);
let refreshed = follower.refresh().unwrap();
assert_eq!(refreshed.tip, Position::new(3));
assert!(
refreshed.sealed_added >= 1,
"the follower should have sealed a segment"
);
assert_eq!(follower.sealed_len(), writer.sealed_len());
assert_eq!(follower.active_base(), writer.active_base());
for i in 1..=3u64 {
assert_eq!(
follower.read_at(Position::new(i)).unwrap().data,
writer.read_at(Position::new(i)).unwrap().data
);
}
}
#[test]
fn refresh_adopts_multiple_new_segments_in_one_call() {
let dir = TempDir::new().unwrap();
let segment_size = HEADER + (REC_OVERHEAD + 8 + MARKER) * 2;
let mut writer = open(dir.path(), segment_size);
append_one(&mut writer, b"aaaaaaaa");
let mut follower = follow(dir.path(), segment_size);
for i in 2..=12u64 {
append_one(&mut writer, format!("{i:08}").as_bytes());
}
assert!(writer.sealed_len() >= 3, "several rollovers expected");
let refreshed = follower.refresh().unwrap();
assert_eq!(refreshed.tip, Position::new(12));
assert_eq!(follower.sealed_len(), writer.sealed_len());
assert_eq!(refreshed.sealed_added, writer.sealed_len());
let scanned: Vec<_> = drain(follower.scan_from(Position::new(1)))
.into_iter()
.map(|r| r.position)
.collect();
assert_eq!(
scanned.len(),
12,
"every position is readable across the seams"
);
}
#[test]
fn refresh_tolerates_a_header_only_trailing_segment() {
let dir = TempDir::new().unwrap();
let segment_size = 4096;
let config = SegmentConfig::new(segment_size);
{
let mut set = open(dir.path(), segment_size);
append_one(&mut set, b"event");
}
SegmentSet::create_segment(dir.path(), &config, Position::new(2)).unwrap();
let mut follower = follow(dir.path(), segment_size);
assert_eq!(follower.sealed_len(), 1);
assert_eq!(follower.active_base(), Position::new(2));
let refreshed = follower.refresh().unwrap();
assert_eq!(refreshed.sealed_added, 0);
assert_eq!(refreshed.tip, Position::new(1));
assert_eq!(follower.next_position(), Position::new(2));
}
}