This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/rocketmq-clients.git
The following commit(s) were added to refs/heads/master by this push:
new 62850895 [ISSUE #1413] [Rust] Increase delivery attempt on FIFO local
retry (#1414)
62850895 is described below
commit 62850895dab3742abc0675c9a73366c6eff82518
Author: Lei Zhiyuan <[email protected]>
AuthorDate: Fri Oct 23 18:40:10 2026 +0800
[ISSUE #1413] [Rust] Increase delivery attempt on FIFO local retry (#1414)
FifoConsumerWorker kept the attempt in a local counter, so the listener saw
the server-provided delivery attempt on every local retry, the first retry
used the delay of attempt 2, and the DLQ request reported max attempts + 1.
Align with the Java client (ProcessQueueImpl#eraseFifoMessage): while the
delivery attempt is below the max attempts, take the delay of the current
attempt, increase the attempt on the MessageView
(MessageViewImpl#incrementAndGetDeliveryAttempt) and consume again;
otherwise forward the message to the DLQ with its current delivery
attempt. The listener now sees 1, 2, ..., max attempts. The standard
(non-FIFO) path is unchanged.
---
rust/src/model/message.rs | 8 ++
rust/src/push_consumer.rs | 243 ++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 244 insertions(+), 7 deletions(-)
diff --git a/rust/src/model/message.rs b/rust/src/model/message.rs
index f118d9e3..5c58c2f6 100644
--- a/rust/src/model/message.rs
+++ b/rust/src/model/message.rs
@@ -564,6 +564,14 @@ impl MessageView {
self.delivery_attempt
}
+ /// Increase the delivery attempt before a local redelivery and return the
new value.
+ ///
+ /// Reference Java: `MessageViewImpl#incrementAndGetDeliveryAttempt`.
+ pub(crate) fn increment_and_get_delivery_attempt(&mut self) -> i32 {
+ self.delivery_attempt += 1;
+ self.delivery_attempt
+ }
+
/// Get message priority (1-10, higher value means higher priority)
pub fn priority(&self) -> Option<i32> {
self.priority
diff --git a/rust/src/push_consumer.rs b/rust/src/push_consumer.rs
index 0058ba82..b4c5fcff 100644
--- a/rust/src/push_consumer.rs
+++ b/rust/src/push_consumer.rs
@@ -946,13 +946,17 @@ impl FifoConsumerWorker {
}
/// Consume a single message sequentially with retries (static method for
use in spawned tasks)
+ ///
+ /// Reference Java: `ProcessQueueImpl#eraseFifoMessage`. While the
delivery attempt is below
+ /// the max attempts, a failed message is consumed again locally after the
delay of the
+ /// current attempt, with its delivery attempt increased, so the listener
sees 1, 2, ...,
+ /// max attempts. Then the message is forwarded to the DLQ with its
current delivery attempt.
async fn consume_message_sequentially_static(
- message: MessageView,
+ mut message: MessageView,
message_listener: &Arc<MessageListener>,
ack_processor: &mut AckEntryProcessor,
retry_policy: &BackOffRetryPolicy,
) {
- let mut delivery_attempt = message.delivery_attempt();
let max_delivery_attempts = retry_policy.get_max_attempts();
loop {
let consume_result = consume_message(message_listener, &message);
@@ -962,8 +966,12 @@ impl FifoConsumerWorker {
break;
}
ConsumeResult::FAILURE => {
- delivery_attempt += 1;
- if delivery_attempt > max_delivery_attempts {
+ let delivery_attempt = message.delivery_attempt();
+ if delivery_attempt < max_delivery_attempts {
+ let delay =
retry_policy.get_next_attempt_delay(delivery_attempt);
+ message.increment_and_get_delivery_attempt();
+ tokio::time::sleep(delay).await;
+ } else {
ack_processor
.forward_to_deadletter_queue(
message,
@@ -972,9 +980,6 @@ impl FifoConsumerWorker {
)
.await;
break;
- } else {
-
tokio::time::sleep(retry_policy.get_next_attempt_delay(delivery_attempt))
- .await;
}
}
}
@@ -2434,4 +2439,228 @@ mod tests {
// The panic counts as one failed attempt; the retry succeeds and is
acked.
assert_eq!(2, outer_call_counter.load(Ordering::Relaxed));
}
+
+ fn new_message_with_delivery_attempt(delivery_attempt: i32) -> pb::Message
{
+ let mut message = new_message();
+ message.system_properties.as_mut().unwrap().delivery_attempt =
Some(delivery_attempt);
+ message
+ }
+
+ fn new_fifo_worker_recording_attempts(
+ rpc_client: Session,
+ failures: usize,
+ ) -> (FifoConsumerWorker, Arc<Mutex<Vec<i32>>>) {
+ let attempts = Arc::new(Mutex::new(Vec::new()));
+ let recorded = Arc::clone(&attempts);
+ let worker = FifoConsumerWorker::new(
+ rpc_client,
+ Arc::new(Box::new(move |message: &MessageView| {
+ let mut recorded = recorded.lock();
+ recorded.push(message.delivery_attempt());
+ if recorded.len() <= failures {
+ ConsumeResult::FAILURE
+ } else {
+ ConsumeResult::SUCCESS
+ }
+ })),
+ );
+ (worker, attempts)
+ }
+
+ fn new_test_resources() -> (Resource, Resource) {
+ (
+ Resource {
+ name: "test_group".to_string(),
+ resource_namespace: "".to_string(),
+ },
+ Resource {
+ name: "test_topic".to_string(),
+ resource_namespace: "".to_string(),
+ },
+ )
+ }
+
+ // Reference Java: ProcessQueueImpl#eraseFifoMessage. While attempt <
maxAttempts, a failed
+ // FIFO message is consumed again locally after
messageView.incrementAndGetDeliveryAttempt(),
+ // so the listener sees 1, 2, ..., maxAttempts. Once attempt reaches
maxAttempts the message
+ // is forwarded to the DLQ with the current delivery attempt.
+ #[tokio::test]
+ async fn test_fifo_local_retry_increments_delivery_attempt_until_dlq() {
+ let mut rpc_client = Session::default();
+ rpc_client.expect_receive_message().returning(|_| {
+ Ok(vec![ReceiveMessageResponse {
+ content:
Some(Content::Message(new_message_with_delivery_attempt(1))),
+ }])
+ });
+ let (consumer_worker, attempts) =
+ new_fifo_worker_recording_attempts(rpc_client, usize::MAX);
+ let (consumer_group, topic) = new_test_resources();
+ let retry_policy =
&BackOffRetryPolicy::Customized(CustomizedBackOffRetryPolicy::new(
+ pb::CustomizedBackoff {
+ next: vec![prost_types::Duration::from_str("0.01s").unwrap()],
+ },
+ 4,
+ ));
+ let mut option = PushConsumerOption::default();
+ option.subscribe("test_topic", FilterExpression::new(FilterType::Tag,
"*"));
+ let mut session = Session::default();
+ session
+ .expect_shadow_session()
+ .returning(|| Session::default());
+ session.expect_ack_message().never();
+ session
+ .expect_forward_to_deadletter_queue()
+ .withf(|request| request.delivery_attempt == 4 &&
request.max_delivery_attempts == 4)
+ .times(1)
+ .returning(|_| {
+ Ok(ForwardMessageToDeadLetterQueueResponse {
+ status: Some(Status {
+ code: Code::Ok as i32,
+ message: "".to_string(),
+ }),
+ })
+ });
+ let mut ack_processor = AckEntryProcessor::new(session,
consumer_group, topic, false);
+ let result = ConsumerWorker::Fifo(consumer_worker)
+ .receive_messages(
+ &new_message_queue(),
+ &FilterExpression::sub_all(),
+ &option,
+ &mut ack_processor,
+ retry_policy,
+ )
+ .await;
+ assert!(result.is_ok(), "{:?}", result);
+ assert_eq!(vec![1, 2, 3, 4], *attempts.lock());
+ }
+
+ #[tokio::test]
+ async fn
test_fifo_local_retry_succeeds_with_incremented_delivery_attempt() {
+ let mut rpc_client = Session::default();
+ rpc_client.expect_receive_message().returning(|_| {
+ Ok(vec![ReceiveMessageResponse {
+ content:
Some(Content::Message(new_message_with_delivery_attempt(1))),
+ }])
+ });
+ let (consumer_worker, attempts) =
new_fifo_worker_recording_attempts(rpc_client, 2);
+ let (consumer_group, topic) = new_test_resources();
+ // The first retry must wait next[0] (the delay for attempt 1), as in
Java
+ // (retryPolicy.getNextAttemptDelay(attempt) is called before the
increment).
+ let retry_policy =
&BackOffRetryPolicy::Customized(CustomizedBackOffRetryPolicy::new(
+ pb::CustomizedBackoff {
+ next: vec![
+ prost_types::Duration::from_str("0.01s").unwrap(),
+ prost_types::Duration::from_str("0.01s").unwrap(),
+ prost_types::Duration::from_str("5s").unwrap(),
+ ],
+ },
+ 17,
+ ));
+ let mut option = PushConsumerOption::default();
+ option.subscribe("test_topic", FilterExpression::new(FilterType::Tag,
"*"));
+ let mut session = Session::default();
+ session
+ .expect_shadow_session()
+ .returning(|| Session::default());
+ session.expect_forward_to_deadletter_queue().never();
+ session.expect_ack_message().times(1).returning(|_| {
+ Ok(AckMessageResponse {
+ status: Some(Status {
+ code: Code::Ok as i32,
+ message: "".to_string(),
+ }),
+ entries: vec![],
+ })
+ });
+ let mut ack_processor = AckEntryProcessor::new(session,
consumer_group, topic, false);
+ let started = std::time::Instant::now();
+ let result = ConsumerWorker::Fifo(consumer_worker)
+ .receive_messages(
+ &new_message_queue(),
+ &FilterExpression::sub_all(),
+ &option,
+ &mut ack_processor,
+ retry_policy,
+ )
+ .await;
+ assert!(result.is_ok(), "{:?}", result);
+ assert_eq!(vec![1, 2, 3], *attempts.lock());
+ assert!(
+ started.elapsed() < Duration::from_secs(2),
+ "retries must use the delays of attempts 1 and 2, took {:?}",
+ started.elapsed()
+ );
+ }
+
+ // The standard (non-FIFO) path redelivers through the server and must
keep reporting the
+ // server-provided delivery attempt, with the delay for that attempt.
+ #[tokio::test]
+ async fn test_standard_failure_keeps_server_delivery_attempt() {
+ let mut rpc_client = Session::default();
+ rpc_client.expect_receive_message().returning(|_| {
+ Ok(vec![ReceiveMessageResponse {
+ content:
Some(Content::Message(new_message_with_delivery_attempt(2))),
+ }])
+ });
+ let attempts = Arc::new(Mutex::new(Vec::new()));
+ let recorded = Arc::clone(&attempts);
+ let consumer_worker = StandardConsumerWorker::new(
+ rpc_client,
+ Arc::new(Box::new(move |message: &MessageView| {
+ recorded.lock().push(message.delivery_attempt());
+ ConsumeResult::FAILURE
+ })),
+ );
+ let (consumer_group, topic) = new_test_resources();
+ let retry_policy =
&BackOffRetryPolicy::Customized(CustomizedBackOffRetryPolicy::new(
+ pb::CustomizedBackoff {
+ next: vec![
+ prost_types::Duration::from_str("1s").unwrap(),
+ prost_types::Duration::from_str("7s").unwrap(),
+ ],
+ },
+ 17,
+ ));
+ let mut option = PushConsumerOption::default();
+ option.subscribe("test_topic", FilterExpression::new(FilterType::Tag,
"*"));
+ let mut session = Session::default();
+ session
+ .expect_shadow_session()
+ .returning(|| Session::default());
+ session.expect_ack_message().never();
+ session.expect_forward_to_deadletter_queue().never();
+ session
+ .expect_change_invisible_duration()
+ .withf(|request| {
+ request.invisible_duration
+ == Some(prost_types::Duration {
+ seconds: 7,
+ nanos: 0,
+ })
+ })
+ .times(1)
+ .returning(|_| {
+ Ok(ChangeInvisibleDurationResponse {
+ status: Some(Status {
+ code: Code::Ok as i32,
+ message: "".to_string(),
+ }),
+ receipt_handle: "".to_string(),
+ })
+ });
+ let mut ack_processor = AckEntryProcessor::new(session,
consumer_group, topic, false);
+ let is_start_ok = ack_processor.start().await;
+ assert!(is_start_ok.is_ok());
+ let result = ConsumerWorker::Standard(consumer_worker)
+ .receive_messages(
+ &new_message_queue(),
+ &FilterExpression::sub_all(),
+ &option,
+ &mut ack_processor,
+ retry_policy,
+ )
+ .await;
+ assert!(result.is_ok(), "{:?}", result);
+ assert_eq!(vec![2], *attempts.lock());
+ }
}