use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::mpsc::error::TrySendError;
use super::emission_policy::{handle_broadcast_send_failure, validate_lagged_backpressure};
use super::scoped_subscriptions::{
collect_active_scoped_senders, route_event_to_scoped_subscribers,
};
use super::session_subscriptions::{
collect_active_session_senders, route_event_to_session_subscribers,
};
use super::{
AsxError, AsxEvent, ErrorCode, ErrorContext, EventBus, EventEmissionMode, Result,
ScopedAsxEvent, SessionContext,
};
fn strict_emit_error(session: &SessionContext, message: &'static str) -> AsxError {
AsxError::new(
ErrorCode::ReliabilityFailure,
message,
ErrorContext::new("event_bus_emit")
.with_session_and_partner(session.session_id(), session.partner_id()),
)
}
fn persist_via_audit_fallback(
bus: &EventBus,
session: &SessionContext,
event: &AsxEvent,
stage: &'static str,
) -> Result<()> {
bus.persist_audit_event(session, event, stage)?;
bus.metrics.emitted.fetch_add(1, Ordering::Relaxed);
validate_lagged_backpressure(bus, session)
}
impl EventBus {
#[cfg_attr(feature = "trace", tracing::instrument(skip_all, fields(session_id = %session.session_id())))]
pub fn emit(&self, session: &SessionContext, event: AsxEvent) -> Result<()> {
let shared_event = Arc::new(event);
self.metrics
.observe_event(shared_event.as_ref(), self.metrics_sink.as_ref());
if matches!(self.emission_mode, EventEmissionMode::StrictTransactional)
&& collect_active_scoped_senders(self).is_empty()
&& !self.session_senders.contains_key(session.session_id())
{
let dropped = self.metrics.inc_dropped();
return handle_broadcast_send_failure(self, session, shared_event.as_ref(), dropped);
}
if matches!(
self.emission_mode,
EventEmissionMode::StrictWithAuditFallback
) && collect_active_scoped_senders(self).is_empty()
&& !self.session_senders.contains_key(session.session_id())
{
tracing::debug!(
session_id = %session.session_id(),
partner_id = %session.partner_id(),
event_kind = shared_event.kind(),
"no scoped subscribers; routing event to audit fallback sink"
);
if self.audit_sink.is_some() {
persist_via_audit_fallback(
self,
session,
shared_event.as_ref(),
"event_bus_audit_fallback",
)?;
return Ok(());
}
self.metrics.inc_dropped();
return Ok(());
}
let mut transactional_scoped_permits = Vec::new();
let mut transactional_permits = Vec::new();
if matches!(self.emission_mode, EventEmissionMode::StrictTransactional) {
for sender in collect_active_scoped_senders(self) {
match sender.try_reserve_owned() {
Ok(permit) => transactional_scoped_permits.push(permit),
Err(TrySendError::Full(_)) => {
self.metrics.inc_lagged(1);
return Err(strict_emit_error(
session,
"transactional event emission failed: scoped subscriber queue is full",
));
}
Err(TrySendError::Closed(_)) => {
return Err(strict_emit_error(
session,
"transactional event emission failed: scoped subscriber closed",
));
}
}
}
for sender in collect_active_session_senders(self, session.session_id()) {
match sender.try_reserve_owned() {
Ok(permit) => transactional_permits.push(permit),
Err(TrySendError::Full(_)) => {
self.metrics.inc_lagged(1);
return Err(strict_emit_error(
session,
"transactional event emission failed: session subscriber queue is full",
));
}
Err(TrySendError::Closed(_)) => {
return Err(strict_emit_error(
session,
"transactional event emission failed: session subscriber closed",
));
}
}
}
}
let timestamp_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
let scoped = ScopedAsxEvent {
session_id: session.session_id().to_string(),
partner_id: session.partner_id().to_string(),
timestamp_ms,
traceparent: session.correlation_scope().traceparent.clone(),
event: Arc::clone(&shared_event),
};
if matches!(self.emission_mode, EventEmissionMode::StrictTransactional) {
for permit in transactional_scoped_permits {
permit.send(scoped.clone());
}
for permit in transactional_permits {
permit.send(Arc::clone(&shared_event));
}
} else {
let scoped_route = route_event_to_scoped_subscribers(self, &scoped);
if scoped_route.lagged_count > 0
&& matches!(
self.emission_mode,
EventEmissionMode::StrictWithAuditFallback
)
{
return Err(strict_emit_error(
session,
"strict event emission failed: scoped subscriber queue is full",
));
}
let session_route =
route_event_to_session_subscribers(self, session.session_id(), &shared_event);
if session_route.lagged_count > 0
&& matches!(
self.emission_mode,
EventEmissionMode::StrictWithAuditFallback
)
{
return Err(strict_emit_error(
session,
"strict event emission failed: session subscriber queue is full",
));
}
if scoped_route.delivered_count == 0 && session_route.delivered_count == 0 {
if matches!(
self.emission_mode,
EventEmissionMode::StrictWithAuditFallback
) && self.audit_sink.is_some()
{
persist_via_audit_fallback(
self,
session,
shared_event.as_ref(),
"event_bus_audit_fallback",
)?;
return Ok(());
}
let dropped = self.metrics.inc_dropped();
return handle_broadcast_send_failure(
self,
session,
shared_event.as_ref(),
dropped,
);
}
}
self.metrics.emitted.fetch_add(1, Ordering::Relaxed);
validate_lagged_backpressure(self, session)?;
Ok(())
}
}
pub fn emit_audit_event(
bus: &EventBus,
session: &SessionContext,
event: AsxEvent,
fail_closed: bool,
stage: &'static str,
) -> Result<()> {
match bus.persist_audit_event(session, &event, stage) {
Ok(()) => {}
Err(_) if !fail_closed => {}
Err(err) => return Err(err),
}
match bus.emit(session, event) {
Ok(()) => Ok(()),
Err(_) if !fail_closed => Ok(()),
Err(_) => Err(AsxError::new(
ErrorCode::ReliabilityFailure,
"audit event emission failed under fail-closed policy",
ErrorContext::new(stage)
.with_session_and_partner(session.session_id(), session.partner_id()),
)),
}
}
#[cfg(any(feature = "as2", feature = "as4"))]
pub(crate) fn emit_protocol_event(
bus: &EventBus,
session: &SessionContext,
event: AsxEvent,
fail_closed: bool,
stage: &'static str,
) -> Result<()> {
match bus.emit(session, event) {
Ok(()) => Ok(()),
Err(_) if !fail_closed => Ok(()),
Err(_) => Err(AsxError::new(
ErrorCode::ReliabilityFailure,
"protocol event emission failed under fail-closed policy",
ErrorContext::new(stage)
.with_session_and_partner(session.session_id(), session.partner_id()),
)),
}
}
#[cfg(any(feature = "as2", feature = "as4"))]
pub(crate) fn require_durable_audit_sink(
session: &SessionContext,
event_bus: &EventBus,
fail_closed_audit_events: bool,
stage: &'static str,
) -> Result<()> {
if !fail_closed_audit_events {
return Ok(());
}
#[cfg(not(feature = "testing"))]
if !event_bus.has_production_durable_audit_sink() {
return Err(AsxError::new(
ErrorCode::ReliabilityFailure,
"fail-closed audit policy requires production-durable audit sink",
ErrorContext::for_session(stage, session),
));
}
#[cfg(feature = "testing")]
if !event_bus.has_durable_audit_sink() {
return Err(AsxError::new(
ErrorCode::ReliabilityFailure,
"fail-closed audit policy requires configured durable audit sink",
ErrorContext::for_session(stage, session),
));
}
Ok(())
}