From 3315c4c6c392ef1bbcf68bc2c0018d229a28a37a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 May 2020 18:30:15 -0500 Subject: [PATCH] Pre-filter zeroconf service browser updates (#35518) Each ServerBrowser currently runs in its own thread which processes every A or AAAA record update per instance. As the list of zeroconf names we watch for grows, each additional ServiceBrowser would process all the A and AAAA updates on the network. To avoid overwhemling the system we pre-filter here and only process DNSPointers for the configured record name (type) --- homeassistant/components/zeroconf/__init__.py | 27 +++++++++++++++++-- tests/components/zeroconf/test_init.py | 12 ++++----- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 3ab7b03b5d4..343897a44c0 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -5,6 +5,8 @@ import socket import voluptuous as vol from zeroconf import ( + DNSPointer, + DNSRecord, InterfaceChoice, NonUniqueNameException, ServiceBrowser, @@ -75,6 +77,27 @@ def _get_instance(hass, default_interface=False): return zeroconf +class HaServiceBrowser(ServiceBrowser): + """ServiceBrowser that only consumes DNSPointer records.""" + + def update_record(self, zc: "Zeroconf", now: float, record: DNSRecord) -> None: + """Pre-Filter update_record to DNSPointers for the configured type.""" + + # + # Each ServerBrowser currently runs in its own thread which + # processes every A or AAAA record update per instance. + # + # As the list of zeroconf names we watch for grows, each additional + # ServiceBrowser would process all the A and AAAA updates on the network. + # + # To avoid overwhemling the system we pre-filter here and only process + # DNSPointers for the configured record name (type) + # + if record.name != self.type or not isinstance(record, DNSPointer): + return + super().update_record(zc, now, record) + + class HaZeroconf(Zeroconf): """Zeroconf that cannot be closed.""" @@ -166,10 +189,10 @@ def setup(hass, config): ) for service in ZEROCONF: - ServiceBrowser(zeroconf, service, handlers=[service_update]) + HaServiceBrowser(zeroconf, service, handlers=[service_update]) if HOMEKIT_TYPE not in ZEROCONF: - ServiceBrowser(zeroconf, HOMEKIT_TYPE, handlers=[service_update]) + HaServiceBrowser(zeroconf, HOMEKIT_TYPE, handlers=[service_update]) return True diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 1110f9d145f..66a4a5bf44c 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -68,7 +68,7 @@ async def test_setup(hass, mock_zeroconf): with patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "ServiceBrowser", side_effect=service_update_mock + zeroconf, "HaServiceBrowser", side_effect=service_update_mock ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -87,7 +87,7 @@ async def test_setup(hass, mock_zeroconf): async def test_setup_with_default_interface(hass, mock_zeroconf): """Test default interface config.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "ServiceBrowser", side_effect=service_update_mock + zeroconf, "HaServiceBrowser", side_effect=service_update_mock ): mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component( @@ -100,7 +100,7 @@ async def test_setup_with_default_interface(hass, mock_zeroconf): async def test_setup_without_default_interface(hass, mock_zeroconf): """Test without default interface config.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "ServiceBrowser", side_effect=service_update_mock + zeroconf, "HaServiceBrowser", side_effect=service_update_mock ): mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component( @@ -117,7 +117,7 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf): ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "ServiceBrowser", side_effect=service_update_mock + zeroconf, "HaServiceBrowser", side_effect=service_update_mock ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock("LIFX bulb") assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -134,7 +134,7 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf): ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "ServiceBrowser", side_effect=service_update_mock + zeroconf, "HaServiceBrowser", side_effect=service_update_mock ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "Rachio-fa46ba" @@ -153,7 +153,7 @@ async def test_homekit_match_full(hass, mock_zeroconf): ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "ServiceBrowser", side_effect=service_update_mock + zeroconf, "HaServiceBrowser", side_effect=service_update_mock ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock("BSB002") assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})