use std::sync::Arc;
use std::time::Duration;
use camel_api::MetricsCollector;
use crate::{HealthCheckRegistry, RuntimeObservability};
pub mod tls {
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
static PEM_COUNTER: AtomicU64 = AtomicU64::new(0);
pub fn gen_server_cert() -> (String, String, String) {
use rcgen::{BasicConstraints, CertificateParams, DnType, IsCa, KeyPair};
use std::net::IpAddr;
let ca_key = KeyPair::generate().expect("ca keygen");
let mut ca_params = CertificateParams::default();
ca_params
.distinguished_name
.push(DnType::CommonName, "Test CA");
ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
let ca_cert = ca_params.self_signed(&ca_key).expect("ca self-sign");
let server_key = KeyPair::generate().expect("server keygen");
let mut server_params = CertificateParams::default();
server_params.subject_alt_names = vec![
rcgen::SanType::DnsName("localhost".try_into().expect("localhost dns name")),
rcgen::SanType::IpAddress(IpAddr::V4([127, 0, 0, 1].into())),
rcgen::SanType::IpAddress("::1".parse().expect("::1 ip address")),
];
let issuer = rcgen::Issuer::from_params(&ca_params, &ca_key);
let server_cert = server_params
.signed_by(&server_key, &issuer)
.expect("server cert sign");
(ca_cert.pem(), server_cert.pem(), server_key.serialize_pem())
}
pub fn write_pem_tmp(name: &str, pem: &str) -> PathBuf {
let dir = std::env::temp_dir().join("camel-tls-test");
std::fs::create_dir_all(&dir).expect("create tmp dir");
let counter = PEM_COUNTER.fetch_add(1, Ordering::Relaxed);
let stem = std::path::Path::new(name)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(name);
let ext = std::path::Path::new(name)
.extension()
.and_then(|s| s.to_str())
.unwrap_or("pem");
let unique_name = format!("{stem}_{}_{counter}.{ext}", std::process::id());
let path = dir.join(unique_name);
std::fs::write(&path, pem).expect("write pem");
path
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct PanicRuntimeObservability;
impl MetricsCollector for PanicRuntimeObservability {
fn record_exchange_duration(&self, _: &str, _: Duration) {
panic!("PanicRuntimeObservability::record_exchange_duration invoked")
}
fn increment_errors(&self, _: &str, _: &str) {
panic!("PanicRuntimeObservability::increment_errors invoked")
}
fn increment_exchanges(&self, _: &str) {
panic!("PanicRuntimeObservability::increment_exchanges invoked")
}
fn set_queue_depth(&self, _: &str, _: usize) {
panic!("PanicRuntimeObservability::set_queue_depth invoked")
}
fn record_circuit_breaker_change(&self, _: &str, _: &str, _: &str) {
panic!("PanicRuntimeObservability::record_circuit_breaker_change invoked")
}
}
impl HealthCheckRegistry for PanicRuntimeObservability {
fn force_unhealthy_for_route(&self, _: &str, _: &str, _: &str) {
panic!("PanicRuntimeObservability::force_unhealthy_for_route invoked")
}
}
impl RuntimeObservability for PanicRuntimeObservability {
fn metrics(&self) -> Arc<dyn MetricsCollector> {
panic!("PanicRuntimeObservability::metrics invoked")
}
fn health(&self) -> Arc<dyn HealthCheckRegistry> {
panic!("PanicRuntimeObservability::health invoked")
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopRuntimeObservability;
impl MetricsCollector for NoopRuntimeObservability {
fn record_exchange_duration(&self, _: &str, _: Duration) {}
fn increment_errors(&self, _: &str, _: &str) {}
fn increment_exchanges(&self, _: &str) {}
fn set_queue_depth(&self, _: &str, _: usize) {}
fn record_circuit_breaker_change(&self, _: &str, _: &str, _: &str) {}
}
impl HealthCheckRegistry for NoopRuntimeObservability {
fn force_unhealthy_for_route(&self, _: &str, _: &str, _: &str) {}
}
impl RuntimeObservability for NoopRuntimeObservability {
fn metrics(&self) -> Arc<dyn MetricsCollector> {
Arc::new(*self)
}
fn health(&self) -> Arc<dyn HealthCheckRegistry> {
Arc::new(*self)
}
}
pub const TEST_LOCK_DEADLINE: std::time::Duration = std::time::Duration::from_secs(900);
#[track_caller]
pub fn acquire_deadline<'a, T: ?Sized>(
lock: &'a tokio::sync::Mutex<T>,
what: &'a str,
deadline: std::time::Duration,
) -> impl std::future::Future<Output = tokio::sync::MutexGuard<'a, T>> + 'a {
let caller = std::panic::Location::caller();
async move {
match tokio::time::timeout(deadline, lock.lock()).await {
Ok(guard) => guard,
Err(_) => panic!(
"test lock {what} not acquired within {deadline:?} \
— holder stalled (bd rc-88old), site {caller}"
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn panic_runtime_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<PanicRuntimeObservability>();
}
#[test]
fn noop_runtime_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<NoopRuntimeObservability>();
}
#[test]
fn noop_runtime_metrics_does_not_panic() {
let rt = NoopRuntimeObservability;
rt.metrics().increment_errors("any-route", "any-label");
}
#[test]
fn noop_runtime_health_does_not_panic() {
let rt = NoopRuntimeObservability;
rt.health()
.force_unhealthy_for_route("any-route", "any-name", "any-reason");
}
#[test]
fn tls_gen_server_cert_returns_valid_pem() {
let (ca, cert, key) = super::tls::gen_server_cert();
assert!(ca.contains("BEGIN CERTIFICATE"), "CA must be PEM cert");
assert!(
cert.contains("BEGIN CERTIFICATE"),
"server cert must be PEM"
);
assert!(
key.contains("PRIVATE KEY"),
"server key must be PEM private key"
);
}
#[test]
fn tls_write_pem_tmp_creates_readable_file() {
let path = super::tls::write_pem_tmp("test-write.pem", "test content");
let content = std::fs::read_to_string(&path).unwrap();
assert_eq!(content, "test content");
}
}
#[cfg(test)]
mod lock_deadline_tests {
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use super::acquire_deadline;
#[tokio::test]
async fn acquire_deadline_uncontended_returns_guard() {
let lock = Mutex::new(());
let _guard = acquire_deadline(&lock, "UNIT_LOCK", Duration::from_secs(60)).await;
drop(_guard);
}
#[tokio::test]
#[should_panic(expected = "test lock STALLED_UNIT_LOCK not acquired within 100ms")]
async fn acquire_deadline_stalled_holder_panics_naming_lock() {
let lock = Arc::new(Mutex::new(()));
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
let holder = {
let lock = Arc::clone(&lock);
tokio::spawn(async move {
let _guard =
acquire_deadline(&lock, "unit fixture holder", Duration::from_secs(60)).await;
let _ = tx.send(());
std::future::pending::<()>().await;
})
};
rx.await
.expect("stalled holder acks after acquiring the lock");
let _ = acquire_deadline(&lock, "STALLED_UNIT_LOCK", Duration::from_millis(100)).await;
holder.abort(); }
#[test]
fn acquire_deadline_panic_names_call_site() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.expect("current-thread tokio runtime");
let lock = Arc::new(Mutex::new(()));
let captured_site: std::cell::Cell<Option<&'static std::panic::Location<'static>>> =
std::cell::Cell::new(None);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
rt.block_on(async {
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
let holder = {
let lock = Arc::clone(&lock);
tokio::spawn(async move {
let _guard =
acquire_deadline(&lock, "unit fixture holder", Duration::from_secs(60))
.await;
let _ = tx.send(());
std::future::pending::<()>().await;
})
};
rx.await
.expect("stalled holder acks after acquiring the lock");
#[track_caller]
fn checked_acquire<'a, T: ?Sized>(
lock: &'a tokio::sync::Mutex<T>,
what: &'a str,
deadline: std::time::Duration,
) -> (
&'static std::panic::Location<'static>,
impl std::future::Future<Output = tokio::sync::MutexGuard<'a, T>> + 'a,
) {
let site = std::panic::Location::caller();
let fut = acquire_deadline(lock, what, deadline);
(site, fut)
}
let (site, fut) =
checked_acquire(&lock, "STALLED_UNIT_LOCK", Duration::from_millis(100));
captured_site.set(Some(site));
let _ = std::pin::pin!(fut).await;
holder.abort(); });
}));
let err = result.expect_err("stalled acquisition must panic");
let msg = err
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| err.downcast_ref::<&'static str>().copied())
.expect("panic payload downcasts to a string");
assert!(
msg.contains("STALLED_UNIT_LOCK"),
"panic must name the lock: {msg}"
);
assert!(msg.contains("100ms"), "panic must name the deadline: {msg}");
let site = captured_site
.get()
.expect("site captured before the stalled acquisition");
assert!(
msg.contains(&format!("site {site}")),
"panic must name the exact acquisition call site: {msg}"
);
}
}