1
mirror of https://github.com/home-assistant/core synced 2024-09-06 10:29:55 +02:00

Fixing race condition in geo location platforms (#17581)

* fixed race condition where obsolete entities kept listening for dispatcher signals

* making tests python 3.5 compatible
This commit is contained in:
Malte Franken 2018-10-19 00:25:48 +11:00 committed by Martin Hjelmare
parent 7e91c0dc83
commit 7eb6e49df7
3 changed files with 100 additions and 4 deletions

View File

@ -152,19 +152,23 @@ class GeoJsonLocationEvent(GeoLocationEvent):
self._distance = None
self._latitude = None
self._longitude = None
self._remove_signal_delete = None
self._remove_signal_update = None
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
async_dispatcher_connect(
self._remove_signal_delete = async_dispatcher_connect(
self.hass, SIGNAL_DELETE_ENTITY.format(self._external_id),
self._delete_callback)
async_dispatcher_connect(
self._remove_signal_update = async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_ENTITY.format(self._external_id),
self._update_callback)
@callback
def _delete_callback(self):
"""Remove this entity."""
self._remove_signal_delete()
self._remove_signal_update()
self.hass.async_create_task(self.async_remove())
@callback

View File

@ -183,19 +183,23 @@ class NswRuralFireServiceLocationEvent(GeoLocationEvent):
self._fire = None
self._size = None
self._responsible_agency = None
self._remove_signal_delete = None
self._remove_signal_update = None
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
async_dispatcher_connect(
self._remove_signal_delete = async_dispatcher_connect(
self.hass, SIGNAL_DELETE_ENTITY.format(self._external_id),
self._delete_callback)
async_dispatcher_connect(
self._remove_signal_update = async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_ENTITY.format(self._external_id),
self._update_callback)
@callback
def _delete_callback(self):
"""Remove this entity."""
self._remove_signal_delete()
self._remove_signal_update()
self.hass.async_create_task(self.async_remove())
@callback

View File

@ -3,6 +3,7 @@ import unittest
from unittest import mock
from unittest.mock import patch, MagicMock
import homeassistant
from homeassistant.components import geo_location
from homeassistant.components.geo_location import ATTR_SOURCE
from homeassistant.components.geo_location.geo_json_events import \
@ -138,3 +139,90 @@ class TestGeoJsonPlatform(unittest.TestCase):
all_states = self.hass.states.all()
assert len(all_states) == 0
@mock.patch('geojson_client.generic_feed.GenericFeed')
def test_setup_race_condition(self, mock_feed):
"""Test a particular race condition experienced."""
# 1. Feed returns 1 entry -> Feed manager creates 1 entity.
# 2. Feed returns error -> Feed manager removes 1 entity.
# However, this stayed on and kept listening for dispatcher signals.
# 3. Feed returns 1 entry -> Feed manager creates 1 entity.
# 4. Feed returns 1 entry -> Feed manager updates 1 entity.
# Internally, the previous entity is updating itself, too.
# 5. Feed returns error -> Feed manager removes 1 entity.
# There are now 2 entities trying to remove themselves from HA, but
# the second attempt fails of course.
# Set up some mock feed entries for this test.
mock_entry_1 = self._generate_mock_feed_entry('1234', 'Title 1', 15.5,
(-31.0, 150.0))
mock_feed.return_value.update.return_value = 'OK', [mock_entry_1]
utcnow = dt_util.utcnow()
# Patching 'utcnow' to gain more control over the timed update.
with patch('homeassistant.util.dt.utcnow', return_value=utcnow):
with assert_setup_component(1, geo_location.DOMAIN):
self.assertTrue(setup_component(self.hass, geo_location.DOMAIN,
CONFIG))
# This gives us the ability to assert the '_delete_callback'
# has been called while still executing it.
original_delete_callback = homeassistant.components\
.geo_location.geo_json_events.GeoJsonLocationEvent\
._delete_callback
def mock_delete_callback(entity):
original_delete_callback(entity)
with patch('homeassistant.components.geo_location'
'.geo_json_events.GeoJsonLocationEvent'
'._delete_callback',
side_effect=mock_delete_callback,
autospec=True) as mocked_delete_callback:
# Artificially trigger update.
self.hass.bus.fire(EVENT_HOMEASSISTANT_START)
# Collect events.
self.hass.block_till_done()
all_states = self.hass.states.all()
assert len(all_states) == 1
# Simulate an update - empty data, removes all entities
mock_feed.return_value.update.return_value = 'ERROR', None
fire_time_changed(self.hass, utcnow + SCAN_INTERVAL)
self.hass.block_till_done()
assert mocked_delete_callback.call_count == 1
all_states = self.hass.states.all()
assert len(all_states) == 0
# Simulate an update - 1 entry
mock_feed.return_value.update.return_value = 'OK', [
mock_entry_1]
fire_time_changed(self.hass, utcnow + 2 * SCAN_INTERVAL)
self.hass.block_till_done()
all_states = self.hass.states.all()
assert len(all_states) == 1
# Simulate an update - 1 entry
mock_feed.return_value.update.return_value = 'OK', [
mock_entry_1]
fire_time_changed(self.hass, utcnow + 3 * SCAN_INTERVAL)
self.hass.block_till_done()
all_states = self.hass.states.all()
assert len(all_states) == 1
# Reset mocked method for the next test.
mocked_delete_callback.reset_mock()
# Simulate an update - empty data, removes all entities
mock_feed.return_value.update.return_value = 'ERROR', None
fire_time_changed(self.hass, utcnow + 4 * SCAN_INTERVAL)
self.hass.block_till_done()
assert mocked_delete_callback.call_count == 1
all_states = self.hass.states.all()
assert len(all_states) == 0