use std::{
sync::{
Arc,
atomic::{AtomicBool, AtomicI64, Ordering},
},
thread,
time::Duration,
};
use parking_lot::Mutex;
pub const POLL_INTERVAL_MS: u64 = 1;
pub trait LogTail: Send + Sync {
fn get_tail_address(&self, sublog_idx: usize) -> i64;
}
pub struct AofBackpressure {
per_sublog_budget: AtomicI64,
publish_delta_bytes: AtomicI64,
enabled: AtomicBool,
sublog_count: usize,
shipped_watermark: Vec<AtomicI64>,
disposed: AtomicBool,
log: Mutex<Option<Arc<dyn LogTail>>>,
}
impl AofBackpressure {
pub fn new(sublog_count: usize, aof_sync_max_lag_bytes: i64) -> Self {
let shipped_watermark = (0..sublog_count)
.map(|_| AtomicI64::new(i64::MAX))
.collect();
let gate = Self {
per_sublog_budget: AtomicI64::new(i64::MAX),
publish_delta_bytes: AtomicI64::new(1),
enabled: AtomicBool::new(false),
sublog_count,
shipped_watermark,
disposed: AtomicBool::new(false),
log: Mutex::new(None),
};
gate.set_budget(aof_sync_max_lag_bytes);
gate
}
pub fn set_budget(&self, aof_sync_max_lag_bytes: i64) {
if aof_sync_max_lag_bytes > 0 {
let per_sublog_budget = (aof_sync_max_lag_bytes / self.sublog_count as i64).max(1);
self
.per_sublog_budget
.store(per_sublog_budget, Ordering::Relaxed);
self
.publish_delta_bytes
.store((per_sublog_budget / 8).max(1), Ordering::Relaxed);
self.enabled.store(true, Ordering::Relaxed);
} else {
self.per_sublog_budget.store(i64::MAX, Ordering::Relaxed);
self.publish_delta_bytes.store(1, Ordering::Relaxed);
self.enabled.store(false, Ordering::Relaxed);
}
}
pub fn enabled(&self) -> bool {
self.enabled.load(Ordering::Relaxed)
}
pub fn publish_delta_bytes(&self) -> i64 {
self.publish_delta_bytes.load(Ordering::Relaxed)
}
pub fn wait(&self, sublog_idx: usize, tail_address: i64) {
if !self.enabled() {
return;
}
let watermark = self.shipped_watermark[sublog_idx].load(Ordering::Acquire);
if tail_address - watermark <= self.per_sublog_budget.load(Ordering::Relaxed) {
return;
}
self.wait_slow(sublog_idx, tail_address);
}
pub fn wait_slow(&self, sublog_idx: usize, captured_tail: i64) {
while !self.disposed.load(Ordering::Relaxed) {
let live_tail = self
.log
.lock()
.as_ref()
.map_or(captured_tail, |log| log.get_tail_address(sublog_idx));
let watermark = self.shipped_watermark[sublog_idx].load(Ordering::Acquire);
if live_tail - watermark <= self.per_sublog_budget.load(Ordering::Relaxed) {
break;
}
thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
}
}
pub fn any_stalled(&self) -> bool {
if !self.enabled() {
return false;
}
let log = self.log.lock();
for (i, watermark) in self.shipped_watermark.iter().enumerate() {
let tail = log.as_ref().map_or(0, |log| log.get_tail_address(i));
if tail - watermark.load(Ordering::Acquire) > self.per_sublog_budget.load(Ordering::Relaxed) {
return true;
}
}
false
}
pub fn set_log(&self, log: Arc<dyn LogTail>) {
*self.log.lock() = Some(log);
}
pub fn publish_shipped_address(&self, sublog_idx: usize, min_shipped_address: i64) {
self.shipped_watermark[sublog_idx].store(min_shipped_address, Ordering::Release);
}
pub fn dispose(&self) {
self.disposed.store(true, Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use std::sync::{
Arc,
atomic::{AtomicU64, Ordering as AtomicOrdering},
};
use super::{AofBackpressure, LogTail};
struct FakeLog {
tail: AtomicU64,
}
impl LogTail for FakeLog {
fn get_tail_address(&self, _sublog_idx: usize) -> i64 {
self.tail.load(AtomicOrdering::Relaxed) as i64
}
}
#[test]
fn budget_enables_and_disables_gate() {
let gate = AofBackpressure::new(2, 1024);
assert!(gate.enabled());
assert_eq!(gate.publish_delta_bytes(), 1024 / 2 / 8);
assert_eq!(gate.per_sublog_budget.load(AtomicOrdering::Relaxed), 512);
let off = AofBackpressure::new(2, -1);
assert!(!off.enabled());
assert!(!off.any_stalled());
}
#[test]
fn wait_passes_within_budget_and_stalls_beyond() {
let log = Arc::new(FakeLog {
tail: AtomicU64::new(0),
});
let gate = AofBackpressure::new(1, 100);
assert_eq!(gate.publish_delta_bytes(), 100 / 8);
gate.set_log(log.clone());
gate.wait(0, 10_000);
assert!(!gate.any_stalled());
gate.publish_shipped_address(0, 0);
log.tail.store(50, AtomicOrdering::Relaxed);
assert!(!gate.any_stalled());
log.tail.store(200, AtomicOrdering::Relaxed);
assert!(gate.any_stalled());
gate.publish_shipped_address(0, 150);
assert!(!gate.any_stalled());
}
#[test]
fn dispose_releases_all() {
let gate = AofBackpressure::new(1, 100);
gate.publish_shipped_address(0, 0);
gate.dispose();
gate.wait_slow(0, 1_000_000);
}
}