Skip to content

Commit f14465c

Browse files
Merge pull request #3789 from didier-wenzek/fix/duplicated-mqtt-message
fix: MQTT messages are forwarded twice to subscribers with overlapping subscriptions
2 parents 98b05e3 + 0453d80 commit f14465c

2 files changed

Lines changed: 55 additions & 3 deletions

File tree

crates/extensions/tedge_mqtt_ext/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use mqtt_channel::StreamExt;
1414
use mqtt_channel::SubscriberOps;
1515
pub use mqtt_channel::Topic;
1616
pub use mqtt_channel::TopicFilter;
17+
use std::collections::HashSet;
1718
use std::convert::Infallible;
1819
use std::sync::Arc;
1920
use std::time::Duration;
@@ -356,10 +357,10 @@ impl TrieService {
356357
}
357358

358359
#[cfg(feature = "test-helpers")]
359-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
360+
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
360361
pub struct ClientId(pub usize);
361362
#[cfg(not(feature = "test-helpers"))]
362-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363+
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
363364
pub struct ClientId(usize);
364365

365366
type MatchRequest = String;
@@ -627,7 +628,7 @@ impl ToPeers {
627628
let TrieResponse::Matched(matches) = subscribed else {
628629
unreachable!("MatchRequest always returns Matched")
629630
};
630-
for client in matches {
631+
for client in HashSet::<ClientId>::from_iter(matches) {
631632
self.sender_by_id(client).send(message.clone()).await?;
632633
}
633634
Ok(())

crates/extensions/tedge_mqtt_ext/src/tests.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,57 @@ async fn dynamic_subscriptions() {
250250
);
251251
}
252252

253+
#[tokio::test]
254+
async fn client_with_overlapping_subscriptions() {
255+
let broker = mqtt_tests::test_mqtt_broker();
256+
let mqtt_config = MqttConfig::default().with_port(broker.port);
257+
let mut mqtt = MqttActorBuilder::new(mqtt_config);
258+
let mut publisher = SimpleMessageBoxBuilder::<MqttRequest, _>::new("publisher", 16);
259+
publisher.connect_sink(NoConfig, &mqtt);
260+
261+
// A client starts with a single subscription
262+
let mut client = SimpleMessageBoxBuilder::<_, MqttRequest>::new("dyn-subscriber", 16);
263+
let client_id = mqtt.connect_id_sink(TopicFilter::new_unchecked("a/+"), &client);
264+
client.connect_sink(NoConfig, &mqtt);
265+
266+
let mqtt = mqtt.build();
267+
tokio::spawn(async move { mqtt.run().await.unwrap() });
268+
let mut publisher = publisher.build();
269+
let mut client = client.build();
270+
271+
// The client then dynamically subscribes to an overlapping topic
272+
client
273+
.send(MqttRequest::Subscribe(SubscriptionRequest {
274+
diff: SubscriptionDiff {
275+
// a more convincing example would be to subscribe to "+/b"
276+
// but in that case rumqttd sends the message twice
277+
// something we can do little about
278+
subscribe: ["a/b".into()].into(),
279+
unsubscribe: [].into(), // still wants to receive messages from "a/+"
280+
},
281+
client_id,
282+
}))
283+
.await
284+
.unwrap();
285+
286+
// A message published on the overlap ...
287+
let msg = MqttMessage::new(&Topic::new_unchecked("a/b"), "hello");
288+
publisher
289+
.send(MqttRequest::Publish(msg.clone()))
290+
.await
291+
.unwrap();
292+
293+
// ... should then not be received twice
294+
assert_eq!(timeout(client.recv()).await.unwrap(), msg);
295+
if let Ok(Some(duplicate)) =
296+
tokio::time::timeout(Duration::from_millis(10), client.recv()).await
297+
{
298+
if duplicate == msg {
299+
panic!("A message has been delivered twice to the subscriber");
300+
}
301+
}
302+
}
303+
253304
#[tokio::test]
254305
async fn dynamic_subscribers_receive_retain_messages() {
255306
let broker = mqtt_tests::test_mqtt_broker();

0 commit comments

Comments
 (0)