sz_rust_core/runtime/
mqtt.rs1use std::sync::Arc;
23use std::time::Duration;
24
25use tokio::sync::Mutex;
26use tokio_util::sync::CancellationToken;
27
28use sz_orm_mqtt::{MqttConfig, MqttError, MqttPlugin, QoS};
29
30#[derive(Debug, Clone)]
32pub struct MqttRuntimeConfig {
33 pub client_id: String,
35 pub keep_alive_secs: u16,
37 pub topics: Vec<String>,
39 pub broker_url: String,
41}
42
43impl Default for MqttRuntimeConfig {
44 fn default() -> Self {
45 Self {
46 client_id: "sz-rust-mqtt".to_string(),
47 keep_alive_secs: 60,
48 topics: Vec::new(),
49 broker_url: "tcp://localhost:1883".to_string(),
50 }
51 }
52}
53
54impl MqttRuntimeConfig {
55 pub fn new(client_id: impl Into<String>) -> Self {
57 Self {
58 client_id: client_id.into(),
59 ..Default::default()
60 }
61 }
62
63 pub fn with_keep_alive(mut self, secs: u16) -> Self {
65 self.keep_alive_secs = secs;
66 self
67 }
68
69 pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
71 self.topics.push(topic.into());
72 self
73 }
74
75 pub fn with_broker_url(mut self, url: impl Into<String>) -> Self {
77 self.broker_url = url.into();
78 self
79 }
80}
81
82pub struct MqttRuntime {
92 config: MqttRuntimeConfig,
93 plugin: Arc<Mutex<MqttPlugin>>,
94}
95
96impl MqttRuntime {
97 pub fn new(config: MqttRuntimeConfig) -> Self {
99 let mqtt_config = MqttConfig::new(config.broker_url.clone())
100 .with_client_id(config.client_id.clone())
101 .with_keep_alive(config.keep_alive_secs);
102 let plugin = MqttPlugin::new(mqtt_config);
103 Self {
104 config,
105 plugin: Arc::new(Mutex::new(plugin)),
106 }
107 }
108
109 pub async fn connect(&self) -> Result<(), MqttError> {
111 let mut plugin = self.plugin.lock().await;
112 plugin.connect().await
113 }
114
115 pub async fn disconnect(&self) -> Result<(), MqttError> {
117 let mut plugin = self.plugin.lock().await;
118 plugin.disconnect().await
119 }
120
121 pub async fn is_connected(&self) -> bool {
123 let plugin = self.plugin.lock().await;
124 plugin.is_connected()
125 }
126
127 pub async fn subscribe(&self, topic: &str, qos: QoS) -> Result<(), MqttError> {
129 let plugin = self.plugin.lock().await;
130 plugin.subscribe(topic, qos).await
131 }
132
133 pub async fn unsubscribe(&self, topic: &str) -> Result<(), MqttError> {
135 let plugin = self.plugin.lock().await;
136 plugin.unsubscribe(topic).await
137 }
138
139 pub async fn publish(&self, topic: &str, payload: Vec<u8>, qos: QoS) -> Result<(), MqttError> {
141 let plugin = self.plugin.lock().await;
142 plugin.publish(topic, payload, qos).await
143 }
144
145 pub fn start_keepalive(&self, token: CancellationToken) -> tokio::task::JoinHandle<()> {
150 let plugin = self.plugin.clone();
151 let interval_secs = self.config.keep_alive_secs.max(1) as u64;
152
153 tokio::spawn(async move {
154 let mut ticker = tokio::time::interval(Duration::from_secs(interval_secs));
155 loop {
156 tokio::select! {
157 _ = token.cancelled() => break,
158 _ = ticker.tick() => {
159 let mut p = plugin.lock().await;
160 if !p.is_connected() {
161 if let Err(e) = p.connect().await {
163 tracing::warn!("mqtt reconnect failed: {}", e);
164 }
165 }
166 }
167 }
168 }
169 })
170 }
171
172 pub async fn subscribe_default_topics(&self) -> Result<(), MqttError> {
174 for topic in &self.config.topics {
175 self.subscribe(topic, QoS::AtLeastOnce).await?;
176 }
177 Ok(())
178 }
179
180 pub fn config(&self) -> &MqttRuntimeConfig {
182 &self.config
183 }
184
185 pub async fn subscription_count(&self) -> usize {
187 let plugin = self.plugin.lock().await;
188 plugin.subscription_count().await
189 }
190
191 pub async fn message_count(&self) -> usize {
193 let plugin = self.plugin.lock().await;
194 plugin.message_count().await
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn test_mqtt_runtime_config_default() {
204 let config = MqttRuntimeConfig::default();
205 assert_eq!(config.client_id, "sz-rust-mqtt");
206 assert_eq!(config.keep_alive_secs, 60);
207 assert!(config.topics.is_empty());
208 assert_eq!(config.broker_url, "tcp://localhost:1883");
209 }
210
211 #[test]
212 fn test_mqtt_runtime_config_builder() {
213 let config = MqttRuntimeConfig::new("client-1")
214 .with_keep_alive(30)
215 .with_topic("orders/#")
216 .with_topic("payments/#")
217 .with_broker_url("ssl://broker.example.com:8883");
218 assert_eq!(config.client_id, "client-1");
219 assert_eq!(config.keep_alive_secs, 30);
220 assert_eq!(config.topics.len(), 2);
221 assert_eq!(config.broker_url, "ssl://broker.example.com:8883");
222 }
223
224 #[tokio::test]
225 async fn test_mqtt_connect_disconnect() {
226 let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test-client"));
227 assert!(!runtime.is_connected().await);
228 runtime.connect().await.unwrap();
229 assert!(runtime.is_connected().await);
230 runtime.disconnect().await.unwrap();
231 assert!(!runtime.is_connected().await);
232 }
233
234 #[tokio::test]
235 async fn test_mqtt_subscribe_unsubscribe() {
236 let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test-client"));
237 runtime.connect().await.unwrap();
238
239 runtime
240 .subscribe("test/topic", QoS::AtLeastOnce)
241 .await
242 .unwrap();
243 assert_eq!(runtime.subscription_count().await, 1);
244
245 runtime.unsubscribe("test/topic").await.unwrap();
246 assert_eq!(runtime.subscription_count().await, 0);
247 }
248
249 #[tokio::test]
250 async fn test_mqtt_publish() {
251 let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test-client"));
252 runtime.connect().await.unwrap();
253 runtime
254 .subscribe("test/topic", QoS::AtLeastOnce)
255 .await
256 .unwrap();
257
258 runtime
259 .publish("test/topic", b"hello mqtt".to_vec(), QoS::AtLeastOnce)
260 .await
261 .unwrap();
262
263 assert_eq!(runtime.message_count().await, 1);
264 }
265
266 #[tokio::test]
267 async fn test_mqtt_publish_multiple() {
268 let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test-client"));
269 runtime.connect().await.unwrap();
270 runtime
271 .subscribe("test/topic", QoS::AtLeastOnce)
272 .await
273 .unwrap();
274
275 for i in 0..5 {
276 runtime
277 .publish(
278 "test/topic",
279 format!("msg-{}", i).into_bytes(),
280 QoS::AtLeastOnce,
281 )
282 .await
283 .unwrap();
284 }
285 assert_eq!(runtime.message_count().await, 5);
286 }
287
288 #[tokio::test]
289 async fn test_subscribe_default_topics() {
290 let config = MqttRuntimeConfig::new("test-client")
291 .with_topic("orders/#")
292 .with_topic("payments/#");
293 let runtime = MqttRuntime::new(config);
294 runtime.connect().await.unwrap();
295 runtime.subscribe_default_topics().await.unwrap();
296 assert_eq!(runtime.subscription_count().await, 2);
297 }
298
299 #[tokio::test]
300 async fn test_keepalive_task_stops_on_cancel() {
301 let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test-client").with_keep_alive(1));
302 let token = CancellationToken::new();
303 let handle = runtime.start_keepalive(token.clone());
304
305 tokio::time::sleep(Duration::from_millis(100)).await;
307 token.cancel();
308
309 let result = tokio::time::timeout(Duration::from_secs(2), handle).await;
311 assert!(result.is_ok(), "keepalive task should stop on cancel");
312 }
313
314 #[tokio::test]
315 async fn test_keepalive_reconnects_after_disconnect() {
316 let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test-client").with_keep_alive(1));
317 runtime.connect().await.unwrap();
318 assert!(runtime.is_connected().await);
319
320 let token = CancellationToken::new();
321 let plugin_clone = runtime.plugin.clone();
322 let handle = runtime.start_keepalive(token.clone());
323
324 {
326 let mut p = plugin_clone.lock().await;
327 p.disconnect().await.unwrap();
328 }
329 assert!(!runtime.is_connected().await);
330
331 tokio::time::sleep(Duration::from_millis(1500)).await;
333 assert!(runtime.is_connected().await);
334
335 token.cancel();
336 let _ = handle.await;
337 }
338
339 #[test]
340 fn test_config_accessor() {
341 let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test").with_keep_alive(45));
342 assert_eq!(runtime.config().client_id, "test");
343 assert_eq!(runtime.config().keep_alive_secs, 45);
344 }
345
346 #[tokio::test]
347 async fn test_publish_without_connect_returns_error() {
348 let runtime = MqttRuntime::new(MqttRuntimeConfig::new("test-client"));
349 let result = runtime
351 .publish("test", b"data".to_vec(), QoS::AtMostOnce)
352 .await;
353 let _ = result;
355 }
356}