Filter replaying unrelated retained MQTT messages when subscribing to share topics (#88826)

* Do not replay already processed retained subscr.

* Add tests

* Always replay wildcards

* Update tests for debouncer

* Rework for retained topics

* Fix test

* Correct comment

* Add cleanup and test

* Fix key error

* Correct helper

* Rename mock

* Add comment on function _retained_init

* Always replay initial retained payload

* Apply suggestion moving msg.retain to outer check

* Improve test on edge case

* Improve comment formatting

* Follow up comment - improve comments on test

* Update homeassistant/components/mqtt/client.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Jan Bouwhuis 2023-05-12 15:23:05 +02:00 committed by GitHub
parent bd7e943efe
commit a05c20a498
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 328 additions and 11 deletions

View file

@ -376,6 +376,11 @@ class MQTT:
self._simple_subscriptions: dict[str, list[Subscription]] = {}
self._wildcard_subscriptions: list[Subscription] = []
# _retained_topics prevents a Subscription from receiving a
# retained message more than once per topic. This prevents flooding
# already active subscribers when new subscribers subscribe to a topic
# which has subscribed messages.
self._retained_topics: dict[Subscription, set[str]] = {}
self.connected = False
self._ha_started = asyncio.Event()
self._cleanup_on_unload: list[Callable[[], None]] = []
@ -618,6 +623,8 @@ class MQTT:
"""Remove subscription."""
self._async_untrack_subscription(subscription)
self._matching_subscriptions.cache_clear()
if subscription in self._retained_topics:
del self._retained_topics[subscription]
# Only unsubscribe if currently connected
if self.connected:
self._async_unsubscribe(topic)
@ -637,7 +644,7 @@ class MQTT:
if topic in self._max_qos:
del self._max_qos[topic]
if topic in self._pending_subscriptions:
# avoid any pending subscription to be executed
# Avoid any pending subscription to be executed
del self._pending_subscriptions[topic]
self._pending_unsubscribes.add(topic)
@ -754,8 +761,9 @@ class MQTT:
async def _async_resubscribe(self) -> None:
"""Resubscribe on reconnect."""
# Group subscriptions to only re-subscribe once for each topic.
self._max_qos.clear()
self._retained_topics.clear()
# Group subscriptions to only re-subscribe once for each topic.
keyfunc = attrgetter("topic")
self._async_queue_subscriptions(
[
@ -799,6 +807,14 @@ class MQTT:
subscriptions = self._matching_subscriptions(msg.topic)
for subscription in subscriptions:
if msg.retain:
retained_topics = self._retained_topics.setdefault(subscription, set())
# Skip if the subscription already received a retained message
if msg.topic in retained_topics:
continue
# Remember the subscription had an initial retained message
self._retained_topics[subscription].add(msg.topic)
payload: SubscribePayloadType = msg.payload
if subscription.encoding is not None:
try: