1mod log_filter;
21
22use std::collections::HashMap;
23use std::collections::HashSet;
24
25use crate::ClientError;
26use cheetah_string::CheetahString;
27use rand::seq::IndexedRandom;
28use rocketmq_model::common::attribute::attribute_parser::AttributeParser;
29use rocketmq_model::common::config::TopicConfig;
30use rocketmq_model::common::message::message_enum::MessageRequestMode;
31use rocketmq_model::common::message::message_ext::MessageExt;
32use rocketmq_model::common::message::message_queue::MessageQueue;
33use rocketmq_model::common::mix_all;
34use rocketmq_model::common::topic::TopicValidator;
35use rocketmq_protocol::code::response_code::ResponseCode;
36use rocketmq_protocol::common::message::message_decoder as MessageDecoder;
37use rocketmq_protocol::protocol::admin::offset_wrapper::OffsetWrapper;
38use rocketmq_protocol::protocol::admin::rollback_stats::RollbackStats;
39use rocketmq_protocol::protocol::admin::topic_offset::TopicOffset;
40use rocketmq_protocol::protocol::body::broker_body::cluster_info::ClusterInfo;
41use rocketmq_protocol::protocol::body::consume_message_directly_result::ConsumeMessageDirectlyResult;
42use rocketmq_protocol::protocol::body::consumer_connection::ConsumerConnection;
43use rocketmq_protocol::protocol::body::proxy_drain::ProxyDrainStateResponseBody;
44use rocketmq_protocol::protocol::header::consume_message_directly_result_request_header::ConsumeMessageDirectlyResultRequestHeader;
45use rocketmq_protocol::protocol::header::create_topic_request_header::CreateTopicRequestHeader;
46use rocketmq_protocol::protocol::header::delete_topic_request_header::DeleteTopicRequestHeader;
47use rocketmq_protocol::protocol::header::get_consume_stats_request_header::GetConsumeStatsRequestHeader;
48use rocketmq_protocol::protocol::header::get_topic_stats_info_request_header::GetTopicStatsInfoRequestHeader;
49use rocketmq_protocol::protocol::header::namesrv::topic_operation_header::DeleteTopicFromNamesrvRequestHeader;
50use rocketmq_protocol::protocol::header::query_consumer_offset_request_header::QueryConsumerOffsetRequestHeader;
51use rocketmq_protocol::protocol::header::reset_offset_request_header::ResetOffsetRequestHeader;
52use rocketmq_protocol::protocol::header::update_consumer_offset_header::UpdateConsumerOffsetRequestHeader;
53use rocketmq_protocol::protocol::header::view_message_request_header::ViewMessageRequestHeader;
54use rocketmq_protocol::protocol::route::route_data_view::QueueData;
55use rocketmq_protocol::protocol::route::topic_route_data::TopicRouteData;
56use rocketmq_protocol::protocol::route_facade::BrokerDataExt;
57use rocketmq_protocol::protocol::subscription::subscription_group_config::SubscriptionGroupConfig;
58
59use crate::admin::mq_admin_mutation_ext::BrokerConfigPatchOutcome;
60use crate::admin::mq_admin_mutation_ext::BrokerMutationConfigState;
61use crate::admin::mq_admin_mutation_ext::ConditionalConsumerOffsetOutcome;
62use crate::admin::mq_admin_mutation_ext::MQAdminMutationExt;
63use crate::admin::mq_admin_mutation_ext::MutationConsumerOffsetPreview;
64use crate::admin::mq_admin_mutation_ext::MutationExpectedMessageRequestMode;
65use crate::admin::mq_admin_mutation_ext::MutationExpectedState;
66use crate::admin::mq_admin_mutation_ext::MutationMessageRequestMode;
67use crate::admin::mq_admin_mutation_ext::MutationMessageRequestModeOutcome;
68use crate::admin::mq_admin_mutation_ext::MutationStateCasOutcome;
69use crate::admin::mq_admin_mutation_ext::MutationSubscriptionGroupConfig;
70use crate::admin::mq_admin_mutation_ext::MutationSubscriptionGroupConfigState;
71use crate::admin::mq_admin_mutation_ext::MutationTopicConfig;
72use crate::admin::mq_admin_mutation_ext::MutationTopicConfigState;
73use crate::admin::mq_admin_mutation_ext::SubscriptionGroupConfigPatch;
74use crate::admin::mq_admin_mutation_ext::SubscriptionGroupConfigPatchOutcome;
75use crate::admin::mq_admin_mutation_ext::TopicConfigPatch;
76use crate::admin::mq_admin_mutation_ext::TopicConfigPatchOutcome;
77use crate::admin::mq_admin_mutation_ext::TopicOffsetMutationFailureCode;
78use crate::admin::mq_admin_mutation_ext::TopicOffsetMutationOutcome;
79use crate::admin::mq_admin_mutation_ext::TopicOffsetMutationTargetOutcome;
80
81use super::DefaultMQAdminExtImpl;
82use super::NAMESPACE_ORDER_TOPIC_CONFIG;
83
84const MAX_SUPERVISED_OFFSET_TARGETS: usize = 1_000;
85
86fn timestamp_to_java_long(operation: &'static str, timestamp: u64) -> crate::ClientResult<i64> {
87 i64::try_from(timestamp)
88 .map_err(|_| ClientError::illegal_argument(format!("{operation} timestamp exceeds Java long range")))
89}
90
91fn java_long_to_u64(operation: &'static str, field: &'static str, value: i64) -> crate::ClientResult<u64> {
92 u64::try_from(value).map_err(|_| {
93 ClientError::illegal_argument(format!(
94 "{operation} {field} is negative and cannot be represented as Rust u64"
95 ))
96 })
97}
98
99fn encode_topic_attributes(attributes: &HashMap<CheetahString, CheetahString>) -> Option<CheetahString> {
100 if attributes.is_empty() {
101 return None;
102 }
103 let serialized = AttributeParser::parse_to_string(
104 &attributes
105 .iter()
106 .map(|(key, value)| (key.to_string(), value.to_string()))
107 .collect::<HashMap<String, String>>(),
108 );
109 (!serialized.is_empty()).then(|| serialized.into())
110}
111
112fn merge_order_conf_entries(existing: &str, value: &str) -> String {
113 let mut entries = HashMap::new();
114 for item in existing.split(';').filter(|item| !item.trim().is_empty()) {
115 if let Some((broker_name, _)) = item.split_once(':') {
116 entries.insert(broker_name.to_string(), item.to_string());
117 }
118 }
119 if let Some((broker_name, _)) = value.split_once(':') {
120 entries.insert(broker_name.to_string(), value.to_string());
121 } else if !value.trim().is_empty() {
122 entries.insert(value.to_string(), value.to_string());
123 }
124 let mut broker_names = entries.keys().cloned().collect::<Vec<_>>();
125 broker_names.sort();
126 broker_names
127 .into_iter()
128 .filter_map(|broker_name| entries.remove(&broker_name))
129 .collect::<Vec<_>>()
130 .join(";")
131}
132
133fn offset_failure(broker_name: &str, queue_id: Option<i32>, error: &ClientError) -> TopicOffsetMutationTargetOutcome {
134 TopicOffsetMutationTargetOutcome {
135 broker_name: broker_name.to_owned(),
136 queue_id,
137 applied: false,
138 offset: None,
139 failure: Some(TopicOffsetMutationFailureCode::Unavailable),
140 retryable: matches!(
141 error.descriptor().recovery_hint(),
142 rocketmq_error::RecoveryHint::Backoff
143 | rocketmq_error::RecoveryHint::RefreshRoute
144 | rocketmq_error::RecoveryHint::RefreshLeader
145 | rocketmq_error::RecoveryHint::SwitchBroker
146 ),
147 }
148}
149
150fn invalid_offset(broker_name: &str, queue_id: i32) -> TopicOffsetMutationTargetOutcome {
151 TopicOffsetMutationTargetOutcome {
152 broker_name: broker_name.to_owned(),
153 queue_id: Some(queue_id),
154 applied: false,
155 offset: None,
156 failure: Some(TopicOffsetMutationFailureCode::InvalidData),
157 retryable: false,
158 }
159}
160
161fn applied_offset(broker_name: &str, queue_id: i32, offset: u64) -> TopicOffsetMutationTargetOutcome {
162 TopicOffsetMutationTargetOutcome {
163 broker_name: broker_name.to_owned(),
164 queue_id: Some(queue_id),
165 applied: true,
166 offset: Some(offset),
167 failure: None,
168 retryable: false,
169 }
170}
171
172fn select_consumer_direct_connection(
173 consumer_group: &CheetahString,
174 consumer_connection: &ConsumerConnection,
175 requested_client_id: Option<&CheetahString>,
176) -> crate::ClientResult<(CheetahString, CheetahString)> {
177 let requested = requested_client_id.filter(|client_id| !client_id.is_empty());
178 let connection = consumer_connection
179 .get_connection_set()
180 .iter()
181 .find(|connection| {
182 requested
183 .map(|client_id| connection.get_client_id() == *client_id)
184 .unwrap_or_else(|| !connection.get_client_id().is_empty())
185 })
186 .ok_or_else(|| {
187 let message = requested
188 .map(|client_id| {
189 format!(
190 "Client `{}` was not found in consumer group `{}`",
191 client_id, consumer_group
192 )
193 })
194 .unwrap_or_else(|| format!("NO CONSUMER for consumer group `{consumer_group}`"));
195 ClientError::illegal_argument(message)
196 })?;
197 Ok((connection.get_client_id(), connection.get_client_addr()))
198}
199
200impl DefaultMQAdminExtImpl {
201 async fn mutation_reset_offset_on_broker(
202 &self,
203 broker_addr: CheetahString,
204 queue_data: &QueueData,
205 consumer_group: CheetahString,
206 topic: CheetahString,
207 timestamp: i64,
208 force: bool,
209 ) -> crate::ClientResult<Vec<RollbackStats>> {
210 let consume_stats = self
211 .mq_client_api()?
212 .get_consume_stats_for_mutation(
213 &broker_addr,
214 GetConsumeStatsRequestHeader {
215 consumer_group: consumer_group.clone(),
216 topic: topic.clone(),
217 topic_list: None,
218 topic_request_header: None,
219 },
220 self.remoting_timeout_millis()?,
221 )
222 .await?;
223 let mut rollback_stats = Vec::new();
224 let mut has_consumed = false;
225 for (queue, offset_wrapper) in &consume_stats.offset_table {
226 if queue.topic() == &topic {
227 has_consumed = true;
228 rollback_stats.push(
229 self.mutation_reset_queue_offset(
230 broker_addr.clone(),
231 consumer_group.clone(),
232 queue.clone(),
233 offset_wrapper,
234 timestamp,
235 force,
236 )
237 .await?,
238 );
239 }
240 }
241 if !has_consumed {
242 let topic_status = self
243 .mq_client_api()?
244 .get_topic_stats_info(
245 &broker_addr,
246 GetTopicStatsInfoRequestHeader {
247 topic: topic.clone(),
248 topic_request_header: None,
249 },
250 self.remoting_timeout_millis()?,
251 )
252 .await?;
253 for queue_id in 0..queue_data.read_queue_nums() {
254 let queue = MessageQueue::from_parts(topic.clone(), queue_data.broker_name().clone(), queue_id as i32);
255 let mut offset_wrapper = OffsetWrapper::new();
256 let topic_offset = topic_status
257 .get_offset_table()
258 .get(&queue)
259 .cloned()
260 .unwrap_or_else(TopicOffset::new);
261 offset_wrapper.set_broker_offset(topic_offset.get_max_offset());
262 offset_wrapper.set_consumer_offset(topic_offset.get_min_offset());
263 rollback_stats.push(
264 self.mutation_reset_queue_offset(
265 broker_addr.clone(),
266 consumer_group.clone(),
267 queue,
268 &offset_wrapper,
269 timestamp,
270 force,
271 )
272 .await?,
273 );
274 }
275 }
276 Ok(rollback_stats)
277 }
278
279 async fn mutation_reset_queue_offset(
280 &self,
281 broker_addr: CheetahString,
282 consumer_group: CheetahString,
283 queue: MessageQueue,
284 offset_wrapper: &OffsetWrapper,
285 timestamp: i64,
286 force: bool,
287 ) -> crate::ClientResult<RollbackStats> {
288 let reset_offset = if timestamp == -1 {
289 self.mq_client_api()?
290 .get_max_offset(broker_addr.as_str(), &queue, self.remoting_timeout_millis()?)
291 .await?
292 } else {
293 self.mq_client_api()?
294 .search_offset_by_timestamp(
295 broker_addr.as_str(),
296 &queue,
297 timestamp,
298 rocketmq_model::common::boundary_type::BoundaryType::Lower,
299 self.remoting_timeout_millis()?,
300 )
301 .await?
302 };
303 let mut rollback_stats = RollbackStats {
304 broker_name: queue.broker_name().clone(),
305 queue_id: queue.queue_id() as i64,
306 broker_offset: offset_wrapper.get_broker_offset(),
307 consumer_offset: offset_wrapper.get_consumer_offset(),
308 timestamp_offset: reset_offset,
309 rollback_offset: offset_wrapper.get_consumer_offset(),
310 };
311 if force || reset_offset <= offset_wrapper.get_consumer_offset() {
312 rollback_stats.rollback_offset = reset_offset;
313 self.mq_client_api()?
314 .update_consumer_offset(
315 &broker_addr,
316 UpdateConsumerOffsetRequestHeader {
317 consumer_group,
318 topic: queue.topic().clone(),
319 queue_id: queue.queue_id(),
320 commit_offset: reset_offset,
321 topic_request_header: None,
322 },
323 self.remoting_timeout_millis()?,
324 )
325 .await?;
326 }
327 Ok(rollback_stats)
328 }
329
330 async fn mutation_reset_offset_on_broker_detailed(
331 &self,
332 broker_addr: CheetahString,
333 queue_data: &QueueData,
334 consumer_group: CheetahString,
335 topic: CheetahString,
336 timestamp: i64,
337 force: bool,
338 ) -> Vec<TopicOffsetMutationTargetOutcome> {
339 let broker_name = queue_data.broker_name().to_string();
340 let api = match self.mq_client_api() {
341 Ok(api) => api,
342 Err(error) => return vec![offset_failure(&broker_name, None, &error)],
343 };
344 let timeout = match self.remoting_timeout_millis() {
345 Ok(timeout) => timeout,
346 Err(error) => return vec![offset_failure(&broker_name, None, &error)],
347 };
348 let consume_stats = match api
349 .get_consume_stats_for_mutation(
350 &broker_addr,
351 GetConsumeStatsRequestHeader {
352 consumer_group: consumer_group.clone(),
353 topic: topic.clone(),
354 topic_list: None,
355 topic_request_header: None,
356 },
357 timeout,
358 )
359 .await
360 {
361 Ok(stats) => stats,
362 Err(error) => return vec![offset_failure(&broker_name, None, &error)],
363 };
364 let consumed = consume_stats
365 .offset_table
366 .iter()
367 .filter(|(queue, _)| queue.topic() == &topic)
368 .map(|(queue, wrapper)| (queue.clone(), wrapper.clone()))
369 .collect::<Vec<_>>();
370 let queues = if consumed.is_empty() {
371 let topic_status = match api
372 .get_topic_stats_info(
373 &broker_addr,
374 GetTopicStatsInfoRequestHeader {
375 topic: topic.clone(),
376 topic_request_header: None,
377 },
378 timeout,
379 )
380 .await
381 {
382 Ok(status) => status,
383 Err(error) => return vec![offset_failure(&broker_name, None, &error)],
384 };
385 (0..queue_data.read_queue_nums())
386 .map(|queue_id| {
387 let queue =
388 MessageQueue::from_parts(topic.clone(), queue_data.broker_name().clone(), queue_id as i32);
389 let topic_offset = topic_status
390 .get_offset_table()
391 .get(&queue)
392 .cloned()
393 .unwrap_or_else(TopicOffset::new);
394 let mut wrapper = OffsetWrapper::new();
395 wrapper.set_broker_offset(topic_offset.get_max_offset());
396 wrapper.set_consumer_offset(topic_offset.get_min_offset());
397 (queue, wrapper)
398 })
399 .collect()
400 } else {
401 consumed
402 };
403 let mut outcomes = Vec::with_capacity(queues.len());
404 for (queue, wrapper) in queues {
405 let queue_id = queue.queue_id();
406 match self
407 .mutation_reset_queue_offset(
408 broker_addr.clone(),
409 consumer_group.clone(),
410 queue,
411 &wrapper,
412 timestamp,
413 force,
414 )
415 .await
416 {
417 Ok(stats) => match u64::try_from(stats.rollback_offset) {
418 Ok(offset) => outcomes.push(applied_offset(&broker_name, queue_id, offset)),
419 Err(_) => outcomes.push(invalid_offset(&broker_name, queue_id)),
420 },
421 Err(error) => outcomes.push(offset_failure(&broker_name, Some(queue_id), &error)),
422 }
423 }
424 outcomes
425 }
426
427 async fn mutation_offset_detailed(
428 &self,
429 cluster_name: CheetahString,
430 topic: CheetahString,
431 consumer_group: CheetahString,
432 timestamp: i64,
433 force: bool,
434 ) -> crate::ClientResult<TopicOffsetMutationOutcome> {
435 let topic_route = MQAdminMutationExt::mutation_topic_route(self, topic.clone()).await?;
436 let timeout = self.remoting_timeout_millis()?;
437 let api = self.mq_client_api()?;
438 let mut targets = Vec::new();
439 if let Some(route_data) = topic_route {
440 let queue_data = route_data
441 .queue_datas
442 .iter()
443 .map(|queue| (queue.broker_name().to_string(), queue))
444 .collect::<HashMap<_, _>>();
445 for broker in &route_data.broker_datas {
446 if broker.cluster() != cluster_name {
447 continue;
448 }
449 let broker_name = broker.broker_name().to_string();
450 let Some(master_addr) = broker.broker_addrs().get(&mix_all::MASTER_ID) else {
451 targets.push(TopicOffsetMutationTargetOutcome {
452 broker_name,
453 queue_id: None,
454 applied: false,
455 offset: None,
456 failure: Some(TopicOffsetMutationFailureCode::Unavailable),
457 retryable: true,
458 });
459 continue;
460 };
461 let current = api
462 .invoke_broker_to_reset_offset(
463 master_addr,
464 ResetOffsetRequestHeader {
465 topic: topic.clone(),
466 group: consumer_group.clone(),
467 queue_id: -1,
468 offset: Some(-1),
469 timestamp,
470 is_force: force,
471 topic_request_header: None,
472 },
473 timeout,
474 )
475 .await;
476 match current {
477 Ok(offsets) => {
478 for (queue, offset) in offsets {
479 match u64::try_from(offset) {
480 Ok(offset) => {
481 targets.push(applied_offset(queue.broker_name().as_str(), queue.queue_id(), offset))
482 }
483 Err(_) => targets.push(invalid_offset(queue.broker_name().as_str(), queue.queue_id())),
484 }
485 }
486 }
487 Err(error)
488 if error.broker_response_code().map(ResponseCode::from)
489 == Some(ResponseCode::ConsumerNotOnline) =>
490 {
491 if let (Some(addr), Some(queue)) = (broker.select_broker_addr(), queue_data.get(&broker_name)) {
492 targets.extend(
493 self.mutation_reset_offset_on_broker_detailed(
494 addr,
495 queue,
496 consumer_group.clone(),
497 topic.clone(),
498 timestamp,
499 force,
500 )
501 .await,
502 );
503 } else {
504 targets.push(TopicOffsetMutationTargetOutcome {
505 broker_name,
506 queue_id: None,
507 applied: false,
508 offset: None,
509 failure: Some(TopicOffsetMutationFailureCode::Unavailable),
510 retryable: true,
511 });
512 }
513 }
514 Err(error) => targets.push(offset_failure(&broker_name, None, &error)),
515 }
516 }
517 }
518 Ok(TopicOffsetMutationOutcome { targets })
519 }
520}
521
522impl MQAdminMutationExt for DefaultMQAdminExtImpl {
523 async fn begin_proxy_drain(
524 &self,
525 proxy_addr: CheetahString,
526 operation_id: CheetahString,
527 ) -> crate::ClientResult<ProxyDrainStateResponseBody> {
528 self.mq_client_api()?
529 .begin_proxy_drain(&proxy_addr, operation_id, self.remoting_timeout_millis()?)
530 .await
531 }
532
533 async fn cancel_proxy_drain(
534 &self,
535 proxy_addr: CheetahString,
536 operation_id: CheetahString,
537 ) -> crate::ClientResult<ProxyDrainStateResponseBody> {
538 self.mq_client_api()?
539 .cancel_proxy_drain(&proxy_addr, operation_id, self.remoting_timeout_millis()?)
540 .await
541 }
542
543 async fn broker_config_generation(&self, broker_addr: CheetahString) -> crate::ClientResult<u64> {
544 let runtime = self
545 .mq_client_api()?
546 .get_broker_runtime_info(&broker_addr, self.remoting_timeout_millis()?)
547 .await?;
548 let generation = runtime.table.get("brokerConfigGeneration").ok_or_else(|| {
549 crate::ClientError::response_process_failed(
550 "broker_config_generation",
551 "broker runtime info does not contain brokerConfigGeneration",
552 )
553 })?;
554 generation
555 .parse::<u64>()
556 .ok()
557 .filter(|generation| *generation > 0)
558 .ok_or_else(|| {
559 crate::ClientError::response_process_failed(
560 "broker_config_generation",
561 "brokerConfigGeneration is not a positive unsigned integer",
562 )
563 })
564 }
565
566 async fn patch_broker_config_if_generation(
567 &self,
568 broker_addr: CheetahString,
569 expected_generation: u64,
570 properties: HashMap<CheetahString, CheetahString>,
571 ) -> crate::ClientResult<BrokerConfigPatchOutcome> {
572 self.mq_client_api()?
573 .update_broker_config_if_generation(
574 &broker_addr,
575 expected_generation,
576 properties,
577 self.remoting_timeout_millis()?,
578 )
579 .await
580 }
581
582 async fn patch_topic_config_if_version(
583 &self,
584 broker_addr: CheetahString,
585 topic: CheetahString,
586 expected_version: u64,
587 patch: TopicConfigPatch,
588 ) -> crate::ClientResult<TopicConfigPatchOutcome> {
589 self.mq_client_api()?
590 .update_topic_config_if_version(
591 &broker_addr,
592 topic,
593 expected_version,
594 patch,
595 self.remoting_timeout_millis()?,
596 )
597 .await
598 }
599
600 async fn mutation_topic_config_with_version(
601 &self,
602 broker_addr: CheetahString,
603 topic: CheetahString,
604 ) -> crate::ClientResult<crate::admin::MutationTopicConfigVersioned> {
605 self.mq_client_api()?
606 .get_topic_config_with_version_for_mutation(&broker_addr, topic, self.remoting_timeout_millis()?)
607 .await
608 }
609
610 async fn mutation_topic_config_state(
611 &self,
612 broker_addr: CheetahString,
613 topic: CheetahString,
614 ) -> crate::ClientResult<MutationTopicConfigState> {
615 self.mq_client_api()?
616 .get_topic_config_state_for_mutation(&broker_addr, topic, self.remoting_timeout_millis()?)
617 .await
618 }
619
620 async fn replace_topic_config_if_state(
621 &self,
622 broker_addr: CheetahString,
623 topic: CheetahString,
624 expected_state: MutationExpectedState,
625 replacement: MutationTopicConfig,
626 ) -> crate::ClientResult<MutationStateCasOutcome> {
627 self.mq_client_api()?
628 .replace_topic_config_if_state(
629 &broker_addr,
630 topic,
631 expected_state,
632 replacement,
633 self.remoting_timeout_millis()?,
634 )
635 .await
636 }
637
638 async fn patch_subscription_group_config_if_version(
639 &self,
640 broker_addr: CheetahString,
641 group: CheetahString,
642 expected_version: u64,
643 patch: SubscriptionGroupConfigPatch,
644 ) -> crate::ClientResult<SubscriptionGroupConfigPatchOutcome> {
645 self.mq_client_api()?
646 .update_subscription_group_config_if_version(
647 &broker_addr,
648 group,
649 expected_version,
650 patch,
651 self.remoting_timeout_millis()?,
652 )
653 .await
654 }
655
656 async fn mutation_subscription_group_config_state(
657 &self,
658 broker_addr: CheetahString,
659 group: CheetahString,
660 ) -> crate::ClientResult<MutationSubscriptionGroupConfigState> {
661 self.mq_client_api()?
662 .get_subscription_group_config_state_for_mutation(&broker_addr, group, self.remoting_timeout_millis()?)
663 .await
664 }
665
666 async fn replace_subscription_group_config_if_state(
667 &self,
668 broker_addr: CheetahString,
669 group: CheetahString,
670 expected_state: MutationExpectedState,
671 replacement: MutationSubscriptionGroupConfig,
672 ) -> crate::ClientResult<MutationStateCasOutcome> {
673 self.mq_client_api()?
674 .replace_subscription_group_config_if_state(
675 &broker_addr,
676 group,
677 expected_state,
678 replacement,
679 self.remoting_timeout_millis()?,
680 )
681 .await
682 }
683
684 async fn broker_mutation_config_state(
685 &self,
686 broker_addr: CheetahString,
687 ) -> crate::ClientResult<BrokerMutationConfigState> {
688 self.mq_client_api()?
689 .get_broker_mutation_config_state(&broker_addr, self.remoting_timeout_millis()?)
690 .await
691 }
692
693 async fn reset_consumer_offset_if_current(
694 &self,
695 broker_addr: CheetahString,
696 consumer_group: CheetahString,
697 topic: CheetahString,
698 queue_id: i32,
699 expected_offset: i64,
700 new_offset: i64,
701 ) -> crate::ClientResult<ConditionalConsumerOffsetOutcome> {
702 self.mq_client_api()?
703 .reset_consumer_offset_if_current(
704 &broker_addr,
705 consumer_group,
706 topic,
707 queue_id,
708 expected_offset,
709 new_offset,
710 self.remoting_timeout_millis()?,
711 )
712 .await
713 }
714
715 async fn preview_consumer_offset_reset_on_broker(
716 &self,
717 broker_addr: CheetahString,
718 broker_name: CheetahString,
719 read_queue_nums: u32,
720 consumer_group: CheetahString,
721 topic: CheetahString,
722 timestamp: i64,
723 ) -> crate::ClientResult<Vec<MutationConsumerOffsetPreview>> {
724 if read_queue_nums as usize > MAX_SUPERVISED_OFFSET_TARGETS {
725 return Err(ClientError::illegal_argument(
726 "supervised offset preview exceeds 1000 queue targets",
727 ));
728 }
729 let timeout = self.remoting_timeout_millis()?;
730 let api = self.mq_client_api()?;
731 let consume_stats = api
732 .get_consume_stats_for_supervised_mutation(
733 &broker_addr,
734 &broker_name,
735 read_queue_nums,
736 GetConsumeStatsRequestHeader {
737 consumer_group: consumer_group.clone(),
738 topic: topic.clone(),
739 topic_list: None,
740 topic_request_header: None,
741 },
742 timeout,
743 )
744 .await?;
745 if consume_stats.offset_table.values().any(|wrapper| {
746 wrapper.get_consumer_offset() < -1 || wrapper.get_broker_offset() < 0 || wrapper.get_pull_offset() < -1
747 }) || consume_stats.offset_table.keys().any(|queue| {
748 queue.topic() != &topic
749 || queue.broker_name() != &broker_name
750 || queue.queue_id() < 0
751 || queue.queue_id() >= read_queue_nums as i32
752 }) {
753 return Err(ClientError::response_process_failed(
754 "preview_consumer_offset_reset_on_broker",
755 "Broker returned consume-stats rows outside the exact Topic/Broker queue set",
756 ));
757 }
758 let mut consumed = consume_stats
759 .offset_table
760 .into_iter()
761 .map(|(queue, wrapper)| (queue, wrapper.get_consumer_offset()))
762 .collect::<Vec<_>>();
763 consumed.sort_by_key(|(queue, _)| queue.queue_id());
764 if !consumed.is_empty()
765 && (consumed.len() != read_queue_nums as usize
766 || consumed
767 .iter()
768 .enumerate()
769 .any(|(queue_id, (queue, _))| queue.queue_id() != queue_id as i32))
770 {
771 return Err(ClientError::response_process_failed(
772 "preview_consumer_offset_reset_on_broker",
773 "Broker returned an incomplete consume-stats queue set",
774 ));
775 }
776 let queues = if consumed.is_empty() {
777 let stats = api
778 .get_topic_stats_info(
779 &broker_addr,
780 GetTopicStatsInfoRequestHeader {
781 topic: topic.clone(),
782 topic_request_header: None,
783 },
784 timeout,
785 )
786 .await?;
787 (0..read_queue_nums)
788 .map(|queue_id| {
789 let queue = MessageQueue::from_parts(topic.clone(), broker_name.clone(), queue_id as i32);
790 let current = stats
791 .get_offset_table()
792 .get(&queue)
793 .map(TopicOffset::get_min_offset)
794 .unwrap_or(0);
795 (queue, current)
796 })
797 .collect()
798 } else {
799 consumed
800 };
801 let mut preview = Vec::with_capacity(queues.len());
802 for (queue, current_offset) in queues {
803 let planned_offset = if timestamp == -1 {
804 api.get_max_offset(broker_addr.as_str(), &queue, timeout).await?
805 } else {
806 api.search_offset_by_timestamp(
807 broker_addr.as_str(),
808 &queue,
809 timestamp,
810 rocketmq_model::common::boundary_type::BoundaryType::Lower,
811 timeout,
812 )
813 .await?
814 };
815 if current_offset < -1 || planned_offset < 0 {
816 return Err(ClientError::response_process_failed(
817 "preview_consumer_offset_reset_on_broker",
818 "Broker returned an invalid consumer offset",
819 ));
820 }
821 preview.push(MutationConsumerOffsetPreview {
822 broker_name: broker_name.to_string(),
823 queue_id: queue.queue_id(),
824 current_offset,
825 planned_offset,
826 });
827 }
828 preview.sort_by_key(|row| row.queue_id);
829 Ok(preview)
830 }
831
832 async fn mutation_consumer_offset(
833 &self,
834 broker_addr: CheetahString,
835 consumer_group: CheetahString,
836 topic: CheetahString,
837 queue_id: i32,
838 ) -> crate::ClientResult<i64> {
839 if queue_id < 0 {
840 return Err(ClientError::illegal_argument("queueId must be non-negative"));
841 }
842 self.mq_client_api()?
843 .query_consumer_offset(
844 broker_addr.as_str(),
845 QueryConsumerOffsetRequestHeader::new(consumer_group, topic, queue_id),
846 self.remoting_timeout_millis()?,
847 )
848 .await
849 }
850
851 async fn mutation_message_request_mode(
852 &self,
853 broker_addr: CheetahString,
854 topic: CheetahString,
855 consumer_group: CheetahString,
856 ) -> crate::ClientResult<Option<MutationMessageRequestMode>> {
857 self.mq_client_api()?
858 .get_message_request_mode_for_mutation(&broker_addr, topic, consumer_group, self.remoting_timeout_millis()?)
859 .await
860 }
861
862 async fn replace_message_request_mode_if_current(
863 &self,
864 broker_addr: CheetahString,
865 topic: CheetahString,
866 consumer_group: CheetahString,
867 expected: MutationExpectedMessageRequestMode,
868 replacement: MutationMessageRequestMode,
869 ) -> crate::ClientResult<MutationMessageRequestModeOutcome> {
870 self.mq_client_api()?
871 .replace_message_request_mode_if_current(
872 &broker_addr,
873 topic,
874 consumer_group,
875 expected,
876 replacement,
877 self.remoting_timeout_millis()?,
878 )
879 .await
880 }
881
882 async fn replace_message_request_mode_if_current_with_timeout(
883 &self,
884 broker_addr: CheetahString,
885 topic: CheetahString,
886 consumer_group: CheetahString,
887 expected: MutationExpectedMessageRequestMode,
888 replacement: MutationMessageRequestMode,
889 timeout_millis: u64,
890 ) -> crate::ClientResult<MutationMessageRequestModeOutcome> {
891 if timeout_millis == 0 || timeout_millis > 24_000 {
892 return Err(ClientError::illegal_argument(
893 "request-mode timeoutMillis must be between 1 and 24000",
894 ));
895 }
896 self.mq_client_api()?
897 .replace_message_request_mode_if_current(
898 &broker_addr,
899 topic,
900 consumer_group,
901 expected,
902 replacement,
903 timeout_millis,
904 )
905 .await
906 }
907
908 async fn set_broker_log_filter_ttl(
909 &self,
910 broker_addr: CheetahString,
911 logger: CheetahString,
912 level: CheetahString,
913 ttl_seconds: u32,
914 operation_id: CheetahString,
915 ) -> crate::ClientResult<()> {
916 let properties = log_filter::set_properties(&broker_addr, &logger, &level, ttl_seconds, &operation_id)?;
917 self.mq_client_api()?
918 .update_broker_config(&broker_addr, properties, self.remoting_timeout_millis()?)
919 .await
920 }
921
922 async fn restore_broker_log_filter(
923 &self,
924 broker_addr: CheetahString,
925 operation_id: CheetahString,
926 ) -> crate::ClientResult<()> {
927 let properties = log_filter::restore_properties(&broker_addr, &operation_id)?;
928 self.mq_client_api()?
929 .update_broker_config(&broker_addr, properties, self.remoting_timeout_millis()?)
930 .await
931 }
932
933 async fn upsert_topic_config(&self, broker_addr: CheetahString, config: TopicConfig) -> crate::ClientResult<()> {
934 let topic = config
935 .topic_name
936 .clone()
937 .ok_or_else(|| crate::ClientError::illegal_argument("Topic name is required"))?;
938 let request_header = CreateTopicRequestHeader {
939 topic,
940 default_topic: CheetahString::from_static_str(TopicValidator::AUTO_CREATE_TOPIC_KEY_TOPIC),
941 read_queue_nums: config.read_queue_nums as i32,
942 write_queue_nums: config.write_queue_nums as i32,
943 perm: config.perm as i32,
944 topic_filter_type: CheetahString::from_static_str(config.topic_filter_type.as_str()),
945 topic_sys_flag: Some(config.topic_sys_flag as i32),
946 order: config.order,
947 attributes: encode_topic_attributes(&config.attributes),
948 force: Some(false),
949 topic_request_header: None,
950 };
951
952 self.mq_client_api()?
953 .update_or_create_topic(&broker_addr, request_header, self.remoting_timeout_millis()?)
954 .await
955 }
956
957 async fn remove_topic(&self, topic_name: CheetahString, cluster_name: CheetahString) -> crate::ClientResult<()> {
958 let cluster_info = MQAdminMutationExt::mutation_cluster_info(self).await?;
959 let mut broker_addrs = HashSet::new();
960 if let Some(cluster_addr_table) = cluster_info.cluster_addr_table.as_ref() {
961 if let Some(broker_names) = cluster_addr_table.get(&cluster_name) {
962 if let Some(broker_addr_table) = cluster_info.broker_addr_table.as_ref() {
963 for broker_name in broker_names {
964 if let Some(broker_data) = broker_addr_table.get(broker_name) {
965 broker_addrs.extend(broker_data.broker_addrs().values().cloned());
966 }
967 }
968 }
969 }
970 }
971 MQAdminMutationExt::remove_topic_from_brokers(self, broker_addrs, topic_name.clone()).await?;
972 let namesrv_addrs = MQAdminMutationExt::mutation_name_server_addresses(self)
973 .await?
974 .into_iter()
975 .collect();
976 MQAdminMutationExt::remove_topic_from_name_servers(self, namesrv_addrs, Some(cluster_name), topic_name).await
977 }
978
979 async fn reset_consumer_offset(
980 &self,
981 cluster_name: Option<CheetahString>,
982 topic: CheetahString,
983 consumer_group: CheetahString,
984 timestamp: u64,
985 force: bool,
986 ) -> crate::ClientResult<HashMap<MessageQueue, u64>> {
987 let timestamp = timestamp_to_java_long("resetOffsetByTimestamp", timestamp)?;
988 let topic_route = MQAdminMutationExt::mutation_topic_route(self, topic.clone()).await?;
989 let mut offset_table = HashMap::new();
990 let timeout = self.remoting_timeout_millis()?;
991
992 if let Some(route_data) = topic_route {
993 for broker_data in &route_data.broker_datas {
994 if cluster_name
995 .as_ref()
996 .is_some_and(|expected| broker_data.cluster() != expected)
997 {
998 continue;
999 }
1000 if let Some(master_addr) = broker_data.broker_addrs().get(&mix_all::MASTER_ID) {
1001 let offsets = self
1002 .mq_client_api()?
1003 .invoke_broker_to_reset_offset(
1004 master_addr,
1005 ResetOffsetRequestHeader {
1006 topic: topic.clone(),
1007 group: consumer_group.clone(),
1008 queue_id: -1,
1009 offset: Some(-1),
1010 timestamp,
1011 is_force: force,
1012 topic_request_header: None,
1013 },
1014 timeout,
1015 )
1016 .await?;
1017 for (queue, offset) in offsets {
1018 offset_table.insert(queue, java_long_to_u64("resetOffsetByTimestamp", "offset", offset)?);
1019 }
1020 }
1021 }
1022 }
1023
1024 Ok(offset_table)
1025 }
1026
1027 async fn reset_consumer_offset_detailed(
1028 &self,
1029 cluster_name: CheetahString,
1030 topic: CheetahString,
1031 consumer_group: CheetahString,
1032 timestamp: u64,
1033 force: bool,
1034 ) -> crate::ClientResult<TopicOffsetMutationOutcome> {
1035 let timestamp = timestamp_to_java_long("resetOffsetByTimestampDetailed", timestamp)?;
1036 self.mutation_offset_detailed(cluster_name, topic, consumer_group, timestamp, force)
1037 .await
1038 }
1039
1040 async fn skip_accumulated_message(
1041 &self,
1042 cluster_name: Option<CheetahString>,
1043 topic: CheetahString,
1044 consumer_group: CheetahString,
1045 force: bool,
1046 ) -> crate::ClientResult<usize> {
1047 let topic_route = MQAdminMutationExt::mutation_topic_route(self, topic.clone()).await?;
1048 let mut offset_table = HashMap::new();
1049 let timeout = self.remoting_timeout_millis()?;
1050 let current = async {
1051 if let Some(route_data) = &topic_route {
1052 for broker_data in &route_data.broker_datas {
1053 if cluster_name
1054 .as_ref()
1055 .is_some_and(|expected| broker_data.cluster() != expected)
1056 {
1057 continue;
1058 }
1059 if let Some(master_addr) = broker_data.broker_addrs().get(&mix_all::MASTER_ID) {
1060 let offsets = self
1061 .mq_client_api()?
1062 .invoke_broker_to_reset_offset(
1063 master_addr,
1064 ResetOffsetRequestHeader {
1065 topic: topic.clone(),
1066 group: consumer_group.clone(),
1067 queue_id: -1,
1068 offset: Some(-1),
1069 timestamp: -1,
1070 is_force: force,
1071 topic_request_header: None,
1072 },
1073 timeout,
1074 )
1075 .await?;
1076 for (queue, offset) in offsets {
1077 offset_table.insert(queue, java_long_to_u64("skipAccumulatedMessage", "offset", offset)?);
1078 }
1079 }
1080 }
1081 }
1082 Ok::<_, ClientError>(offset_table.len())
1083 }
1084 .await;
1085 match current {
1086 Ok(count) => Ok(count),
1087 Err(error)
1088 if error.broker_response_code().map(ResponseCode::from) == Some(ResponseCode::ConsumerNotOnline) =>
1089 {
1090 let mut rollback_count = 0usize;
1091 if let Some(route_data) = topic_route {
1092 let mut topic_route_map = HashMap::new();
1093 for queue_data in &route_data.queue_datas {
1094 topic_route_map.insert(queue_data.broker_name().to_string(), queue_data.clone());
1095 }
1096 for broker_data in &route_data.broker_datas {
1097 if cluster_name
1098 .as_ref()
1099 .is_some_and(|expected| broker_data.cluster() != expected)
1100 {
1101 continue;
1102 }
1103 if let Some(addr) = broker_data.select_broker_addr() {
1104 if let Some(queue_data) = topic_route_map.get(broker_data.broker_name().as_str()) {
1105 rollback_count += self
1106 .mutation_reset_offset_on_broker(
1107 addr,
1108 queue_data,
1109 consumer_group.clone(),
1110 topic.clone(),
1111 -1,
1112 force,
1113 )
1114 .await?
1115 .len();
1116 }
1117 }
1118 }
1119 }
1120 Ok(rollback_count)
1121 }
1122 Err(error) => Err(error),
1123 }
1124 }
1125
1126 async fn skip_accumulated_message_detailed(
1127 &self,
1128 cluster_name: CheetahString,
1129 topic: CheetahString,
1130 consumer_group: CheetahString,
1131 force: bool,
1132 ) -> crate::ClientResult<TopicOffsetMutationOutcome> {
1133 self.mutation_offset_detailed(cluster_name, topic, consumer_group, -1, force)
1134 .await
1135 }
1136
1137 async fn upsert_subscription_group(
1138 &self,
1139 broker_addr: CheetahString,
1140 config: SubscriptionGroupConfig,
1141 ) -> crate::ClientResult<()> {
1142 self.mq_client_api()?
1143 .create_subscription_group(&broker_addr, &config, self.remoting_timeout_millis()?)
1144 .await
1145 }
1146
1147 async fn remove_subscription_group(
1148 &self,
1149 broker_addr: CheetahString,
1150 group_name: CheetahString,
1151 remove_offset: Option<bool>,
1152 ) -> crate::ClientResult<()> {
1153 self.mq_client_api()?
1154 .delete_subscription_group(
1155 &broker_addr,
1156 group_name,
1157 remove_offset.unwrap_or(false),
1158 self.remoting_timeout_millis()?,
1159 )
1160 .await
1161 }
1162
1163 async fn remove_subscription_groups(
1164 &self,
1165 broker_addr: CheetahString,
1166 group_names: Vec<CheetahString>,
1167 clean_offset: bool,
1168 ) -> crate::ClientResult<()> {
1169 self.mq_client_api()?
1170 .delete_subscription_group_list(&broker_addr, group_names, clean_offset, self.remoting_timeout_millis()?)
1171 .await
1172 }
1173
1174 async fn configure_message_request_mode(
1175 &self,
1176 broker_addr: CheetahString,
1177 topic: CheetahString,
1178 consumer_group: CheetahString,
1179 mode: MessageRequestMode,
1180 pop_work_group_size: i32,
1181 timeout_millis: u64,
1182 ) -> crate::ClientResult<()> {
1183 self.mq_client_api()?
1184 .set_message_request_mode(
1185 &broker_addr,
1186 &topic,
1187 &consumer_group,
1188 mode,
1189 pop_work_group_size,
1190 timeout_millis,
1191 )
1192 .await
1193 }
1194
1195 async fn consume_directly(
1196 &self,
1197 consumer_group: CheetahString,
1198 client_id: CheetahString,
1199 topic: CheetahString,
1200 message_id: CheetahString,
1201 ) -> crate::ClientResult<ConsumeMessageDirectlyResult> {
1202 let timeout = self.remoting_timeout_millis()?;
1203 let retry_topic = CheetahString::from_string(mix_all::get_retry_topic(consumer_group.as_str()));
1204 let selected_addr = self
1205 .mq_client_api()?
1206 .get_topic_route_info_from_name_server(&retry_topic, timeout)
1207 .await?
1208 .and_then(|route| {
1209 route
1210 .broker_datas
1211 .choose(&mut rand::rng())
1212 .and_then(|broker| broker.select_broker_addr())
1213 });
1214 let mut connection = ConsumerConnection::new();
1215 if let Some(broker_addr) = selected_addr {
1216 connection = self
1217 .mq_client_api()?
1218 .get_consumer_connection_list(broker_addr.as_str(), consumer_group.clone(), timeout)
1219 .await?;
1220 }
1221 if connection.get_connection_set().is_empty() {
1222 return Err(mq_client_err!(
1223 ResponseCode::ConsumerNotOnline,
1224 "Not found the consumer group connection"
1225 ));
1226 }
1227
1228 let (resolved_client_id, client_addr) =
1229 select_consumer_direct_connection(&consumer_group, &connection, Some(&client_id))?;
1230 let message = MQAdminMutationExt::view_message_for_mutation(self, topic.clone(), message_id.clone()).await?;
1231 let request_header = ConsumeMessageDirectlyResultRequestHeader {
1232 consumer_group,
1233 client_id: Some(resolved_client_id),
1234 msg_id: Some(message_id),
1235 broker_name: (!message.broker_name().is_empty()).then(|| message.broker_name.clone()),
1236 topic: Some(topic),
1237 topic_sys_flag: None,
1238 group_sys_flag: None,
1239 topic_request_header: None,
1240 };
1241
1242 self.mq_client_api()?
1243 .consume_message_directly(&client_addr, request_header, &message, timeout)
1244 .await
1245 }
1246
1247 async fn clone_consumer_group_offset(
1248 &self,
1249 source_group: CheetahString,
1250 destination_group: CheetahString,
1251 topic: CheetahString,
1252 offline: bool,
1253 ) -> crate::ClientResult<()> {
1254 let retry_topic = CheetahString::from_string(mix_all::get_retry_topic(source_group.as_str()));
1255 let route = MQAdminMutationExt::mutation_topic_route(self, retry_topic.clone())
1256 .await?
1257 .ok_or_else(|| mq_client_err!(format!("Topic route not found for retry topic: {retry_topic}")))?;
1258 let timeout = self.remoting_timeout_millis()?;
1259 let api = self.mq_client_api()?;
1260 for broker_data in &route.broker_datas {
1261 if let Some(addr) = broker_data.select_broker_addr() {
1262 api.clone_group_offset(
1263 &addr,
1264 source_group.clone(),
1265 destination_group.clone(),
1266 topic.clone(),
1267 offline,
1268 timeout,
1269 )
1270 .await?;
1271 }
1272 }
1273 Ok(())
1274 }
1275
1276 async fn mutation_cluster_info(&self) -> crate::ClientResult<ClusterInfo> {
1277 self.mq_client_api()?
1278 .get_broker_cluster_info(self.remoting_timeout_millis()?)
1279 .await
1280 }
1281
1282 async fn mutation_topic_route(&self, topic: CheetahString) -> crate::ClientResult<Option<TopicRouteData>> {
1283 self.mq_client_api()?
1284 .get_topic_route_info_from_name_server(&topic, self.remoting_timeout_millis()?)
1285 .await
1286 }
1287
1288 async fn mutation_topic_config(
1289 &self,
1290 broker_addr: CheetahString,
1291 topic: CheetahString,
1292 ) -> crate::ClientResult<TopicConfig> {
1293 self.client_instance
1294 .as_ref()
1295 .ok_or(crate::ClientError::not_started())?
1296 .get_topic_config(&broker_addr, topic, self.remoting_timeout_millis()?)
1297 .await
1298 }
1299
1300 async fn remove_topic_from_brokers(
1301 &self,
1302 broker_addrs: HashSet<CheetahString>,
1303 topic: CheetahString,
1304 ) -> crate::ClientResult<()> {
1305 let api = self.mq_client_api()?;
1306 let timeout = self.remoting_timeout_millis()?;
1307 for broker_addr in broker_addrs {
1308 api.delete_topic_in_broker(
1309 &broker_addr,
1310 DeleteTopicRequestHeader {
1311 topic: topic.clone(),
1312 topic_request_header: None,
1313 },
1314 timeout,
1315 )
1316 .await?;
1317 }
1318 Ok(())
1319 }
1320
1321 async fn remove_topics_from_broker(
1322 &self,
1323 broker_addr: CheetahString,
1324 topics: Vec<CheetahString>,
1325 ) -> crate::ClientResult<()> {
1326 self.mq_client_api()?
1327 .delete_topic_in_broker_list(&broker_addr, topics, self.remoting_timeout_millis()?)
1328 .await
1329 }
1330
1331 async fn remove_topic_from_name_servers(
1332 &self,
1333 namesrv_addrs: HashSet<CheetahString>,
1334 cluster_name: Option<CheetahString>,
1335 topic: CheetahString,
1336 ) -> crate::ClientResult<()> {
1337 let request_header = DeleteTopicFromNamesrvRequestHeader::new(topic, cluster_name);
1338 let api = self.mq_client_api()?;
1339 let timeout = self.remoting_timeout_millis()?;
1340 for namesrv_addr in namesrv_addrs {
1341 api.delete_topic_in_nameserver(&namesrv_addr, request_header.clone(), timeout)
1342 .await?;
1343 }
1344 Ok(())
1345 }
1346
1347 async fn mutation_name_server_addresses(&self) -> crate::ClientResult<Vec<CheetahString>> {
1348 Ok(self.mq_client_api()?.get_name_server_address_list().to_vec())
1349 }
1350
1351 async fn upsert_order_topic_config(
1352 &self,
1353 topic: CheetahString,
1354 value: CheetahString,
1355 cluster_wide: bool,
1356 ) -> crate::ClientResult<()> {
1357 let api = self.mq_client_api()?;
1358 let timeout = self.remoting_timeout_millis()?;
1359 let namespace = CheetahString::from_static_str(NAMESPACE_ORDER_TOPIC_CONFIG);
1360 if cluster_wide {
1361 return api.put_kvconfig_value(namespace, topic, value, timeout).await;
1362 }
1363
1364 let existing = api
1365 .get_kvconfig_value(namespace.clone(), topic.clone(), timeout)
1366 .await?
1367 .unwrap_or_default();
1368 api.put_kvconfig_value(
1369 namespace,
1370 topic,
1371 merge_order_conf_entries(existing.as_str(), value.as_str()).into(),
1372 timeout,
1373 )
1374 .await
1375 }
1376
1377 async fn mutation_order_topic_config(&self, topic: CheetahString) -> crate::ClientResult<Option<CheetahString>> {
1378 self.mq_client_api()?
1379 .get_kvconfig_value(
1380 CheetahString::from_static_str(NAMESPACE_ORDER_TOPIC_CONFIG),
1381 topic,
1382 self.remoting_timeout_millis()?,
1383 )
1384 .await
1385 }
1386
1387 async fn delete_order_topic_config(&self, topic: CheetahString) -> crate::ClientResult<()> {
1388 self.mq_client_api()?
1389 .delete_kvconfig_value(
1390 CheetahString::from_static_str(NAMESPACE_ORDER_TOPIC_CONFIG),
1391 topic,
1392 self.remoting_timeout_millis()?,
1393 )
1394 .await
1395 }
1396
1397 async fn reset_consumer_offset_legacy(
1398 &self,
1399 cluster_name: Option<CheetahString>,
1400 consumer_group: CheetahString,
1401 topic: CheetahString,
1402 timestamp: u64,
1403 force: bool,
1404 ) -> crate::ClientResult<Vec<RollbackStats>> {
1405 let timestamp = timestamp_to_java_long("resetOffsetByTimestampOld", timestamp)?;
1406 let mut route_topic = topic.clone();
1407 if !topic.is_empty()
1408 && (mix_all::is_lmq(Some(topic.as_str()))
1409 || topic.as_str() == format!("{}wheel_timer", TopicValidator::SYSTEM_TOPIC_PREFIX))
1410 && cluster_name.as_ref().is_some_and(|name| !name.is_empty())
1411 {
1412 if let Some(cluster_name) = cluster_name {
1413 route_topic = cluster_name;
1414 }
1415 }
1416 let topic_route_data = MQAdminMutationExt::mutation_topic_route(self, route_topic).await?;
1417 let mut rollback_stats_list = Vec::new();
1418
1419 if let Some(route_data) = topic_route_data {
1420 let mut topic_route_map = HashMap::new();
1421 for queue_data in &route_data.queue_datas {
1422 topic_route_map.insert(queue_data.broker_name().to_string(), queue_data.clone());
1423 }
1424 for broker_data in &route_data.broker_datas {
1425 if let Some(addr) = broker_data.select_broker_addr() {
1426 if let Some(queue_data) = topic_route_map.get(broker_data.broker_name().as_str()) {
1427 let mut rollback_stats = self
1428 .mutation_reset_offset_on_broker(
1429 addr,
1430 queue_data,
1431 consumer_group.clone(),
1432 topic.clone(),
1433 timestamp,
1434 force,
1435 )
1436 .await?;
1437 rollback_stats_list.append(&mut rollback_stats);
1438 }
1439 }
1440 }
1441 }
1442
1443 Ok(rollback_stats_list)
1444 }
1445
1446 async fn view_message_for_mutation(
1447 &self,
1448 topic: CheetahString,
1449 message_id: CheetahString,
1450 ) -> crate::ClientResult<MessageExt> {
1451 MessageDecoder::validate_message_id(message_id.as_str())
1452 .map_err(|error| crate::ClientError::illegal_argument(format!("Invalid message ID: {error}")))?;
1453 let decoded = MessageDecoder::decode_message_id(message_id.as_str())
1454 .map_err(|error| crate::ClientError::illegal_argument(format!("Failed to decode message ID: {error}")))?;
1455 let broker_addr = CheetahString::from_string(format!("{}:{}", decoded.address.ip(), decoded.address.port()));
1456 self.mq_client_api()?
1457 .view_message(
1458 &broker_addr,
1459 ViewMessageRequestHeader {
1460 topic: Some(topic),
1461 offset: decoded.offset,
1462 },
1463 self.remoting_timeout_millis()?,
1464 )
1465 .await
1466 }
1467}
1468
1469#[cfg(test)]
1470mod detailed_offset_tests {
1471 use std::collections::BTreeMap;
1472 use std::collections::VecDeque;
1473 use std::future::Future;
1474 use std::pin::Pin;
1475 use std::sync::Arc;
1476 use std::sync::Mutex;
1477 use std::time::Duration;
1478
1479 use rocketmq_protocol::code::request_code::RequestCode;
1480 use rocketmq_protocol::protocol::admin::consume_stats::ConsumeStats;
1481 use rocketmq_protocol::protocol::admin::offset_wrapper::OffsetWrapper;
1482 use rocketmq_protocol::protocol::body::supervised_mutation::MessageRequestModeMutationResultBody;
1483 use rocketmq_protocol::protocol::body::supervised_mutation::MutationPersistenceState;
1484 use rocketmq_protocol::protocol::body::supervised_mutation::SupervisedMessageRequestMode;
1485 use rocketmq_protocol::protocol::header::get_max_offset_response_header::GetMaxOffsetResponseHeader;
1486 use rocketmq_protocol::protocol::header::namesrv::kv_config_header::GetKVConfigRequestHeader;
1487 use rocketmq_protocol::protocol::header::namesrv::kv_config_header::GetKVConfigResponseHeader;
1488 use rocketmq_protocol::protocol::header::query_consumer_offset_response_header::QueryConsumerOffsetResponseHeader;
1489 use rocketmq_protocol::protocol::header::search_offset_response_header::SearchOffsetResponseHeader;
1490 use rocketmq_protocol::protocol::RemotingSerializable;
1491 use rocketmq_protocol::RemotingCommand;
1492 use rocketmq_runtime::RuntimeContext;
1493 use rocketmq_runtime::ShutdownDeadline;
1494 use rocketmq_transport::api::AdmissionController;
1495 use rocketmq_transport::api::AdmissionLimits;
1496 use rocketmq_transport::test_support::SessionProcessor;
1497 use rocketmq_transport::test_support::SessionTransportServer;
1498 use rocketmq_transport::test_support::SessionTransportServerConfig;
1499
1500 use crate::admin::default_mq_admin_ext::DefaultMQAdminExt;
1501 use crate::base::client_config::ClientConfig;
1502 use crate::factory::mq_client_instance::MQClientInstance;
1503 use crate::runtime::test_client_runtime;
1504
1505 use super::*;
1506
1507 #[derive(Default)]
1508 struct ScriptedRequestLedger {
1509 responses: Mutex<HashMap<i32, VecDeque<RemotingCommand>>>,
1510 delays: Mutex<HashMap<i32, VecDeque<Duration>>>,
1511 counts: Mutex<HashMap<i32, usize>>,
1512 kv_reads: Mutex<Vec<(String, String)>>,
1513 }
1514
1515 impl ScriptedRequestLedger {
1516 fn push(&self, code: RequestCode, response: RemotingCommand) {
1517 self.responses
1518 .lock()
1519 .expect("scripted responses")
1520 .entry(code.to_i32())
1521 .or_default()
1522 .push_back(response);
1523 }
1524
1525 fn push_delayed(&self, code: RequestCode, delay: Duration, response: RemotingCommand) {
1526 self.push(code, response);
1527 self.delays
1528 .lock()
1529 .expect("scripted delays")
1530 .entry(code.to_i32())
1531 .or_default()
1532 .push_back(delay);
1533 }
1534
1535 fn snapshot(&self) -> HashMap<i32, usize> {
1536 self.counts.lock().expect("request counts").clone()
1537 }
1538
1539 fn kv_reads(&self) -> Vec<(String, String)> {
1540 self.kv_reads.lock().expect("KV reads").clone()
1541 }
1542 }
1543
1544 impl SessionProcessor for ScriptedRequestLedger {
1545 fn process(
1546 &self,
1547 request: RemotingCommand,
1548 ) -> Pin<Box<dyn Future<Output = Result<RemotingCommand, rocketmq_error::SharedError>> + Send + '_>> {
1549 Box::pin(async move {
1550 if request.code() == RequestCode::GetKvConfig.to_i32() {
1551 let header = request.decode_command_custom_header::<GetKVConfigRequestHeader>()?;
1552 self.kv_reads
1553 .lock()
1554 .expect("KV reads")
1555 .push((header.namespace.to_string(), header.key.to_string()));
1556 }
1557 *self
1558 .counts
1559 .lock()
1560 .expect("request counts")
1561 .entry(request.code())
1562 .or_default() += 1;
1563 let delay = self
1564 .delays
1565 .lock()
1566 .expect("scripted delays")
1567 .get_mut(&request.code())
1568 .and_then(VecDeque::pop_front);
1569 if let Some(delay) = delay {
1570 tokio::time::sleep(delay).await;
1571 }
1572 let response = self
1573 .responses
1574 .lock()
1575 .expect("scripted responses")
1576 .get_mut(&request.code())
1577 .and_then(VecDeque::pop_front)
1578 .ok_or_else(|| {
1579 ClientError::illegal_argument(format!("unexpected request code {}", request.code()))
1580 })?;
1581 Ok(response.set_opaque(request.opaque()))
1582 })
1583 }
1584 }
1585
1586 struct ProductionAdminHarness {
1587 admin: DefaultMQAdminExtImpl,
1588 facade: DefaultMQAdminExt,
1589 broker_addr: CheetahString,
1590 client_instance: Arc<MQClientInstance>,
1591 client_runtime: Arc<crate::runtime::ClientRuntime>,
1592 ledger: Arc<ScriptedRequestLedger>,
1593 server: Arc<SessionTransportServer>,
1594 server_runtime: RuntimeContext,
1595 }
1596
1597 impl ProductionAdminHarness {
1598 async fn new(scope: &'static str) -> Self {
1599 let server_runtime = RuntimeContext::from_current(scope);
1600 let admission = Arc::new(AdmissionController::new(AdmissionLimits::default()));
1601 let ledger = Arc::new(ScriptedRequestLedger::default());
1602 let server = SessionTransportServer::bind(
1603 server_runtime.service_context("scripted-broker"),
1604 SessionTransportServerConfig::loopback(),
1605 Arc::clone(&ledger) as Arc<dyn SessionProcessor>,
1606 admission,
1607 )
1608 .await
1609 .expect("bind scripted broker");
1610 let broker_addr = CheetahString::from_string(server.local_addr().to_string());
1611 server.start().expect("start scripted broker");
1612
1613 let client_runtime = test_client_runtime(scope);
1614 let mut client_config = ClientConfig::default();
1615 client_config.set_vip_channel_enabled(false);
1616 let client_instance = MQClientInstance::new_arc(
1617 client_config.clone(),
1618 0,
1619 CheetahString::from_string(format!("{scope}-client")),
1620 None,
1621 client_runtime.component("instance"),
1622 client_runtime.telemetry_handle().clone(),
1623 client_runtime.pool().request_future_holder(),
1624 );
1625 client_instance
1626 .get_mq_client_api_impl()
1627 .expect("client API")
1628 .start()
1629 .await
1630 .expect("start client API transport");
1631
1632 let mut admin = DefaultMQAdminExtImpl::new(
1633 Arc::clone(&client_runtime),
1634 None,
1635 Duration::from_secs(5),
1636 client_config,
1637 CheetahString::from_string(format!("{scope}-admin")),
1638 );
1639 admin.client_instance = Some(Arc::clone(&client_instance));
1640 let mut facade = DefaultMQAdminExt::with_admin_ext_group_and_timeout(
1641 Arc::clone(&client_runtime),
1642 format!("{scope}-facade"),
1643 Duration::from_secs(5),
1644 );
1645 facade.client_config_mut().set_vip_channel_enabled(false);
1646 facade.inner_mut().client_instance = Some(Arc::clone(&client_instance));
1647
1648 Self {
1649 admin,
1650 facade,
1651 broker_addr,
1652 client_instance,
1653 client_runtime,
1654 ledger,
1655 server,
1656 server_runtime,
1657 }
1658 }
1659
1660 async fn shutdown(self) {
1661 let Self {
1662 admin,
1663 facade,
1664 client_instance,
1665 client_runtime,
1666 server,
1667 server_runtime,
1668 ..
1669 } = self;
1670 drop((admin, facade));
1671 client_instance.shutdown().await;
1672 client_runtime
1673 .shutdown()
1674 .await
1675 .assert_no_task_leak()
1676 .expect("client runtime tasks drained");
1677 server
1678 .shutdown_until(ShutdownDeadline::after(Duration::from_secs(5)))
1679 .await
1680 .assert_no_task_leak()
1681 .expect("scripted broker tasks drained");
1682 server_runtime
1683 .shutdown_tasks(Duration::from_secs(5))
1684 .await
1685 .assert_no_task_leak()
1686 .expect("scripted broker runtime tasks drained");
1687 }
1688 }
1689
1690 fn request_delta(before: &HashMap<i32, usize>, after: &HashMap<i32, usize>, code: RequestCode) -> usize {
1691 after.get(&code.to_i32()).copied().unwrap_or(0) - before.get(&code.to_i32()).copied().unwrap_or(0)
1692 }
1693
1694 fn assert_request_delta(
1695 before: &HashMap<i32, usize>,
1696 after: &HashMap<i32, usize>,
1697 expected: &[(RequestCode, usize)],
1698 ) {
1699 let expected = expected
1700 .iter()
1701 .map(|(code, count)| (code.to_i32(), *count))
1702 .collect::<BTreeMap<_, _>>();
1703 let actual = after
1704 .iter()
1705 .filter_map(|(code, count)| {
1706 let delta = count - before.get(code).copied().unwrap_or(0);
1707 (delta != 0).then_some((*code, delta))
1708 })
1709 .collect::<BTreeMap<_, _>>();
1710 assert_eq!(actual, expected);
1711 }
1712
1713 fn consume_stats_response(rows: Vec<MessageQueue>) -> RemotingCommand {
1714 let mut stats = ConsumeStats::new();
1715 for row in rows {
1716 stats.offset_table.insert(row, OffsetWrapper::new());
1717 }
1718 RemotingCommand::create_success_response_command()
1719 .set_body(stats.encode_java_compatible().expect("consume stats body"))
1720 }
1721
1722 fn duplicate_consume_stats_response() -> RemotingCommand {
1723 let mut stats = ConsumeStats::new();
1724 stats
1725 .offset_table
1726 .insert(MessageQueue::from_parts("orders", "broker-a", 0), OffsetWrapper::new());
1727 let one = stats.to_java_compatible_json().expect("single row JSON");
1728 let body = one.strip_prefix("{\"offsetTable\":{").expect("offset table prefix");
1729 let (entry, suffix) = body.split_once("},\"consumeTps\":").expect("offset table suffix");
1730 RemotingCommand::create_success_response_command()
1731 .set_body(format!("{{\"offsetTable\":{{{entry},{entry}}},\"consumeTps\":{suffix}"))
1732 }
1733
1734 fn response_with_offset_header(code: ResponseCode, offset: i64) -> RemotingCommand {
1735 let mut response = RemotingCommand::create_response_command_with_code_and_header(
1736 code,
1737 QueryConsumerOffsetResponseHeader { offset: Some(offset) },
1738 );
1739 response.make_custom_header_to_net();
1740 response
1741 }
1742
1743 fn request_mode_success_response() -> RemotingCommand {
1744 RemotingCommand::create_success_response_command().set_body(
1745 MessageRequestModeMutationResultBody {
1746 applied: true,
1747 changed: true,
1748 current: Some(SupervisedMessageRequestMode {
1749 mode: "POP".to_owned(),
1750 pop_share_queue_num: 4,
1751 }),
1752 persistence: MutationPersistenceState::Persisted,
1753 }
1754 .encode()
1755 .expect("request-mode response body"),
1756 )
1757 }
1758
1759 #[tokio::test]
1760 async fn production_request_mode_uses_the_exact_caller_timeout_on_loopback() {
1761 let harness = ProductionAdminHarness::new("request-mode-exact-timeout-test").await;
1762 harness.ledger.push_delayed(
1763 RequestCode::SetMessageRequestModeCas,
1764 Duration::from_millis(50),
1765 request_mode_success_response(),
1766 );
1767 let replacement = MutationMessageRequestMode {
1768 mode: MessageRequestMode::Pop,
1769 pop_share_queue_num: 4,
1770 };
1771 MQAdminMutationExt::replace_message_request_mode_if_current_with_timeout(
1772 &harness.admin,
1773 harness.broker_addr.clone(),
1774 CheetahString::from_static_str("orders"),
1775 CheetahString::from_static_str("orders-consumer"),
1776 MutationExpectedMessageRequestMode::Absent,
1777 replacement,
1778 1,
1779 )
1780 .await
1781 .expect_err("one millisecond caller timeout must reach remoting");
1782 tokio::time::sleep(Duration::from_millis(60)).await;
1783
1784 harness
1785 .ledger
1786 .push(RequestCode::SetMessageRequestModeCas, request_mode_success_response());
1787 let outcome = MQAdminMutationExt::replace_message_request_mode_if_current_with_timeout(
1788 &harness.admin,
1789 harness.broker_addr.clone(),
1790 CheetahString::from_static_str("orders"),
1791 CheetahString::from_static_str("orders-consumer"),
1792 MutationExpectedMessageRequestMode::Absent,
1793 replacement,
1794 24_000,
1795 )
1796 .await
1797 .expect("bounded caller timeout should permit immediate response");
1798 assert!(outcome.applied);
1799 assert!(outcome.changed);
1800 assert_eq!(outcome.current, Some(replacement));
1801 harness.shutdown().await;
1802 }
1803
1804 #[tokio::test]
1805 async fn public_admin_facade_delegates_the_exact_request_mode_timeout_on_loopback() {
1806 let harness = ProductionAdminHarness::new("request-mode-facade-timeout-test").await;
1807 harness.ledger.push_delayed(
1808 RequestCode::SetMessageRequestModeCas,
1809 Duration::from_millis(50),
1810 request_mode_success_response(),
1811 );
1812 let replacement = MutationMessageRequestMode {
1813 mode: MessageRequestMode::Pop,
1814 pop_share_queue_num: 4,
1815 };
1816 MQAdminMutationExt::replace_message_request_mode_if_current_with_timeout(
1817 &harness.facade,
1818 harness.broker_addr.clone(),
1819 CheetahString::from_static_str("orders"),
1820 CheetahString::from_static_str("orders-consumer"),
1821 MutationExpectedMessageRequestMode::Absent,
1822 replacement,
1823 1,
1824 )
1825 .await
1826 .expect_err("the public facade must forward the one millisecond timeout");
1827 tokio::time::sleep(Duration::from_millis(60)).await;
1828
1829 harness
1830 .ledger
1831 .push(RequestCode::SetMessageRequestModeCas, request_mode_success_response());
1832 let outcome = MQAdminMutationExt::replace_message_request_mode_if_current_with_timeout(
1833 &harness.facade,
1834 harness.broker_addr.clone(),
1835 CheetahString::from_static_str("orders"),
1836 CheetahString::from_static_str("orders-consumer"),
1837 MutationExpectedMessageRequestMode::Absent,
1838 replacement,
1839 24_000,
1840 )
1841 .await
1842 .expect("the public facade must forward the bounded caller timeout");
1843 assert!(outcome.applied);
1844 assert!(outcome.changed);
1845 assert_eq!(outcome.current, Some(replacement));
1846 harness.shutdown().await;
1847 }
1848
1849 #[tokio::test]
1850 async fn production_admin_reads_the_exact_order_topic_value_without_merging() {
1851 let harness = ProductionAdminHarness::new("order-topic-exact-read-test").await;
1852 harness
1853 .client_instance
1854 .get_mq_client_api_impl()
1855 .expect("client API")
1856 .update_name_server_address_list_sync(harness.broker_addr.as_str());
1857 let exact = "broker-a:8;broker-z:4";
1858 let mut found = RemotingCommand::create_response_command_with_code_and_header(
1859 ResponseCode::Success,
1860 GetKVConfigResponseHeader::new(Some(exact.into())),
1861 );
1862 found.make_custom_header_to_net();
1863 harness.ledger.push(RequestCode::GetKvConfig, found);
1864 let before = harness.ledger.snapshot();
1865 let value =
1866 MQAdminMutationExt::mutation_order_topic_config(&harness.admin, CheetahString::from_static_str("orders"))
1867 .await
1868 .expect("exact order Topic read");
1869 assert_eq!(value.as_deref(), Some(exact));
1870 let after = harness.ledger.snapshot();
1871 assert_request_delta(&before, &after, &[(RequestCode::GetKvConfig, 1)]);
1872 assert_eq!(
1873 harness.ledger.kv_reads(),
1874 vec![(NAMESPACE_ORDER_TOPIC_CONFIG.to_owned(), "orders".to_owned())]
1875 );
1876
1877 harness.ledger.push(
1878 RequestCode::GetKvConfig,
1879 RemotingCommand::create_response_command_with_code(ResponseCode::QueryNotFound),
1880 );
1881 let missing =
1882 MQAdminMutationExt::mutation_order_topic_config(&harness.admin, CheetahString::from_static_str("missing"))
1883 .await
1884 .expect("missing exact order Topic read");
1885 assert_eq!(missing, None);
1886 assert_eq!(
1887 harness.ledger.kv_reads(),
1888 vec![
1889 (NAMESPACE_ORDER_TOPIC_CONFIG.to_owned(), "orders".to_owned()),
1890 (NAMESPACE_ORDER_TOPIC_CONFIG.to_owned(), "missing".to_owned()),
1891 ]
1892 );
1893 harness.shutdown().await;
1894 }
1895
1896 #[tokio::test]
1897 async fn production_admin_preview_stops_after_real_remoting_rejects_consume_stats() {
1898 let harness = ProductionAdminHarness::new("supervised-preview-wire-test").await;
1899 let cases = [
1900 (
1901 "wrong topic",
1902 consume_stats_response(vec![MessageQueue::from_parts("wrong-topic", "broker-a", 0)]),
1903 1,
1904 ),
1905 (
1906 "wrong broker",
1907 consume_stats_response(vec![MessageQueue::from_parts("orders", "wrong-broker", 0)]),
1908 1,
1909 ),
1910 (
1911 "wrong queue id",
1912 consume_stats_response(vec![MessageQueue::from_parts("orders", "broker-a", 1)]),
1913 1,
1914 ),
1915 ("duplicate queue", duplicate_consume_stats_response(), 1),
1916 (
1917 "queue gap",
1918 consume_stats_response(vec![MessageQueue::from_parts("orders", "broker-a", 0)]),
1919 2,
1920 ),
1921 (
1922 "oversized body",
1923 RemotingCommand::create_success_response_command().set_body(vec![b' '; 1024 * 1024 + 1]),
1924 1,
1925 ),
1926 ];
1927
1928 for (label, response, read_queue_nums) in cases {
1929 harness.ledger.push(RequestCode::GetConsumeStats, response);
1930 let before = harness.ledger.snapshot();
1931 let error = MQAdminMutationExt::preview_consumer_offset_reset_on_broker(
1932 &harness.admin,
1933 harness.broker_addr.clone(),
1934 CheetahString::from_static_str("broker-a"),
1935 read_queue_nums,
1936 CheetahString::from_static_str("orders-consumer"),
1937 CheetahString::from_static_str("orders"),
1938 123,
1939 )
1940 .await
1941 .expect_err(label);
1942 let rendered = error.to_string();
1943 assert!(!rendered.contains("wrong-topic"));
1944 assert!(!rendered.contains("wrong-broker"));
1945 let after = harness.ledger.snapshot();
1946 assert_request_delta(&before, &after, &[(RequestCode::GetConsumeStats, 1)]);
1947 assert_eq!(request_delta(&before, &after, RequestCode::GetTopicStatsInfo), 0);
1948 assert_eq!(request_delta(&before, &after, RequestCode::SearchOffsetByTimestamp), 0);
1949 assert_eq!(request_delta(&before, &after, RequestCode::GetMaxOffset), 0);
1950 assert_eq!(
1951 request_delta(&before, &after, RequestCode::UpdateConsumerOffsetConditional),
1952 0
1953 );
1954 assert_eq!(request_delta(&before, &after, RequestCode::QueryConsumerOffset), 0);
1955 }
1956
1957 harness.ledger.push(
1958 RequestCode::GetConsumeStats,
1959 consume_stats_response(vec![MessageQueue::from_parts("orders", "broker-a", 0)]),
1960 );
1961 let mut search =
1962 RemotingCommand::create_success_response_command_with_header(SearchOffsetResponseHeader { offset: 5 });
1963 search.make_custom_header_to_net();
1964 harness.ledger.push(RequestCode::SearchOffsetByTimestamp, search);
1965 let before = harness.ledger.snapshot();
1966 let preview = MQAdminMutationExt::preview_consumer_offset_reset_on_broker(
1967 &harness.admin,
1968 harness.broker_addr.clone(),
1969 CheetahString::from_static_str("broker-a"),
1970 1,
1971 CheetahString::from_static_str("orders-consumer"),
1972 CheetahString::from_static_str("orders"),
1973 123,
1974 )
1975 .await
1976 .expect("valid search-offset preview");
1977 assert_eq!(preview.len(), 1);
1978 assert_eq!(preview[0].planned_offset, 5);
1979 let after = harness.ledger.snapshot();
1980 assert_request_delta(
1981 &before,
1982 &after,
1983 &[
1984 (RequestCode::GetConsumeStats, 1),
1985 (RequestCode::SearchOffsetByTimestamp, 1),
1986 ],
1987 );
1988
1989 harness.ledger.push(
1990 RequestCode::GetConsumeStats,
1991 consume_stats_response(vec![MessageQueue::from_parts("orders", "broker-a", 0)]),
1992 );
1993 let mut maximum =
1994 RemotingCommand::create_success_response_command_with_header(GetMaxOffsetResponseHeader { offset: 9 });
1995 maximum.make_custom_header_to_net();
1996 harness.ledger.push(RequestCode::GetMaxOffset, maximum);
1997 let before = harness.ledger.snapshot();
1998 let preview = MQAdminMutationExt::preview_consumer_offset_reset_on_broker(
1999 &harness.admin,
2000 harness.broker_addr.clone(),
2001 CheetahString::from_static_str("broker-a"),
2002 1,
2003 CheetahString::from_static_str("orders-consumer"),
2004 CheetahString::from_static_str("orders"),
2005 -1,
2006 )
2007 .await
2008 .expect("valid max-offset preview");
2009 assert_eq!(preview[0].planned_offset, 9);
2010 let after = harness.ledger.snapshot();
2011 assert_request_delta(
2012 &before,
2013 &after,
2014 &[(RequestCode::GetConsumeStats, 1), (RequestCode::GetMaxOffset, 1)],
2015 );
2016
2017 harness.shutdown().await;
2018 }
2019
2020 #[tokio::test]
2021 async fn production_admin_conditional_offset_rejects_invalid_real_remoting_responses_before_verify() {
2022 let harness = ProductionAdminHarness::new("conditional-offset-wire-test").await;
2023 let invalid = [
2024 response_with_offset_header(ResponseCode::SystemError, 3).set_remark("accessKey=secret at 10.0.0.9:10911"),
2025 RemotingCommand::create_success_response_command(),
2026 response_with_offset_header(ResponseCode::Success, 4),
2027 response_with_offset_header(ResponseCode::InvalidParameter, 7),
2028 response_with_offset_header(ResponseCode::Success, -2),
2029 ];
2030
2031 for response in invalid {
2032 harness
2033 .ledger
2034 .push(RequestCode::UpdateConsumerOffsetConditional, response);
2035 let before = harness.ledger.snapshot();
2036 let error = MQAdminMutationExt::reset_consumer_offset_if_current(
2037 &harness.admin,
2038 harness.broker_addr.clone(),
2039 CheetahString::from_static_str("orders-consumer"),
2040 CheetahString::from_static_str("orders"),
2041 0,
2042 7,
2043 3,
2044 )
2045 .await
2046 .expect_err("invalid conditional response");
2047 let rendered = error.to_string();
2048 assert!(!rendered.contains("secret"));
2049 assert!(!rendered.contains("10.0.0.9"));
2050 let after = harness.ledger.snapshot();
2051 assert_request_delta(&before, &after, &[(RequestCode::UpdateConsumerOffsetConditional, 1)]);
2052 assert_eq!(request_delta(&before, &after, RequestCode::QueryConsumerOffset), 0);
2053 }
2054
2055 harness.ledger.push(
2056 RequestCode::UpdateConsumerOffsetConditional,
2057 response_with_offset_header(ResponseCode::Success, 3),
2058 );
2059 let before = harness.ledger.snapshot();
2060 let outcome = MQAdminMutationExt::reset_consumer_offset_if_current(
2061 &harness.admin,
2062 harness.broker_addr.clone(),
2063 CheetahString::from_static_str("orders-consumer"),
2064 CheetahString::from_static_str("orders"),
2065 0,
2066 7,
2067 3,
2068 )
2069 .await
2070 .expect("valid conditional response");
2071 assert!(outcome.applied);
2072 assert_eq!(outcome.actual_offset, 3);
2073 let after = harness.ledger.snapshot();
2074 assert_request_delta(&before, &after, &[(RequestCode::UpdateConsumerOffsetConditional, 1)]);
2075
2076 harness.shutdown().await;
2077 }
2078
2079 #[test]
2080 fn detailed_offset_outcomes_retain_applied_and_failed_queue_targets_in_order() {
2081 let unavailable = ClientError::illegal_argument("safe test failure");
2082 let outcomes = [
2083 applied_offset("broker-a", 0, 41),
2084 offset_failure("broker-a", Some(1), &unavailable),
2085 invalid_offset("broker-b", 2),
2086 ];
2087
2088 assert!(outcomes[0].applied);
2089 assert_eq!(outcomes[0].offset, Some(41));
2090 assert_eq!(outcomes[0].queue_id, Some(0));
2091 assert!(!outcomes[1].applied);
2092 assert_eq!(outcomes[1].queue_id, Some(1));
2093 assert_eq!(outcomes[1].failure, Some(TopicOffsetMutationFailureCode::Unavailable));
2094 assert!(!outcomes[2].applied);
2095 assert_eq!(outcomes[2].queue_id, Some(2));
2096 assert_eq!(outcomes[2].failure, Some(TopicOffsetMutationFailureCode::InvalidData));
2097 assert!(!outcomes[2].retryable);
2098 }
2099}