use std::sync::{Arc, Mutex, RwLock};
#[derive(Debug, Clone)]
pub struct BridgeFault {
pub method: BridgeFaultMethod,
pub kind: BridgeFaultKind,
pub status: u16,
pub error_type: Option<String>,
pub count: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BridgeFaultMethod {
PollForWork,
RegisterBridgeEnvironment,
ReconnectSession,
HeartbeatWork,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BridgeFaultKind {
Fatal,
Transient,
}
pub trait BridgeDebugHandle: Send + Sync {
fn fire_close(&self, code: u16);
fn force_reconnect(&self);
fn inject_fault(&self, fault: BridgeFault);
fn wake_poll_loop(&self);
fn describe(&self) -> String;
}
static DEBUG_HANDLE: std::sync::OnceLock<Arc<dyn BridgeDebugHandle>> = std::sync::OnceLock::new();
static FAULT_QUEUE: std::sync::OnceLock<Mutex<Vec<BridgeFault>>> = std::sync::OnceLock::new();
static DEBUG_HANDLE_MUT: std::sync::OnceLock<RwLock<Option<Arc<dyn BridgeDebugHandle>>>> =
std::sync::OnceLock::new();
pub fn register_bridge_debug_handle(h: Arc<dyn BridgeDebugHandle>) {
let _ = DEBUG_HANDLE.set(h.clone());
let _ = DEBUG_HANDLE_MUT.set(RwLock::new(Some(h)));
}
pub fn clear_bridge_debug_handle() {
if let Some(queue) = FAULT_QUEUE.get() {
if let Ok(mut faults) = queue.lock() {
faults.clear();
}
}
if let Some(handle) = DEBUG_HANDLE_MUT.get() {
if let Ok(mut guard) = handle.write() {
*guard = None;
}
}
}
pub fn get_bridge_debug_handle() -> Option<Arc<dyn BridgeDebugHandle>> {
DEBUG_HANDLE.get().cloned()
}
pub fn inject_bridge_fault(fault: BridgeFault) {
let queue = FAULT_QUEUE.get_or_init(|| Mutex::new(Vec::new()));
if let Ok(mut faults) = queue.lock() {
eprintln!(
"[bridge:debug] Queued fault: {:?} {}/{}{} ×{}",
fault.method,
fault.kind.as_str(),
fault.status,
fault
.error_type
.as_ref()
.map(|e| format!("/{}", e))
.unwrap_or_default(),
fault.count
);
faults.push(fault);
}
}
pub fn consume_fault(method: &BridgeFaultMethod) -> Option<BridgeFault> {
let queue = FAULT_QUEUE.get()?;
let mut faults = match queue.lock() {
Ok(f) => f,
Err(_) => return None,
};
let idx = faults.iter().position(|f| &f.method == method)?;
let mut fault = faults.remove(idx);
fault.count -= 1;
Some(fault)
}
pub fn throw_fault(fault: &BridgeFault, context: &str) -> Result<(), String> {
eprintln!(
"[bridge:debug] Injecting {} fault into {}: status={} errorType={}",
fault.kind.as_str(),
context,
fault.status,
fault.error_type.as_deref().unwrap_or("none")
);
if fault.kind == BridgeFaultKind::Fatal {
Err(format!("[injected] {} {}", context, fault.status))
} else {
Err(format!("[injected transient] {} {}", context, fault.status))
}
}
impl BridgeFaultKind {
pub fn as_str(&self) -> &'static str {
match self {
BridgeFaultKind::Fatal => "fatal",
BridgeFaultKind::Transient => "transient",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_inject_fault() {
let fault = BridgeFault {
method: BridgeFaultMethod::PollForWork,
kind: BridgeFaultKind::Fatal,
status: 404,
error_type: Some("not_found".to_string()),
count: 1,
};
inject_bridge_fault(fault);
let consumed = consume_fault(&BridgeFaultMethod::PollForWork);
assert!(consumed.is_some());
let consumed2 = consume_fault(&BridgeFaultMethod::PollForWork);
assert!(consumed2.is_none());
}
}