use core::fmt;
use reliar_core::{Classify, FailureKind, MessageType, Publisher, SerializedEnvelope};
use tokio::sync::Mutex;
use tracing::Instrument as _;
use crate::metrics::{NoopMetrics, OutboxMetrics};
use crate::policy::{OutboxPolicy, RouteKind};
use crate::staging::OutboxStaging;
#[cfg_attr(not(feature = "test-support"), doc = "```ignore")]
#[cfg_attr(feature = "test-support", doc = "```")]
pub struct OutboxPublisher<S, P, M = NoopMetrics> {
staging: S,
publisher: P,
policy: OutboxPolicy,
metrics: M,
}
impl<S: Clone, P: Clone, M: Clone> Clone for OutboxPublisher<S, P, M> {
fn clone(&self) -> Self {
Self {
staging: self.staging.clone(),
publisher: self.publisher.clone(),
policy: self.policy.clone(),
metrics: self.metrics.clone(),
}
}
}
impl<S, P, M> fmt::Debug for OutboxPublisher<S, P, M> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OutboxPublisher")
.field("policy", &self.policy)
.finish_non_exhaustive()
}
}
impl<S, P, M> OutboxPublisher<S, P, M> {
#[must_use]
pub const fn policy(&self) -> &OutboxPolicy {
&self.policy
}
}
impl<S, P> OutboxPublisher<S, P>
where
P: Publisher,
{
pub fn new(staging: S, publisher: P, policy: OutboxPolicy) -> Self {
Self {
staging,
publisher,
policy,
metrics: NoopMetrics,
}
}
}
impl<S, P, M> OutboxPublisher<S, P, M>
where
P: Publisher,
M: OutboxMetrics,
{
pub fn with_metrics(staging: S, publisher: P, policy: OutboxPolicy, metrics: M) -> Self {
Self {
staging,
publisher,
policy,
metrics,
}
}
#[must_use]
pub fn in_transaction<'a, Tx>(
&'a self,
tx: &'a mut Tx,
) -> ScopedOutboxPublisher<'a, S, P, Tx, M>
where
S: OutboxStaging<Tx>,
Tx: Send,
{
ScopedOutboxPublisher {
owner: self,
tx: Mutex::new(tx),
}
}
pub async fn publish_direct(
&self,
envelope: &SerializedEnvelope,
) -> Result<(), DirectPublishError<P::Error>> {
let span = tracing::debug_span!(
"reliar.outbox.route",
message.id = %envelope.id,
message.type = %envelope.message_type,
route = tracing::field::Empty,
);
async {
let route = self.policy.decide(&envelope.message_type);
tracing::Span::current().record("route", route.as_str());
if route.is_outbox() {
return Err(DirectPublishError::TransactionRequired {
message_type: envelope.message_type.clone(),
});
}
self.publisher
.publish(envelope)
.await
.map_err(DirectPublishError::Publish)?;
self.metrics.routed(route, &envelope.message_type);
Ok(())
}
.instrument(span)
.await
}
}
#[cfg_attr(not(feature = "test-support"), doc = "```ignore")]
#[cfg_attr(feature = "test-support", doc = "```compile_fail")]
pub struct ScopedOutboxPublisher<'a, S, P, Tx, M = NoopMetrics> {
owner: &'a OutboxPublisher<S, P, M>,
tx: Mutex<&'a mut Tx>,
}
impl<S, P, Tx, M> fmt::Debug for ScopedOutboxPublisher<'_, S, P, Tx, M> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ScopedOutboxPublisher")
.field("policy", &self.owner.policy)
.finish_non_exhaustive()
}
}
impl<S, P, Tx, M> Publisher for ScopedOutboxPublisher<'_, S, P, Tx, M>
where
S: OutboxStaging<Tx>,
P: Publisher,
Tx: Send,
M: OutboxMetrics,
{
type Error = RouteError<<S as OutboxStaging<Tx>>::Error, P::Error>;
fn publish(
&self,
envelope: &SerializedEnvelope,
) -> impl Future<Output = Result<(), Self::Error>> + Send {
let span = tracing::debug_span!(
"reliar.outbox.route",
message.id = %envelope.id,
message.type = %envelope.message_type,
route = tracing::field::Empty,
);
async move {
let route = self.owner.policy.decide(&envelope.message_type);
tracing::Span::current().record("route", route.as_str());
match route {
RouteKind::Outbox => {
let mut guard = self.tx.lock().await;
self.owner
.staging
.stage(&mut **guard, envelope)
.await
.map_err(RouteError::Stage)?;
}
RouteKind::Direct => {
self.owner
.publisher
.publish(envelope)
.await
.map_err(RouteError::Publish)?;
}
}
self.owner.metrics.routed(route, &envelope.message_type);
Ok(())
}
.instrument(span)
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum RouteError<S, P> {
Stage(S),
Publish(P),
}
impl<S: fmt::Display, P: fmt::Display> fmt::Display for RouteError<S, P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Stage(err) => write!(f, "failed to stage the routed message: {err}"),
Self::Publish(err) => write!(f, "failed to publish the message directly: {err}"),
}
}
}
impl<S, P> std::error::Error for RouteError<S, P>
where
S: std::error::Error + 'static,
P: std::error::Error + 'static,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Stage(err) => Some(err),
Self::Publish(err) => Some(err),
}
}
}
impl<S: Classify, P: Classify> Classify for RouteError<S, P> {
fn kind(&self) -> FailureKind {
match self {
Self::Stage(err) => err.kind(),
Self::Publish(err) => err.kind(),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum DirectPublishError<P> {
TransactionRequired {
message_type: MessageType,
},
Publish(P),
}
impl<P: fmt::Display> fmt::Display for DirectPublishError<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TransactionRequired { message_type } => write!(
f,
"message type {message_type} routes through the outbox, but no transaction was \
supplied; call OutboxPublisher::in_transaction, or stop routing this type"
),
Self::Publish(err) => write!(f, "failed to publish the message directly: {err}"),
}
}
}
impl<P> std::error::Error for DirectPublishError<P>
where
P: std::error::Error + 'static,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::TransactionRequired { .. } => None,
Self::Publish(err) => Some(err),
}
}
}