use std::sync::{
Arc,
atomic::{AtomicU64, Ordering},
};
use std::time::{SystemTime, UNIX_EPOCH};
use dashmap::DashMap;
use tokio::sync::mpsc;
use crate::core::{AsxError, ErrorCode, ErrorContext, Result, SessionContext};
use crate::observability::audit_sink::{
AuditEvent, AuditMetadata, AuditSeverity, AuditSinkDurability, DurableAuditSink, ReplayCursor,
};
mod audit_persistence;
mod audit_runtime;
pub mod audit_sink;
mod construction;
mod construction_api;
mod emission_policy;
mod emission_runtime;
mod event_taxonomy;
pub mod metric_names;
mod metrics_analysis;
mod metrics_observation;
#[cfg(feature = "opentelemetry")]
pub mod opentelemetry;
#[cfg(feature = "prometheus")]
pub mod prometheus;
mod scoped_subscriptions;
mod session_subscriptions;
mod sink_forwarding;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct As4ReceiptTaxonomySnapshot {
pub security_verification_failed: u64,
pub semantic_interop_failure: u64,
pub total: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct As2ProviderHealthSnapshot {
pub transition_to_failing: u64,
pub total_transitions: u64,
}
#[cfg(test)]
use audit_persistence::{event_code, event_message};
use construction::{
new_with_config_and_mode as new_with_config_and_mode_impl,
new_with_config_and_mode_and_metrics as new_with_config_and_mode_and_metrics_impl,
validate_regulated_audit_sink,
};
pub use construction_api::EventBusBuilder;
pub use emission_runtime::emit_audit_event;
#[cfg(any(feature = "as2", feature = "as4"))]
pub(crate) use emission_runtime::{emit_protocol_event, require_durable_audit_sink};
pub use event_taxonomy::{AsxEvent, AsxIngressStage, AsxProtocol, ScopedAsxEvent, SharedAsxEvent};
#[cfg(feature = "opentelemetry")]
pub use opentelemetry::OtelMetricsSink;
#[cfg(feature = "prometheus")]
pub use prometheus::PrometheusMetricsSink;
use scoped_subscriptions::subscribe_scoped_events_impl;
pub use scoped_subscriptions::{ScopedEventSubscription, ScopedEventTryRecvError};
pub use session_subscriptions::SessionEventSubscription;
use session_subscriptions::{SessionSenderEntry, subscribe_session_events_impl};
pub use sink_forwarding::{EventSink, forward_to_sink};
pub trait MetricsSink: Send + Sync + std::fmt::Debug {
fn increment_counter(&self, name: &'static str, value: u64, labels: &[(&'static str, &str)]);
fn record_histogram(&self, name: &'static str, value: f64, labels: &[(&'static str, &str)]);
fn set_gauge(&self, name: &'static str, value: f64, labels: &[(&'static str, &str)]);
}
const AS4_RECEIPT_TAXONOMY_OUTCOME_TOTAL: &str = "asx_as4_receipt_taxonomy_outcome_total";
#[derive(Debug, Clone, Default)]
pub struct NoopMetricsSink;
impl MetricsSink for NoopMetricsSink {
#[inline]
fn increment_counter(
&self,
_name: &'static str,
_value: u64,
_labels: &[(&'static str, &str)],
) {
}
#[inline]
fn record_histogram(&self, _name: &'static str, _value: f64, _labels: &[(&'static str, &str)]) {
}
#[inline]
fn set_gauge(&self, _name: &'static str, _value: f64, _labels: &[(&'static str, &str)]) {}
}
#[derive(Debug, Default)]
pub struct EventBusMetrics {
emitted: AtomicU64,
dropped: AtomicU64,
lagged: AtomicU64,
receipt_taxonomy_total: AtomicU64,
receipt_taxonomy_security_verification_failed: AtomicU64,
receipt_taxonomy_semantic_interop_failure: AtomicU64,
provider_health_transition_total: AtomicU64,
provider_health_transition_to_failing: AtomicU64,
window_epoch: AtomicU64,
window_dropped: AtomicU64,
window_lagged: AtomicU64,
window_secs: u64,
}
impl EventBusMetrics {
pub fn emitted(&self) -> u64 {
self.emitted.load(Ordering::Relaxed)
}
pub fn dropped(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
pub fn lagged(&self) -> u64 {
self.lagged.load(Ordering::Relaxed)
}
fn observe_event(&self, event: &AsxEvent, sink: &dyn MetricsSink) {
metrics_observation::observe_event(self, event, sink);
}
fn inc_dropped(&self) -> u64 {
let window_secs = self.window_secs;
self.dropped.fetch_add(1, Ordering::Relaxed);
self.window_count_inc(&self.window_dropped, window_secs)
}
fn inc_lagged(&self, n: u64) -> u64 {
let window_secs = self.window_secs;
self.lagged.fetch_add(n, Ordering::Relaxed);
self.window_count_add(&self.window_lagged, n, window_secs)
}
fn window_count_inc(&self, counter: &AtomicU64, window_secs: u64) -> u64 {
self.window_count_add(counter, 1, window_secs)
}
fn window_count_add(&self, counter: &AtomicU64, n: u64, window_secs: u64) -> u64 {
let now_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let epoch = self.window_epoch.load(Ordering::Acquire);
if epoch == 0 {
let _ = self.window_epoch.compare_exchange(
0,
now_secs,
Ordering::AcqRel,
Ordering::Acquire,
);
} else if now_secs >= epoch + window_secs {
if self
.window_epoch
.compare_exchange(epoch, now_secs, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
self.window_dropped.store(0, Ordering::Release);
self.window_lagged.store(0, Ordering::Release);
}
}
counter.fetch_add(n, Ordering::AcqRel) + n
}
fn reset_window_if_expired(&self) {
let now_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let epoch = self.window_epoch.load(Ordering::Acquire);
if epoch != 0
&& now_secs >= epoch + self.window_secs
&& self
.window_epoch
.compare_exchange(epoch, now_secs, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
self.window_dropped.store(0, Ordering::Release);
self.window_lagged.store(0, Ordering::Release);
}
}
pub(super) fn current_window_lagged(&self) -> u64 {
self.reset_window_if_expired();
self.window_lagged.load(Ordering::Acquire)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EventEmissionMode {
BestEffort,
StrictTransactional,
StrictWithAuditFallback,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum BackpressureAction {
Track,
FailClosed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackpressurePolicy {
pub max_dropped: Option<u64>,
pub max_lagged: Option<u64>,
pub action: BackpressureAction,
pub window_secs: u64,
pub session_channel_capacity: usize,
}
impl Default for BackpressurePolicy {
fn default() -> Self {
Self {
max_dropped: None,
max_lagged: None,
action: BackpressureAction::Track,
window_secs: 60,
session_channel_capacity: 64,
}
}
}
impl BackpressurePolicy {
#[must_use]
pub fn regulated() -> Self {
Self {
max_dropped: Some(1),
max_lagged: Some(64),
action: BackpressureAction::FailClosed,
window_secs: 60,
session_channel_capacity: 128,
}
}
}
#[derive(Clone)]
pub struct EventBus {
scoped_senders: Arc<DashMap<u64, mpsc::Sender<ScopedAsxEvent>>>,
next_scoped_subscription_id: Arc<AtomicU64>,
session_senders: Arc<DashMap<String, Vec<SessionSenderEntry>>>,
next_session_subscription_id: Arc<AtomicU64>,
metrics: Arc<EventBusMetrics>,
metrics_sink: Arc<dyn MetricsSink>,
audit_sink: Option<Arc<dyn DurableAuditSink>>,
audit_sequence: Arc<AtomicU64>,
emission_mode: EventEmissionMode,
backpressure: BackpressurePolicy,
scoped_channel_capacity: usize,
session_channel_capacity: usize,
}
impl std::fmt::Debug for EventBus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EventBus")
.field("emission_mode", &self.emission_mode)
.field("backpressure", &self.backpressure)
.field("scoped_channel_capacity", &self.scoped_channel_capacity)
.field("session_channel_capacity", &self.session_channel_capacity)
.field("has_audit_sink", &self.audit_sink.is_some())
.field("scoped_subscriptions", &self.scoped_senders.len())
.field("subscribed_sessions", &self.session_senders.len())
.finish()
}
}
impl EventBus {
pub fn metrics(&self) -> Arc<EventBusMetrics> {
Arc::clone(&self.metrics)
}
pub fn emission_mode(&self) -> EventEmissionMode {
self.emission_mode
}
pub fn has_durable_audit_sink(&self) -> bool {
self.audit_sink.is_some()
}
pub fn has_production_durable_audit_sink(&self) -> bool {
self.audit_sink
.as_ref()
.map(|sink| sink.durability() == AuditSinkDurability::Durable)
.unwrap_or(false)
}
pub fn is_compatible_with_fail_closed(&self) -> bool {
self.emission_mode != EventEmissionMode::BestEffort
}
pub fn subscribe_scoped_events(&self) -> ScopedEventSubscription {
subscribe_scoped_events_impl(self)
}
pub fn subscribe_session_events(
&self,
session_id: impl Into<String>,
) -> Result<SessionEventSubscription> {
subscribe_session_events_impl(self, session_id.into())
}
pub fn shutdown(&self) {
self.scoped_senders.clear();
self.session_senders.clear();
}
}
#[cfg(test)]
mod tests;