use std::any::{Any, TypeId};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use tokio::sync::mpsc::UnboundedSender;
pub(crate) struct MergeRequest {
pub(crate) type_id: TypeId,
pub(crate) reply: std::sync::mpsc::Sender<Option<Box<dyn Any + Send>>>,
}
type FoldFn = Box<dyn FnMut(&mut dyn Any, Box<dyn Any + Send>) + Send>;
type ObserveFn = Box<dyn Fn(&dyn Any) + Send>;
pub(crate) struct MergeSpec {
type_id: TypeId,
period: Duration,
primary: Box<dyn Any + Send>,
fold: FoldFn,
observe: Option<ObserveFn>,
}
impl MergeSpec {
pub(crate) fn new<T, F>(period: Duration, mut merge: F) -> Self
where
T: Default + Send + 'static,
F: FnMut(&mut T, T) + Send + 'static,
{
Self {
type_id: TypeId::of::<T>(),
period,
primary: Box::new(T::default()),
fold: Box::new(move |primary, taken| {
if let (Some(p), Ok(t)) = (primary.downcast_mut::<T>(), taken.downcast::<T>()) {
merge(p, *t);
}
}),
observe: None,
}
}
pub(crate) fn type_id(&self) -> TypeId {
self.type_id
}
pub(crate) fn set_observe<T, G>(&mut self, observe: G)
where
T: 'static,
G: Fn(&T) + Send + 'static,
{
self.observe = Some(Box::new(move |primary| {
if let Some(p) = primary.downcast_ref::<T>() {
observe(p);
}
}));
}
}
pub(crate) fn merge_worker(
txs: Vec<UnboundedSender<MergeRequest>>,
mut specs: Vec<MergeSpec>,
stop: Arc<AtomicBool>,
) {
if specs.is_empty() || txs.is_empty() {
return;
}
let reply_timeout = Duration::from_millis(500);
let mut next_fire: Vec<Instant> = specs.iter().map(|s| Instant::now() + s.period).collect();
while !stop.load(Ordering::Relaxed) {
let now = Instant::now();
let soonest = next_fire
.iter()
.copied()
.min()
.unwrap_or_else(|| now + Duration::from_millis(50));
let nap = soonest
.saturating_duration_since(now)
.min(Duration::from_millis(50));
if !nap.is_zero() {
std::thread::sleep(nap);
}
if stop.load(Ordering::Relaxed) {
break;
}
let now = Instant::now();
for (i, spec) in specs.iter_mut().enumerate() {
if now >= next_fire[i] {
next_fire[i] = now + spec.period;
probe_and_fold(spec, &txs, reply_timeout);
}
}
}
for spec in specs.iter_mut() {
probe_and_fold(spec, &txs, Duration::from_millis(100));
}
}
fn probe_and_fold(spec: &mut MergeSpec, txs: &[UnboundedSender<MergeRequest>], timeout: Duration) {
for tx in txs {
let (reply_tx, reply_rx) = std::sync::mpsc::channel();
if tx
.send(MergeRequest {
type_id: spec.type_id,
reply: reply_tx,
})
.is_err()
{
continue; }
if let Ok(Some(boxed)) = reply_rx.recv_timeout(timeout) {
(spec.fold)(spec.primary.as_mut(), boxed);
}
}
if let Some(obs) = &spec.observe {
obs(spec.primary.as_ref());
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::AtomicU64;
use super::*;
use crate::ctx::StateMap;
#[derive(Default)]
struct Counter(u64);
#[test]
fn worker_folds_each_shard_and_observes_grand_total() {
let stop = Arc::new(AtomicBool::new(false));
let num_shards: u64 = 2;
let delta: u64 = 5;
let observed = Arc::new(AtomicU64::new(0));
let mut spec = MergeSpec::new::<Counter, _>(
Duration::from_millis(20),
|p: &mut Counter, t: Counter| {
p.0 += t.0;
},
);
{
let observed = Arc::clone(&observed);
spec.set_observe::<Counter, _>(move |c: &Counter| {
observed.store(c.0, Ordering::Relaxed);
});
}
let mut txs = Vec::new();
let mut shard_handles = Vec::new();
for _ in 0..num_shards {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<MergeRequest>();
txs.push(tx);
let stop = Arc::clone(&stop);
shard_handles.push(std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async move {
let mut state = StateMap::default();
state.get_or_init_mut::<Counter>().0 = delta;
loop {
if stop.load(Ordering::Relaxed) {
break;
}
tokio::select! {
req = rx.recv() => match req {
Some(req) => {
let taken = state.take_dyn(req.type_id);
let _ = req.reply.send(taken);
}
None => break,
},
_ = tokio::time::sleep(Duration::from_millis(5)) => {}
}
}
});
}));
}
let worker_stop = Arc::clone(&stop);
let worker = std::thread::spawn(move || merge_worker(txs, vec![spec], worker_stop));
std::thread::sleep(Duration::from_millis(120));
stop.store(true, Ordering::Relaxed);
worker.join().unwrap();
for h in shard_handles {
let _ = h.join();
}
assert_eq!(observed.load(Ordering::Relaxed), num_shards * delta);
}
}