Skip to content

Commit

Permalink
Move thread safety check in area_registry sooner (#116265)
Browse files Browse the repository at this point in the history
It turns out we have custom components that are writing to the area registry using the async APIs from threads. We now catch it at the point async_fire is called. Instead we should check sooner and use async_fire_internal so we catch the unsafe operation before it can corrupt the registry.
  • Loading branch information
bdraco authored and balloob committed Apr 27, 2024
1 parent 85baa25 commit 46dff86
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 2 deletions.
11 changes: 9 additions & 2 deletions homeassistant/helpers/area_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ def async_create(
picture: str | None = None,
) -> AreaEntry:
"""Create a new area."""
self.hass.verify_event_loop_thread("async_create")
normalized_name = normalize_name(name)

if self.async_get_area_by_name(name):
Expand All @@ -221,7 +222,7 @@ def async_create(
assert area.id is not None
self.areas[area.id] = area
self.async_schedule_save()
self.hass.bus.async_fire(
self.hass.bus.async_fire_internal(
EVENT_AREA_REGISTRY_UPDATED,
EventAreaRegistryUpdatedData(action="create", area_id=area.id),
)
Expand All @@ -230,14 +231,15 @@ def async_create(
@callback
def async_delete(self, area_id: str) -> None:
"""Delete area."""
self.hass.verify_event_loop_thread("async_delete")
device_registry = dr.async_get(self.hass)
entity_registry = er.async_get(self.hass)
device_registry.async_clear_area_id(area_id)
entity_registry.async_clear_area_id(area_id)

del self.areas[area_id]

self.hass.bus.async_fire(
self.hass.bus.async_fire_internal(
EVENT_AREA_REGISTRY_UPDATED,
EventAreaRegistryUpdatedData(action="remove", area_id=area_id),
)
Expand Down Expand Up @@ -266,6 +268,10 @@ def async_update(
name=name,
picture=picture,
)
# Since updated may be the old or the new and we always fire
# an event even if nothing has changed we cannot use async_fire_internal
# here because we do not know if the thread safety check already
# happened or not in _async_update.
self.hass.bus.async_fire(
EVENT_AREA_REGISTRY_UPDATED,
EventAreaRegistryUpdatedData(action="update", area_id=area_id),
Expand Down Expand Up @@ -306,6 +312,7 @@ def _async_update(
if not new_values:
return old

self.hass.verify_event_loop_thread("_async_update")
new = self.areas[area_id] = dataclasses.replace(old, **new_values) # type: ignore[arg-type]

self.async_schedule_save()
Expand Down
38 changes: 38 additions & 0 deletions tests/helpers/test_area_registry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for the Area Registry."""

from functools import partial
from typing import Any

import pytest
Expand Down Expand Up @@ -491,3 +492,40 @@ async def test_entries_for_label(

assert not ar.async_entries_for_label(area_registry, "unknown")
assert not ar.async_entries_for_label(area_registry, "")


async def test_async_get_or_create_thread_checks(
hass: HomeAssistant, area_registry: ar.AreaRegistry
) -> None:
"""We raise when trying to create in the wrong thread."""
with pytest.raises(
RuntimeError,
match="Detected code that calls async_create from a thread. Please report this issue.",
):
await hass.async_add_executor_job(area_registry.async_create, "Mock1")


async def test_async_update_thread_checks(
hass: HomeAssistant, area_registry: ar.AreaRegistry
) -> None:
"""We raise when trying to update in the wrong thread."""
area = area_registry.async_create("Mock1")
with pytest.raises(
RuntimeError,
match="Detected code that calls _async_update from a thread. Please report this issue.",
):
await hass.async_add_executor_job(
partial(area_registry.async_update, area.id, name="Mock2")
)


async def test_async_delete_thread_checks(
hass: HomeAssistant, area_registry: ar.AreaRegistry
) -> None:
"""We raise when trying to delete in the wrong thread."""
area = area_registry.async_create("Mock1")
with pytest.raises(
RuntimeError,
match="Detected code that calls async_delete from a thread. Please report this issue.",
):
await hass.async_add_executor_job(area_registry.async_delete, area.id)

0 comments on commit 46dff86

Please sign in to comment.