Skip to main content

rustfs_targets/target/
mqtt.rs

1// Copyright 2024 RustFS Team
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
15use crate::plugin::PluginEvent;
16use crate::{
17    StoreError, Target,
18    arn::TargetID,
19    error::TargetError,
20    runtime::tls::{
21        ReloadableTargetTls, TargetTlsGeneration, TargetTlsInputSet, TargetTlsState, TlsReloadAdapter, config::ReloadApplyMode,
22        validate_tls_material,
23    },
24    store::{Key, Store},
25    target::{
26        ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
27        TargetType, build_queued_payload_with_records, mark_target_disconnected_on_connectivity_error, open_target_queue_store,
28        persist_queued_payload_to_store, redacted_secret,
29    },
30};
31use arc_swap::ArcSwap;
32use async_trait::async_trait;
33use hyper_rustls::ConfigBuilderExt;
34use rumqttc::{
35    AsyncClient, Broker, ClientError, ConnectionError, EventLoop, Incoming, MqttOptions, Outgoing, ProtocolViolation,
36    PublishNoticeError, PublishOptions, QoS, Transport, mqttbytes::Error as MqttBytesError,
37};
38use rustfs_config::{
39    EnableState, MQTT_TLS_CA, MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY, MQTT_TLS_TRUST_LEAF_AS_CA, MQTT_WS_PATH_ALLOWLIST,
40};
41use rustfs_tls_runtime::{load_certs, load_private_key};
42use rustls::ClientConfig;
43use std::fmt;
44use std::sync::Arc;
45use std::{
46    marker::PhantomData,
47    path::Path,
48    sync::atomic::{AtomicBool, Ordering},
49    time::Duration,
50};
51use tokio::sync::{Mutex, OnceCell, mpsc};
52use tracing::{debug, error, info, instrument, trace, warn};
53use url::Url;
54
55const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_secs(15);
56const EVENT_LOOP_POLL_TIMEOUT: Duration = Duration::from_secs(10); // For initial connection check in task
57const DEFAULT_MQTT_TCP_PORT: u16 = 1883;
58const DEFAULT_MQTT_TLS_PORT: u16 = 8883;
59const DEFAULT_MQTT_WSS_PORT: u16 = 443;
60const MAX_MQTT_PACKET_SIZE_BYTES: u32 = 100 * 1024 * 1024;
61/// Upper bound on how long a single publish may wait for broker acknowledgement
62/// (PUBACK/PUBCOMP for QoS>=1, or network flush for QoS0) before it is treated as
63/// a timeout so the durable copy is retained and replayed (backlog#971).
64const MQTT_PUBLISH_CONFIRM_TIMEOUT: Duration = Duration::from_secs(30);
65/// Minimum delay before the supervisor rebuilds the client and event loop
66/// after a session exits. Also the delay used right after a session that had
67/// successfully connected, so a transient drop reconnects promptly.
68const MQTT_RECONNECT_BACKOFF_MIN: Duration = Duration::from_secs(1);
69/// Upper bound for the exponential reconnect backoff, so repeated fatal
70/// failures never turn into a tight reconnect storm.
71const MQTT_RECONNECT_BACKOFF_MAX: Duration = Duration::from_secs(30);
72const DEFAULT_MQTT_WS_PATH_ALLOWLIST: &[&str] = &["/", "/mqtt"];
73const LOG_COMPONENT_TARGETS: &str = "targets";
74const LOG_SUBSYSTEM_MQTT: &str = "mqtt";
75const EVENT_MQTT_TARGET_STATE: &str = "mqtt_target_state";
76const EVENT_MQTT_DELIVERY_STATE: &str = "mqtt_delivery_state";
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum MQTTTlsPolicy {
80    SystemCa,
81    CustomCa,
82}
83
84impl MQTTTlsPolicy {
85    fn parse(value: &str) -> Result<Self, TargetError> {
86        match value.trim() {
87            value if value.eq_ignore_ascii_case("system_ca") => Ok(Self::SystemCa),
88            value if value.eq_ignore_ascii_case("custom_ca") => Ok(Self::CustomCa),
89            _ => Err(TargetError::Configuration(
90                "MQTT tls_policy must be one of: system_ca, custom_ca".to_string(),
91            )),
92        }
93    }
94}
95
96#[derive(Clone, Default, PartialEq, Eq)]
97pub struct MQTTTlsConfig {
98    pub policy: Option<MQTTTlsPolicy>,
99    pub ca_path: String,
100    pub client_cert_path: String,
101    pub client_key_path: String,
102    pub trust_leaf_as_ca: bool,
103    pub ws_path_allowlist: Vec<String>,
104}
105
106impl fmt::Debug for MQTTTlsConfig {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        f.debug_struct("MQTTTlsConfig")
109            .field("policy", &self.policy)
110            .field("ca_path", &self.ca_path)
111            .field("client_cert_path", &self.client_cert_path)
112            .field("client_key_path", &redacted_secret(&self.client_key_path))
113            .field("trust_leaf_as_ca", &self.trust_leaf_as_ca)
114            .field("ws_path_allowlist", &self.ws_path_allowlist)
115            .finish()
116    }
117}
118
119impl MQTTTlsConfig {
120    pub fn from_values(
121        policy: Option<&str>,
122        ca_path: Option<&str>,
123        client_cert_path: Option<&str>,
124        client_key_path: Option<&str>,
125        trust_leaf_as_ca: Option<&str>,
126        ws_path_allowlist: Option<&str>,
127    ) -> Result<Self, TargetError> {
128        let policy = match policy.map(str::trim).filter(|value| !value.is_empty()) {
129            Some(value) => Some(MQTTTlsPolicy::parse(value)?),
130            None => None,
131        };
132
133        let trust_leaf_as_ca = match trust_leaf_as_ca.map(str::trim).filter(|value| !value.is_empty()) {
134            Some(value) => value
135                .parse::<EnableState>()
136                .map(EnableState::is_enabled)
137                .map_err(|_| TargetError::Configuration(format!("Invalid value for {MQTT_TLS_TRUST_LEAF_AS_CA}")))?,
138            None => false,
139        };
140
141        let ws_path_allowlist = match ws_path_allowlist.map(str::trim).filter(|value| !value.is_empty()) {
142            Some(value) => parse_ws_path_allowlist(value)?,
143            None => Vec::new(),
144        };
145
146        Ok(Self {
147            policy,
148            ca_path: ca_path.unwrap_or_default().trim().to_string(),
149            client_cert_path: client_cert_path.unwrap_or_default().trim().to_string(),
150            client_key_path: client_key_path.unwrap_or_default().trim().to_string(),
151            trust_leaf_as_ca,
152            ws_path_allowlist,
153        })
154    }
155
156    fn effective_ws_path_allowlist(&self) -> Vec<&str> {
157        if self.ws_path_allowlist.is_empty() {
158            DEFAULT_MQTT_WS_PATH_ALLOWLIST.to_vec()
159        } else {
160            self.ws_path_allowlist.iter().map(String::as_str).collect()
161        }
162    }
163}
164
165fn parse_ws_path_allowlist(value: &str) -> Result<Vec<String>, TargetError> {
166    let mut allowlist = Vec::new();
167    for raw in value.split(',') {
168        let path = raw.trim();
169        if path.is_empty() {
170            continue;
171        }
172        if !path.starts_with('/') || path.contains('?') || path.contains('#') {
173            return Err(TargetError::Configuration(format!(
174                "{MQTT_WS_PATH_ALLOWLIST} entries must be absolute paths without query or fragment"
175            )));
176        }
177        allowlist.push(path.to_string());
178    }
179
180    if allowlist.is_empty() {
181        return Err(TargetError::Configuration(format!(
182            "{MQTT_WS_PATH_ALLOWLIST} must contain at least one websocket path"
183        )));
184    }
185
186    Ok(allowlist)
187}
188
189fn keep_alive_seconds(duration: Duration) -> u16 {
190    duration.as_secs().min(u64::from(u16::MAX)) as u16
191}
192
193fn default_broker_port(scheme: &str) -> u16 {
194    match scheme {
195        "ssl" | "tls" | "tcps" | "mqtts" => DEFAULT_MQTT_TLS_PORT,
196        "wss" => DEFAULT_MQTT_WSS_PORT,
197        _ => DEFAULT_MQTT_TCP_PORT,
198    }
199}
200
201fn websocket_broker_url(broker: &Url, secure: bool) -> Result<String, TargetError> {
202    let mut url = broker.clone();
203    url.set_scheme("ws")
204        .map_err(|_| TargetError::Configuration("Failed to normalize websocket broker URL scheme".to_string()))?;
205
206    if secure && url.port().is_none() {
207        url.set_port(Some(DEFAULT_MQTT_WSS_PORT))
208            .map_err(|_| TargetError::Configuration("Failed to set default secure websocket broker port".to_string()))?;
209    }
210
211    Ok(url.to_string())
212}
213
214fn validate_path_is_absolute(path: &str, field: &str) -> Result<(), TargetError> {
215    if !Path::new(path).is_absolute() {
216        return Err(TargetError::Configuration(format!("{field} must be an absolute path")));
217    }
218    Ok(())
219}
220
221fn build_root_store(ca_path: &str, trust_leaf_as_ca: bool) -> Result<rustls::RootCertStore, TargetError> {
222    let certs = load_certs(ca_path).map_err(|e| TargetError::Configuration(format!("Failed to load MQTT tls_ca: {e}")))?;
223    let mut store = rustls::RootCertStore::empty();
224
225    if trust_leaf_as_ca {
226        let (valid, invalid) = store.add_parsable_certificates(certs);
227        if valid == 0 {
228            return Err(TargetError::Configuration(format!(
229                "MQTT tls_ca did not contain any parsable trust anchors (ignored {invalid} entries)"
230            )));
231        }
232    } else {
233        for cert in certs {
234            store
235                .add(cert)
236                .map_err(|e| TargetError::Configuration(format!("Failed to add MQTT tls_ca to root store: {e}")))?;
237        }
238    }
239
240    Ok(store)
241}
242
243fn build_mqtt_tls_transport(broker: &Url, tls: &MQTTTlsConfig) -> Result<Transport, TargetError> {
244    super::ensure_rustls_provider_installed();
245
246    let client_config = match tls
247        .policy
248        .ok_or_else(|| TargetError::Configuration("Secure MQTT schemes require an explicit tls_policy".to_string()))?
249    {
250        MQTTTlsPolicy::SystemCa => {
251            let builder = ClientConfig::builder()
252                .with_native_roots()
253                .map_err(|e| TargetError::Configuration(format!("Failed to load native root certificates: {e}")))?;
254
255            if tls.client_cert_path.is_empty() {
256                builder.with_no_client_auth()
257            } else {
258                let certs = load_certs(&tls.client_cert_path)
259                    .map_err(|e| TargetError::Configuration(format!("Failed to load MQTT tls_client_cert: {e}")))?;
260                let key = load_private_key(&tls.client_key_path)
261                    .map_err(|e| TargetError::Configuration(format!("Failed to load MQTT tls_client_key: {e}")))?;
262                builder
263                    .with_client_auth_cert(certs, key)
264                    .map_err(|e| TargetError::Configuration(format!("Failed to build MQTT client mTLS identity: {e}")))?
265            }
266        }
267        MQTTTlsPolicy::CustomCa => {
268            let builder = ClientConfig::builder().with_root_certificates(build_root_store(&tls.ca_path, tls.trust_leaf_as_ca)?);
269
270            if tls.client_cert_path.is_empty() {
271                builder.with_no_client_auth()
272            } else {
273                let certs = load_certs(&tls.client_cert_path)
274                    .map_err(|e| TargetError::Configuration(format!("Failed to load MQTT tls_client_cert: {e}")))?;
275                let key = load_private_key(&tls.client_key_path)
276                    .map_err(|e| TargetError::Configuration(format!("Failed to load MQTT tls_client_key: {e}")))?;
277                builder
278                    .with_client_auth_cert(certs, key)
279                    .map_err(|e| TargetError::Configuration(format!("Failed to build MQTT client mTLS identity: {e}")))?
280            }
281        }
282    };
283
284    if matches!(broker.scheme(), "wss") {
285        Ok(Transport::wss_with_config(client_config.into()))
286    } else {
287        Ok(Transport::tls_with_config(client_config.into()))
288    }
289}
290
291pub fn validate_mqtt_broker_url(broker: &Url, tls: &MQTTTlsConfig) -> Result<(), TargetError> {
292    match broker.scheme() {
293        "ws" | "wss" | "tcp" | "ssl" | "tls" | "tcps" | "mqtt" | "mqtts" => {}
294        _ => {
295            return Err(TargetError::Configuration("unknown protocol in broker address".to_string()));
296        }
297    }
298
299    if !broker.username().is_empty() || broker.password().is_some() {
300        return Err(TargetError::Configuration("Broker URL must not embed username or password".to_string()));
301    }
302
303    broker
304        .host_str()
305        .ok_or_else(|| TargetError::Configuration("Broker is missing host".to_string()))?;
306
307    let secure_scheme = matches!(broker.scheme(), "wss" | "ssl" | "tls" | "tcps" | "mqtts");
308    let websocket_scheme = matches!(broker.scheme(), "ws" | "wss");
309
310    if !websocket_scheme {
311        if !matches!(broker.path(), "" | "/") {
312            return Err(TargetError::Configuration(
313                "Broker URL path is only supported for ws/wss schemes".to_string(),
314            ));
315        }
316
317        if broker.query().is_some() {
318            return Err(TargetError::Configuration(
319                "Broker URL query is only supported for ws/wss schemes".to_string(),
320            ));
321        }
322
323        if broker.fragment().is_some() {
324            return Err(TargetError::Configuration(
325                "Broker URL fragment is only supported for ws/wss schemes".to_string(),
326            ));
327        }
328
329        if !tls.ws_path_allowlist.is_empty() {
330            return Err(TargetError::Configuration(format!(
331                "{MQTT_WS_PATH_ALLOWLIST} is only supported for ws/wss schemes"
332            )));
333        }
334    } else if !tls
335        .effective_ws_path_allowlist()
336        .iter()
337        .any(|allowed_path| *allowed_path == broker.path())
338    {
339        return Err(TargetError::Configuration(format!(
340            "Websocket broker path '{}' is not in the {MQTT_WS_PATH_ALLOWLIST} allowlist",
341            broker.path()
342        )));
343    }
344
345    if secure_scheme {
346        let policy = tls
347            .policy
348            .ok_or_else(|| TargetError::Configuration("Secure MQTT schemes require an explicit tls_policy".to_string()))?;
349
350        if !tls.client_cert_path.is_empty() {
351            validate_path_is_absolute(&tls.client_cert_path, MQTT_TLS_CLIENT_CERT)?;
352        }
353
354        if !tls.client_key_path.is_empty() {
355            validate_path_is_absolute(&tls.client_key_path, MQTT_TLS_CLIENT_KEY)?;
356        }
357
358        if tls.client_cert_path.is_empty() != tls.client_key_path.is_empty() {
359            return Err(TargetError::Configuration(
360                "MQTT tls_client_cert and tls_client_key must be specified together".to_string(),
361            ));
362        }
363
364        match policy {
365            MQTTTlsPolicy::SystemCa => {
366                if !tls.ca_path.is_empty() {
367                    return Err(TargetError::Configuration(format!(
368                        "{MQTT_TLS_CA} is not allowed when tls_policy=system_ca"
369                    )));
370                }
371                if tls.trust_leaf_as_ca {
372                    return Err(TargetError::Configuration(format!(
373                        "{MQTT_TLS_TRUST_LEAF_AS_CA} requires tls_policy=custom_ca"
374                    )));
375                }
376            }
377            MQTTTlsPolicy::CustomCa => {
378                if tls.ca_path.is_empty() {
379                    return Err(TargetError::Configuration(format!("{MQTT_TLS_CA} is required when tls_policy=custom_ca")));
380                }
381                validate_path_is_absolute(&tls.ca_path, MQTT_TLS_CA)?;
382            }
383        }
384    } else if tls.policy.is_some()
385        || !tls.ca_path.is_empty()
386        || !tls.client_cert_path.is_empty()
387        || !tls.client_key_path.is_empty()
388        || tls.trust_leaf_as_ca
389    {
390        return Err(TargetError::Configuration(
391            "TLS settings are only allowed for mqtts/ssl/tls/tcps/wss schemes".to_string(),
392        ));
393    }
394
395    Ok(())
396}
397
398pub(crate) fn build_mqtt_options(
399    client_id: String,
400    broker: &Url,
401    username: Option<&str>,
402    password: Option<&str>,
403    tls: &MQTTTlsConfig,
404    keep_alive: Duration,
405    max_packet_size: Option<u32>,
406) -> Result<MqttOptions, TargetError> {
407    validate_mqtt_broker_url(broker, tls)?;
408
409    let host = broker
410        .host_str()
411        .ok_or_else(|| TargetError::Configuration("Broker is missing host".to_string()))?;
412    let port = broker.port().unwrap_or_else(|| default_broker_port(broker.scheme()));
413    let mut mqtt_options = match broker.scheme() {
414        "tcp" | "mqtt" => MqttOptions::new(client_id, (host, port)),
415        "ssl" | "tls" | "tcps" | "mqtts" => {
416            let mut options = MqttOptions::new(client_id, (host, port));
417            options.set_transport(build_mqtt_tls_transport(broker, tls)?);
418            options
419        }
420        "ws" => {
421            let websocket_broker = Broker::websocket(broker.as_str().to_string())
422                .map_err(|e| TargetError::Configuration(format!("Invalid websocket broker URL: {e}")))?;
423            MqttOptions::new(client_id, websocket_broker)
424        }
425        "wss" => {
426            let websocket_broker = Broker::websocket(websocket_broker_url(broker, true)?)
427                .map_err(|e| TargetError::Configuration(format!("Invalid secure websocket broker URL: {e}")))?;
428            let mut options = MqttOptions::new(client_id, websocket_broker);
429            options.set_transport(build_mqtt_tls_transport(broker, tls)?);
430            options
431        }
432        _ => {
433            return Err(TargetError::Configuration("unknown protocol in broker address".to_string()));
434        }
435    };
436
437    mqtt_options.set_keep_alive(keep_alive_seconds(keep_alive));
438
439    if let Some(max_packet_size) = max_packet_size {
440        mqtt_options.set_max_packet_size(Some(max_packet_size));
441    }
442
443    if let Some(user) = username
444        && !user.is_empty()
445    {
446        mqtt_options.set_credentials(user.to_string(), password.unwrap_or("").to_string());
447    }
448
449    Ok(mqtt_options)
450}
451
452/// Arguments for configuring an MQTT target
453#[derive(Clone)]
454pub struct MQTTArgs {
455    /// Whether the target is enabled
456    pub enable: bool,
457    /// The broker URL
458    pub broker: Url,
459    /// The topic to publish to
460    pub topic: String,
461    /// The quality of service level
462    pub qos: QoS,
463    /// The username for the broker
464    pub username: String,
465    /// The password for the broker
466    pub password: String,
467    /// Explicit TLS configuration for secure MQTT transports
468    pub tls: MQTTTlsConfig,
469    /// The maximum interval for reconnection attempts (Note: rumqttc has internal strategy)
470    pub max_reconnect_interval: Duration,
471    /// The keep alive interval
472    pub keep_alive: Duration,
473    /// The directory to store events in case of failure
474    pub queue_dir: String,
475    /// The maximum number of events to store
476    pub queue_limit: u64,
477    /// the target type
478    pub target_type: TargetType,
479}
480
481impl fmt::Debug for MQTTArgs {
482    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
483        f.debug_struct("MQTTArgs")
484            .field("enable", &self.enable)
485            .field("broker", &self.broker)
486            .field("topic", &self.topic)
487            .field("qos", &self.qos)
488            .field("username", &self.username)
489            .field("password", &redacted_secret(&self.password))
490            .field("tls", &self.tls)
491            .field("max_reconnect_interval", &self.max_reconnect_interval)
492            .field("keep_alive", &self.keep_alive)
493            .field("queue_dir", &self.queue_dir)
494            .field("queue_limit", &self.queue_limit)
495            .field("target_type", &self.target_type)
496            .finish()
497    }
498}
499
500impl MQTTArgs {
501    pub fn validate(&self) -> Result<(), TargetError> {
502        if !self.enable {
503            return Ok(());
504        }
505
506        validate_mqtt_broker_url(&self.broker, &self.tls)?;
507
508        if self.topic.is_empty() {
509            return Err(TargetError::Configuration("MQTT topic cannot be empty".to_string()));
510        }
511
512        if !self.queue_dir.is_empty() {
513            let path = Path::new(&self.queue_dir);
514            if !path.is_absolute() {
515                return Err(TargetError::Configuration("mqtt queue_dir path should be absolute".to_string()));
516            }
517
518            if self.qos == QoS::AtMostOnce {
519                return Err(TargetError::Configuration(
520                    "QoS should be AtLeastOnce (1) or ExactlyOnce (2) if queue_dir is set".to_string(),
521                ));
522            }
523        }
524        Ok(())
525    }
526}
527
528struct BgTaskManager {
529    init_cell: OnceCell<tokio::task::JoinHandle<()>>,
530    cancel_tx: mpsc::Sender<()>,
531    initial_cancel_rx: Mutex<Option<mpsc::Receiver<()>>>,
532}
533
534/// A target that sends events to an MQTT broker
535pub struct MQTTTarget<E>
536where
537    E: PluginEvent,
538{
539    id: TargetID,
540    args: MQTTArgs,
541    client: Arc<Mutex<Option<AsyncClient>>>,
542    store: Option<Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>>,
543    connected: Arc<AtomicBool>,
544    bg_task_manager: Arc<BgTaskManager>,
545    /// TLS fingerprint tracking for inline fallback path.
546    tls_state: Arc<parking_lot::Mutex<TargetTlsState>>,
547    /// When set, the coordinator drives TLS reload; inline fingerprint check is skipped.
548    tls_adapter: Option<TlsReloadAdapter<MqttOptions>>,
549    /// Updated MqttOptions from coordinator for use on next reconnection.
550    pending_mqtt_options: Arc<ArcSwap<MqttOptions>>,
551    delivery_counters: Arc<TargetDeliveryCounters>,
552    _phantom: PhantomData<E>,
553}
554
555impl<E> MQTTTarget<E>
556where
557    E: PluginEvent,
558{
559    /// Creates a new MQTTTarget
560    #[instrument(skip(args), fields(target_id_as_string = %id))]
561    pub fn new(id: String, args: MQTTArgs) -> Result<Self, TargetError> {
562        args.validate()?;
563        let target_id = TargetID::new(id, ChannelTargetType::Mqtt.as_str().to_string());
564        let queue_store = open_target_queue_store(
565            &args.queue_dir,
566            args.queue_limit,
567            args.target_type,
568            ChannelTargetType::Mqtt.as_str(),
569            &target_id,
570            "Failed to open store for MQTT target",
571        )?;
572
573        let (cancel_tx, cancel_rx) = mpsc::channel(1);
574        let bg_task_manager = Arc::new(BgTaskManager {
575            init_cell: OnceCell::new(),
576            cancel_tx,
577            initial_cancel_rx: Mutex::new(Some(cancel_rx)),
578        });
579
580        // Build the initial MqttOptions for TLS reload support.
581        let initial_mqtt_options = build_mqtt_options(
582            format!("rustfs_notify_{}", uuid::Uuid::new_v4()),
583            &args.broker,
584            Some(args.username.as_str()),
585            Some(args.password.as_str()),
586            &args.tls,
587            args.keep_alive,
588            Some(MAX_MQTT_PACKET_SIZE_BYTES),
589        )?;
590
591        info!(
592            event = EVENT_MQTT_TARGET_STATE,
593            component = LOG_COMPONENT_TARGETS,
594            subsystem = LOG_SUBSYSTEM_MQTT,
595            target_id = %target_id,
596            state = "created",
597            "mqtt target state"
598        );
599        Ok(MQTTTarget::<E> {
600            id: target_id,
601            args,
602            client: Arc::new(Mutex::new(None)),
603            store: queue_store,
604            connected: Arc::new(AtomicBool::new(false)),
605            bg_task_manager,
606            tls_state: Arc::new(parking_lot::Mutex::new(TargetTlsState::default())),
607            tls_adapter: None,
608            pending_mqtt_options: Arc::new(ArcSwap::from(Arc::new(initial_mqtt_options))),
609            delivery_counters: Arc::new(TargetDeliveryCounters::default()),
610            _phantom: PhantomData,
611        })
612    }
613
614    #[instrument(skip(self), fields(target_id = %self.id))]
615    async fn init(&self) -> Result<(), TargetError> {
616        if self.connected.load(Ordering::SeqCst) {
617            debug!(
618                event = EVENT_MQTT_TARGET_STATE,
619                component = LOG_COMPONENT_TARGETS,
620                subsystem = LOG_SUBSYSTEM_MQTT,
621                target_id = %self.id,
622                state = "already_connected",
623                "mqtt target state"
624            );
625            return Ok(());
626        }
627
628        let bg_task_manager = Arc::clone(&self.bg_task_manager);
629        let client_arc = Arc::clone(&self.client);
630        let connected_arc = Arc::clone(&self.connected);
631        let target_id_clone = self.id.clone();
632        let args_clone = self.args.clone();
633        let pending_mqtt_options = Arc::clone(&self.pending_mqtt_options);
634
635        let _ = bg_task_manager
636            .init_cell
637            .get_or_try_init(|| async {
638                debug!(
639                    event = EVENT_MQTT_TARGET_STATE,
640                    component = LOG_COMPONENT_TARGETS,
641                    subsystem = LOG_SUBSYSTEM_MQTT,
642                    target_id = %target_id_clone,
643                    state = "background_task_initializing",
644                    "mqtt target state"
645                );
646
647                let mut rx_guard = bg_task_manager.initial_cancel_rx.lock().await;
648                let cancel_rx = rx_guard.take().ok_or_else(|| {
649                    error!(
650                        event = EVENT_MQTT_TARGET_STATE,
651                        component = LOG_COMPONENT_TARGETS,
652                        subsystem = LOG_SUBSYSTEM_MQTT,
653                        target_id = %target_id_clone,
654                        state = "cancel_receiver_unavailable",
655                        "mqtt target state"
656                    );
657                    TargetError::Configuration("MQTT cancel receiver already taken for task".to_string())
658                })?;
659                drop(rx_guard);
660
661                info!(
662                    event = EVENT_MQTT_TARGET_STATE,
663                    component = LOG_COMPONENT_TARGETS,
664                    subsystem = LOG_SUBSYSTEM_MQTT,
665                    target_id = %target_id_clone,
666                    state = "supervisor_spawning",
667                    "mqtt target state"
668                );
669                // Spawn a supervisor that owns the reconnect loop. Building the
670                // client/event loop, subscribing, and publishing the client to
671                // `client_arc` all happen per session inside the supervisor, so a
672                // fatal protocol error that ends one session is followed by a
673                // backoff and a fresh session instead of permanent silence.
674                let task_handle = tokio::spawn(supervise_mqtt_event_loop(
675                    pending_mqtt_options,
676                    args_clone,
677                    client_arc,
678                    connected_arc,
679                    target_id_clone,
680                    cancel_rx,
681                ));
682                Ok(task_handle)
683            })
684            .await
685            .map_err(|e: TargetError| {
686                error!(
687                    event = EVENT_MQTT_TARGET_STATE,
688                    component = LOG_COMPONENT_TARGETS,
689                    subsystem = LOG_SUBSYSTEM_MQTT,
690                    target_id = %self.id,
691                    state = "background_task_init_failed",
692                    error = %e,
693                    "mqtt target state"
694                );
695                e
696            })?;
697        debug!(
698            event = EVENT_MQTT_TARGET_STATE,
699            component = LOG_COMPONENT_TARGETS,
700            subsystem = LOG_SUBSYSTEM_MQTT,
701            target_id = %self.id,
702            state = "background_task_initialized",
703            "mqtt target state"
704        );
705
706        match tokio::time::timeout(DEFAULT_CONNECTION_TIMEOUT, async {
707            while !self.connected.load(Ordering::SeqCst) {
708                if let Some(handle) = self.bg_task_manager.init_cell.get()
709                    && handle.is_finished()
710                    && !self.connected.load(Ordering::SeqCst)
711                {
712                    error!(
713                        event = EVENT_MQTT_TARGET_STATE,
714                        component = LOG_COMPONENT_TARGETS,
715                        subsystem = LOG_SUBSYSTEM_MQTT,
716                        target_id = %self.id,
717                        state = "background_task_exited_before_connect",
718                        "mqtt target state"
719                    );
720                    return Err(TargetError::Network("MQTT background task exited prematurely".to_string()));
721                }
722                tokio::time::sleep(Duration::from_millis(100)).await;
723            }
724            debug!(
725                event = EVENT_MQTT_TARGET_STATE,
726                component = LOG_COMPONENT_TARGETS,
727                subsystem = LOG_SUBSYSTEM_MQTT,
728                target_id = %self.id,
729                state = "connected",
730                "mqtt target state"
731            );
732            Ok(())
733        })
734        .await
735        {
736            Ok(Ok(_)) => {
737                info!(
738                    event = EVENT_MQTT_TARGET_STATE,
739                    component = LOG_COMPONENT_TARGETS,
740                    subsystem = LOG_SUBSYSTEM_MQTT,
741                    target_id = %self.id,
742                    state = "ready",
743                    "mqtt target state"
744                );
745                Ok(())
746            }
747            Ok(Err(e)) => Err(e),
748            Err(_) => {
749                error!(
750                    event = EVENT_MQTT_TARGET_STATE,
751                    component = LOG_COMPONENT_TARGETS,
752                    subsystem = LOG_SUBSYSTEM_MQTT,
753                    target_id = %self.id,
754                    state = "connect_timeout",
755                    "mqtt target state"
756                );
757                Err(TargetError::Network("Timeout waiting for MQTT connection".to_string()))
758            }
759        }
760    }
761
762    fn build_queued_payload(&self, event: &EntityTarget<E>) -> Result<QueuedPayload, TargetError> {
763        build_queued_payload_with_records(event, vec![event.clone()])
764    }
765
766    #[instrument(skip(self, body, meta), fields(target_id = %self.id))]
767    async fn send_body(&self, body: Vec<u8>, meta: &QueuedPayloadMeta) -> Result<(), TargetError> {
768        debug!(
769            event = EVENT_MQTT_DELIVERY_STATE,
770            component = LOG_COMPONENT_TARGETS,
771            subsystem = LOG_SUBSYSTEM_MQTT,
772            target_id = %self.id,
773            bucket = %meta.bucket_name,
774            object = %meta.object_name,
775            event = %meta.event_name,
776            payload_len = body.len(),
777            state = "publishing",
778            "mqtt delivery state"
779        );
780
781        // Enqueue a tracked publish so we can wait for broker acknowledgement
782        // (PUBACK for QoS1, PUBCOMP for QoS2, or network flush for QoS0) before
783        // reporting success. Previously the publish was treated as delivered as
784        // soon as it was queued on the event loop, so a disconnect after queueing
785        // silently dropped the event while its durable copy was already deleted
786        // (backlog#971). Error classification now matches on the typed error
787        // instead of substring matching on the display string.
788        let notice = match tokio::time::timeout(MQTT_PUBLISH_CONFIRM_TIMEOUT, async {
789            let client_guard = self.client.lock().await;
790            let client = client_guard
791                .as_ref()
792                .ok_or_else(|| TargetError::Configuration("MQTT client not initialized".to_string()))?;
793            let notice = client
794                .publish_tracked(&self.args.topic, body, PublishOptions::new(self.args.qos))
795                .await
796                .map_err(|error| classify_mqtt_client_error(&error))?;
797            drop(client_guard);
798            Ok(notice)
799        })
800        .await
801        {
802            Ok(Ok(notice)) => notice,
803            Ok(Err(err)) => {
804                warn!(
805                    event = EVENT_MQTT_DELIVERY_STATE,
806                    component = LOG_COMPONENT_TARGETS,
807                    subsystem = LOG_SUBSYSTEM_MQTT,
808                    target_id = %self.id,
809                    state = "publish_failed",
810                    reason = "enqueue_error",
811                    error = %err,
812                    "mqtt delivery state"
813                );
814                mark_target_disconnected_on_connectivity_error(&self.connected, &err);
815                return Err(err);
816            }
817            Err(_) => {
818                warn!(
819                    event = EVENT_MQTT_DELIVERY_STATE,
820                    component = LOG_COMPONENT_TARGETS,
821                    subsystem = LOG_SUBSYSTEM_MQTT,
822                    target_id = %self.id,
823                    state = "publish_failed",
824                    reason = "enqueue_timeout",
825                    "mqtt delivery state"
826                );
827                // Admission can time out because the local bounded request
828                // channel is full while the MQTT session remains connected.
829                // Only protocol/client failures are evidence of disconnect.
830                return Err(TargetError::Timeout("MQTT publish enqueue timed out".to_string()));
831            }
832        };
833
834        match tokio::time::timeout(MQTT_PUBLISH_CONFIRM_TIMEOUT, notice.wait_completion_async()).await {
835            Ok(Ok(())) => {
836                debug!(
837                    event = EVENT_MQTT_DELIVERY_STATE,
838                    component = LOG_COMPONENT_TARGETS,
839                    subsystem = LOG_SUBSYSTEM_MQTT,
840                    target_id = %self.id,
841                    topic = %self.args.topic,
842                    state = "published",
843                    "mqtt delivery state"
844                );
845                self.delivery_counters.record_success();
846                Ok(())
847            }
848            Ok(Err(e)) => {
849                let err = classify_mqtt_notice_error(&e);
850                warn!(
851                    event = EVENT_MQTT_DELIVERY_STATE,
852                    component = LOG_COMPONENT_TARGETS,
853                    subsystem = LOG_SUBSYSTEM_MQTT,
854                    target_id = %self.id,
855                    state = "publish_unconfirmed",
856                    error = %e,
857                    "mqtt delivery state"
858                );
859                mark_target_disconnected_on_connectivity_error(&self.connected, &err);
860                Err(err)
861            }
862            Err(_) => {
863                let err = TargetError::Timeout("Timed out waiting for MQTT publish acknowledgement".to_string());
864                warn!(
865                    event = EVENT_MQTT_DELIVERY_STATE,
866                    component = LOG_COMPONENT_TARGETS,
867                    subsystem = LOG_SUBSYSTEM_MQTT,
868                    target_id = %self.id,
869                    state = "publish_confirm_timeout",
870                    "mqtt delivery state"
871                );
872                mark_target_disconnected_on_connectivity_error(&self.connected, &err);
873                Err(err)
874            }
875        }
876    }
877
878    pub fn clone_target(&self) -> Box<dyn Target<E> + Send + Sync> {
879        Box::new(MQTTTarget::<E> {
880            id: self.id.clone(),
881            args: self.args.clone(),
882            client: self.client.clone(),
883            store: self.store.as_ref().map(|s| s.boxed_clone()),
884            connected: self.connected.clone(),
885            bg_task_manager: self.bg_task_manager.clone(),
886            tls_state: Arc::clone(&self.tls_state),
887            tls_adapter: self.tls_adapter.clone(),
888            pending_mqtt_options: Arc::clone(&self.pending_mqtt_options),
889            delivery_counters: self.delivery_counters.clone(),
890            _phantom: PhantomData,
891        })
892    }
893}
894
895/// Coordinated TLS hot-reload implementation for MQTT targets.
896///
897/// MQTT uses `MqttOptions` as the material type. The coordinator rebuilds
898/// `MqttOptions` on TLS file changes, and `apply_tls_material` stores it in
899/// an `ArcSwap` for use on the next reconnection. The running event loop is
900/// not interrupted; rumqttc handles reconnection internally.
901#[async_trait]
902impl<E> ReloadableTargetTls for MQTTTarget<E>
903where
904    E: PluginEvent,
905{
906    type Material = MqttOptions;
907
908    fn tls_input_set(&self) -> TargetTlsInputSet {
909        TargetTlsInputSet {
910            ca_path: self.args.tls.ca_path.clone(),
911            client_cert_path: self.args.tls.client_cert_path.clone(),
912            client_key_path: self.args.tls.client_key_path.clone(),
913            target_label: format!("mqtt:{}", self.id.id),
914        }
915    }
916
917    async fn build_tls_material(&self) -> Result<Self::Material, TargetError> {
918        build_mqtt_options(
919            format!("rustfs_notify_{}", uuid::Uuid::new_v4()),
920            &self.args.broker,
921            Some(self.args.username.as_str()),
922            Some(self.args.password.as_str()),
923            &self.args.tls,
924            self.args.keep_alive,
925            Some(MAX_MQTT_PACKET_SIZE_BYTES),
926        )
927    }
928
929    async fn apply_tls_material(
930        &self,
931        _generation: TargetTlsGeneration,
932        material: Arc<Self::Material>,
933        _mode: ReloadApplyMode,
934    ) -> Result<(), TargetError> {
935        // Store the new MqttOptions for use on next reconnection.
936        // The running event loop is not interrupted; rumqttc handles reconnection.
937        self.pending_mqtt_options.store(material);
938        Ok(())
939    }
940
941    async fn validate_tls_files(&self) -> Result<(), TargetError> {
942        validate_tls_material(&self.args.tls.ca_path, &self.args.tls.client_cert_path, &self.args.tls.client_key_path)
943    }
944}
945
946/// Computes the next reconnect backoff by doubling the current delay, capped at
947/// [`MQTT_RECONNECT_BACKOFF_MAX`]. Kept as a pure function so the backoff policy
948/// can be unit tested without a live broker.
949fn next_reconnect_backoff(current: Duration) -> Duration {
950    current.saturating_mul(2).min(MQTT_RECONNECT_BACKOFF_MAX)
951}
952
953/// Drives the supervised reconnect loop: run a session, then wait a backoff
954/// before restarting, until a cancellation signal arrives. Cancellation drops
955/// the in-flight session future (the outer `select!`), so `close()` stops the
956/// loop promptly without the session needing its own cancel channel.
957///
958/// `run_session` returns whether its session connected at least once; a
959/// connected session resets the backoff so a transient drop reconnects quickly,
960/// while repeated immediate failures back off exponentially.
961async fn reconnect_supervisor<F, Fut>(mut cancel_rx: mpsc::Receiver<()>, mut run_session: F)
962where
963    F: FnMut() -> Fut,
964    Fut: std::future::Future<Output = bool>,
965{
966    let mut backoff = MQTT_RECONNECT_BACKOFF_MIN;
967    loop {
968        let connected = tokio::select! {
969            biased;
970            _ = cancel_rx.recv() => break,
971            connected = run_session() => connected,
972        };
973
974        if connected {
975            backoff = MQTT_RECONNECT_BACKOFF_MIN;
976        }
977
978        tokio::select! {
979            biased;
980            _ = cancel_rx.recv() => break,
981            _ = tokio::time::sleep(backoff) => {}
982        }
983
984        backoff = next_reconnect_backoff(backoff);
985    }
986}
987
988/// Supervises the MQTT event loop for the lifetime of the target. Each session
989/// rebuilds the client and event loop from the latest `MqttOptions`, so TLS
990/// reloads are picked up on reconnect, and a session that exits (including on a
991/// fatal protocol error) is restarted after a backoff instead of leaving the
992/// target permanently wedged.
993async fn supervise_mqtt_event_loop(
994    pending_mqtt_options: Arc<ArcSwap<MqttOptions>>,
995    args: MQTTArgs,
996    client_arc: Arc<Mutex<Option<AsyncClient>>>,
997    connected_status: Arc<AtomicBool>,
998    target_id: TargetID,
999    cancel_rx: mpsc::Receiver<()>,
1000) {
1001    info!(
1002        event = EVENT_MQTT_TARGET_STATE,
1003        component = LOG_COMPONENT_TARGETS,
1004        subsystem = LOG_SUBSYSTEM_MQTT,
1005        target_id = %target_id,
1006        state = "supervisor_started",
1007        "mqtt target state"
1008    );
1009
1010    reconnect_supervisor(cancel_rx, || {
1011        let pending_mqtt_options = Arc::clone(&pending_mqtt_options);
1012        let args = args.clone();
1013        let client_arc = Arc::clone(&client_arc);
1014        let connected_status = Arc::clone(&connected_status);
1015        let target_id = target_id.clone();
1016        async move { run_one_mqtt_session(pending_mqtt_options, args, client_arc, connected_status, target_id).await }
1017    })
1018    .await;
1019
1020    connected_status.store(false, Ordering::SeqCst);
1021    info!(
1022        event = EVENT_MQTT_TARGET_STATE,
1023        component = LOG_COMPONENT_TARGETS,
1024        subsystem = LOG_SUBSYSTEM_MQTT,
1025        target_id = %target_id,
1026        state = "supervisor_stopped",
1027        "mqtt target state"
1028    );
1029}
1030
1031/// Builds a client and event loop, subscribes, publishes the client for
1032/// `send_body`, then runs the event loop until it exits. Returns whether the
1033/// session established a connection at least once.
1034async fn run_one_mqtt_session(
1035    pending_mqtt_options: Arc<ArcSwap<MqttOptions>>,
1036    args: MQTTArgs,
1037    client_arc: Arc<Mutex<Option<AsyncClient>>>,
1038    connected_status: Arc<AtomicBool>,
1039    target_id: TargetID,
1040) -> bool {
1041    // Use the latest MqttOptions (may have been updated by TLS reload coordinator).
1042    let mqtt_options: MqttOptions = (**pending_mqtt_options.load()).clone();
1043    let (new_client, eventloop) = AsyncClient::builder(mqtt_options).capacity(10).build();
1044
1045    if let Err(e) = new_client.subscribe(&args.topic, args.qos).await {
1046        error!(
1047            event = EVENT_MQTT_TARGET_STATE,
1048            component = LOG_COMPONENT_TARGETS,
1049            subsystem = LOG_SUBSYSTEM_MQTT,
1050            target_id = %target_id,
1051            state = "subscribe_failed",
1052            error = %e,
1053            "mqtt target state"
1054        );
1055        return false;
1056    }
1057
1058    *client_arc.lock().await = Some(new_client);
1059    connected_status.store(false, Ordering::SeqCst);
1060
1061    info!(
1062        event = EVENT_MQTT_TARGET_STATE,
1063        component = LOG_COMPONENT_TARGETS,
1064        subsystem = LOG_SUBSYSTEM_MQTT,
1065        target_id = %target_id,
1066        state = "event_loop_spawning",
1067        "mqtt target state"
1068    );
1069
1070    run_mqtt_event_loop(eventloop, connected_status, target_id).await
1071}
1072
1073/// Runs a single MQTT event-loop session until it exits (fatal protocol error
1074/// or `RequestsDone`). Returns whether the session connected at least once.
1075/// Cancellation is handled by the supervisor dropping this future, so no cancel
1076/// channel is needed here.
1077async fn run_mqtt_event_loop(mut eventloop: EventLoop, connected_status: Arc<AtomicBool>, target_id: TargetID) -> bool {
1078    info!(
1079        event = EVENT_MQTT_TARGET_STATE,
1080        component = LOG_COMPONENT_TARGETS,
1081        subsystem = LOG_SUBSYSTEM_MQTT,
1082        target_id = %target_id,
1083        state = "event_loop_started",
1084        "mqtt target state"
1085    );
1086    let mut initial_connection_established = false;
1087
1088    loop {
1089        let polled_event_result = if !initial_connection_established || !connected_status.load(Ordering::SeqCst) {
1090            match tokio::time::timeout(EVENT_LOOP_POLL_TIMEOUT, eventloop.poll()).await {
1091                Ok(result) => Some(result),
1092                Err(_) => {
1093                    debug!(
1094                        event = EVENT_MQTT_TARGET_STATE,
1095                        component = LOG_COMPONENT_TARGETS,
1096                        subsystem = LOG_SUBSYSTEM_MQTT,
1097                        target_id = %target_id,
1098                        state = "poll_timeout",
1099                        "mqtt target state"
1100                    );
1101                    connected_status.store(false, Ordering::SeqCst);
1102                    None
1103                }
1104            }
1105        } else {
1106            Some(eventloop.poll().await)
1107        };
1108
1109        match polled_event_result {
1110            Some(Ok(notification)) => {
1111                trace!(target_id = %target_id, event = ?notification, "Received MQTT event");
1112                match notification {
1113                    rumqttc::Event::Incoming(Incoming::ConnAck(_conn_ack)) => {
1114                        info!(
1115                            event = EVENT_MQTT_TARGET_STATE,
1116                            component = LOG_COMPONENT_TARGETS,
1117                            subsystem = LOG_SUBSYSTEM_MQTT,
1118                            target_id = %target_id,
1119                            state = "connack_received",
1120                            "mqtt target state"
1121                        );
1122                        connected_status.store(true, Ordering::SeqCst);
1123                        initial_connection_established = true;
1124                    }
1125                    rumqttc::Event::Incoming(Incoming::Publish(publish)) => {
1126                        debug!(
1127                            event = EVENT_MQTT_TARGET_STATE,
1128                            component = LOG_COMPONENT_TARGETS,
1129                            subsystem = LOG_SUBSYSTEM_MQTT,
1130                            target_id = %target_id,
1131                            state = "publish_received",
1132                            topic = ?publish.topic,
1133                            payload_len = publish.payload.len(),
1134                            "mqtt target state"
1135                        );
1136                    }
1137                    rumqttc::Event::Incoming(Incoming::Disconnect(_)) => {
1138                        info!(
1139                            event = EVENT_MQTT_TARGET_STATE,
1140                            component = LOG_COMPONENT_TARGETS,
1141                            subsystem = LOG_SUBSYSTEM_MQTT,
1142                            target_id = %target_id,
1143                            state = "broker_disconnected",
1144                            "mqtt target state"
1145                        );
1146                        connected_status.store(false, Ordering::SeqCst);
1147                    }
1148                    rumqttc::Event::Incoming(Incoming::PingResp) => {
1149                        trace!(target_id = %target_id, "Received PingResp from broker. Connection is alive.");
1150                    }
1151                    rumqttc::Event::Incoming(Incoming::SubAck(suback)) => {
1152                        trace!(target_id = %target_id, "Received SubAck for pkid: {}", suback.pkid);
1153                    }
1154                    rumqttc::Event::Incoming(Incoming::PubAck(puback)) => {
1155                        trace!(target_id = %target_id, "Received PubAck for pkid: {}", puback.pkid);
1156                    }
1157                    // Process other incoming packet types as needed (PubRec, PubRel, PubComp, UnsubAck)
1158                    rumqttc::Event::Outgoing(Outgoing::Disconnect) => {
1159                        info!(
1160                            event = EVENT_MQTT_TARGET_STATE,
1161                            component = LOG_COMPONENT_TARGETS,
1162                            subsystem = LOG_SUBSYSTEM_MQTT,
1163                            target_id = %target_id,
1164                            state = "client_disconnect_requested",
1165                            "mqtt target state"
1166                        );
1167                        connected_status.store(false, Ordering::SeqCst);
1168                    }
1169                    rumqttc::Event::Outgoing(Outgoing::PingReq) => {
1170                        trace!(target_id = %target_id, "Client sent PingReq to broker.");
1171                    }
1172                    // Other Outgoing events (Subscribe, Unsubscribe, Publish) usually do not need to handle connection status here,
1173                    // Because they are actions initiated by the client.
1174                    _ => {
1175                        // Log other unspecified MQTT events that are not handled, which helps debug
1176                        trace!(target_id = %target_id, "Unhandled or generic MQTT event: {:?}", notification);
1177                    }
1178                }
1179            }
1180            Some(Err(e)) => {
1181                connected_status.store(false, Ordering::SeqCst);
1182                error!(
1183                    event = EVENT_MQTT_TARGET_STATE,
1184                    component = LOG_COMPONENT_TARGETS,
1185                    subsystem = LOG_SUBSYSTEM_MQTT,
1186                    target_id = %target_id,
1187                    state = "poll_failed",
1188                    error = %e,
1189                    "mqtt target state"
1190                );
1191
1192                if matches!(
1193                    e,
1194                    ConnectionError::Io(_)
1195                        | ConnectionError::Timeout(_)
1196                        | ConnectionError::ConnectionRefused(_)
1197                        | ConnectionError::Tls(_)
1198                ) {
1199                    warn!(
1200                        event = EVENT_MQTT_TARGET_STATE,
1201                        component = LOG_COMPONENT_TARGETS,
1202                        subsystem = LOG_SUBSYSTEM_MQTT,
1203                        target_id = %target_id,
1204                        state = "reconnect_pending",
1205                        error = %e,
1206                        "mqtt target state"
1207                    );
1208                }
1209                // Fatal protocol errors end this session; the supervisor rebuilds
1210                // the client and event loop after a backoff. Non-fatal errors are
1211                // usually handled by rumqttc's internal reconnection, so keep
1212                // polling after a short pause to avoid a busy loop on rapid failure.
1213                if is_fatal_mqtt_error(&e) {
1214                    error!(
1215                        event = EVENT_MQTT_TARGET_STATE,
1216                        component = LOG_COMPONENT_TARGETS,
1217                        subsystem = LOG_SUBSYSTEM_MQTT,
1218                        target_id = %target_id,
1219                        state = "fatal_error",
1220                        error = %e,
1221                        "mqtt target state"
1222                    );
1223                    break;
1224                }
1225                tokio::time::sleep(Duration::from_secs(1)).await;
1226            }
1227            None => {
1228                warn!(
1229                    event = EVENT_MQTT_TARGET_STATE,
1230                    component = LOG_COMPONENT_TARGETS,
1231                    subsystem = LOG_SUBSYSTEM_MQTT,
1232                    target_id = %target_id,
1233                    state = "poll_retry_scheduled",
1234                    "mqtt target state"
1235                );
1236                continue;
1237            }
1238        }
1239    }
1240    connected_status.store(false, Ordering::SeqCst);
1241    info!(
1242        event = EVENT_MQTT_TARGET_STATE,
1243        component = LOG_COMPONENT_TARGETS,
1244        subsystem = LOG_SUBSYSTEM_MQTT,
1245        target_id = %target_id,
1246        state = "event_loop_finished",
1247        "mqtt target state"
1248    );
1249
1250    initial_connection_established
1251}
1252
1253/// Classifies a publish-enqueue failure. Every [`ClientError`] variant means the
1254/// publish could not be handed to the event loop (channel closed/full, or the
1255/// tracked-publish API is unavailable), i.e. the client is not currently able to
1256/// deliver. These are treated as retriable connectivity errors so the durable
1257/// copy is preserved and replayed rather than dropped (backlog#971).
1258fn classify_mqtt_client_error(err: &ClientError) -> TargetError {
1259    match err {
1260        ClientError::RequestChannelFull(_) | ClientError::RequestChannelDisconnected(_) | ClientError::TrackingUnavailable => {
1261            TargetError::NotConnected
1262        }
1263        ClientError::InvalidRequest(_) => TargetError::Request(format!("Invalid MQTT publish request: {err}")),
1264        _ => TargetError::NotConnected,
1265    }
1266}
1267
1268/// Classifies a publish acknowledgement failure returned while waiting for the
1269/// broker to confirm delivery. Connectivity/session problems keep the event for
1270/// replay; a broker rejection with a failing reason code is surfaced as a
1271/// request-level error (backlog#971).
1272fn classify_mqtt_notice_error(err: &PublishNoticeError) -> TargetError {
1273    match err {
1274        PublishNoticeError::Recv
1275        | PublishNoticeError::SessionReset
1276        | PublishNoticeError::Qos0NotFlushed
1277        | PublishNoticeError::BrokerOnlySessionResume
1278        | PublishNoticeError::SessionPersistence(_)
1279        | PublishNoticeError::TopicAliasReplayUnavailable(_) => TargetError::NotConnected,
1280        PublishNoticeError::RetainNotSupported => TargetError::Request(format!("MQTT broker rejected publish: {err}")),
1281        PublishNoticeError::V5PubAck(_) | PublishNoticeError::V5PubRec(_) | PublishNoticeError::V5PubComp(_) => {
1282            TargetError::Request(format!("MQTT broker rejected publish: {err}"))
1283        }
1284        _ => TargetError::NotConnected,
1285    }
1286}
1287
1288/// Check whether the given MQTT connection error should be considered a fatal error,
1289/// For fatal errors, the event loop should terminate.
1290fn is_fatal_mqtt_error(err: &ConnectionError) -> bool {
1291    match err {
1292        // If the client request has been processed all (for example, AsyncClient is dropped), the event loop can end.
1293        ConnectionError::RequestsDone => true,
1294
1295        // Check for the underlying MQTT status error
1296        ConnectionError::MqttState(state_err) => {
1297            // The type of state_err is &rumqttc::StateError
1298            match state_err {
1299                // If StateError is caused by deserialization issues, check the underlying MqttBytesError
1300                rumqttc::StateError::Deserialization(mqtt_bytes_err) => { // The type of mqtt_bytes_err is &rumqttc::mqttbytes::Error
1301                    matches!(
1302                        mqtt_bytes_err,
1303                        MqttBytesError::InvalidProtocol // Invalid agreement
1304                        | MqttBytesError::InvalidProtocolLevel(_) // Invalid protocol level
1305                        | MqttBytesError::IncorrectPacketFormat // Package format is incorrect
1306                        | MqttBytesError::InvalidPacketType(_) // Invalid package type
1307                        | MqttBytesError::MalformedPacket // Package format error
1308                        | MqttBytesError::PayloadTooLong // Too long load
1309                        | MqttBytesError::PayloadSizeLimitExceeded { .. } // Load size limit exceeded
1310                        | MqttBytesError::TopicNotUtf8 { .. } // Topic Non-UTF-8 (Serious Agreement Violation)
1311                    )
1312                }
1313                // Others that are fatal StateError variants
1314                rumqttc::StateError::InvalidState          // The internal state machine is in invalid state
1315                | rumqttc::StateError::ProtocolViolation(ProtocolViolation::UnexpectedIncomingPacket(_)) // Agreement Violation: Unexpected Data Packet Received
1316                | rumqttc::StateError::ProtocolViolation(_) // Agreement Violation
1317                | rumqttc::StateError::Unsolicited(_)      // Agreement Violation: Unsolicited ACK Received
1318                | rumqttc::StateError::CollisionTimeout    // Agreement Violation (if this stage occurs)
1319                | rumqttc::StateError::EmptySubscription   // Agreement violation (if this stage occurs)
1320                => true,
1321
1322                // Other StateErrors (such as Io, AwaitPingResp, CollisionTimeout) are not considered deadly here.
1323                // They may be processed internally by rumqttc or upgraded to other ConnectionError types.
1324                _ => false,
1325            }
1326        }
1327
1328        // Other types of ConnectionErrors (such as Io, Tls, NetworkTimeout, ConnectionRefused, NotConnAck, etc.)
1329        // It is usually considered temporary, or the reconnect logic inside rumqttc will be processed.
1330        _ => false,
1331    }
1332}
1333
1334#[async_trait]
1335impl<E> Target<E> for MQTTTarget<E>
1336where
1337    E: PluginEvent,
1338{
1339    fn id(&self) -> TargetID {
1340        self.id.clone()
1341    }
1342
1343    #[instrument(skip(self), fields(target_id = %self.id))]
1344    async fn is_active(&self) -> Result<bool, TargetError> {
1345        debug!(
1346            event = EVENT_MQTT_TARGET_STATE,
1347            component = LOG_COMPONENT_TARGETS,
1348            subsystem = LOG_SUBSYSTEM_MQTT,
1349            target_id = %self.id,
1350            state = "activity_check",
1351            "mqtt target state"
1352        );
1353        if self.client.lock().await.is_none() && !self.connected.load(Ordering::SeqCst) {
1354            // Check if the background task is running and has not panicked
1355            if let Some(handle) = self.bg_task_manager.init_cell.get()
1356                && handle.is_finished()
1357            {
1358                error!(
1359                    event = EVENT_MQTT_TARGET_STATE,
1360                    component = LOG_COMPONENT_TARGETS,
1361                    subsystem = LOG_SUBSYSTEM_MQTT,
1362                    target_id = %self.id,
1363                    state = "inactive_background_task_finished",
1364                    "mqtt target state"
1365                );
1366                return Err(TargetError::Network("MQTT background task terminated".to_string()));
1367            }
1368            debug!(
1369                event = EVENT_MQTT_TARGET_STATE,
1370                component = LOG_COMPONENT_TARGETS,
1371                subsystem = LOG_SUBSYSTEM_MQTT,
1372                target_id = %self.id,
1373                state = "inactive_client_unavailable",
1374                "mqtt target state"
1375            );
1376            return Err(TargetError::Configuration(
1377                "MQTT client not available or not initialized/connected".to_string(),
1378            ));
1379        }
1380
1381        if self.connected.load(Ordering::SeqCst) {
1382            debug!(
1383                event = EVENT_MQTT_TARGET_STATE,
1384                component = LOG_COMPONENT_TARGETS,
1385                subsystem = LOG_SUBSYSTEM_MQTT,
1386                target_id = %self.id,
1387                state = "active",
1388                "mqtt target state"
1389            );
1390            Ok(true)
1391        } else {
1392            debug!(
1393                event = EVENT_MQTT_TARGET_STATE,
1394                component = LOG_COMPONENT_TARGETS,
1395                subsystem = LOG_SUBSYSTEM_MQTT,
1396                target_id = %self.id,
1397                state = "inactive_not_connected",
1398                "mqtt target state"
1399            );
1400            Err(TargetError::NotConnected)
1401        }
1402    }
1403
1404    #[instrument(skip(self, event), fields(target_id = %self.id))]
1405    async fn save(&self, event: Arc<EntityTarget<E>>) -> Result<(), TargetError> {
1406        let queued = match self.build_queued_payload(&event) {
1407            Ok(queued) => queued,
1408            Err(err) => {
1409                self.delivery_counters.record_final_failure();
1410                return Err(err);
1411            }
1412        };
1413
1414        if let Some(store) = &self.store {
1415            debug!(
1416                event = EVENT_MQTT_DELIVERY_STATE,
1417                component = LOG_COMPONENT_TARGETS,
1418                subsystem = LOG_SUBSYSTEM_MQTT,
1419                target_id = %self.id,
1420                state = "store_enqueue_started",
1421                "mqtt delivery state"
1422            );
1423            match persist_queued_payload_to_store(store.as_ref(), &queued) {
1424                Ok(_) => {
1425                    debug!(
1426                        event = EVENT_MQTT_DELIVERY_STATE,
1427                        component = LOG_COMPONENT_TARGETS,
1428                        subsystem = LOG_SUBSYSTEM_MQTT,
1429                        target_id = %self.id,
1430                        state = "store_enqueued",
1431                        "mqtt delivery state"
1432                    );
1433                    Ok(())
1434                }
1435                Err(e) => {
1436                    error!(
1437                        event = EVENT_MQTT_DELIVERY_STATE,
1438                        component = LOG_COMPONENT_TARGETS,
1439                        subsystem = LOG_SUBSYSTEM_MQTT,
1440                        target_id = %self.id,
1441                        state = "store_enqueue_failed",
1442                        error = %e,
1443                        "mqtt delivery state"
1444                    );
1445                    self.delivery_counters.record_final_failure();
1446                    Err(e)
1447                }
1448            }
1449        } else {
1450            if !self.is_enabled() {
1451                return Err(TargetError::Disabled);
1452            }
1453
1454            if !self.connected.load(Ordering::SeqCst) {
1455                warn!(
1456                    event = EVENT_MQTT_TARGET_STATE,
1457                    component = LOG_COMPONENT_TARGETS,
1458                    subsystem = LOG_SUBSYSTEM_MQTT,
1459                    target_id = %self.id,
1460                    state = "direct_send_requires_init",
1461                    "mqtt target state"
1462                );
1463                // Call the struct's init method, not the trait's default
1464                match MQTTTarget::<E>::init(self).await {
1465                    Ok(_) => debug!(
1466                        event = EVENT_MQTT_TARGET_STATE,
1467                        component = LOG_COMPONENT_TARGETS,
1468                        subsystem = LOG_SUBSYSTEM_MQTT,
1469                        target_id = %self.id,
1470                        state = "init_completed",
1471                        "mqtt target state"
1472                    ),
1473                    Err(e) => {
1474                        error!(
1475                            event = EVENT_MQTT_TARGET_STATE,
1476                            component = LOG_COMPONENT_TARGETS,
1477                            subsystem = LOG_SUBSYSTEM_MQTT,
1478                            target_id = %self.id,
1479                            state = "init_failed",
1480                            error = %e,
1481                            "mqtt target state"
1482                        );
1483                        self.delivery_counters.record_final_failure();
1484                        return Err(TargetError::NotConnected);
1485                    }
1486                }
1487                if !self.connected.load(Ordering::SeqCst) {
1488                    error!(
1489                        event = EVENT_MQTT_TARGET_STATE,
1490                        component = LOG_COMPONENT_TARGETS,
1491                        subsystem = LOG_SUBSYSTEM_MQTT,
1492                        target_id = %self.id,
1493                        state = "init_completed_not_connected",
1494                        "mqtt target state"
1495                    );
1496                    self.delivery_counters.record_final_failure();
1497                    return Err(TargetError::NotConnected);
1498                }
1499            }
1500            if let Err(err) = self.send_body(queued.body, &queued.meta).await {
1501                self.delivery_counters.record_final_failure();
1502                return Err(err);
1503            }
1504            Ok(())
1505        }
1506    }
1507
1508    #[instrument(skip(self, body, meta), fields(target_id = %self.id))]
1509    async fn send_raw_from_store(&self, key: Key, body: Vec<u8>, meta: QueuedPayloadMeta) -> Result<(), TargetError> {
1510        debug!(
1511            event = EVENT_MQTT_DELIVERY_STATE,
1512            component = LOG_COMPONENT_TARGETS,
1513            subsystem = LOG_SUBSYSTEM_MQTT,
1514            target_id = %self.id,
1515            ?key,
1516            state = "store_replay_started",
1517            "mqtt delivery state"
1518        );
1519
1520        if !self.is_enabled() {
1521            return Err(TargetError::Disabled);
1522        }
1523
1524        if !self.connected.load(Ordering::SeqCst) {
1525            warn!(
1526                event = EVENT_MQTT_TARGET_STATE,
1527                component = LOG_COMPONENT_TARGETS,
1528                subsystem = LOG_SUBSYSTEM_MQTT,
1529                target_id = %self.id,
1530                state = "store_replay_requires_init",
1531                "mqtt target state"
1532            );
1533            match MQTTTarget::<E>::init(self).await {
1534                Ok(_) => debug!(
1535                    event = EVENT_MQTT_TARGET_STATE,
1536                    component = LOG_COMPONENT_TARGETS,
1537                    subsystem = LOG_SUBSYSTEM_MQTT,
1538                    target_id = %self.id,
1539                    state = "init_completed",
1540                    "mqtt target state"
1541                ),
1542                Err(e) => {
1543                    error!(
1544                        event = EVENT_MQTT_TARGET_STATE,
1545                        component = LOG_COMPONENT_TARGETS,
1546                        subsystem = LOG_SUBSYSTEM_MQTT,
1547                        target_id = %self.id,
1548                        state = "init_failed",
1549                        error = %e,
1550                        "mqtt target state"
1551                    );
1552                    return Err(TargetError::NotConnected);
1553                }
1554            }
1555            if !self.connected.load(Ordering::SeqCst) {
1556                error!(
1557                    event = EVENT_MQTT_TARGET_STATE,
1558                    component = LOG_COMPONENT_TARGETS,
1559                    subsystem = LOG_SUBSYSTEM_MQTT,
1560                    target_id = %self.id,
1561                    state = "init_completed_not_connected",
1562                    "mqtt target state"
1563                );
1564                return Err(TargetError::NotConnected);
1565            }
1566        }
1567
1568        debug!(
1569            event = EVENT_MQTT_DELIVERY_STATE,
1570            component = LOG_COMPONENT_TARGETS,
1571            subsystem = LOG_SUBSYSTEM_MQTT,
1572            target_id = %self.id,
1573            ?key,
1574            state = "store_replay_publishing",
1575            "mqtt delivery state"
1576        );
1577        if let Err(e) = self.send_body(body, &meta).await {
1578            if matches!(e, TargetError::NotConnected) {
1579                warn!(
1580                    event = EVENT_MQTT_DELIVERY_STATE,
1581                    component = LOG_COMPONENT_TARGETS,
1582                    subsystem = LOG_SUBSYSTEM_MQTT,
1583                    target_id = %self.id,
1584                    ?key,
1585                    state = "store_replay_deferred",
1586                    reason = "not_connected",
1587                    "mqtt delivery state"
1588                );
1589                return Err(TargetError::NotConnected);
1590            }
1591            error!(
1592                event = EVENT_MQTT_DELIVERY_STATE,
1593                component = LOG_COMPONENT_TARGETS,
1594                subsystem = LOG_SUBSYSTEM_MQTT,
1595                target_id = %self.id,
1596                ?key,
1597                state = "store_replay_failed",
1598                error = %e,
1599                "mqtt delivery state"
1600            );
1601            return Err(e);
1602        }
1603        debug!(
1604            event = EVENT_MQTT_DELIVERY_STATE,
1605            component = LOG_COMPONENT_TARGETS,
1606            subsystem = LOG_SUBSYSTEM_MQTT,
1607            target_id = %self.id,
1608            ?key,
1609            state = "store_replay_published",
1610            "mqtt delivery state"
1611        );
1612        Ok(())
1613    }
1614
1615    async fn close(&self) -> Result<(), TargetError> {
1616        info!(
1617            event = EVENT_MQTT_TARGET_STATE,
1618            component = LOG_COMPONENT_TARGETS,
1619            subsystem = LOG_SUBSYSTEM_MQTT,
1620            target_id = %self.id,
1621            state = "closing",
1622            "mqtt target state"
1623        );
1624
1625        if let Err(e) = self.bg_task_manager.cancel_tx.send(()).await {
1626            warn!(
1627                event = EVENT_MQTT_TARGET_STATE,
1628                component = LOG_COMPONENT_TARGETS,
1629                subsystem = LOG_SUBSYSTEM_MQTT,
1630                target_id = %self.id,
1631                state = "cancel_signal_failed",
1632                error = %e,
1633                "mqtt target state"
1634            );
1635        }
1636
1637        // The cancel signal above makes the supervisor's `select!` drop the
1638        // in-flight session and stop the reconnect loop. The `JoinHandle` lives
1639        // in a `OnceCell` shared across `clone_target()` clones, so it cannot be
1640        // taken out to be joined here; we rely on the cancel signal for a prompt,
1641        // graceful stop.
1642        if self.bg_task_manager.init_cell.get().is_some() {
1643            debug!(
1644                event = EVENT_MQTT_TARGET_STATE,
1645                component = LOG_COMPONENT_TARGETS,
1646                subsystem = LOG_SUBSYSTEM_MQTT,
1647                target_id = %self.id,
1648                state = "supervisor_stop_signalled",
1649                "mqtt target state"
1650            );
1651        }
1652
1653        if let Some(client_instance) = self.client.lock().await.take() {
1654            info!(
1655                event = EVENT_MQTT_TARGET_STATE,
1656                component = LOG_COMPONENT_TARGETS,
1657                subsystem = LOG_SUBSYSTEM_MQTT,
1658                target_id = %self.id,
1659                state = "disconnecting_client",
1660                "mqtt target state"
1661            );
1662            if let Err(e) = client_instance.disconnect().await {
1663                warn!(
1664                    event = EVENT_MQTT_TARGET_STATE,
1665                    component = LOG_COMPONENT_TARGETS,
1666                    subsystem = LOG_SUBSYSTEM_MQTT,
1667                    target_id = %self.id,
1668                    state = "disconnect_failed",
1669                    error = %e,
1670                    "mqtt target state"
1671                );
1672            }
1673        }
1674
1675        self.tls_state.lock().reset();
1676        // If a TLS reload adapter is attached, reset its error tracking
1677        // so that a future re-init does not inherit stale failure state.
1678        if let Some(adapter) = &self.tls_adapter {
1679            *adapter.runtime_state().last_error.write() = None;
1680        }
1681
1682        self.connected.store(false, Ordering::SeqCst);
1683        info!(
1684            event = EVENT_MQTT_TARGET_STATE,
1685            component = LOG_COMPONENT_TARGETS,
1686            subsystem = LOG_SUBSYSTEM_MQTT,
1687            target_id = %self.id,
1688            state = "closed",
1689            "mqtt target state"
1690        );
1691        Ok(())
1692    }
1693
1694    fn store(&self) -> Option<&(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync)> {
1695        self.store.as_deref()
1696    }
1697
1698    fn clone_dyn(&self) -> Box<dyn Target<E> + Send + Sync> {
1699        self.clone_target()
1700    }
1701
1702    async fn init(&self) -> Result<(), TargetError> {
1703        if !self.is_enabled() {
1704            debug!(
1705                event = EVENT_MQTT_TARGET_STATE,
1706                component = LOG_COMPONENT_TARGETS,
1707                subsystem = LOG_SUBSYSTEM_MQTT,
1708                target_id = %self.id,
1709                state = "disabled",
1710                "mqtt target state"
1711            );
1712            return Ok(());
1713        }
1714        // Call the internal init logic
1715        MQTTTarget::<E>::init(self).await
1716    }
1717
1718    fn is_enabled(&self) -> bool {
1719        self.args.enable
1720    }
1721
1722    fn delivery_snapshot(&self) -> TargetDeliverySnapshot {
1723        self.delivery_counters.snapshot(
1724            self.store.as_deref().map_or(0, |store| store.len() as u64),
1725            // MQTT targets record no terminal failures and keep no failed store.
1726            0,
1727        )
1728    }
1729
1730    fn record_final_failure(&self) {
1731        self.delivery_counters.record_final_failure();
1732    }
1733}
1734
1735#[cfg(test)]
1736mod tests {
1737    use super::{
1738        AsyncClient, ClientError, MQTT_RECONNECT_BACKOFF_MAX, MQTT_RECONNECT_BACKOFF_MIN, MQTTArgs, MQTTTarget, MQTTTlsConfig,
1739        MqttOptions, PublishNoticeError, PublishOptions, QoS, QueuedPayloadMeta, classify_mqtt_client_error,
1740        classify_mqtt_notice_error, next_reconnect_backoff, reconnect_supervisor, validate_mqtt_broker_url,
1741    };
1742    use crate::error::TargetError;
1743    use crate::target::{REDACTED_SECRET, TargetType};
1744    use std::sync::Arc;
1745    use std::sync::atomic::{AtomicUsize, Ordering};
1746    use std::time::Duration;
1747    use tokio::sync::mpsc;
1748    use url::Url;
1749
1750    fn base_mqtt_args() -> MQTTArgs {
1751        MQTTArgs {
1752            enable: true,
1753            broker: Url::parse("mqtt://broker.example.com:1883").expect("valid broker"),
1754            topic: "rustfs/events".to_string(),
1755            qos: QoS::AtLeastOnce,
1756            username: String::new(),
1757            password: String::new(),
1758            tls: MQTTTlsConfig::default(),
1759            max_reconnect_interval: Duration::from_secs(1),
1760            keep_alive: Duration::from_secs(30),
1761            queue_dir: String::new(),
1762            queue_limit: 0,
1763            target_type: TargetType::NotifyEvent,
1764        }
1765    }
1766
1767    #[test]
1768    fn mqtt_client_error_classified_as_not_connected() {
1769        // A publish that cannot be handed to the event loop means the client is
1770        // not connected; the durable copy must be kept for replay (backlog#971).
1771        assert!(matches!(
1772            classify_mqtt_client_error(&ClientError::TrackingUnavailable),
1773            TargetError::NotConnected
1774        ));
1775    }
1776
1777    #[test]
1778    fn mqtt_notice_connectivity_errors_kept_for_replay() {
1779        for err in [
1780            PublishNoticeError::SessionReset,
1781            PublishNoticeError::Qos0NotFlushed,
1782            PublishNoticeError::Recv,
1783        ] {
1784            assert!(
1785                matches!(classify_mqtt_notice_error(&err), TargetError::NotConnected),
1786                "unconfirmed publish {err:?} should be retriable"
1787            );
1788        }
1789    }
1790
1791    #[test]
1792    fn mqtt_notice_broker_rejection_is_request_error() {
1793        // The broker acknowledged the publish but rejected it: this is a
1794        // request-level failure, not a transient disconnect.
1795        let err = PublishNoticeError::V5PubAck(rumqttc::PubAckReason::NotAuthorized);
1796        assert!(matches!(classify_mqtt_notice_error(&err), TargetError::Request(_)));
1797    }
1798
1799    #[tokio::test(start_paused = true)]
1800    async fn enqueue_timeout_keeps_a_live_session_connected() {
1801        let target = MQTTTarget::<String>::new("mqtt:test".to_string(), base_mqtt_args()).expect("target should build");
1802        let (client, _event_loop) = AsyncClient::builder(MqttOptions::new("mqtt-timeout-test", ("localhost", 1883)))
1803            .capacity(1)
1804            .build();
1805        client
1806            .publish("fill", b"fill".as_slice(), PublishOptions::new(QoS::AtLeastOnce))
1807            .await
1808            .expect("first publish should fill the local channel");
1809        *target.client.lock().await = Some(client);
1810        target.connected.store(true, Ordering::SeqCst);
1811        let meta = QueuedPayloadMeta::new(
1812            rustfs_s3_types::EventName::ObjectCreatedPut,
1813            "bucket".to_string(),
1814            "object".to_string(),
1815            "application/json",
1816            2,
1817        );
1818
1819        let error = target
1820            .send_body(b"{}".to_vec(), &meta)
1821            .await
1822            .expect_err("a full local request channel should hit the enqueue deadline");
1823
1824        assert!(matches!(error, TargetError::Timeout(_)));
1825        assert!(
1826            target.connected.load(Ordering::SeqCst),
1827            "local admission pressure is not evidence that the MQTT session disconnected"
1828        );
1829    }
1830
1831    #[test]
1832    fn next_reconnect_backoff_doubles_until_capped() {
1833        let mut backoff = MQTT_RECONNECT_BACKOFF_MIN;
1834        // Doubles on each step.
1835        backoff = next_reconnect_backoff(backoff);
1836        assert_eq!(backoff, MQTT_RECONNECT_BACKOFF_MIN * 2);
1837        backoff = next_reconnect_backoff(backoff);
1838        assert_eq!(backoff, MQTT_RECONNECT_BACKOFF_MIN * 4);
1839
1840        // Never exceeds the cap, even from a huge starting point.
1841        assert_eq!(next_reconnect_backoff(MQTT_RECONNECT_BACKOFF_MAX), MQTT_RECONNECT_BACKOFF_MAX);
1842        assert_eq!(next_reconnect_backoff(Duration::from_secs(3600)), MQTT_RECONNECT_BACKOFF_MAX);
1843    }
1844
1845    #[tokio::test(start_paused = true)]
1846    async fn supervisor_restarts_session_until_cancelled() {
1847        // A session that exits immediately (as after a fatal protocol error)
1848        // must be restarted by the supervisor rather than leaving the target
1849        // permanently silent. Time is paused so the reconnect backoff advances
1850        // automatically without real waits.
1851        let (cancel_tx, cancel_rx) = mpsc::channel(1);
1852        let (attempt_tx, mut attempt_rx) = mpsc::unbounded_channel();
1853        let attempts = Arc::new(AtomicUsize::new(0));
1854
1855        let attempts_in_task = Arc::clone(&attempts);
1856        let handle = tokio::spawn(reconnect_supervisor(cancel_rx, move || {
1857            let attempt_tx = attempt_tx.clone();
1858            let attempts_in_task = Arc::clone(&attempts_in_task);
1859            async move {
1860                attempts_in_task.fetch_add(1, Ordering::SeqCst);
1861                let _ = attempt_tx.send(());
1862                // Session exits immediately and never connected.
1863                false
1864            }
1865        }));
1866
1867        // Observe several automatic restarts driven purely by the supervisor.
1868        for _ in 0..4 {
1869            attempt_rx.recv().await.expect("supervisor should restart the session");
1870        }
1871
1872        cancel_tx.send(()).await.expect("cancel signal should be delivered");
1873        handle.await.expect("supervisor task should stop cleanly");
1874
1875        assert!(attempts.load(Ordering::SeqCst) >= 4, "session should have been restarted repeatedly");
1876    }
1877
1878    #[tokio::test(start_paused = true)]
1879    async fn supervisor_stops_promptly_on_cancel() {
1880        // A connected session that stays up must be torn down by cancellation
1881        // (the supervisor drops the in-flight session future).
1882        let (cancel_tx, cancel_rx) = mpsc::channel(1);
1883        let started = Arc::new(AtomicUsize::new(0));
1884
1885        let started_in_task = Arc::clone(&started);
1886        let handle = tokio::spawn(reconnect_supervisor(cancel_rx, move || {
1887            let started_in_task = Arc::clone(&started_in_task);
1888            async move {
1889                started_in_task.fetch_add(1, Ordering::SeqCst);
1890                // Long-lived, "connected" session that never returns on its own.
1891                std::future::pending::<bool>().await
1892            }
1893        }));
1894
1895        // Let the session start, then cancel; the supervisor must stop.
1896        while started.load(Ordering::SeqCst) == 0 {
1897            tokio::task::yield_now().await;
1898        }
1899        cancel_tx.send(()).await.expect("cancel signal should be delivered");
1900        handle.await.expect("supervisor task should stop cleanly on cancel");
1901
1902        assert_eq!(started.load(Ordering::SeqCst), 1, "session should not be restarted after cancel");
1903    }
1904
1905    #[test]
1906    fn validate_mqtt_broker_url_rejects_non_websocket_path() {
1907        let url = Url::parse("mqtt://broker.example.com:1883/custom").expect("valid url");
1908        let err = validate_mqtt_broker_url(&url, &MQTTTlsConfig::default()).expect_err("non-websocket path should be rejected");
1909        assert!(err.to_string().contains("path is only supported"));
1910    }
1911
1912    #[test]
1913    fn validate_mqtt_broker_url_rejects_non_websocket_query() {
1914        let url = Url::parse("mqtt://broker.example.com:1883?client_id=test").expect("valid url");
1915        let err = validate_mqtt_broker_url(&url, &MQTTTlsConfig::default()).expect_err("non-websocket query should be rejected");
1916        assert!(err.to_string().contains("query is only supported"));
1917    }
1918
1919    #[test]
1920    fn validate_mqtt_broker_url_rejects_non_websocket_fragment() {
1921        let url = Url::parse("mqtt://broker.example.com:1883/#section").expect("valid url");
1922        let err =
1923            validate_mqtt_broker_url(&url, &MQTTTlsConfig::default()).expect_err("non-websocket fragment should be rejected");
1924        assert!(err.to_string().contains("fragment is only supported"));
1925    }
1926
1927    #[test]
1928    fn validate_mqtt_broker_url_allows_websocket_path_and_query() {
1929        let url = Url::parse("ws://broker.example.com:8080/mqtt?client_id=test").expect("valid url");
1930        validate_mqtt_broker_url(&url, &MQTTTlsConfig::default()).expect("websocket path and query should be allowed");
1931    }
1932
1933    #[test]
1934    fn validate_mqtt_broker_url_rejects_url_embedded_credentials() {
1935        let url = Url::parse("mqtt://user:pass@broker.example.com:1883").expect("valid url");
1936        let err = validate_mqtt_broker_url(&url, &MQTTTlsConfig::default()).expect_err("url credentials should be rejected");
1937        assert!(err.to_string().contains("must not embed username or password"));
1938    }
1939
1940    #[test]
1941    fn debug_redacts_mqtt_secret_fields() {
1942        let args = MQTTArgs {
1943            username: "mqtt-user".to_string(),
1944            password: "mqtt-password".to_string(),
1945            tls: MQTTTlsConfig {
1946                client_key_path: "/etc/rustfs/mqtt.key".to_string(),
1947                ..MQTTTlsConfig::default()
1948            },
1949            ..base_mqtt_args()
1950        };
1951
1952        let rendered = format!("{args:?}");
1953
1954        assert!(!rendered.contains("mqtt-password"));
1955        assert!(!rendered.contains("/etc/rustfs/mqtt.key"));
1956        assert!(rendered.contains(REDACTED_SECRET));
1957        assert!(rendered.contains("mqtt-user"));
1958    }
1959
1960    #[test]
1961    fn validate_mqtt_broker_url_requires_explicit_tls_policy_for_secure_scheme() {
1962        let url = Url::parse("mqtts://broker.example.com:8883").expect("valid url");
1963        let err = validate_mqtt_broker_url(&url, &MQTTTlsConfig::default())
1964            .expect_err("secure scheme should require explicit tls policy");
1965        assert!(err.to_string().contains("explicit tls_policy"));
1966    }
1967
1968    #[test]
1969    fn validate_mqtt_broker_url_rejects_disallowed_websocket_path() {
1970        let url = Url::parse("wss://broker.example.com/private").expect("valid url");
1971        let tls = MQTTTlsConfig::from_values(Some("system_ca"), None, None, None, None, Some("/mqtt")).expect("valid tls config");
1972        let err = validate_mqtt_broker_url(&url, &tls).expect_err("path outside allowlist should be rejected");
1973        assert!(err.to_string().contains("allowlist"));
1974    }
1975
1976    #[test]
1977    fn validate_mqtt_broker_url_requires_tls_ca_for_custom_ca_policy() {
1978        let url = Url::parse("mqtts://broker.example.com:8883").expect("valid url");
1979        let tls = MQTTTlsConfig::from_values(Some("custom_ca"), None, None, None, None, None).expect("valid tls config");
1980        let err = validate_mqtt_broker_url(&url, &tls).expect_err("custom_ca policy without path should be rejected");
1981        assert!(err.to_string().contains("tls_ca"));
1982    }
1983}