#![allow(dead_code)]
use std::fs;
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use crate::config::Config;
use crate::pipeline::signal::ShutdownState;
use crate::reader::Reader;
use crate::record::Record;
use crate::ring::Ring;
pub trait RemoteSink: Send + 'static {
fn push_batch(&mut self, records: &[Record]) -> Result<(), PushError>;
}
#[derive(Debug)]
pub enum PushError {
Retriable(String),
Fatal(String),
}
impl std::fmt::Display for PushError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Retriable(s) => write!(f, "retriable: {}", s),
Self::Fatal(s) => write!(f, "fatal: {}", s),
}
}
}
const PROGRESS_FILE_SIZE: usize = 12;
const PROGRESS_FILE: &str = "pusher_progress.dat";
fn load_progress(dir: &Path) -> Option<u64> {
let path = dir.join(PROGRESS_FILE);
let data = fs::read(&path).ok()?;
if data.len() != PROGRESS_FILE_SIZE {
return None;
}
let seq = u64::from_le_bytes([
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
]);
let stored_crc = u32::from_le_bytes([data[8], data[9], data[10], data[11]]);
let computed_crc = crc32c::crc32c(&data[..8]);
if stored_crc != computed_crc {
return None; }
Some(seq)
}
fn save_progress(dir: &Path, seq: u64) -> io::Result<()> {
let path = dir.join(PROGRESS_FILE);
let tmp = dir.join("pusher_progress.tmp");
let mut buf = [0u8; PROGRESS_FILE_SIZE];
buf[0..8].copy_from_slice(&seq.to_le_bytes());
let crc = crc32c::crc32c(&buf[..8]);
buf[8..12].copy_from_slice(&crc.to_le_bytes());
let mut f = fs::File::create(&tmp)?;
f.write_all(&buf)?;
crate::platform::fdatasync(&f)?;
drop(f);
fs::rename(&tmp, &path)?;
let dir_f = fs::File::open(dir)?;
crate::platform::sync_dir(&dir_f)?;
Ok(())
}
struct Backoff {
base: Duration,
attempt: u32,
max_attempts: u32,
}
impl Backoff {
fn new(config: &Config) -> Self {
Self {
base: config.push_retry_base,
attempt: 0,
max_attempts: config.push_max_retries,
}
}
fn wait(&mut self) -> bool {
if self.max_attempts > 0 && self.attempt >= self.max_attempts {
return false;
}
let delay = self.base * 2u32.pow(self.attempt.min(6)); let delay = delay.min(Duration::from_secs(60));
std::thread::sleep(delay);
self.attempt += 1;
true
}
fn reset(&mut self) {
self.attempt = 0;
}
}
pub fn run_pusher(
data_dir: PathBuf,
ring: Arc<Ring>,
mut sink: Box<dyn RemoteSink>,
config: Config,
shutdown: Arc<ShutdownState>,
) {
let mut push_seq = load_progress(&data_dir).unwrap_or(0);
let mut backoff = Backoff::new(&config);
let mut batches_since_save: u32 = 0;
loop {
if shutdown.aborted() {
break;
}
if shutdown.draining() {
let durable = ring.durable_cursor.load(Ordering::Acquire);
if push_seq >= durable {
break;
}
}
let durable = ring.durable_cursor.load(Ordering::Acquire);
if durable <= push_seq {
std::thread::sleep(Duration::from_millis(10));
continue;
}
let to = (push_seq + config.push_batch_size as u64).min(durable);
let batch = match read_batch(&data_dir, push_seq, to) {
Ok(b) => b,
Err(e) => {
eprintln!("[pusher] read error at seq={}: {}", push_seq, e);
std::thread::sleep(Duration::from_millis(100));
continue;
}
};
if batch.is_empty() {
std::thread::sleep(Duration::from_millis(100));
continue;
}
match sink.push_batch(&batch) {
Ok(()) => {
if let Some(last) = batch.last() {
push_seq = last.id.sequence + 1;
}
backoff.reset();
batches_since_save += 1;
if batches_since_save >= config.push_progress_interval {
if let Err(e) = save_progress(&data_dir, push_seq) {
eprintln!("[pusher] save progress error: {}", e);
}
batches_since_save = 0;
}
}
Err(PushError::Retriable(e)) => {
eprintln!("[pusher] retriable error: {}", e);
if !backoff.wait() {
eprintln!("[pusher] max retries reached, giving up");
break;
}
}
Err(PushError::Fatal(e)) => {
eprintln!("[pusher] fatal error: {}", e);
break;
}
}
}
let _ = save_progress(&data_dir, push_seq);
}
fn read_batch(dir: &Path, from: u64, to: u64) -> Result<Vec<Record>, String> {
let manifest = std::sync::Arc::new(std::sync::Mutex::new(crate::reader::SegmentManifest::new(
dir.to_path_buf(),
)));
let reader = Reader::new(manifest, None);
let mut records = Vec::with_capacity((to - from) as usize);
let iter = reader
.scan(from, to)
.map_err(|e| format!("scan: {:?}", e))?;
for result in iter {
match result {
Ok(r) => records.push(r),
Err(e) => return Err(format!("read record: {:?}", e)),
}
}
Ok(records)
}
pub struct PusherHandle {
handle: Option<std::thread::JoinHandle<()>>,
push_seq: Arc<AtomicU64>,
}
impl PusherHandle {
pub fn spawn(
data_dir: PathBuf,
ring: Arc<Ring>,
sink: Box<dyn RemoteSink>,
config: Config,
shutdown: Arc<ShutdownState>,
) -> Self {
let push_seq = Arc::new(AtomicU64::new(0));
let handle = std::thread::Builder::new()
.name("logdb-pusher".into())
.spawn(move || {
run_pusher(data_dir.clone(), ring, sink, config, shutdown);
})
.ok();
Self { handle, push_seq }
}
pub fn join(&mut self) {
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
pub fn push_cursor(&self) -> u64 {
self.push_seq.load(Ordering::Acquire)
}
}
impl Drop for PusherHandle {
fn drop(&mut self) {
self.join();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::LogDb;
use crate::config::{Config, DurabilityMode};
use crate::record::Record;
use std::sync::Mutex;
struct TestSink {
received: Mutex<Vec<Vec<Record>>>,
}
impl TestSink {
fn new() -> Self {
Self {
received: Mutex::new(Vec::new()),
}
}
fn total(&self) -> usize {
self.received.lock().unwrap().iter().map(|b| b.len()).sum()
}
}
impl RemoteSink for TestSink {
fn push_batch(&mut self, records: &[Record]) -> Result<(), PushError> {
self.received.lock().unwrap().push(records.to_vec());
Ok(())
}
}
#[test]
fn progress_roundtrip() {
let dir = tempfile::tempdir().unwrap();
assert!(load_progress(dir.path()).is_none());
save_progress(dir.path(), 50000).unwrap();
assert_eq!(load_progress(dir.path()), Some(50000));
save_progress(dir.path(), 99999).unwrap();
assert_eq!(load_progress(dir.path()), Some(99999));
}
#[test]
fn pusher_pushes_durable_records() {
let dir = tempfile::tempdir().unwrap();
let mut config = Config::default();
config.data_dir = dir.path().to_path_buf();
config.ring_size = 128;
config.durability_mode = DurabilityMode::Sync;
config.push_batch_size = 10;
config.flush_timeout = Duration::from_secs(5);
let db = LogDb::open(config).unwrap();
for i in 0..50u64 {
db.append(format!("rec-{}", i).as_bytes()).unwrap();
}
db.flush().unwrap();
std::thread::sleep(Duration::from_millis(50));
let durable = db.durable_cursor();
assert!(durable >= 50);
let records = read_batch(dir.path(), 0, 50).unwrap();
assert_eq!(records.len(), 50);
assert_eq!(records[0].content, b"rec-0");
assert_eq!(records[49].content, b"rec-49");
}
#[test]
fn progress_corruption_returns_none() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(PROGRESS_FILE);
std::fs::write(&path, &[0xFFu8; 12]).unwrap();
assert!(load_progress(dir.path()).is_none());
}
}