mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Shield config entry removal from client disconnect (#178207)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""Http views to control the config manager."""
|
||||
|
||||
from asyncio import shield
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
@@ -110,8 +111,18 @@ class ConfigManagerEntryResourceView(HomeAssistantView):
|
||||
|
||||
hass = request.app[KEY_HASS]
|
||||
|
||||
# Shield the removal from cancellation on connection drop, otherwise the
|
||||
# entry is dropped from memory but never saved or cleaned up. The task is
|
||||
# created through hass so a strong reference is held for its lifetime,
|
||||
# which keeps it from being garbage collected once the request handler
|
||||
# has gone away.
|
||||
remove_task = hass.async_create_task(
|
||||
hass.config_entries.async_remove(entry_id),
|
||||
f"config entry remove {entry_id}",
|
||||
)
|
||||
|
||||
try:
|
||||
result = await hass.config_entries.async_remove(entry_id)
|
||||
result = await shield(remove_task)
|
||||
except config_entries.UnknownEntry:
|
||||
return self.json_message("Invalid entry specified", HTTPStatus.NOT_FOUND)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Test config entries API."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Generator
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
@@ -13,6 +14,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries as core_ce, data_entry_flow, loader
|
||||
from homeassistant.components.config import DOMAIN, config_entries
|
||||
from homeassistant.components.http import KEY_HASS
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -269,6 +271,55 @@ async def test_remove_entry(hass: HomeAssistant, client: TestClient) -> None:
|
||||
assert len(hass.config_entries.async_entries()) == 0
|
||||
|
||||
|
||||
async def test_remove_entry_survives_client_disconnect(
|
||||
hass: HomeAssistant, hass_admin_user: MockUser
|
||||
) -> None:
|
||||
"""Test a client disconnect does not truncate the removal.
|
||||
|
||||
The HTTP runner is created with handler_cancellation=True, so a disconnect
|
||||
cancels the request handler. Removal deletes the entry from memory before
|
||||
awaiting the integration, so an unshielded cancel leaves the entry on disk
|
||||
and its registry rows behind.
|
||||
"""
|
||||
entry = MockConfigEntry(domain="test", state=core_ce.ConfigEntryState.LOADED)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
removing = asyncio.Event()
|
||||
disconnected = asyncio.Event()
|
||||
original_async_remove = core_ce.ConfigEntry.async_remove
|
||||
|
||||
async def blocking_async_remove(self: core_ce.ConfigEntry, *args: Any) -> None:
|
||||
"""Stall inside the removal, so the cancel lands on this await."""
|
||||
removing.set()
|
||||
await disconnected.wait()
|
||||
await original_async_remove(self, *args)
|
||||
|
||||
view = config_entries.ConfigManagerEntryResourceView()
|
||||
request = Mock()
|
||||
request.__getitem__ = Mock(side_effect={"hass_user": hass_admin_user}.__getitem__)
|
||||
request.app = {KEY_HASS: hass}
|
||||
|
||||
with (
|
||||
patch.object(core_ce.ConfigEntry, "async_remove", blocking_async_remove),
|
||||
patch.object(hass.config_entries, "_async_schedule_save") as mock_schedule_save,
|
||||
):
|
||||
task = hass.async_create_task(view.delete(request, entry.entry_id))
|
||||
await removing.wait()
|
||||
|
||||
# The client goes away mid-removal.
|
||||
task.cancel()
|
||||
disconnected.set()
|
||||
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# The rest of the removal must still have run.
|
||||
assert mock_schedule_save.called
|
||||
assert hass.config_entries.async_entries() == []
|
||||
|
||||
|
||||
async def test_reload_entry(hass: HomeAssistant, client: TestClient) -> None:
|
||||
"""Test reloading an entry via the API."""
|
||||
entry = MockConfigEntry(domain="test", state=core_ce.ConfigEntryState.LOADED)
|
||||
|
||||
Reference in New Issue
Block a user