Skip to main content

rocketmq_client_rust/admin/default_mq_admin_ext_impl/
mutation_api.rs

1// Copyright 2026 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Direct implementation of the explicit mutation capability.
16//!
17//! This module deliberately talks to the concrete client APIs instead of
18//! routing mutation-only builds through the scoped administration traits.
19
20mod 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}