Don't write storage to disk while stopping (#33456)

* Don't write storage to disk while stopping

* rework change

* lint

* remove delay save and schedule final write at stop

* update tests

* fix test component using private methods

* cleanup

* always listen

* use stop in restore state again

* whitelist JSON exceptions for later

* review comment

* make zwave tests use mock storage
This commit is contained in:
David F. Mulcahey 2020-04-02 13:25:28 -04:00 committed by GitHub
parent 9fd0192441
commit 8b0a0ee521
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 147 additions and 34 deletions

View file

@ -152,7 +152,7 @@ class CoreState(enum.Enum):
starting = "STARTING" starting = "STARTING"
running = "RUNNING" running = "RUNNING"
stopping = "STOPPING" stopping = "STOPPING"
writing_data = "WRITING_DATA" final_write = "FINAL_WRITE"
def __str__(self) -> str: def __str__(self) -> str:
"""Return the event.""" """Return the event."""
@ -414,7 +414,7 @@ class HomeAssistant:
# regardless of the state of the loop. # regardless of the state of the loop.
if self.state == CoreState.not_running: # just ignore if self.state == CoreState.not_running: # just ignore
return return
if self.state == CoreState.stopping or self.state == CoreState.writing_data: if self.state == CoreState.stopping or self.state == CoreState.final_write:
_LOGGER.info("async_stop called twice: ignored") _LOGGER.info("async_stop called twice: ignored")
return return
if self.state == CoreState.starting: if self.state == CoreState.starting:
@ -428,7 +428,7 @@ class HomeAssistant:
await self.async_block_till_done() await self.async_block_till_done()
# stage 2 # stage 2
self.state = CoreState.writing_data self.state = CoreState.final_write
self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE)
await self.async_block_till_done() await self.async_block_till_done()

View file

@ -4,10 +4,7 @@ from datetime import datetime, timedelta
import logging import logging
from typing import Any, Awaitable, Dict, List, Optional, Set, cast from typing import Any, Awaitable, Dict, List, Optional, Set, cast
from homeassistant.const import ( from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
EVENT_HOMEASSISTANT_FINAL_WRITE,
EVENT_HOMEASSISTANT_START,
)
from homeassistant.core import ( from homeassistant.core import (
CoreState, CoreState,
HomeAssistant, HomeAssistant,
@ -187,9 +184,7 @@ class RestoreStateData:
async_track_time_interval(self.hass, _async_dump_states, STATE_DUMP_INTERVAL) async_track_time_interval(self.hass, _async_dump_states, STATE_DUMP_INTERVAL)
# Dump states when stopping hass # Dump states when stopping hass
self.hass.bus.async_listen_once( self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_dump_states)
EVENT_HOMEASSISTANT_FINAL_WRITE, _async_dump_states
)
@callback @callback
def async_restore_entity_added(self, entity_id: str) -> None: def async_restore_entity_added(self, entity_id: str) -> None:

View file

@ -6,7 +6,7 @@ import os
from typing import Any, Callable, Dict, List, Optional, Type, Union from typing import Any, Callable, Dict, List, Optional, Type, Union
from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.util import json as json_util from homeassistant.util import json as json_util
@ -72,7 +72,7 @@ class Store:
self._private = private self._private = private
self._data: Optional[Dict[str, Any]] = None self._data: Optional[Dict[str, Any]] = None
self._unsub_delay_listener: Optional[CALLBACK_TYPE] = None self._unsub_delay_listener: Optional[CALLBACK_TYPE] = None
self._unsub_stop_listener: Optional[CALLBACK_TYPE] = None self._unsub_final_write_listener: Optional[CALLBACK_TYPE] = None
self._write_lock = asyncio.Lock() self._write_lock = asyncio.Lock()
self._load_task: Optional[asyncio.Future] = None self._load_task: Optional[asyncio.Future] = None
self._encoder = encoder self._encoder = encoder
@ -132,7 +132,12 @@ class Store:
self._data = {"version": self.version, "key": self.key, "data": data} self._data = {"version": self.version, "key": self.key, "data": data}
self._async_cleanup_delay_listener() self._async_cleanup_delay_listener()
self._async_cleanup_stop_listener() self._async_cleanup_final_write_listener()
if self.hass.state == CoreState.stopping:
self._async_ensure_final_write_listener()
return
await self._async_handle_write_data() await self._async_handle_write_data()
@callback @callback
@ -141,27 +146,31 @@ class Store:
self._data = {"version": self.version, "key": self.key, "data_func": data_func} self._data = {"version": self.version, "key": self.key, "data_func": data_func}
self._async_cleanup_delay_listener() self._async_cleanup_delay_listener()
self._async_cleanup_final_write_listener()
if self.hass.state == CoreState.stopping:
self._async_ensure_final_write_listener()
return
self._unsub_delay_listener = async_call_later( self._unsub_delay_listener = async_call_later(
self.hass, delay, self._async_callback_delayed_write self.hass, delay, self._async_callback_delayed_write
) )
self._async_ensure_final_write_listener()
self._async_ensure_stop_listener()
@callback @callback
def _async_ensure_stop_listener(self): def _async_ensure_final_write_listener(self):
"""Ensure that we write if we quit before delay has passed.""" """Ensure that we write if we quit before delay has passed."""
if self._unsub_stop_listener is None: if self._unsub_final_write_listener is None:
self._unsub_stop_listener = self.hass.bus.async_listen_once( self._unsub_final_write_listener = self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_callback_stop_write EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_callback_final_write
) )
@callback @callback
def _async_cleanup_stop_listener(self): def _async_cleanup_final_write_listener(self):
"""Clean up a stop listener.""" """Clean up a stop listener."""
if self._unsub_stop_listener is not None: if self._unsub_final_write_listener is not None:
self._unsub_stop_listener() self._unsub_final_write_listener()
self._unsub_stop_listener = None self._unsub_final_write_listener = None
@callback @callback
def _async_cleanup_delay_listener(self): def _async_cleanup_delay_listener(self):
@ -172,13 +181,17 @@ class Store:
async def _async_callback_delayed_write(self, _now): async def _async_callback_delayed_write(self, _now):
"""Handle a delayed write callback.""" """Handle a delayed write callback."""
# catch the case where a call is scheduled and then we stop Home Assistant
if self.hass.state == CoreState.stopping:
self._async_ensure_final_write_listener()
return
self._unsub_delay_listener = None self._unsub_delay_listener = None
self._async_cleanup_stop_listener() self._async_cleanup_final_write_listener()
await self._async_handle_write_data() await self._async_handle_write_data()
async def _async_callback_stop_write(self, _event): async def _async_callback_final_write(self, _event):
"""Handle a write because Home Assistant is stopping.""" """Handle a write because Home Assistant is in final write state."""
self._unsub_stop_listener = None self._unsub_final_write_listener = None
self._async_cleanup_delay_listener() self._async_cleanup_delay_listener()
await self._async_handle_write_data() await self._async_handle_write_data()

View file

@ -1005,7 +1005,7 @@ async def flush_store(store):
if store._data is None: if store._data is None:
return return
store._async_cleanup_stop_listener() store._async_cleanup_final_write_listener()
store._async_cleanup_delay_listener() store._async_cleanup_delay_listener()
await store._async_handle_write_data() await store._async_handle_write_data()
@ -1018,7 +1018,7 @@ async def get_system_health_info(hass, domain):
def mock_integration(hass, module): def mock_integration(hass, module):
"""Mock an integration.""" """Mock an integration."""
integration = loader.Integration( integration = loader.Integration(
hass, f"homeassistant.components.{module.DOMAIN}", None, module.mock_manifest(), hass, f"homeassistant.components.{module.DOMAIN}", None, module.mock_manifest()
) )
_LOGGER.info("Adding mock integration: %s", module.DOMAIN) _LOGGER.info("Adding mock integration: %s", module.DOMAIN)

View file

@ -28,6 +28,7 @@ from tests.common import (
get_test_home_assistant, get_test_home_assistant,
mock_coro, mock_coro,
mock_registry, mock_registry,
mock_storage,
) )
from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue
@ -827,6 +828,8 @@ class TestZWaveDeviceEntityValues(unittest.TestCase):
def setUp(self): def setUp(self):
"""Initialize values for this testcase class.""" """Initialize values for this testcase class."""
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
self.mock_storage = mock_storage()
self.mock_storage.__enter__()
self.hass.start() self.hass.start()
self.registry = mock_registry(self.hass) self.registry = mock_registry(self.hass)
@ -862,6 +865,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase):
def tearDown(self): # pylint: disable=invalid-name def tearDown(self): # pylint: disable=invalid-name
"""Stop everything that was started.""" """Stop everything that was started."""
self.hass.stop() self.hass.stop()
self.mock_storage.__exit__(None, None, None)
@patch.object(zwave, "import_module") @patch.object(zwave, "import_module")
@patch.object(zwave, "discovery") @patch.object(zwave, "discovery")
@ -1194,6 +1198,8 @@ class TestZWaveServices(unittest.TestCase):
def setUp(self): def setUp(self):
"""Initialize values for this testcase class.""" """Initialize values for this testcase class."""
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
self.mock_storage = mock_storage()
self.mock_storage.__enter__()
self.hass.start() self.hass.start()
# Initialize zwave # Initialize zwave
@ -1209,6 +1215,7 @@ class TestZWaveServices(unittest.TestCase):
self.hass.services.call("zwave", "stop_network", {}) self.hass.services.call("zwave", "stop_network", {})
self.hass.block_till_done() self.hass.block_till_done()
self.hass.stop() self.hass.stop()
self.mock_storage.__exit__(None, None, None)
def test_add_node(self): def test_add_node(self):
"""Test zwave add_node service.""" """Test zwave add_node service."""

View file

@ -19,7 +19,10 @@ from homeassistant.exceptions import ServiceNotFound
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import location from homeassistant.util import location
from tests.ignore_uncaught_exceptions import IGNORE_UNCAUGHT_EXCEPTIONS from tests.ignore_uncaught_exceptions import (
IGNORE_UNCAUGHT_EXCEPTIONS,
IGNORE_UNCAUGHT_JSON_EXCEPTIONS,
)
pytest.register_assert_rewrite("tests.common") pytest.register_assert_rewrite("tests.common")
@ -104,6 +107,13 @@ def hass(loop, hass_storage, request):
continue continue
if isinstance(ex, ServiceNotFound): if isinstance(ex, ServiceNotFound):
continue continue
if (
isinstance(ex, TypeError)
and "is not JSON serializable" in str(ex)
and (request.module.__name__, request.function.__name__)
in IGNORE_UNCAUGHT_JSON_EXCEPTIONS
):
continue
raise ex raise ex
@ -211,7 +221,7 @@ def hass_client(hass, aiohttp_client, hass_access_token):
async def auth_client(): async def auth_client():
"""Return an authenticated client.""" """Return an authenticated client."""
return await aiohttp_client( return await aiohttp_client(
hass.http.app, headers={"Authorization": f"Bearer {hass_access_token}"}, hass.http.app, headers={"Authorization": f"Bearer {hass_access_token}"}
) )
return auth_client return auth_client

View file

@ -6,7 +6,11 @@ from unittest.mock import Mock, patch
import pytest import pytest
from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE from homeassistant.const import (
EVENT_HOMEASSISTANT_FINAL_WRITE,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import CoreState
from homeassistant.helpers import storage from homeassistant.helpers import storage
from homeassistant.util import dt from homeassistant.util import dt
@ -79,10 +83,18 @@ async def test_saving_with_delay(hass, store, hass_storage):
} }
async def test_saving_on_stop(hass, hass_storage): async def test_saving_on_final_write(hass, hass_storage):
"""Test delayed saves trigger when we quit Home Assistant.""" """Test delayed saves trigger when we quit Home Assistant."""
store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) store = storage.Store(hass, MOCK_VERSION, MOCK_KEY)
store.async_delay_save(lambda: MOCK_DATA, 1) store.async_delay_save(lambda: MOCK_DATA, 5)
assert store.key not in hass_storage
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
hass.state = CoreState.stopping
await hass.async_block_till_done()
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert store.key not in hass_storage assert store.key not in hass_storage
hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE)
@ -94,6 +106,43 @@ async def test_saving_on_stop(hass, hass_storage):
} }
async def test_not_delayed_saving_while_stopping(hass, hass_storage):
"""Test delayed saves don't write after the stop event has fired."""
store = storage.Store(hass, MOCK_VERSION, MOCK_KEY)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
hass.state = CoreState.stopping
store.async_delay_save(lambda: MOCK_DATA, 1)
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=2))
await hass.async_block_till_done()
assert store.key not in hass_storage
async def test_not_delayed_saving_after_stopping(hass, hass_storage):
"""Test delayed saves don't write after stop if issued before stopping Home Assistant."""
store = storage.Store(hass, MOCK_VERSION, MOCK_KEY)
store.async_delay_save(lambda: MOCK_DATA, 10)
assert store.key not in hass_storage
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
hass.state = CoreState.stopping
await hass.async_block_till_done()
assert store.key not in hass_storage
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=15))
await hass.async_block_till_done()
assert store.key not in hass_storage
async def test_not_saving_while_stopping(hass, hass_storage):
"""Test saves don't write when stopping Home Assistant."""
store = storage.Store(hass, MOCK_VERSION, MOCK_KEY)
hass.state = CoreState.stopping
await store.async_save(MOCK_DATA)
assert store.key not in hass_storage
async def test_loading_while_delay(hass, store, hass_storage): async def test_loading_while_delay(hass, store, hass_storage):
"""Test we load new data even if not written yet.""" """Test we load new data even if not written yet."""
await store.async_save({"delay": "no"}) await store.async_save({"delay": "no"})

View file

@ -89,3 +89,42 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [
("tests.components.yr.test_sensor", "test_forecast_setup"), ("tests.components.yr.test_sensor", "test_forecast_setup"),
("tests.components.zwave.test_init", "test_power_schemes"), ("tests.components.zwave.test_init", "test_power_schemes"),
] ]
IGNORE_UNCAUGHT_JSON_EXCEPTIONS = [
("tests.components.spotify.test_config_flow", "test_full_flow"),
("tests.components.smartthings.test_init", "test_config_entry_loads_platforms"),
(
"tests.components.smartthings.test_init",
"test_scenes_unauthorized_loads_platforms",
),
(
"tests.components.smartthings.test_init",
"test_config_entry_loads_unconnected_cloud",
),
("tests.components.samsungtv.test_config_flow", "test_ssdp"),
("tests.components.samsungtv.test_config_flow", "test_user_websocket"),
("tests.components.samsungtv.test_config_flow", "test_user_already_configured"),
("tests.components.samsungtv.test_config_flow", "test_autodetect_websocket"),
("tests.components.samsungtv.test_config_flow", "test_autodetect_websocket_ssl"),
("tests.components.samsungtv.test_config_flow", "test_ssdp_already_configured"),
("tests.components.samsungtv.test_config_flow", "test_ssdp_noprefix"),
("tests.components.samsungtv.test_config_flow", "test_user_legacy"),
("tests.components.samsungtv.test_config_flow", "test_autodetect_legacy"),
(
"tests.components.samsungtv.test_media_player",
"test_select_source_invalid_source",
),
(
"tests.components.samsungtv.test_media_player",
"test_play_media_channel_as_string",
),
(
"tests.components.samsungtv.test_media_player",
"test_play_media_channel_as_non_positive",
),
("tests.components.samsungtv.test_media_player", "test_turn_off_websocket"),
("tests.components.samsungtv.test_media_player", "test_play_media_invalid_type"),
("tests.components.harmony.test_config_flow", "test_form_import"),
("tests.components.harmony.test_config_flow", "test_form_ssdp"),
("tests.components.harmony.test_config_flow", "test_user_form"),
]