use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Arc;
use super::record::TelemetryRecord;
use super::sink::TelemetrySink;
pub(crate) fn dispatch_record(sink: &Arc<dyn TelemetrySink>, record: &TelemetryRecord) {
let result = catch_unwind(AssertUnwindSafe(|| sink.emit(record)));
if let Err(payload) = result {
let message = panic_message(&*payload);
tracing::error!(
telemetry_record = record.name(),
panic = %message,
"TelemetrySink panicked; record dropped. Sinks must not panic — see the TelemetrySink docstring.",
);
}
}
pub(crate) fn dispatch_protocol_event(
sink: &Arc<dyn TelemetrySink>,
event: &crate::events::Event,
) -> bool {
match catch_unwind(AssertUnwindSafe(|| sink.try_emit_protocol_event(event))) {
Ok(consumed) => consumed,
Err(payload) => {
let message = panic_message(&*payload);
tracing::error!(
telemetry_record = event.telemetry_name(),
panic = %message,
"TelemetrySink panicked; record dropped. Sinks must not panic — see the TelemetrySink docstring.",
);
true
}
}
}
fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(s) = payload.downcast_ref::<&'static str>() {
return (*s).to_string();
}
if let Some(s) = payload.downcast_ref::<String>() {
return s.clone();
}
"<non-string panic payload>".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::events::Event;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
#[derive(Default)]
struct RecordingSink {
seen: Mutex<Vec<&'static str>>,
}
impl TelemetrySink for RecordingSink {
fn emit(&self, record: &TelemetryRecord) {
self.seen.lock().unwrap().push(record.name());
}
}
fn sample_record() -> TelemetryRecord {
TelemetryRecord::Protocol(Box::new(Event::neighbor_lost("alice".into())))
}
#[test]
fn dispatch_record_calls_sink_with_record() {
let concrete = Arc::new(RecordingSink::default());
let dyn_sink: Arc<dyn TelemetrySink> = concrete.clone();
dispatch_record(&dyn_sink, &sample_record());
let names = concrete.seen.lock().unwrap().clone();
assert_eq!(names, vec!["protocol.neighbor.lost"]);
}
struct StringPanicSink;
impl TelemetrySink for StringPanicSink {
fn emit(&self, _record: &TelemetryRecord) {
panic!("simulated sink panic");
}
}
#[test]
fn dispatch_record_swallows_static_str_panic() {
let sink: Arc<dyn TelemetrySink> = Arc::new(StringPanicSink);
dispatch_record(&sink, &sample_record());
}
struct FormattedPanicSink;
impl TelemetrySink for FormattedPanicSink {
fn emit(&self, _record: &TelemetryRecord) {
let cause = "boom";
panic!("formatted: {}", cause);
}
}
#[test]
fn dispatch_record_swallows_formatted_string_panic() {
let sink: Arc<dyn TelemetrySink> = Arc::new(FormattedPanicSink);
dispatch_record(&sink, &sample_record());
}
struct NonStringPanicSink;
impl TelemetrySink for NonStringPanicSink {
fn emit(&self, _record: &TelemetryRecord) {
std::panic::panic_any(123_i32);
}
}
#[test]
fn dispatch_record_swallows_panic_any_payload() {
let sink: Arc<dyn TelemetrySink> = Arc::new(NonStringPanicSink);
dispatch_record(&sink, &sample_record());
}
#[test]
fn dispatch_record_continues_dispatching_after_panic() {
struct PanicOnceThenRecord {
count: AtomicUsize,
seen: Mutex<Vec<&'static str>>,
}
impl TelemetrySink for PanicOnceThenRecord {
fn emit(&self, record: &TelemetryRecord) {
if self.count.fetch_add(1, Ordering::SeqCst) == 0 {
panic!("first call panics");
}
self.seen.lock().unwrap().push(record.name());
}
}
let concrete = Arc::new(PanicOnceThenRecord {
count: AtomicUsize::new(0),
seen: Mutex::new(Vec::new()),
});
let dyn_sink: Arc<dyn TelemetrySink> = concrete.clone();
dispatch_record(&dyn_sink, &sample_record());
dispatch_record(&dyn_sink, &sample_record());
let names = concrete.seen.lock().unwrap().clone();
assert_eq!(
names,
vec!["protocol.neighbor.lost"],
"second dispatch must fire even after the first panicked",
);
}
#[allow(dead_code)]
fn signature_matches_call_sites(sink: &Arc<dyn TelemetrySink>, record: &TelemetryRecord) {
dispatch_record(sink, record);
}
}