Skip to main content

rocketmq_client_rust/admin/
mq_admin_infrastructure_observation_read_ext.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//! Exact-target infrastructure observations for the `admin-read` capability.
16
17use std::collections::HashMap;
18use std::time::Duration;
19
20use crate::ClientError;
21use cheetah_string::CheetahString;
22use rocketmq_protocol::protocol::body::broker_replicas_info::BrokerReplicasInfo;
23use rocketmq_protocol::protocol::body::ha_runtime_info::HARuntimeInfo;
24use rocketmq_protocol::protocol::header::get_meta_data_response_header::GetMetaDataResponseHeader;
25
26use super::default_mq_admin_ext::DefaultMQAdminExt;
27
28/// Stable failure classification for exact infrastructure observation RPCs.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum InfrastructureObservationReadErrorCode {
31    SourceUnavailable,
32    Timeout,
33    PermissionDenied,
34    NotFound,
35    RateLimited,
36    InvalidResponse,
37}
38
39/// Address-free error returned by infrastructure observation RPCs.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct InfrastructureObservationReadError {
42    code: InfrastructureObservationReadErrorCode,
43    retryable: bool,
44}
45
46impl InfrastructureObservationReadError {
47    pub const fn new(code: InfrastructureObservationReadErrorCode, retryable: bool) -> Self {
48        Self { code, retryable }
49    }
50
51    pub const fn code(self) -> InfrastructureObservationReadErrorCode {
52        self.code
53    }
54
55    pub const fn retryable(self) -> bool {
56        self.retryable
57    }
58}
59
60impl std::fmt::Display for InfrastructureObservationReadError {
61    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        write!(formatter, "infrastructure observation failed: {:?}", self.code)
63    }
64}
65
66impl std::error::Error for InfrastructureObservationReadError {}
67
68/// Additive read-only access to four existing exact-target admin RPCs.
69///
70/// Target endpoints are accepted only from trusted adapters. Neither returned
71/// errors nor their `Debug` representation retain endpoint or remote detail.
72#[allow(async_fn_in_trait)]
73pub trait MQAdminInfrastructureObservationReadExt: Send {
74    async fn broker_ha_runtime_at(
75        &self,
76        broker_endpoint: CheetahString,
77    ) -> Result<HARuntimeInfo, InfrastructureObservationReadError>;
78
79    async fn controller_sync_state_at(
80        &self,
81        controller_endpoint: CheetahString,
82        broker_names: Vec<CheetahString>,
83    ) -> Result<BrokerReplicasInfo, InfrastructureObservationReadError>;
84
85    async fn controller_metadata_at(
86        &self,
87        controller_endpoint: CheetahString,
88    ) -> Result<GetMetaDataResponseHeader, InfrastructureObservationReadError>;
89
90    async fn nameserver_config_at(
91        &self,
92        nameserver_endpoint: CheetahString,
93    ) -> Result<HashMap<CheetahString, CheetahString>, InfrastructureObservationReadError>;
94}
95
96impl MQAdminInfrastructureObservationReadExt for DefaultMQAdminExt {
97    async fn broker_ha_runtime_at(
98        &self,
99        broker_endpoint: CheetahString,
100    ) -> Result<HARuntimeInfo, InfrastructureObservationReadError> {
101        self.inner()
102            .mq_client_api()
103            .map_err(sanitized_error)?
104            .get_broker_ha_status(
105                broker_endpoint,
106                self.inner().remoting_timeout_millis().map_err(sanitized_error)?,
107            )
108            .await
109            .map_err(sanitized_error)
110    }
111
112    async fn controller_sync_state_at(
113        &self,
114        controller_endpoint: CheetahString,
115        broker_names: Vec<CheetahString>,
116    ) -> Result<BrokerReplicasInfo, InfrastructureObservationReadError> {
117        self.inner()
118            .mq_client_api()
119            .map_err(sanitized_error)?
120            .get_in_sync_state_data_at(
121                controller_endpoint,
122                broker_names,
123                self.inner().remoting_timeout_millis().map_err(sanitized_error)?,
124            )
125            .await
126            .map_err(sanitized_error)
127    }
128
129    async fn controller_metadata_at(
130        &self,
131        controller_endpoint: CheetahString,
132    ) -> Result<GetMetaDataResponseHeader, InfrastructureObservationReadError> {
133        self.inner()
134            .mq_client_api()
135            .map_err(sanitized_error)?
136            .get_controller_metadata(
137                controller_endpoint,
138                self.inner().remoting_timeout_millis().map_err(sanitized_error)?,
139            )
140            .await
141            .map_err(sanitized_error)
142    }
143
144    async fn nameserver_config_at(
145        &self,
146        nameserver_endpoint: CheetahString,
147    ) -> Result<HashMap<CheetahString, CheetahString>, InfrastructureObservationReadError> {
148        let mut responses = self
149            .inner()
150            .mq_client_api()
151            .map_err(sanitized_error)?
152            .get_name_server_config(
153                Some(vec![nameserver_endpoint.clone()]),
154                Duration::from_millis(self.inner().remoting_timeout_millis().map_err(sanitized_error)?),
155            )
156            .await
157            .map_err(sanitized_error)?
158            .ok_or_else(invalid_response)?;
159        if responses.len() != 1 {
160            return Err(invalid_response());
161        }
162        responses.remove(&nameserver_endpoint).ok_or_else(invalid_response)
163    }
164}
165
166fn sanitized_error(error: ClientError) -> InfrastructureObservationReadError {
167    let descriptor = error.descriptor();
168    let code = match descriptor.projection().http().status.as_u16() {
169        401 | 403 => InfrastructureObservationReadErrorCode::PermissionDenied,
170        404 => InfrastructureObservationReadErrorCode::NotFound,
171        408 | 504 => InfrastructureObservationReadErrorCode::Timeout,
172        429 => InfrastructureObservationReadErrorCode::RateLimited,
173        400 | 413 | 422 => InfrastructureObservationReadErrorCode::InvalidResponse,
174        _ => InfrastructureObservationReadErrorCode::SourceUnavailable,
175    };
176    let retryable = matches!(
177        descriptor.recovery_hint(),
178        rocketmq_error::RecoveryHint::Backoff
179            | rocketmq_error::RecoveryHint::RefreshRoute
180            | rocketmq_error::RecoveryHint::RefreshLeader
181            | rocketmq_error::RecoveryHint::SwitchBroker
182    );
183    InfrastructureObservationReadError::new(code, retryable)
184}
185
186const fn invalid_response() -> InfrastructureObservationReadError {
187    InfrastructureObservationReadError::new(InfrastructureObservationReadErrorCode::InvalidResponse, false)
188}
189
190#[cfg(test)]
191mod tests {
192    use std::sync::Arc;
193
194    use super::*;
195    use rocketmq_error::Error;
196    use rocketmq_error::TRANSPORT_CONNECTION_FAILED;
197
198    #[test]
199    fn public_error_is_typed_and_never_retains_endpoint_detail() {
200        let error = sanitized_error(ClientError::from_shared(Arc::new(Error::caused_by(
201            &TRANSPORT_CONNECTION_FAILED,
202            std::io::Error::other("private-controller.internal:9878 at 10.23.45.67:10911"),
203        ))));
204        assert_eq!(error.code(), InfrastructureObservationReadErrorCode::SourceUnavailable);
205        let rendered = format!("{error:?} {error}");
206        assert!(!rendered.contains("10.23.45.67"));
207        assert!(!rendered.contains("private-controller"));
208    }
209}