use crate::KeyHandle;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use crate::error::TailerError;
use crate::reader::{ScanIter, SegmentManifest};
use crate::record::Record;
use crate::ring::Ring;
use crate::shard::{decode_record_id, encode_record_id};
const LEGACY_PROGRESS_SIZE: usize = 12;
fn tailer_progress_path(dir: &Path, name: &str) -> PathBuf {
dir.join(format!("tailer_{}.dat", name))
}
fn load_progress(dir: &Path, name: &str, num_shards: usize) -> Vec<u64> {
let path = tailer_progress_path(dir, name);
let data = match fs::read(&path) {
Ok(d) => d,
Err(_) => return vec![0u64; num_shards],
};
if num_shards == 1 {
if data.len() != LEGACY_PROGRESS_SIZE {
return vec![0u64; num_shards];
}
let seq = u64::from_le_bytes([
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
]);
let crc = u32::from_le_bytes([data[8], data[9], data[10], data[11]]);
if crc32c::crc32c(&data[..8]) != crc {
return vec![0u64; num_shards];
}
return vec![seq];
}
if data.len() < 4 {
return vec![0u64; num_shards];
}
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let need = 4 + count * 8 + 4;
if data.len() != need || count != num_shards {
return vec![0u64; num_shards];
}
let body_end = 4 + count * 8;
let crc = u32::from_le_bytes([
data[body_end],
data[body_end + 1],
data[body_end + 2],
data[body_end + 3],
]);
if crc32c::crc32c(&data[..body_end]) != crc {
return vec![0u64; num_shards];
}
let mut seqs = Vec::with_capacity(count);
for i in 0..count {
let off = 4 + i * 8;
seqs.push(u64::from_le_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
data[off + 4],
data[off + 5],
data[off + 6],
data[off + 7],
]));
}
seqs
}
fn save_progress(dir: &Path, name: &str, positions: &[u64]) -> io::Result<()> {
let path = tailer_progress_path(dir, name);
let tmp = dir.join(format!(".tailer_{}.tmp", name));
let mut f = fs::File::create(&tmp)?;
if positions.len() == 1 {
let mut buf = [0u8; LEGACY_PROGRESS_SIZE];
buf[0..8].copy_from_slice(&positions[0].to_le_bytes());
let crc = crc32c::crc32c(&buf[..8]);
buf[8..12].copy_from_slice(&crc.to_le_bytes());
f.write_all(&buf)?;
} else {
let count = positions.len() as u32;
let mut body = Vec::with_capacity(4 + positions.len() * 8);
body.extend_from_slice(&count.to_le_bytes());
for s in positions {
body.extend_from_slice(&s.to_le_bytes());
}
let crc = crc32c::crc32c(&body);
f.write_all(&body)?;
f.write_all(&crc.to_le_bytes())?;
}
crate::platform::fdatasync(&f)?;
drop(f);
fs::rename(&tmp, &path)?;
let d = fs::File::open(dir)?;
crate::platform::sync_dir(&d)?;
Ok(())
}
pub struct Tailer {
name: String,
positions: Vec<u64>,
data_dir: PathBuf,
manifests: Vec<Arc<Mutex<SegmentManifest>>>,
rings: Vec<Arc<Ring>>,
shard_bits: u32,
encryption_key: Option<KeyHandle>,
}
impl Tailer {
pub(crate) fn open(
manifests: Vec<Arc<Mutex<SegmentManifest>>>,
rings: Vec<Arc<Ring>>,
shard_bits: u32,
name: &str,
encryption_key: Option<KeyHandle>,
data_dir: PathBuf,
) -> Self {
let num_shards = manifests.len();
let positions = load_progress(&data_dir, name, num_shards);
Self {
name: name.to_string(),
positions,
data_dir,
manifests,
rings,
shard_bits,
encryption_key,
}
}
pub fn position(&self) -> u64 {
self.positions.iter().copied().min().unwrap_or(0)
}
pub fn positions(&self) -> &[u64] {
&self.positions
}
pub fn seek(&mut self, seq: u64) {
for p in self.positions.iter_mut() {
*p = seq;
}
}
pub fn next_batch(&mut self, max_count: usize) -> Result<Option<Vec<Record>>, TailerError> {
let mut all: Vec<Record> = Vec::new();
for s in 0..self.manifests.len() {
let durable = self.rings[s]
.durable_cursor
.load(std::sync::atomic::Ordering::Acquire);
let from_local = self.positions[s];
if from_local >= durable {
continue; }
let to_local = (from_local + max_count as u64).min(durable);
let from_gid = encode_record_id(s, from_local, self.shard_bits);
let to_gid = encode_record_id(s, to_local, self.shard_bits);
let iter = ScanIter::build(
vec![Arc::clone(&self.manifests[s])],
self.encryption_key.clone(),
from_gid,
to_gid,
)?;
for r in iter {
match r {
Ok(rec) => all.push(rec),
Err(e) => return Err(e.into()),
}
}
}
if all.is_empty() {
return Ok(None);
}
all.sort_by_key(|r| r.id.sequence);
if all.len() > max_count {
all.truncate(max_count);
}
for r in &all {
let (shard, local) = decode_record_id(r.id.sequence, self.shard_bits);
let next = local + 1;
if next > self.positions[shard] {
self.positions[shard] = next;
}
}
Ok(Some(all))
}
pub fn commit(&self) -> io::Result<()> {
save_progress(&self.data_dir, &self.name, &self.positions)
}
pub fn reset(&mut self) -> io::Result<()> {
for p in self.positions.iter_mut() {
*p = 0;
}
let path = tailer_progress_path(&self.data_dir, &self.name);
if path.exists() {
fs::remove_file(&path)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::config::{Config, DurabilityMode};
use crate::LogDb;
use std::time::Duration;
fn open_db(dir: &std::path::Path) -> LogDb {
let mut config = Config::default();
config.data_dir = dir.to_path_buf();
config.ring_size = 128;
config.durability_mode = DurabilityMode::Sync;
config.flush_timeout = Duration::from_secs(10);
LogDb::open(config).unwrap()
}
fn wait_durable(db: &LogDb, target: u64) {
for _ in 0..50 {
if db.durable_cursor() >= target {
break;
}
std::thread::sleep(Duration::from_millis(20));
}
}
#[test]
fn tailer_reads_from_beginning() {
let dir = tempfile::tempdir().unwrap();
let db = open_db(dir.path());
for i in 0..100u64 {
db.append(format!("msg-{}", i).as_bytes()).unwrap();
}
db.flush().unwrap();
wait_durable(&db, 100);
let mut t = db.new_tailer("test");
assert_eq!(t.position(), 0);
let batch = t.next_batch(5).unwrap().unwrap();
assert_eq!(batch.len(), 5);
assert_eq!(batch[0].content, b"msg-0");
assert!(t.position() >= 5);
}
#[test]
fn tailer_persists_progress() {
let dir = tempfile::tempdir().unwrap();
let db = open_db(dir.path());
for i in 0..200u64 {
db.append(format!("m{}", i).as_bytes()).unwrap();
}
db.flush().unwrap();
wait_durable(&db, 200);
{
let mut t = db.new_tailer("consumer-a");
t.next_batch(10).unwrap();
t.commit().unwrap();
assert_eq!(t.position(), 10);
}
let t2 = db.new_tailer("consumer-a");
assert_eq!(t2.position(), 10);
}
#[test]
fn multiple_tailers_independent() {
let dir = tempfile::tempdir().unwrap();
let db = open_db(dir.path());
for i in 0..200u64 {
db.append(format!("m{}", i).as_bytes()).unwrap();
}
db.flush().unwrap();
wait_durable(&db, 200);
let mut a = db.new_tailer("a");
let mut b = db.new_tailer("b");
a.next_batch(5).unwrap();
b.next_batch(15).unwrap();
assert_eq!(a.position(), 5);
assert_eq!(b.position(), 15);
a.next_batch(10).unwrap();
assert_eq!(a.position(), 15);
}
}