use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
pub type EventCallbacks<E> = [Arc<dyn Fn(&E) + Send + Sync>];
pub fn dispatch_event_callbacks<E>(owner: &str, callbacks: &EventCallbacks<E>, event: &E) -> usize {
let mut panics = 0;
for (index, callback) in callbacks.iter().enumerate() {
if let Err(payload) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
callback(event);
})) {
panics += 1;
log::error!(
"{} callback #{} panicked: {}; continuing dispatch",
owner,
index,
panic_payload_message(&payload)
);
}
}
panics
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DispatchOutcome {
pub delivered: usize,
pub skipped_by_filter: usize,
pub panics: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TimedDispatchOutcome {
pub outcome: DispatchOutcome,
pub slow: usize,
pub slowest_ms: u64,
}
pub type SubscriptionId = u64;
pub type EventFilter<E> = Arc<dyn Fn(&E) -> bool + Send + Sync>;
struct SubscriptionEntry<E> {
id: SubscriptionId,
callback: Arc<dyn Fn(&E) + Send + Sync>,
filter: Option<EventFilter<E>>,
}
struct SnapshotEntry<E> {
callback: Arc<dyn Fn(&E) + Send + Sync>,
filter: Option<EventFilter<E>>,
}
enum FilterEval {
Matched,
Skip,
Panicked,
}
fn eval_filter<E>(
owner: &str,
index: usize,
filter: Option<&EventFilter<E>>,
event: &E,
) -> FilterEval {
match filter {
None => FilterEval::Matched,
Some(filter) => {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| filter(event))) {
Ok(true) => FilterEval::Matched,
Ok(false) => FilterEval::Skip,
Err(payload) => {
log::error!(
"{} filter #{} panicked: {}; skipping observer",
owner,
index,
panic_payload_message(&payload)
);
FilterEval::Panicked
}
}
}
}
}
fn invoke_callback<E>(
callback: &dyn Fn(&E),
event: &E,
) -> Result<(), Box<dyn std::any::Any + Send>> {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(event)))
}
pub struct EventSubscriptions<E> {
next_id: AtomicU64,
entries: parking_lot::RwLock<Vec<SubscriptionEntry<E>>>,
}
impl<E> Default for EventSubscriptions<E> {
fn default() -> Self {
Self::new()
}
}
impl<E> EventSubscriptions<E> {
pub fn new() -> Self {
Self {
next_id: AtomicU64::new(1),
entries: parking_lot::RwLock::new(Vec::new()),
}
}
pub fn add(&self, callback: Arc<dyn Fn(&E) + Send + Sync>) -> SubscriptionId {
self.add_filtered(callback, None)
}
pub fn add_filtered(
&self,
callback: Arc<dyn Fn(&E) + Send + Sync>,
filter: Option<EventFilter<E>>,
) -> SubscriptionId {
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
self.entries.write().push(SubscriptionEntry {
id,
callback,
filter,
});
id
}
pub fn remove(&self, id: SubscriptionId) -> bool {
let mut entries = self.entries.write();
let before = entries.len();
entries.retain(|entry| entry.id != id);
entries.len() != before
}
pub fn clear(&self) {
self.entries.write().clear();
}
pub fn subscribe(&self, callback: Arc<dyn Fn(&E) + Send + Sync>) -> SubscriptionGuard<'_, E> {
let id = self.add(callback);
SubscriptionGuard::new(self, id)
}
pub fn len(&self) -> usize {
self.entries.read().len()
}
pub fn is_empty(&self) -> bool {
self.entries.read().is_empty()
}
pub fn dispatch(&self, owner: &str, event: &E) -> usize {
self.dispatch_detailed(owner, event).panics
}
fn snapshot_locked(&self) -> Vec<SnapshotEntry<E>> {
let entries = self.entries.read();
if entries.is_empty() {
return Vec::new();
}
entries
.iter()
.map(|entry| SnapshotEntry {
callback: Arc::clone(&entry.callback),
filter: entry.filter.clone(),
})
.collect()
}
pub fn dispatch_detailed(&self, owner: &str, event: &E) -> DispatchOutcome {
let snapshot = self.snapshot_locked();
if snapshot.is_empty() {
return DispatchOutcome::default();
}
let mut outcome = DispatchOutcome::default();
for (index, item) in snapshot.iter().enumerate() {
match eval_filter(owner, index, item.filter.as_ref(), event) {
FilterEval::Skip => {
outcome.skipped_by_filter += 1;
continue;
}
FilterEval::Panicked => {
outcome.panics += 1;
outcome.skipped_by_filter += 1;
continue;
}
FilterEval::Matched => {}
}
match invoke_callback(item.callback.as_ref(), event) {
Ok(()) => {
outcome.delivered += 1;
}
Err(payload) => {
outcome.panics += 1;
log::error!(
"{} callback #{} panicked: {}; continuing dispatch",
owner,
index,
panic_payload_message(&payload)
);
continue;
}
}
}
outcome
}
pub fn dispatch_timed(
&self,
owner: &str,
event: &E,
slow_threshold: std::time::Duration,
) -> TimedDispatchOutcome {
let snapshot = self.snapshot_locked();
if snapshot.is_empty() {
return TimedDispatchOutcome::default();
}
let mut result = TimedDispatchOutcome::default();
for (index, item) in snapshot.iter().enumerate() {
match eval_filter(owner, index, item.filter.as_ref(), event) {
FilterEval::Skip => {
result.outcome.skipped_by_filter += 1;
continue;
}
FilterEval::Panicked => {
result.outcome.panics += 1;
result.outcome.skipped_by_filter += 1;
continue;
}
FilterEval::Matched => {}
}
let start = std::time::Instant::now();
match invoke_callback(item.callback.as_ref(), event) {
Ok(()) => {
result.outcome.delivered += 1;
}
Err(payload) => {
result.outcome.panics += 1;
log::error!(
"{} callback #{} panicked: {}; continuing dispatch",
owner,
index,
panic_payload_message(&payload)
);
}
}
let elapsed = start.elapsed();
if elapsed >= slow_threshold {
result.slow += 1;
let elapsed_ms = elapsed.as_millis() as u64;
result.slowest_ms = result.slowest_ms.max(elapsed_ms);
log::warn!(
"{} callback #{} slow: {}ms >= {}ms threshold",
owner,
index,
elapsed_ms,
slow_threshold.as_millis() as u64,
);
}
}
result
}
}
pub struct SubscriptionGuard<'a, E> {
registry: Option<&'a EventSubscriptions<E>>,
id: SubscriptionId,
}
impl<'a, E> SubscriptionGuard<'a, E> {
fn new(registry: &'a EventSubscriptions<E>, id: SubscriptionId) -> Self {
Self {
registry: Some(registry),
id,
}
}
pub fn id(&self) -> SubscriptionId {
self.id
}
pub fn forget(mut self) -> SubscriptionId {
let id = self.id;
self.registry = None;
std::mem::forget(self);
id
}
}
impl<E> Drop for SubscriptionGuard<'_, E> {
fn drop(&mut self) {
if let Some(registry) = self.registry {
registry.remove(self.id);
}
}
}
pub fn panic_payload_message(payload: &Box<dyn std::any::Any + Send>) -> String {
if let Some(message) = payload.downcast_ref::<&str>() {
message.to_string()
} else if let Some(message) = payload.downcast_ref::<String>() {
message.clone()
} else {
"unknown panic payload".to_string()
}
}
pub struct AsyncForwarder<E> {
sender: std::sync::mpsc::SyncSender<E>,
dropped: std::sync::atomic::AtomicUsize,
}
impl<E> AsyncForwarder<E>
where
E: Clone + Send + 'static,
{
pub fn spawn(
capacity: usize,
callback: impl Fn(&E) + Send + 'static,
) -> (Arc<Self>, std::thread::JoinHandle<()>) {
let (sender, receiver) = std::sync::mpsc::sync_channel(capacity);
let forwarder = Arc::new(Self {
sender,
dropped: std::sync::atomic::AtomicUsize::new(0),
});
let handle = std::thread::spawn(move || {
for event in receiver {
if invoke_callback(&callback, &event).is_err() {
log::error!("async forwarder callback panicked; continuing");
}
}
});
(forwarder, handle)
}
pub fn forward(&self, event: &E) {
if self.sender.try_send(event.clone()).is_err() {
self.dropped
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
pub fn dropped_count(&self) -> usize {
self.dropped.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn adapter(self: &Arc<Self>) -> Arc<dyn Fn(&E) + Send + Sync> {
let forwarder = Arc::clone(self);
Arc::new(move |event: &E| forwarder.forward(event))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn dispatch_reaches_all_and_isolates_panic() {
let registry: EventSubscriptions<u32> = EventSubscriptions::new();
let hits = Arc::new(AtomicUsize::new(0));
let first = Arc::clone(&hits);
registry.add(Arc::new(move |_| {
first.fetch_add(1, Ordering::SeqCst);
}));
registry.add(Arc::new(|_: &u32| panic!("boom")));
let last = Arc::clone(&hits);
registry.add(Arc::new(move |_| {
last.fetch_add(1, Ordering::SeqCst);
}));
let panics = registry.dispatch("test", &1);
assert_eq!(panics, 1);
assert_eq!(hits.load(Ordering::SeqCst), 2);
}
#[test]
fn remove_and_filter_are_honored() {
let registry: EventSubscriptions<u32> = EventSubscriptions::new();
let hits = Arc::new(AtomicUsize::new(0));
let probe = Arc::clone(&hits);
let id = registry.add(Arc::new(move |_| {
probe.fetch_add(1, Ordering::SeqCst);
}));
assert_eq!(registry.len(), 1);
assert!(registry.remove(id));
assert!(!registry.remove(id));
assert_eq!(registry.dispatch("test", &1), 0);
assert_eq!(hits.load(Ordering::SeqCst), 0);
let even_probe = Arc::clone(&hits);
registry.add_filtered(
Arc::new(move |_| {
even_probe.fetch_add(1, Ordering::SeqCst);
}),
Some(Arc::new(|event: &u32| event.is_multiple_of(2))),
);
assert_eq!(registry.dispatch("test", &1), 0);
assert_eq!(registry.dispatch("test", &2), 0);
assert_eq!(hits.load(Ordering::SeqCst), 1);
}
#[test]
fn panicking_filter_skips_observer_without_aborting_dispatch() {
let registry: EventSubscriptions<u32> = EventSubscriptions::new();
let hits = Arc::new(AtomicUsize::new(0));
let bad_probe = Arc::clone(&hits);
registry.add_filtered(
Arc::new(move |_| {
bad_probe.fetch_add(1, Ordering::SeqCst);
}),
Some(Arc::new(|_: &u32| panic!("filter boom"))),
);
let good_probe = Arc::clone(&hits);
registry.add(Arc::new(move |_| {
good_probe.fetch_add(1, Ordering::SeqCst);
}));
let outcome = registry.dispatch_detailed("test", &1);
assert_eq!(outcome.panics, 1);
assert_eq!(outcome.delivered, 1);
assert_eq!(outcome.skipped_by_filter, 1);
assert_eq!(hits.load(Ordering::SeqCst), 1);
}
#[test]
fn dispatch_does_not_hold_lock_during_callbacks() {
let registry: Arc<EventSubscriptions<u32>> = Arc::new(EventSubscriptions::new());
let nested: Arc<EventSubscriptions<u32>> = Arc::clone(®istry);
let hits = Arc::new(AtomicUsize::new(0));
let probe = Arc::clone(&hits);
registry.add(Arc::new(move |_| {
probe.fetch_add(1, Ordering::SeqCst);
nested.add(Arc::new(|_| {}));
}));
let nested_filter: Arc<EventSubscriptions<u32>> = Arc::clone(®istry);
registry.add_filtered(
Arc::new(|_| {}),
Some(Arc::new(move |_| {
nested_filter.len();
true
})),
);
let outcome = registry.dispatch_detailed("test", &1);
assert_eq!(outcome.delivered, 2);
assert_eq!(outcome.panics, 0);
assert_eq!(hits.load(Ordering::SeqCst), 1);
assert_eq!(registry.len(), 3);
}
#[test]
fn subscription_guard_removes_on_drop_and_clear_empties() {
let registry: EventSubscriptions<u32> = EventSubscriptions::new();
{
let _guard = registry.subscribe(Arc::new(|_: &u32| {}));
assert_eq!(registry.len(), 1);
}
assert_eq!(registry.len(), 0);
registry.add(Arc::new(|_: &u32| {}));
registry.add(Arc::new(|_: &u32| {}));
assert_eq!(registry.len(), 2);
registry.clear();
assert!(registry.is_empty());
}
#[test]
fn timed_dispatch_flags_slow_observer() {
use std::time::Duration;
let registry: EventSubscriptions<u32> = EventSubscriptions::new();
registry.add(Arc::new(|_| {}));
registry.add(Arc::new(|_: &u32| {
std::thread::sleep(Duration::from_millis(15));
}));
let outcome = registry.dispatch_timed("test", &1, Duration::from_millis(5));
assert_eq!(outcome.outcome.delivered, 2);
assert_eq!(outcome.outcome.panics, 0);
assert_eq!(outcome.slow, 1);
assert!(outcome.slowest_ms >= 5);
let empty: EventSubscriptions<u32> = EventSubscriptions::new();
assert_eq!(
empty.dispatch_timed("test", &1, Duration::from_millis(1)),
TimedDispatchOutcome::default()
);
}
#[test]
fn async_forwarder_delivers_without_blocking_emitter() {
let registry: EventSubscriptions<u32> = EventSubscriptions::new();
let received = Arc::new(AtomicUsize::new(0));
let probe = Arc::clone(&received);
let (forwarder, handle) = AsyncForwarder::spawn(16, move |_: &u32| {
probe.fetch_add(1, Ordering::SeqCst);
});
registry.add(forwarder.adapter());
for event in 0..5u32 {
registry.dispatch("test", &event);
}
assert_eq!(forwarder.dropped_count(), 0);
drop(registry);
drop(forwarder);
handle.join().expect("worker joins");
assert_eq!(received.load(Ordering::SeqCst), 5);
}
#[test]
fn async_forwarder_counts_drops_on_full_queue() {
let received = Arc::new(AtomicUsize::new(0));
let probe = Arc::clone(&received);
let (forwarder, handle) = AsyncForwarder::spawn(0, move |_: &u32| {
probe.fetch_add(1, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(5));
});
for event in 0..10u32 {
forwarder.forward(&event);
}
let dropped = forwarder.dropped_count();
drop(forwarder);
handle.join().expect("worker joins");
assert_eq!(received.load(Ordering::SeqCst) + dropped, 10);
}
}