thlucas1/homeassistantcomponent_spotifyplus

HA 2026.6 - async_update_entry from a thread other than the event loop

Closed this issue · 16 comments

System Health details

System Information

version core-2024.6.0b1
installation_type Home Assistant OS
dev false
hassio true
docker true
user root
virtualenv false
python_version 3.12.2
os_name Linux
os_version 6.6.28-haos-raspi
arch aarch64
timezone Europe/London
config_dir /config
Home Assistant Community Store
GitHub API ok
GitHub Content ok
GitHub Web ok
GitHub API Calls Remaining 5000
Installed Version 1.34.0
Stage running
Available Repositories 1478
Downloaded Repositories 72
HACS Data ok
Home Assistant Cloud
logged_in true
subscription_expiration 17 June 2024 at 01:00
relayer_connected true
relayer_region eu-central-1
remote_enabled true
remote_connected true
alexa_enabled true
google_enabled true
remote_server eu-central-1-10.ui.nabu.casa
certificate_status ready
instance_id 7a6e260cca76465b8d6c48b7497998f9
can_reach_cert_server ok
can_reach_cloud_auth ok
can_reach_cloud ok
Home Assistant Supervisor
host_os Home Assistant OS 12.3
update_channel beta
supervisor_version supervisor-2024.05.2
agent_version 1.6.0
docker_version 25.0.5
disk_total 228.5 GB
disk_used 36.1 GB
healthy true
supported true
host_connectivity true
supervisor_connectivity true
ntp_synchronized true
virtualization
board rpi4-64
supervisor_api ok
version_api ok
installed_addons Samba share (12.3.1), Grafana (10.0.0), File editor (5.8.0), TasmoAdmin (0.30.2), InfluxDB (5.0.0), Advanced SSH & Web Terminal (18.0.0), Home Assistant Google Drive Backup (0.112.1), Zigbee2MQTT (1.37.1-1), chrony (3.0.1), UniFi Network Application (3.0.5), Nginx Proxy Manager (1.0.1), Tailscale (0.19.1), PS5 MQTT (1.3.3), ESPHome (2024.5.4), Mosquitto broker (6.4.1), Glances (0.21.1), AppDaemon (0.16.6), Increase Swap (1.1.3), SQLite Web (4.1.2), MQTT Explorer (browser-1.0.3)
Dashboards
dashboards 9
resources 34
views 22
mode storage
Recorder
oldest_recorder_run 27 May 2024 at 09:06
current_recorder_run 30 May 2024 at 08:27
estimated_db_size 2213.37 MiB
database_engine sqlite
database_version 3.44.2
Solcast PV Forecast
can_reach_server ok
used_requests 3
rooftop_site_count 1
Sonoff
version 3.7.3 (e240aaf)
cloud_online 0 / 2
local_online 1 / 1
SpotifyPlus
integration_version v1.0.19
clients_configured 1: Neil Brownlee (premium)
api_endpoint_reachable ok

Checklist

  • I have enabled SmartInspect debug logging for my installation.
  • I have filled out the issue template to the best of my ability.
  • This issue only contains 1 issue (if you have multiple issues, open one issue for each issue).
  • This issue is not a duplicate issue of any previous issues..

Describe the issue

HA2024.6.0b1 - errors reported when trying to use the integration. Log below.

Reproduction steps

  1. Automation fails.

...

Debug logs

Logger: homeassistant.components.automation.song_to_spotify_office
Source: components/automation/__init__.py:744
integration: Automation (documentation, issues)
First occurred: 09:33:16 (10 occurrences)
Last logged: 10:33:48

Error while executing automation automation.song_to_spotify_office: SAM0001E - An unhandled exception occured while processing method "MakeRequest". Detected that custom integration 'spotifyplus' calls hass.config_entries.async_update_entry from a thread other than the event loop, which may cause Home Assistant to crash or data to corrupt. For more information, see https://developers.home-assistant.io/docs/asyncio_thread_safety/#hassconfig_entriesasync_update_entry at custom_components/spotifyplus/__init__.py, line 1766: session.hass.config_entries.async_update_entry(. Please report it to the author of the 'spotifyplus' custom integration.

Diagnostics dump

No response

Thanks for bringing this to my attention. The fix will have to wait until Mid next week, as I am currently traveling.

It appears that the HA core team pulled another fast one on me with the 06 release like they did with the 05 release. This integration has been working fine since it was released, and now they are starting to enforce checks for updating state in the event loop! Should be an easy fix. Will keep you posted..

Thanks - enjoy your holiday :)

@bdraco
I'm running into an issue with the HA 2024.6 release, specifically trying to call async_update_entry from a thread not on the event loop. I now understand that updating a config entry must be done in the event loop thread, as there is no sync API to update config entries. I am now trying to use hass.add_job to schedule a function in the event loop that calls hass.config_entries.async_update_entry.

The problem I am having is passing a keyword argument to the async_update_entry method - old call looks like this:

session.hass.config_entries.async_update_entry(
    session.config_entry, 
    data={**session.config_entry.data, "token": token}
    )

I am trying to replace it with the following:

session.hass.add_job(
    session.hass.config_entries.async_update_entry,
    session.config_entry, 
    data={**session.config_entry.data, "token": token}
)

The replaced code is generating the following exception (logged in the System log) when trying to execute:

Error doing job: Exception in callback ConfigEntries.async_update_entry(<ConfigEntry ...xfrmd4bpfhqke>, {'auth_implementation': 'spotifyplus_...6ecc68097a218', 'description': '(Premium account)', 'id': 'xxxxxx', 'name': 'Todd L', ...})
Traceback (most recent call last):
  File "/usr/local/lib/python3.12/asyncio/events.py", line 88, in _run
    self._context.run(self._callback, *self._args)
TypeError: ConfigEntries.async_update_entry() takes 2 positional arguments but 3 were given

Is there a way to pass keyword arguments to the hass.add_job method?

Renamed the thread to better reflect the underlying error. I am back from vacation, and working on a fix for this issue.

You can use functools.partial

You can use functools.partial

@bdraco That seemed to do the trick - thank you so much!

Make sure the job you are running is wrapped in @callback or it will run in the executor

Make sure the job you are running is wrapped in @callback or it will run in the executor
@bdraco
It is calling the async_update_entry, which is marked with a @callback:

    @callback
    def async_update_entry(
        self,
        entry: ConfigEntry,
        *,
        data: Mapping[str, Any] | UndefinedType = UNDEFINED,
        minor_version: int | UndefinedType = UNDEFINED,
        options: Mapping[str, Any] | UndefinedType = UNDEFINED,
        pref_disable_new_entities: bool | UndefinedType = UNDEFINED,
        pref_disable_polling: bool | UndefinedType = UNDEFINED,
        title: str | UndefinedType = UNDEFINED,
        unique_id: str | None | UndefinedType = UNDEFINED,
        version: int | UndefinedType = UNDEFINED,
    ) -> bool:
        if entry.entry_id not in self._entries:
            raise UnknownEntry(entry.entry_id)

        self.hass.verify_event_loop_thread("hass.config_entries.async_update_entry")

Thanks again for your assistance.

@Thrasher2020
Fixed with release v1.0.20 of the SpotifyPlus integration

Give it a try and let me know if you have issues.
Thanks.

Testing right now. Will let you know :)

Thank you.

Make sure the job you are running is wrapped in @callback or it will run in the executor

@bdraco
Just curious - are there performance (or multi-threading) consequences to running a job in the executor, versus the @callback method? Any HA documentation that I could look at to learn more about the differences or when to use one over the other?

It seems to be running fine with the @callback method, but I have other methods that I am calling that all use the await hass.async_add_executor_job(...) syntax. I think I have to use that syntax in these cases, as they are calls to underlying API's that do not support async, but not sure.

I appreciate your help and guidance.

hass.add_job is generally the way to get back to the async (main thread). - as long as its decorated with @callback
https://github.com/home-assistant/core/blob/8c025ea1f7f254251d5d63f436b6da27363c83e4/homeassistant/core.py#L580
https://github.com/home-assistant/core/blob/8c025ea1f7f254251d5d63f436b6da27363c83e4/homeassistant/core.py#L745

await hass.async_add_executor_job is generally the way to offload work that would block the event loop to another thread. There is a limit to 64 executor jobs running at the same time. It also needs to create a future, and schedule it on the other thread, wait for the result, and safely return it to the main thread so there is some additional cost for doing so which sometimes is even more expensive than the work being done.

Ideally everything is async and runs in the main thread and you don't have the overhead of switching between threads since the cost of doing it safely can comparatively be rather high for short/non-cpu bound operations. Also if everything is async you don't have to worry about thread-safety issues, only asyncio problems.

Thanks for that explanation.

Unfortunately, the underlying API’s that I am interfacing with do not support async operations. It sounds like I am stuck executing those types of operations with the async_add_executor_job.

Any functions I write in the HA integration can be written with async and decorated with @callback to make them more efficient.

Please correct me if I’m wrong on those assumptions.

Thanks for that explanation.

Unfortunately, the underlying API’s that I am interfacing with do not support async operations. It sounds like I am stuck executing those types of operations with the async_add_executor_job.

In that case I generally make a new asyncio python package, but thats likely more than you want to do.

Any functions I write in the HA integration can be written with async and decorated with @callback to make them more efficient.

There is a cost to jumping between threads so if you are already in another thread and the work you are doing is thread safe, its best not to jump back into the event loop thread unless the work needs to explicitly do something that would not be thread safe unless it ran in the event loop thread (like working with asyncio primitives)

Please correct me if I’m wrong on those assumptions.

Testing right now. Will let you know :)

Thank you.

I can confirm everything is working as before now. Thanks :)

Good to hear - closing the issue.
Thanks