use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use std::time::Instant;
use dynomite::events::{ClusterEvent, EventManager, TokenRange};
use dynomite::hashkit::DynToken;
use crate::aae::config::ConfAae;
use crate::aae::exchange::{Divergence, Exchange, ExchangeError, PeerView};
use crate::aae::metrics::AaeMetrics;
use crate::aae::tictac::Tree;
pub trait Clock: Send + Sync {
fn now(&self) -> Instant;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> Instant {
Instant::now()
}
}
#[derive(Debug)]
pub struct MockClock {
base: Instant,
offset: Mutex<Duration>,
}
impl MockClock {
#[must_use]
pub fn new() -> Self {
Self {
base: Instant::now(),
offset: Mutex::new(Duration::ZERO),
}
}
pub fn advance(&self, d: Duration) {
let mut g = self.offset.lock().expect("mock clock mutex poisoned");
*g += d;
}
}
impl Default for MockClock {
fn default() -> Self {
Self::new()
}
}
impl Clock for MockClock {
fn now(&self) -> Instant {
let g = self.offset.lock().expect("mock clock mutex poisoned");
self.base + *g
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct SweepTick {
pub peer_idx: u32,
pub time_bucket: u32,
pub token_hex: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SweepPlan {
ticks: Vec<SweepTick>,
}
impl SweepPlan {
#[must_use]
pub fn new(peer_idxs: &[u32], n_time_buckets: u32, cfg: &ConfAae) -> Self {
let total_ticks = cfg
.full_sweep_interval_seconds
.div_ceil(cfg.segment_interval_seconds.max(1));
let total_ticks = usize::try_from(total_ticks).unwrap_or(usize::MAX);
let pair_count = peer_idxs.len().saturating_mul(n_time_buckets as usize);
if pair_count == 0 {
return Self { ticks: Vec::new() };
}
let envelope = total_ticks.min(pair_count);
let mut ticks = Vec::with_capacity(envelope);
for i in 0..envelope {
let peer = peer_idxs[i % peer_idxs.len()];
let bucket =
u32::try_from((i / peer_idxs.len()) % (n_time_buckets as usize)).unwrap_or(0);
ticks.push(SweepTick {
peer_idx: peer,
time_bucket: bucket,
token_hex: None,
});
}
Self { ticks }
}
#[must_use]
pub fn new_per_token(
peers_with_tokens: &[(u32, Vec<DynToken>)],
n_time_buckets: u32,
cfg: &ConfAae,
) -> Self {
let total_ticks = cfg
.full_sweep_interval_seconds
.div_ceil(cfg.segment_interval_seconds.max(1));
let total_ticks = usize::try_from(total_ticks).unwrap_or(usize::MAX);
let mut peer_token_pairs: Vec<(u32, String)> = Vec::new();
let max_tokens = peers_with_tokens
.iter()
.map(|(_, ts)| ts.len())
.max()
.unwrap_or(0);
for ti in 0..max_tokens {
for (peer, tokens) in peers_with_tokens {
if let Some(t) = tokens.get(ti) {
peer_token_pairs.push((*peer, t.to_hex()));
}
}
}
let triple_count = peer_token_pairs
.len()
.saturating_mul(n_time_buckets as usize);
if triple_count == 0 {
return Self { ticks: Vec::new() };
}
let envelope = total_ticks.min(triple_count);
let mut ticks = Vec::with_capacity(envelope);
for i in 0..envelope {
let pair = &peer_token_pairs[i % peer_token_pairs.len()];
let bucket = u32::try_from((i / peer_token_pairs.len()) % (n_time_buckets as usize))
.unwrap_or(0);
ticks.push(SweepTick {
peer_idx: pair.0,
time_bucket: bucket,
token_hex: Some(pair.1.clone()),
});
}
Self { ticks }
}
#[must_use]
pub fn len(&self) -> usize {
self.ticks.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.ticks.is_empty()
}
#[must_use]
pub fn ticks(&self) -> &[SweepTick] {
&self.ticks
}
}
pub struct Scheduler<C: Clock> {
cfg: ConfAae,
clock: Arc<C>,
next_due: Mutex<Instant>,
cursor: Mutex<usize>,
plan: Mutex<Option<SweepPlan>>,
next_snapshot_due: Mutex<Instant>,
metrics: Mutex<Option<Arc<AaeMetrics>>>,
events: Mutex<Option<Arc<EventManager>>>,
}
impl<C: Clock> Scheduler<C> {
#[must_use]
pub fn new(cfg: ConfAae, clock: Arc<C>) -> Self {
let now = clock.now();
let snapshot_due = now + Duration::from_secs(cfg.snapshot_interval_seconds);
Self {
cfg,
clock,
next_due: Mutex::new(now),
cursor: Mutex::new(0),
plan: Mutex::new(None),
next_snapshot_due: Mutex::new(snapshot_due),
metrics: Mutex::new(None),
events: Mutex::new(None),
}
}
pub fn install_metrics(&self, metrics: Arc<AaeMetrics>) {
let mut g = self.metrics.lock().expect("metrics mutex poisoned");
*g = Some(metrics);
}
#[must_use]
pub fn metrics(&self) -> Option<Arc<AaeMetrics>> {
self.metrics.lock().expect("metrics mutex poisoned").clone()
}
pub fn install_events(&self, events: Arc<EventManager>) {
let mut g = self.events.lock().expect("events mutex poisoned");
*g = Some(events);
}
#[must_use]
pub fn events(&self) -> Option<Arc<EventManager>> {
self.events.lock().expect("events mutex poisoned").clone()
}
pub fn notify_exchange_started(&self, with_peer: u32, partition: TokenRange) {
if let Some(ev) = self.events() {
ev.publish(ClusterEvent::AaeExchangeStarted {
with_peer,
partition,
ts: std::time::SystemTime::now(),
});
}
}
pub fn notify_exchange_completed(&self, with_peer: u32, partition: TokenRange, repaired: u64) {
if let Some(ev) = self.events() {
ev.publish(ClusterEvent::AaeExchangeCompleted {
with_peer,
partition,
repaired,
ts: std::time::SystemTime::now(),
});
}
}
pub fn observe_exchange_attempt(&self, peer_idx: u32, dc: &str, rack: &str) {
if let Some(m) = self.metrics() {
m.record_exchange_attempt(peer_idx, dc, rack);
}
}
pub fn observe_exchange_success(&self, peer_idx: u32, dc: &str, rack: &str) {
if let Some(m) = self.metrics() {
m.record_exchange_success(peer_idx, dc, rack);
}
}
pub fn observe_divergent_keys(&self, peer_idx: u32, dc: &str, rack: &str, count: u64) {
if let Some(m) = self.metrics() {
m.record_divergent_keys(peer_idx, dc, rack, count);
}
}
pub fn install_plan(&self, plan: SweepPlan) {
let mut p = self.plan.lock().expect("plan mutex poisoned");
let mut c = self.cursor.lock().expect("cursor mutex poisoned");
*p = Some(plan);
*c = 0;
}
pub fn cursor(&self) -> usize {
*self.cursor.lock().expect("cursor mutex poisoned")
}
pub fn poll(&self) -> Option<SweepTick> {
if !self.cfg.enabled {
return None;
}
let now = self.clock.now();
let mut due = self.next_due.lock().expect("due mutex poisoned");
if now < *due {
return None;
}
let plan_g = self.plan.lock().expect("plan mutex poisoned");
let plan = plan_g.as_ref()?;
if plan.is_empty() {
*due = now + self.cfg.segment_interval();
return None;
}
let mut cursor_g = self.cursor.lock().expect("cursor mutex poisoned");
let i = *cursor_g % plan.ticks().len();
let tick = plan.ticks()[i].clone();
*cursor_g = (*cursor_g + 1) % plan.ticks().len();
*due = now + self.cfg.segment_interval();
Some(tick)
}
pub fn snapshot_due(&self) -> bool {
if !self.cfg.enabled {
return false;
}
let now = self.clock.now();
let due = self
.next_snapshot_due
.lock()
.expect("snapshot_due mutex poisoned");
now >= *due
}
pub fn mark_snapshot_taken(&self) {
let now = self.clock.now();
let mut due = self
.next_snapshot_due
.lock()
.expect("snapshot_due mutex poisoned");
*due = now + Duration::from_secs(self.cfg.snapshot_interval_seconds);
}
pub fn exchange_per_token<V: PeerView + Clone>(
local: &Tree,
remote: &V,
tokens: &[DynToken],
) -> Result<Vec<(String, Vec<Divergence>)>, ExchangeError> {
let mut out = Vec::with_capacity(tokens.len());
for token in tokens {
let exch = Exchange::new(local, remote.clone());
let divs = exch.run()?;
out.push((token.to_hex(), divs));
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg() -> ConfAae {
ConfAae {
enabled: true,
full_sweep_interval_seconds: 600,
segment_interval_seconds: 60,
n_time_buckets: 4,
n_segments: 32,
time_window_seconds: 60,
..ConfAae::default()
}
}
#[test]
fn plan_round_robins_peers_and_buckets() {
let plan = SweepPlan::new(&[1, 2], 4, &cfg());
assert_eq!(plan.len(), 8);
assert_eq!(plan.ticks()[0].time_bucket, 0);
assert_eq!(plan.ticks()[1].time_bucket, 0);
assert_ne!(plan.ticks()[0].peer_idx, plan.ticks()[1].peer_idx);
assert_eq!(plan.ticks()[2].time_bucket, 1);
}
#[test]
fn plan_handles_empty_peers() {
let plan = SweepPlan::new(&[], 4, &cfg());
assert!(plan.is_empty());
}
#[test]
fn scheduler_fires_at_configured_cadence() {
let clock = Arc::new(MockClock::new());
let sched: Scheduler<MockClock> = Scheduler::new(cfg(), clock.clone());
sched.install_plan(SweepPlan::new(&[1, 2], 4, &cfg()));
let t0 = sched.poll().expect("first tick should fire");
assert_eq!(t0.peer_idx, 1);
clock.advance(Duration::from_secs(30));
assert!(sched.poll().is_none());
clock.advance(Duration::from_secs(31));
let t1 = sched.poll().expect("second tick should fire");
assert_eq!(t1.peer_idx, 2);
}
#[test]
fn scheduler_disabled_returns_none() {
let clock = Arc::new(MockClock::new());
let mut c = cfg();
c.enabled = false;
let sched: Scheduler<MockClock> = Scheduler::new(c, clock);
sched.install_plan(SweepPlan::new(&[1], 1, &cfg()));
assert!(sched.poll().is_none());
}
#[test]
fn cursor_wraps_around_plan() {
let clock = Arc::new(MockClock::new());
let sched: Scheduler<MockClock> = Scheduler::new(cfg(), clock.clone());
let plan = SweepPlan::new(&[7], 2, &cfg());
let plan_len = plan.len();
sched.install_plan(plan);
for _ in 0..plan_len {
clock.advance(Duration::from_mins(1));
let _ = sched.poll();
}
assert_eq!(sched.cursor(), 0);
}
#[test]
fn snapshot_due_fires_on_configured_cadence() {
let clock = Arc::new(MockClock::new());
let mut c = cfg();
c.snapshot_interval_seconds = 300;
let sched: Scheduler<MockClock> = Scheduler::new(c, clock.clone());
assert!(!sched.snapshot_due());
clock.advance(Duration::from_secs(299));
assert!(!sched.snapshot_due());
clock.advance(Duration::from_secs(1));
assert!(sched.snapshot_due());
sched.mark_snapshot_taken();
assert!(!sched.snapshot_due());
clock.advance(Duration::from_secs(301));
assert!(sched.snapshot_due());
}
#[test]
fn snapshot_due_returns_false_when_disabled() {
let clock = Arc::new(MockClock::new());
let mut c = cfg();
c.enabled = false;
c.snapshot_interval_seconds = 1;
let sched: Scheduler<MockClock> = Scheduler::new(c, clock.clone());
clock.advance(Duration::from_hours(1));
assert!(!sched.snapshot_due());
}
#[test]
fn per_token_plan_emits_one_tick_per_triple() {
let peers = vec![
(1u32, vec![DynToken::from_u32(100), DynToken::from_u32(200)]),
(2u32, vec![DynToken::from_u32(300)]),
];
let mut c = cfg();
c.full_sweep_interval_seconds = 60 * 60;
c.segment_interval_seconds = 60;
let plan = SweepPlan::new_per_token(&peers, 4, &c);
assert_eq!(plan.len(), 12);
for tick in plan.ticks() {
assert!(tick.token_hex.is_some(), "per-token plan must tag tokens");
}
assert_eq!(plan.ticks()[0].time_bucket, 0);
assert_eq!(plan.ticks()[1].time_bucket, 0);
assert_eq!(plan.ticks()[2].time_bucket, 0);
assert_eq!(plan.ticks()[3].time_bucket, 1);
}
#[test]
fn per_token_plan_handles_no_peers() {
let plan = SweepPlan::new_per_token(&[], 4, &cfg());
assert!(plan.is_empty());
}
#[test]
fn per_token_plan_handles_peers_with_no_tokens() {
let plan = SweepPlan::new_per_token(&[(1u32, Vec::new())], 4, &cfg());
assert!(plan.is_empty());
}
#[test]
fn per_token_plan_token_hex_roundtrips() {
let peers = vec![(7u32, vec![DynToken::from_u32(0xdead)])];
let plan = SweepPlan::new_per_token(&peers, 1, &cfg());
let hex = plan.ticks()[0].token_hex.as_ref().unwrap();
assert_eq!(hex, "0000dead");
}
#[test]
fn exchange_per_token_runs_one_exchange_per_token() {
use crate::aae::exchange::LocalPeerView;
use crate::aae::tictac::{Tree, TreeShape};
let shape = TreeShape {
n_time_buckets: 2,
n_segments: 16,
time_window_seconds: 60,
};
let mut a = Tree::new(shape);
let mut b = Tree::new(shape);
for i in 0..32u32 {
let k = format!("k{i}");
a.insert(b"users", k.as_bytes(), b"vc1", 0);
b.insert(b"users", k.as_bytes(), b"vc1", 0);
}
let view = LocalPeerView::new(&b);
let tokens = vec![DynToken::from_u32(1), DynToken::from_u32(2)];
let pairs =
Scheduler::<MockClock>::exchange_per_token(&a, &view, &tokens).expect("exchange");
assert_eq!(pairs.len(), 2);
for (hex, divs) in &pairs {
assert!(!hex.is_empty());
assert!(divs.is_empty(), "identical trees must not diverge");
}
}
#[test]
fn exchange_per_token_surfaces_divergence() {
use crate::aae::exchange::LocalPeerView;
use crate::aae::tictac::{Tree, TreeShape};
let shape = TreeShape {
n_time_buckets: 2,
n_segments: 16,
time_window_seconds: 60,
};
let mut a = Tree::new(shape);
let b = Tree::new(shape);
a.insert(b"users", b"alice", b"vc1", 0);
let view = LocalPeerView::new(&b);
let tokens = vec![DynToken::from_u32(7)];
let pairs =
Scheduler::<MockClock>::exchange_per_token(&a, &view, &tokens).expect("exchange");
assert_eq!(pairs.len(), 1);
let (_hex, divs) = &pairs[0];
assert!(
divs.iter()
.any(|d| d.local_only.iter().any(|e| e.key == b"alice")),
"alice must surface as local-only"
);
}
#[test]
fn install_metrics_routes_observations_to_handle() {
use crate::aae::metrics::AaeMetrics;
let clock = Arc::new(MockClock::new());
let sched: Scheduler<MockClock> = Scheduler::new(cfg(), clock);
let m = Arc::new(AaeMetrics::new());
sched.install_metrics(Arc::clone(&m));
sched.observe_exchange_attempt(3, "dc1", "rA");
sched.observe_exchange_success(3, "dc1", "rA");
sched.observe_divergent_keys(3, "dc1", "rA", 5);
let snap = m.snapshot();
assert_eq!(snap.exchange_attempts.len(), 1);
assert_eq!(snap.exchange_attempts[0].count, 1);
assert_eq!(snap.exchange_success.len(), 1);
assert_eq!(snap.divergent_keys[0].count, 5);
}
#[test]
fn observations_without_metrics_handle_are_noops() {
let clock = Arc::new(MockClock::new());
let sched: Scheduler<MockClock> = Scheduler::new(cfg(), clock);
sched.observe_exchange_attempt(0, "dc1", "rA");
sched.observe_exchange_success(0, "dc1", "rA");
sched.observe_divergent_keys(0, "dc1", "rA", 7);
assert!(sched.metrics().is_none());
}
}