fix(channels): retry only transient delivery errors

This commit is contained in:
xiaoxixi 2026-07-14 11:57:47 +08:00
parent f42e9d44cc
commit 3f1350c33b
2 changed files with 46 additions and 1 deletions

View File

@ -153,7 +153,7 @@ impl OutboundDispatcher {
let result = tokio::time::timeout(SEND_TIMEOUT, channel.send(msg.clone())).await; let result = tokio::time::timeout(SEND_TIMEOUT, channel.send(msg.clone())).await;
match result { match result {
Ok(Ok(())) => return Ok(()), Ok(Ok(())) => return Ok(()),
Ok(Err(error)) if attempt < DELAYS.len() - 1 => { Ok(Err(error)) if attempt < DELAYS.len() - 1 && error.is_transient() => {
tracing::warn!(attempt = attempt + 1, delay, error = %error, "Send failed, retrying"); tracing::warn!(attempt = attempt + 1, delay, error = %error, "Send failed, retrying");
} }
Ok(Err(error)) => return Err(error), Ok(Err(error)) => return Err(error),
@ -177,6 +177,7 @@ impl OutboundDispatcher {
mod tests { mod tests {
use super::*; use super::*;
use async_trait::async_trait; use async_trait::async_trait;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::{Mutex, Notify}; use tokio::sync::{Mutex, Notify};
struct RecordingChannel { struct RecordingChannel {
@ -184,6 +185,30 @@ mod tests {
notify: Notify, notify: Notify,
} }
struct PermanentFailureChannel {
attempts: AtomicUsize,
}
#[async_trait]
impl Channel for PermanentFailureChannel {
fn name(&self) -> &str {
"permanent-failure"
}
fn is_running(&self) -> bool {
true
}
async fn start(&self, _bus: Arc<MessageBus>) -> Result<(), ChannelError> {
Ok(())
}
async fn stop(&self) -> Result<(), ChannelError> {
Ok(())
}
async fn send(&self, _msg: OutboundMessage) -> Result<(), ChannelError> {
self.attempts.fetch_add(1, Ordering::SeqCst);
Err(ChannelError::Other("invalid destination".to_string()))
}
}
#[async_trait] #[async_trait]
impl Channel for RecordingChannel { impl Channel for RecordingChannel {
fn name(&self) -> &str { fn name(&self) -> &str {
@ -312,4 +337,18 @@ mod tests {
task.abort(); task.abort();
supervisor.shutdown(Duration::from_secs(1)).await; supervisor.shutdown(Duration::from_secs(1)).await;
} }
#[tokio::test]
async fn permanent_send_failure_is_not_retried() {
let channel = PermanentFailureChannel {
attempts: AtomicUsize::new(0),
};
let error = OutboundDispatcher::send_with_retry(&channel, &outbound("invalid", "message"))
.await
.unwrap_err();
assert!(matches!(error, ChannelError::Other(_)));
assert_eq!(channel.attempts.load(Ordering::SeqCst), 1);
}
} }

View File

@ -26,6 +26,12 @@ impl std::fmt::Display for ChannelError {
impl std::error::Error for ChannelError {} impl std::error::Error for ChannelError {}
impl ChannelError {
pub fn is_transient(&self) -> bool {
matches!(self, Self::ConnectionError(_) | Self::SendError(_))
}
}
impl From<BusError> for ChannelError { impl From<BusError> for ChannelError {
fn from(e: BusError) -> Self { fn from(e: BusError) -> Self {
ChannelError::BusError(e.to_string()) ChannelError::BusError(e.to_string())