use std::error::Error;
use std::fmt;
use rocketmq_error::SharedError;
const RESERVED_RESPONSE_OWNER_ID: u64 = u64::MAX;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct RequestId {
owner_id: u64,
sequence: u64,
}
impl RequestId {
pub(crate) const fn real(owner_id: u64, sequence: u64) -> Option<Self> {
if owner_id == 0 || owner_id == RESERVED_RESPONSE_OWNER_ID || sequence == 0 || sequence == u64::MAX {
None
} else {
Some(Self { owner_id, sequence })
}
}
#[must_use]
pub const fn owner_id(self) -> u64 {
self.owner_id
}
#[must_use]
pub const fn sequence(self) -> u64 {
self.sequence
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum WriteProgress {
NotStarted,
PossiblyPartial,
}
impl WriteProgress {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::NotStarted => "not_started",
Self::PossiblyPartial => "possibly_partial",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ResponseTerminalState {
Completed,
Failed {
progress: WriteProgress,
},
Cancelled,
Closed,
}
impl ResponseTerminalState {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Completed => "completed",
Self::Failed { .. } => "failed",
Self::Cancelled => "cancelled",
Self::Closed => "closed",
}
}
}
pub(crate) enum ResponseOperationalFailure {
Encode {
source: SharedError,
},
Transport {
progress: WriteProgress,
source: SharedError,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[must_use]
pub enum ResponseCompletionOutcome {
Completed(ResponseReceipt),
AlreadyCompleted(ResponseTerminalState),
DeadlineExpired,
Cancelled,
SessionClosed,
QueueSaturated,
}
#[must_use]
pub(crate) enum ResponseSendOutcome {
Written,
Rejected(ResponseCompletionOutcome),
OperationalFailure(ResponseOperationalFailure),
}
impl ResponseCompletionOutcome {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Completed(_) => "completed",
Self::AlreadyCompleted(_) => "already_completed",
Self::DeadlineExpired => "deadline_expired",
Self::Cancelled => "cancelled",
Self::SessionClosed => "session_closed",
Self::QueueSaturated => "queue_saturated",
}
}
}
impl ResponseOperationalFailure {
pub(crate) const fn encode(source: SharedError) -> Self {
Self::Encode { source }
}
pub(crate) const fn transport(progress: WriteProgress, source: SharedError) -> Self {
Self::Transport { progress, source }
}
#[must_use]
pub const fn operation(&self) -> &'static str {
match self {
Self::Encode { .. } => "encode",
Self::Transport { .. } => "transport",
}
}
#[must_use]
pub const fn write_progress(&self) -> WriteProgress {
match self {
Self::Encode { .. } => WriteProgress::NotStarted,
Self::Transport { progress, .. } => *progress,
}
}
#[must_use]
#[cfg(test)]
pub const fn retryable(&self) -> bool {
matches!(
self,
Self::Transport {
progress: WriteProgress::NotStarted,
..
}
)
}
}
impl fmt::Debug for ResponseOperationalFailure {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = formatter.debug_struct("ResponseOperationalFailure");
debug.field("operation", &self.operation());
match self {
Self::Encode { source } => {
debug.field("progress", &WriteProgress::NotStarted.as_str());
debug.field("source_code", &source.descriptor().code().as_str());
}
Self::Transport { progress, source } => {
debug.field("progress", &progress.as_str());
debug.field("source_code", &source.descriptor().code().as_str());
}
}
debug.finish()
}
}
impl fmt::Display for ResponseOperationalFailure {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "response delivery failed: {}", self.operation())?;
match self {
Self::Encode { source } => {
write!(
formatter,
" (progress={}, source_code={})",
WriteProgress::NotStarted.as_str(),
source.descriptor().code().as_str()
)?;
}
Self::Transport { progress, source } => {
write!(
formatter,
" (progress={}, source_code={})",
progress.as_str(),
source.descriptor().code().as_str()
)?;
}
}
Ok(())
}
}
impl Error for ResponseOperationalFailure {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Encode { source } => Some(source),
Self::Transport { source, .. } => Some(source.as_ref()),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ResponseReceipt {
request_id: RequestId,
disposition: ResponseDisposition,
}
impl ResponseReceipt {
pub(crate) const fn new(request_id: RequestId, disposition: ResponseDisposition) -> Self {
Self {
request_id,
disposition,
}
}
#[must_use]
pub const fn request_id(&self) -> RequestId {
self.request_id
}
#[must_use]
pub const fn disposition(&self) -> ResponseDisposition {
self.disposition
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ResponseDisposition {
TransportWritten,
InProcessAccepted,
}
impl ResponseDisposition {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::TransportWritten => "transport_written",
Self::InProcessAccepted => "in_process_accepted",
}
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use std::sync::Arc;
use rocketmq_error::Error as CanonicalError;
use rocketmq_error::PROTOCOL_MESSAGE_PROPERTY_INVALID;
use rocketmq_error::TRANSPORT_CONNECTION_FAILED;
use super::*;
#[test]
fn operational_failures_have_stable_operation_progress_and_retry_policy() {
let cases = [
(
ResponseOperationalFailure::Encode {
source: crate::error_helpers::message_property_invalid("encode-canary".to_owned()),
},
"encode",
WriteProgress::NotStarted,
false,
),
(
ResponseOperationalFailure::Transport {
progress: WriteProgress::NotStarted,
source: Arc::new(CanonicalError::new(&TRANSPORT_CONNECTION_FAILED)),
},
"transport",
WriteProgress::NotStarted,
true,
),
(
ResponseOperationalFailure::Transport {
progress: WriteProgress::PossiblyPartial,
source: Arc::new(CanonicalError::new(&TRANSPORT_CONNECTION_FAILED)),
},
"transport",
WriteProgress::PossiblyPartial,
false,
),
];
for (error, operation, progress, retryable) in cases {
assert_eq!(error.operation(), operation);
assert_eq!(error.write_progress(), progress);
assert_eq!(error.retryable(), retryable);
}
}
#[test]
fn source_errors_retain_their_concrete_type_and_identity() {
let encode = ResponseOperationalFailure::Encode {
source: crate::error_helpers::message_property_invalid("encode-source-canary".to_owned()),
};
let ResponseOperationalFailure::Encode { source: expected } = &encode else {
unreachable!()
};
let source = Error::source(&encode).expect("encode error should preserve its source");
let typed = source
.downcast_ref::<SharedError>()
.expect("encode source should remain a SharedError");
assert!(std::ptr::eq(typed, expected));
assert_eq!(typed.descriptor(), &PROTOCOL_MESSAGE_PROPERTY_INVALID);
let canonical = Arc::new(CanonicalError::new(&TRANSPORT_CONNECTION_FAILED));
let transport = ResponseOperationalFailure::Transport {
progress: WriteProgress::NotStarted,
source: Arc::clone(&canonical),
};
let source = Error::source(&transport).expect("transport error should preserve its canonical source");
let typed = source
.downcast_ref::<CanonicalError>()
.expect("transport source should remain the canonical Error");
assert!(std::ptr::eq(typed, canonical.as_ref()));
}
#[test]
fn response_error_formatting_does_not_expose_sensitive_source_text() {
const CANARY: &str = "response-secret-canary";
let errors = [
ResponseOperationalFailure::Encode {
source: crate::error_helpers::message_property_invalid(CANARY.to_owned()),
},
ResponseOperationalFailure::Transport {
progress: WriteProgress::PossiblyPartial,
source: Arc::new(CanonicalError::new(&TRANSPORT_CONNECTION_FAILED)),
},
];
for error in errors {
let display = error.to_string();
let debug = format!("{error:?}");
assert!(
!display.contains(CANARY),
"display leaked sensitive source text: {display}"
);
assert!(!debug.contains(CANARY), "debug leaked sensitive source text: {debug}");
assert!(display.contains("source_code="));
assert!(debug.contains("source_code:"));
}
}
}