use std::path::{Path, PathBuf};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::{self, JoinHandle};
use std::time::Duration;
use thiserror::Error;
use crate::Position;
use crate::index::{IndexError, IndexSet};
use crate::log::set::{LogError, SegmentConfig, SegmentSet};
use crate::read::{ReadConfig, ReadCore, ReadHandle, Snapshot};
#[derive(Clone, Copy, Debug)]
pub struct FollowerConfig {
pub segment: SegmentConfig,
pub read: ReadConfig,
}
impl FollowerConfig {
pub fn new(segment: SegmentConfig) -> FollowerConfig {
FollowerConfig {
segment,
read: ReadConfig::default(),
}
}
}
#[derive(Debug, Error)]
pub enum FollowerError {
#[error(transparent)]
Log(#[from] LogError),
#[error(transparent)]
Index(#[from] IndexError),
}
struct Inner {
set: SegmentSet,
index: IndexSet,
fed_through: Position,
}
pub struct Follower {
inner: Mutex<Inner>,
core: Arc<ReadCore>,
read_config: ReadConfig,
dir: PathBuf,
}
impl Follower {
pub fn open(dir: impl AsRef<Path>, config: FollowerConfig) -> Result<Follower, FollowerError> {
let set = SegmentSet::open_read_only(&dir, config.segment)?;
let index = IndexSet::open_read_only(&set)?;
let core = ReadCore::new(&set, &index);
let fed_through = set.last_position();
Ok(Follower {
dir: set.dir().to_path_buf(),
inner: Mutex::new(Inner {
set,
index,
fed_through,
}),
core,
read_config: config.read,
})
}
pub fn reader(&self) -> ReadHandle {
ReadHandle::new(Arc::clone(&self.core), self.read_config)
}
pub fn head(&self) -> Position {
self.core.head()
}
pub fn dir(&self) -> &Path {
&self.dir
}
pub fn refresh(&self) -> Result<Position, FollowerError> {
let mut guard = self.inner.lock().unwrap();
let inner = &mut *guard;
let refreshed = inner.set.refresh()?;
if inner.fed_through == refreshed.tip && self.core.head() == refreshed.tip {
return Ok(refreshed.tip);
}
let index_sealed = inner.index.catch_up(&inner.set, inner.fed_through)?;
inner.fed_through = refreshed.tip;
if refreshed.sealed_added > 0 || index_sealed {
self.core
.publish_segments(Snapshot::capture(&inner.set, &inner.index));
}
self.core.publish_watermark(refreshed.tip);
self.core.wake();
Ok(refreshed.tip)
}
pub fn close(&self) {
self.core.close();
}
pub fn poll_every(self: &Arc<Self>, interval: Duration) -> FollowerPoller {
let signal = Arc::new(Signal::default());
let health = Arc::new(Mutex::new(PollerHealth::default()));
let follower = Arc::clone(self);
let thread_signal = Arc::clone(&signal);
let thread_health = Arc::clone(&health);
let join = thread::Builder::new()
.name("tephra-follower".to_string())
.spawn(move || {
while !thread_signal.stopped() {
match follower.refresh() {
Ok(_) => {
let mut health = lock(&thread_health);
health.consecutive_failures = 0;
health.last_error = None;
}
Err(err) => {
#[cfg(feature = "tracing")]
tracing::warn!("follower refresh failed: {err}");
let mut health = lock(&thread_health);
health.consecutive_failures += 1;
health.last_error = Some(err.to_string());
}
}
thread_signal.wait(interval);
}
})
.expect("spawn follower thread");
FollowerPoller {
signal,
health,
join: Some(join),
follower: Arc::clone(self),
}
}
}
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[derive(Default)]
struct Signal {
stopped: Mutex<bool>,
wake: Condvar,
}
impl Signal {
fn stopped(&self) -> bool {
*lock(&self.stopped)
}
fn stop(&self) {
*lock(&self.stopped) = true;
self.wake.notify_all();
}
fn wait(&self, interval: Duration) {
let guard = lock(&self.stopped);
if *guard {
return;
}
let _unused = self
.wake
.wait_timeout(guard, interval)
.unwrap_or_else(|poisoned| poisoned.into_inner());
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PollerHealth {
pub consecutive_failures: u64,
pub last_error: Option<String>,
pub thread_died: bool,
}
pub struct FollowerPoller {
signal: Arc<Signal>,
health: Arc<Mutex<PollerHealth>>,
join: Option<JoinHandle<()>>,
follower: Arc<Follower>,
}
impl FollowerPoller {
pub fn health(&self) -> PollerHealth {
let mut health = lock(&self.health).clone();
health.thread_died = self
.join
.as_ref()
.is_some_and(|join| join.is_finished() && !self.signal.stopped());
health
}
pub fn stop(mut self) {
self.shutdown();
}
fn shutdown(&mut self) {
self.signal.stop();
if let Some(join) = self.join.take()
&& join.join().is_err()
{
let mut health = lock(&self.health);
health.thread_died = true;
}
self.follower.close();
}
}
impl Drop for FollowerPoller {
fn drop(&mut self) {
self.shutdown();
}
}