Reduce latency in storage by making the tasks eager (#111500)

* Reduce latancy to load storage by making the task eager

This changes the semantics a bit under the hood because it
can raise sooner which means we do not store the task
as _load_task if it raises right away. That means
concurrent calls that result in failure are likely to try
again now which will be a tiny performance hit for this
case.

* fix

* will now finish in time
This commit is contained in:
J. Nick Koston 2024-02-27 18:27:37 -10:00 committed by GitHub
parent e74e1e3008
commit 78bb6dbe75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 32 additions and 7 deletions

View File

@ -133,12 +133,16 @@ class Store(Generic[_T]):
Will ensure that when a call comes in while another one is in progress, Will ensure that when a call comes in while another one is in progress,
the second call will wait and return the result of the first call. the second call will wait and return the result of the first call.
""" """
if self._load_task is None: if self._load_task:
self._load_task = self.hass.async_create_task( return await self._load_task
self._async_load(), f"Storage load {self.key}"
)
return await self._load_task load_task = self.hass.async_create_task(
self._async_load(), f"Storage load {self.key}", eager_start=True
)
if not load_task.done():
# Only set the load task if it didn't complete immediately
self._load_task = load_task
return await load_task
async def _async_load(self) -> _T | None: async def _async_load(self) -> _T | None:
"""Load the data and ensure the task is removed.""" """Load the data and ensure the task is removed."""
@ -318,7 +322,9 @@ class Store(Generic[_T]):
# wrote. Reschedule the timer to the next write time. # wrote. Reschedule the timer to the next write time.
self._async_reschedule_delayed_write(self._next_write_time) self._async_reschedule_delayed_write(self._next_write_time)
return return
self.hass.async_create_task(self._async_callback_delayed_write()) self.hass.async_create_task(
self._async_callback_delayed_write(), eager_start=True
)
@callback @callback
def _async_ensure_final_write_listener(self) -> None: def _async_ensure_final_write_listener(self) -> None:

View File

@ -115,7 +115,7 @@ async def test_loading_parallel(
results = await asyncio.gather(store.async_load(), store.async_load()) results = await asyncio.gather(store.async_load(), store.async_load())
assert results[0] == MOCK_DATA assert results[0] == MOCK_DATA
assert results[0] is results[1] assert results[1] == MOCK_DATA
assert caplog.text.count(f"Loading data for {store.key}") assert caplog.text.count(f"Loading data for {store.key}")
@ -795,6 +795,25 @@ async def test_os_error_is_fatal(tmpdir: py.path.local) -> None:
): ):
await store.async_load() await store.async_load()
# Verify second load is also failing
with pytest.raises(OSError), patch(
"homeassistant.helpers.storage.json_util.load_json", side_effect=OSError
):
await store.async_load()
await hass.async_stop(force=True)
async def test_json_load_failure(tmpdir: py.path.local) -> None:
"""Test json load raising HomeAssistantError."""
async with async_test_home_assistant() as hass:
tmp_storage = await hass.async_add_executor_job(tmpdir.mkdir, "temp_storage")
hass.config.config_dir = tmp_storage
store = storage.Store(
hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1
)
await store.async_save({"hello": "world"})
base_os_error = OSError() base_os_error = OSError()
base_os_error.errno = 30 base_os_error.errno = 30
home_assistant_error = HomeAssistantError() home_assistant_error = HomeAssistantError()