use std::fmt;
use std::io;
use std::panic::AssertUnwindSafe;
use std::time::Duration;
pub trait Diagnostics: Send + Sync + 'static {
fn report(&self, event: &DiagnosticEvent<'_>);
}
#[derive(Debug)]
#[non_exhaustive]
pub enum DiagnosticEvent<'a> {
AcceptFailed {
error: &'a io::Error,
},
TcpNodelayFailed {
error: &'a io::Error,
},
ShutdownGraceExceeded {
grace: Duration,
},
RebindDrainGraceExceeded {
grace: Duration,
},
}
impl fmt::Display for DiagnosticEvent<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DiagnosticEvent::AcceptFailed { error } => {
write!(f, "accept に失敗しました: {error}")
}
DiagnosticEvent::TcpNodelayFailed { error } => {
write!(
f,
"TCP_NODELAY の設定に失敗しました(接続は継続します): {error}"
)
}
DiagnosticEvent::ShutdownGraceExceeded { grace } => {
write!(
f,
"graceful shutdown の猶予期間({grace:?})を超過したため残存接続を強制クローズします"
)
}
DiagnosticEvent::RebindDrainGraceExceeded { grace } => {
write!(
f,
"rebind による旧世代接続の drain が猶予期間({grace:?})を超過したため強制クローズします"
)
}
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct StderrDiagnostics;
impl StderrDiagnostics {
fn format_line(event: &DiagnosticEvent<'_>) -> String {
format!("fandhe_backend_core::server: {event}")
}
}
impl Diagnostics for StderrDiagnostics {
fn report(&self, event: &DiagnosticEvent<'_>) {
eprintln!("{}", Self::format_line(event));
}
}
impl<F> Diagnostics for F
where
F: Fn(&DiagnosticEvent<'_>) + Send + Sync + 'static,
{
fn report(&self, event: &DiagnosticEvent<'_>) {
self(event);
}
}
pub(crate) fn emit(sink: &dyn Diagnostics, event: DiagnosticEvent<'_>) {
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| sink.report(&event)));
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
#[test]
fn accept_failed_display_matches_legacy_wording() {
let err = io::Error::other("boom");
let event = DiagnosticEvent::AcceptFailed { error: &err };
assert_eq!(event.to_string(), "accept に失敗しました: boom");
}
#[test]
fn tcp_nodelay_failed_display_matches_legacy_wording() {
let err = io::Error::other("nodelay boom");
let event = DiagnosticEvent::TcpNodelayFailed { error: &err };
assert_eq!(
event.to_string(),
"TCP_NODELAY の設定に失敗しました(接続は継続します): nodelay boom"
);
}
#[test]
fn shutdown_grace_exceeded_display_matches_legacy_wording() {
let event = DiagnosticEvent::ShutdownGraceExceeded {
grace: Duration::from_millis(100),
};
assert_eq!(
event.to_string(),
"graceful shutdown の猶予期間(100ms)を超過したため残存接続を強制クローズします"
);
}
#[test]
fn rebind_drain_grace_exceeded_display_matches_legacy_wording() {
let event = DiagnosticEvent::RebindDrainGraceExceeded {
grace: Duration::from_millis(250),
};
assert_eq!(
event.to_string(),
"rebind による旧世代接続の drain が猶予期間(250ms)を超過したため強制クローズします"
);
}
#[test]
fn stderr_diagnostics_format_line_has_fixed_prefix() {
let event = DiagnosticEvent::ShutdownGraceExceeded {
grace: Duration::from_secs(1),
};
assert_eq!(
StderrDiagnostics::format_line(&event),
"fandhe_backend_core::server: graceful shutdown の猶予期間(1s)を超過したため残存接続を強制クローズします"
);
}
#[test]
fn closure_sink_receives_events() {
let received: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let received_for_closure = Arc::clone(&received);
let sink: Box<dyn Diagnostics> = Box::new(move |event: &DiagnosticEvent<'_>| {
received_for_closure.lock().unwrap().push(event.to_string());
});
let err = io::Error::other("x");
emit(&*sink, DiagnosticEvent::AcceptFailed { error: &err });
assert_eq!(received.lock().unwrap().len(), 1);
assert_eq!(received.lock().unwrap()[0], "accept に失敗しました: x");
}
#[test]
fn emit_does_not_propagate_panicking_sink() {
struct PanicSink;
impl Diagnostics for PanicSink {
fn report(&self, _event: &DiagnosticEvent<'_>) {
panic!("sink panicked");
}
}
let err = io::Error::other("y");
emit(&PanicSink, DiagnosticEvent::AcceptFailed { error: &err });
}
}