blob: 390695a98f300025c1efa274f8a2c08125a8226a [file]
use crate::streaming::{
batching::{
iterator::IntoMessagesIterator,
message_batch::{RetainedMessageBatch, RETAINED_BATCH_OVERHEAD},
},
segments::indexes::IndexRange,
};
use bytes::BytesMut;
use error_set::ErrContext;
use iggy::{error::IggyError, utils::byte_size::IggyByteSize};
use seekable_async_file::{SeekableAsyncFile, SeekableAsyncFileMetrics};
use std::{
path::Path,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
time::Duration,
};
use tracing::{trace, warn};
/// A dedicated struct for reading from the log file.
pub struct SegmentLogReader {
file_path: String,
file: Arc<SeekableAsyncFile>,
log_size_bytes: Arc<AtomicU64>,
_metrics: Arc<SeekableAsyncFileMetrics>,
}
impl std::fmt::Debug for SegmentLogReader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SegmentLogReader")
.field("file_path", &self.file_path)
.field("log_size_bytes", &self.log_size_bytes)
.finish()
}
}
impl SegmentLogReader {
/// Opens the log file in read mode.
pub async fn new(file_path: &str, log_size_bytes: Arc<AtomicU64>) -> Result<Self, IggyError> {
let metrics = Arc::new(SeekableAsyncFileMetrics::default());
let file = Arc::new(
SeekableAsyncFile::open(
Path::new(file_path),
Arc::clone(&metrics),
Duration::from_micros(100),
0,
)
.await,
);
let file_clone = file.clone();
tokio::task::spawn(async move {
Arc::clone(&file_clone)
.start_delayed_data_sync_background_loop()
.await
});
Ok(Self {
file_path: file_path.to_string(),
file,
log_size_bytes,
_metrics: metrics,
})
}
/// Loads message batches given an index range.
pub async fn load_batches_by_range_impl(
&self,
index_range: &IndexRange,
) -> Result<Vec<RetainedMessageBatch>, IggyError> {
let file_size = self.log_size_bytes.load(Ordering::Acquire);
if file_size == 0 {
warn!("Log file {} is empty.", self.file_path);
return Ok(Vec::new());
}
let mut offset = index_range.start.position as u64;
let mut batches = Vec::new();
let mut last_batch_to_read = false;
while !last_batch_to_read && offset < file_size {
match self.read_next_batch(offset, file_size).await? {
Some((batch, bytes_read)) => {
offset += bytes_read;
let last_offset_in_batch = batch.base_offset + batch.last_offset_delta as u64;
if last_offset_in_batch >= index_range.end.offset as u64 || offset >= file_size
{
last_batch_to_read = true;
}
batches.push(batch);
}
None => {
break;
}
}
}
trace!("Loaded {} message batches.", batches.len());
Ok(batches)
}
/// Loads and returns all message IDs from the log file.
pub async fn load_message_ids_impl(&self) -> Result<Vec<u128>, IggyError> {
let file_size = self.log_size_bytes.load(Ordering::Acquire);
if file_size == 0 {
warn!("Log file {} is empty.", self.file_path);
return Ok(Vec::new());
}
let mut offset = 0_u64;
let mut message_ids = Vec::new();
while offset < file_size {
match self.read_next_batch(offset, file_size).await? {
Some((batch, bytes_read)) => {
offset += bytes_read;
for msg in batch.into_messages_iter() {
message_ids.push(msg.id);
}
}
None => {
// Possibly reached EOF or truncated
break;
}
}
}
trace!("Loaded {} message IDs from the log.", message_ids.len());
Ok(message_ids)
}
/// Loads message batches given an index range and calls the provided callback for each batch.
pub async fn load_batches_by_range_with_callback<F>(
&self,
index_range: &IndexRange,
mut on_batch: F,
) -> Result<(), IggyError>
where
F: FnMut(RetainedMessageBatch) -> Result<(), IggyError>,
{
let file_size = self.log_size_bytes.load(Ordering::Acquire);
if file_size == 0 {
warn!("Log file {} is empty.", self.file_path);
return Ok(());
}
let mut offset = index_range.start.position as u64;
let mut last_batch_to_read = false;
while !last_batch_to_read && offset < file_size {
match self.read_next_batch(offset, file_size).await? {
Some((batch, bytes_read)) => {
offset += bytes_read;
let last_offset_in_batch = batch.base_offset + batch.last_offset_delta as u64;
if offset >= file_size || last_offset_in_batch >= index_range.end.offset as u64
{
last_batch_to_read = true;
}
on_batch(batch)?;
}
None => {
break;
}
}
}
Ok(())
}
/// Loads message batches up to a given size and calls the provided callback for each batch
/// after a threshold is reached.
pub async fn load_batches_by_size_with_callback(
&self,
bytes_to_load: u64,
mut on_batch: impl FnMut(RetainedMessageBatch) -> Result<(), IggyError>,
) -> Result<(), IggyError> {
let file_size = self.log_size_bytes.load(Ordering::Acquire);
if file_size == 0 {
warn!("Log file {} is empty.", self.file_path);
return Ok(());
}
let threshold = file_size.saturating_sub(bytes_to_load);
let mut offset = 0_u64;
let mut accumulated_size = 0_u64;
while offset < file_size {
match self.read_next_batch(offset, file_size).await? {
Some((batch, bytes_read)) => {
if accumulated_size >= threshold {
on_batch(batch)?;
}
offset += bytes_read;
accumulated_size += bytes_read;
}
None => {
break;
}
}
}
Ok(())
}
async fn read_next_batch(
&self,
offset: u64,
file_size: u64,
) -> Result<Option<(RetainedMessageBatch, u64)>, IggyError> {
let overhead_size = RETAINED_BATCH_OVERHEAD;
if offset + overhead_size > file_size {
return Ok(None);
}
let overhead_buf = self.file.read_at(offset, overhead_size).await;
if overhead_buf.len() < overhead_size as usize {
warn!(
"Cannot read full batch overhead at offset {} in file {}. Possibly truncated.",
offset, self.file_path
);
return Ok(None);
}
let batch_base_offset = u64::from_le_bytes(
overhead_buf[0..8]
.try_into()
.map_err(|_| IggyError::CannotReadBatchBaseOffset)?,
);
let batch_length = u32::from_le_bytes(
overhead_buf[8..12]
.try_into()
.map_err(|_| IggyError::CannotReadBatchLength)?,
);
let last_offset_delta = u32::from_le_bytes(
overhead_buf[12..16]
.try_into()
.map_err(|_| IggyError::CannotReadLastOffsetDelta)?,
);
let max_timestamp = u64::from_le_bytes(
overhead_buf[16..24]
.try_into()
.map_err(|_| IggyError::CannotReadMaxTimestamp)?,
);
warn!(
"batch base offset: {}, length: {}, max timestamp: {}",
batch_base_offset, batch_length, max_timestamp
);
warn!("actual file size: {}", file_size);
let payload_len = batch_length as usize;
let payload_offset = offset + overhead_size;
if payload_offset + payload_len as u64 > file_size {
warn!(
"Cannot read full batch payload at offset {} in file {}. Possibly truncated.",
payload_offset, self.file_path
);
return Ok(None);
}
let payload_buf = self.file.read_at(payload_offset, payload_len as u64).await;
if payload_buf.len() < payload_len {
warn!(
"Cannot read full batch payload at offset {} in file {}. Possibly truncated.",
payload_offset, self.file_path
);
return Ok(None);
}
let bytes_read = overhead_size + payload_len as u64;
let batch = RetainedMessageBatch::new(
batch_base_offset,
last_offset_delta,
max_timestamp,
IggyByteSize::from(payload_len as u64),
BytesMut::from(&payload_buf[..]).freeze(),
);
Ok(Some((batch, bytes_read)))
}
}