1use 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); const 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;
61const MQTT_PUBLISH_CONFIRM_TIMEOUT: Duration = Duration::from_secs(30);
65const MQTT_RECONNECT_BACKOFF_MIN: Duration = Duration::from_secs(1);
69const 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#[derive(Clone)]
454pub struct MQTTArgs {
455 pub enable: bool,
457 pub broker: Url,
459 pub topic: String,
461 pub qos: QoS,
463 pub username: String,
465 pub password: String,
467 pub tls: MQTTTlsConfig,
469 pub max_reconnect_interval: Duration,
471 pub keep_alive: Duration,
473 pub queue_dir: String,
475 pub queue_limit: u64,
477 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
534pub 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_state: Arc<parking_lot::Mutex<TargetTlsState>>,
547 tls_adapter: Option<TlsReloadAdapter<MqttOptions>>,
549 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 #[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 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 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 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 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#[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 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
946fn next_reconnect_backoff(current: Duration) -> Duration {
950 current.saturating_mul(2).min(MQTT_RECONNECT_BACKOFF_MAX)
951}
952
953async 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
988async 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
1031async 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 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
1073async 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 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 _ => {
1175 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 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
1253fn 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
1268fn 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
1288fn is_fatal_mqtt_error(err: &ConnectionError) -> bool {
1291 match err {
1292 ConnectionError::RequestsDone => true,
1294
1295 ConnectionError::MqttState(state_err) => {
1297 match state_err {
1299 rumqttc::StateError::Deserialization(mqtt_bytes_err) => { matches!(
1302 mqtt_bytes_err,
1303 MqttBytesError::InvalidProtocol | MqttBytesError::InvalidProtocolLevel(_) | MqttBytesError::IncorrectPacketFormat | MqttBytesError::InvalidPacketType(_) | MqttBytesError::MalformedPacket | MqttBytesError::PayloadTooLong | MqttBytesError::PayloadSizeLimitExceeded { .. } | MqttBytesError::TopicNotUtf8 { .. } )
1312 }
1313 rumqttc::StateError::InvalidState | rumqttc::StateError::ProtocolViolation(ProtocolViolation::UnexpectedIncomingPacket(_)) | rumqttc::StateError::ProtocolViolation(_) | rumqttc::StateError::Unsolicited(_) | rumqttc::StateError::CollisionTimeout | rumqttc::StateError::EmptySubscription => true,
1321
1322 _ => false,
1325 }
1326 }
1327
1328 _ => 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 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 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 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 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 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 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 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 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 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 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 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 false
1864 }
1865 }));
1866
1867 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 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 std::future::pending::<bool>().await
1892 }
1893 }));
1894
1895 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}