use core::fmt;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::time::Duration;
use reliar_core::{Classify, FailureKind, MessageId, Publisher, SerializedEnvelope};
#[derive(Clone, Debug, Default)]
pub struct RecordingPublisher {
inner: Arc<Mutex<RecordingInner>>,
concurrency_probe: Option<Duration>,
}
#[derive(Debug, Default)]
struct RecordingInner {
published: Vec<MessageId>,
envelopes: Vec<SerializedEnvelope>,
in_flight: usize,
in_flight_peak: usize,
}
impl RecordingPublisher {
#[must_use]
pub fn with_concurrency_probe(delay: Duration) -> Self {
Self {
inner: Arc::default(),
concurrency_probe: Some(delay),
}
}
fn lock(&self) -> MutexGuard<'_, RecordingInner> {
self.inner.lock().unwrap_or_else(PoisonError::into_inner)
}
#[must_use]
pub fn published(&self) -> Vec<MessageId> {
self.lock().published.clone()
}
#[must_use]
pub fn count(&self, id: MessageId) -> usize {
self.lock()
.published
.iter()
.filter(|&&seen| seen == id)
.count()
}
#[must_use]
pub fn envelopes(&self) -> Vec<SerializedEnvelope> {
self.lock().envelopes.clone()
}
#[must_use]
pub fn in_flight_peak(&self) -> usize {
self.lock().in_flight_peak
}
}
impl Publisher for RecordingPublisher {
type Error = FakePublishError;
fn publish(
&self,
envelope: &SerializedEnvelope,
) -> impl Future<Output = Result<(), Self::Error>> + Send {
let inner = Arc::clone(&self.inner);
let id = envelope.id;
let envelope = envelope.clone();
let probe = self.concurrency_probe;
async move {
{
let mut guard = inner.lock().unwrap_or_else(PoisonError::into_inner);
guard.in_flight += 1;
guard.in_flight_peak = guard.in_flight_peak.max(guard.in_flight);
guard.published.push(id);
guard.envelopes.push(envelope);
}
if let Some(delay) = probe {
tokio::time::sleep(delay).await;
}
inner
.lock()
.unwrap_or_else(PoisonError::into_inner)
.in_flight -= 1;
Ok(())
}
}
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum PublishStep {
Ok,
Transient,
Permanent,
Hang(Duration),
Panic,
}
#[derive(Clone, Debug)]
pub struct ScriptedPublisher {
inner: Arc<Mutex<ScriptedInner>>,
}
#[derive(Debug)]
enum Script {
Positional {
steps: Vec<PublishStep>,
next: usize,
},
Keyed(HashMap<MessageId, PublishStep>),
}
#[derive(Debug)]
struct ScriptedInner {
script: Script,
published: Vec<MessageId>,
}
impl ScriptedPublisher {
#[must_use]
pub fn new(script: impl IntoIterator<Item = PublishStep>) -> Self {
Self {
inner: Arc::new(Mutex::new(ScriptedInner {
script: Script::Positional {
steps: script.into_iter().collect(),
next: 0,
},
published: Vec::new(),
})),
}
}
#[must_use]
pub fn keyed(steps: impl IntoIterator<Item = (MessageId, PublishStep)>) -> Self {
Self {
inner: Arc::new(Mutex::new(ScriptedInner {
script: Script::Keyed(steps.into_iter().collect()),
published: Vec::new(),
})),
}
}
#[must_use]
pub fn always(step: PublishStep) -> Self {
Self::new(std::iter::once(step))
}
#[must_use]
pub fn published(&self) -> Vec<MessageId> {
self.lock().published.clone()
}
fn lock(&self) -> MutexGuard<'_, ScriptedInner> {
self.inner.lock().unwrap_or_else(PoisonError::into_inner)
}
fn step_for(inner: &Mutex<ScriptedInner>, id: MessageId) -> PublishStep {
let mut guard = inner.lock().unwrap_or_else(PoisonError::into_inner);
guard.published.push(id);
match &mut guard.script {
Script::Positional { steps, next } => {
let Some(last) = steps.len().checked_sub(1) else {
return PublishStep::Ok;
};
let index = (*next).min(last);
*next = next.saturating_add(1);
steps[index]
}
Script::Keyed(steps) => steps.get(&id).copied().unwrap_or(PublishStep::Ok),
}
}
}
impl Publisher for ScriptedPublisher {
type Error = FakePublishError;
#[allow(
clippy::panic,
reason = "PublishStep::Panic exists to simulate a publish task crashing mid-flight (S4 review, blocker 2) — the panic is the fake's whole purpose here, not an accident"
)]
fn publish(
&self,
envelope: &SerializedEnvelope,
) -> impl Future<Output = Result<(), Self::Error>> + Send {
let inner = Arc::clone(&self.inner);
let id = envelope.id;
async move {
let step = Self::step_for(&inner, id);
match step {
PublishStep::Ok => Ok(()),
PublishStep::Transient => Err(FakePublishError::Transient {
detail: "scripted transient failure",
}),
PublishStep::Permanent => Err(FakePublishError::Permanent {
detail: "scripted permanent failure",
}),
PublishStep::Hang(duration) => {
tokio::time::sleep(duration).await;
Ok(())
}
PublishStep::Panic => panic!("test-support: scripted publish panic"),
}
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum FakePublishError {
Transient {
detail: &'static str,
},
Permanent {
detail: &'static str,
},
}
impl fmt::Display for FakePublishError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Transient { detail } => {
write!(f, "scripted publish failure (transient): {detail}")
}
Self::Permanent { detail } => {
write!(f, "scripted publish failure (permanent): {detail}")
}
}
}
}
impl std::error::Error for FakePublishError {}
impl Classify for FakePublishError {
fn kind(&self) -> FailureKind {
match self {
Self::Transient { .. } => FailureKind::Transient,
Self::Permanent { .. } => FailureKind::Permanent,
}
}
}