From 620433a79dacf442fe0e181b1fa5ab0d29ca7ca8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Mar 2024 14:05:08 -1000 Subject: [PATCH] Run coroutines as eager tasks in async_run_hass_job (#111683) * Run coroutines as eager tasks in async_run_hass_job Note that this does not change async_add_hass_job Do not merge this. For test run only * Phase out periodic tasks * false by default or some tests will block forever, will need to fix each one manually * kwarg works * kwarg works * kwarg works * fixes * fix more tests * fix more tests * fix lifx * opensky * pvpc_hourly_pricing * adjust more * adjust more * smarttub * adjust more * adjust more * adjust more * adjust more * adjust * no eager executor * zha * qnap_qsw * fix more * fix fix * docs * its a wrapper now * add more coverage * coverage * cover all combos * more fixes * more fixes * more fixes * remaining issues are legit bugs in tests * make tplink test more predictable * more fixes * feedreader * grind out some more * make test race safe * limit first scope to triggers * one more * Start tasks eagerly in for async_at_start(ed) A few of these can avoid being scheduled on the loop during startup * fix cloud * Revert "fix cloud" This reverts commit 5eb3ce695da788bcae649f82c9527c0f9307139c. * fix test to do what start does * flip flag * flip flag * Fix here_travel_time creating many refresh requests at startup - Each entity would try to refresh the coordinator which created many tasks. Move the refresh to a single async_at_started - The tests fired the EVENT_HOMEASSISTANT_START event but the code used async_at_started which only worked because the tests did not set CoreState to not_running * fix azure * remove kw * remove kw * rip * cover * more rips * more rips * more rips --- .../components/azure_event_hub/__init__.py | 2 +- .../homeassistant/triggers/event.py | 1 - .../homeassistant/triggers/homeassistant.py | 1 - .../homeassistant/triggers/numeric_state.py | 1 - .../homeassistant/triggers/state.py | 1 - .../components/homeassistant/triggers/time.py | 1 - .../homeassistant/triggers/time_pattern.py | 1 - homeassistant/components/ssdp/__init__.py | 2 +- homeassistant/core.py | 12 +++----- homeassistant/helpers/debounce.py | 4 +-- homeassistant/helpers/discovery.py | 8 ++--- homeassistant/helpers/dispatcher.py | 2 +- homeassistant/helpers/event.py | 10 +++---- homeassistant/helpers/integration_platform.py | 3 +- homeassistant/helpers/service.py | 6 ++-- homeassistant/helpers/start.py | 4 +-- tests/test_core.py | 30 ++++++++++++------- 17 files changed, 40 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py index 668444f9990..0a84ca44141 100644 --- a/homeassistant/components/azure_event_hub/__init__.py +++ b/homeassistant/components/azure_event_hub/__init__.py @@ -158,7 +158,7 @@ class AzureEventHub: """ logging.getLogger("azure.eventhub").setLevel(logging.WARNING) self._listener_remover = self.hass.bus.async_listen( - MATCH_ALL, self.async_listen + MATCH_ALL, self.async_listen, run_immediately=True ) self._schedule_next_send() diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 90ca131fdff..e045ece12ba 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -144,7 +144,6 @@ async def async_attach_trigger( } }, event.context, - eager_start=True, ) removes = [ diff --git a/homeassistant/components/homeassistant/triggers/homeassistant.py b/homeassistant/components/homeassistant/triggers/homeassistant.py index 600bce910cd..51e3a947a29 100644 --- a/homeassistant/components/homeassistant/triggers/homeassistant.py +++ b/homeassistant/components/homeassistant/triggers/homeassistant.py @@ -56,7 +56,6 @@ async def async_attach_trigger( "description": "Home Assistant starting", } }, - eager_start=True, ) return lambda: None diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index aad7cb7d9cb..2575af41401 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -187,7 +187,6 @@ async def async_attach_trigger( } }, to_s.context, - eager_start=True, ) @callback diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 724f0d06680..6f3183e2b40 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -184,7 +184,6 @@ async def async_attach_trigger( } }, event.context, - eager_start=True, ) if not time_delta: diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index 6e648c3994f..b1d19d54795 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -76,7 +76,6 @@ async def async_attach_trigger( "entity_id": entity_id, } }, - eager_start=True, ) @callback diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py index f69968eedbf..df49a79bcb6 100644 --- a/homeassistant/components/homeassistant/triggers/time_pattern.py +++ b/homeassistant/components/homeassistant/triggers/time_pattern.py @@ -93,7 +93,6 @@ async def async_attach_trigger( "description": "time pattern", } }, - eager_start=True, ) return async_track_time_change( diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 72e94996361..5e130a0fc06 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -232,7 +232,7 @@ def _async_process_callbacks( for callback in callbacks: try: hass.async_run_hass_job( - callback, discovery_info, ssdp_change, eager_start=True, background=True + callback, discovery_info, ssdp_change, background=True ) except Exception: # pylint: disable=broad-except _LOGGER.exception("Failed to callback info: %s", discovery_info) diff --git a/homeassistant/core.py b/homeassistant/core.py index 65f120ae8a2..2fe50e4e50b 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -764,7 +764,6 @@ class HomeAssistant: self, hassjob: HassJob[..., Coroutine[Any, Any, _R]], *args: Any, - eager_start: bool = False, background: bool = False, ) -> asyncio.Future[_R] | None: ... @@ -775,7 +774,6 @@ class HomeAssistant: self, hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], *args: Any, - eager_start: bool = False, background: bool = False, ) -> asyncio.Future[_R] | None: ... @@ -785,14 +783,12 @@ class HomeAssistant: self, hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], *args: Any, - eager_start: bool = False, background: bool = False, ) -> asyncio.Future[_R] | None: """Run a HassJob from within the event loop. This method must be run in the event loop. - If eager_start is True, coroutine functions will be scheduled eagerly. If background is True, the task will created as a background task. hassjob: HassJob @@ -809,7 +805,7 @@ class HomeAssistant: return None return self.async_add_hass_job( - hassjob, *args, eager_start=eager_start, background=background + hassjob, *args, eager_start=True, background=background ) @overload @@ -847,7 +843,7 @@ class HomeAssistant: args: parameters for method to call. """ if asyncio.iscoroutine(target): - return self.async_create_task(target) + return self.async_create_task(target, eager_start=True) # This code path is performance sensitive and uses # if TYPE_CHECKING to avoid the overhead of constructing @@ -855,7 +851,7 @@ class HomeAssistant: # https://github.com/home-assistant/core/pull/71960 if TYPE_CHECKING: target = cast(Callable[..., Coroutine[Any, Any, _R] | _R], target) - return self.async_run_hass_job(HassJob(target), *args, eager_start=True) + return self.async_run_hass_job(HassJob(target), *args) def block_till_done(self) -> None: """Block until all pending work is done.""" @@ -1369,7 +1365,7 @@ class EventBus: continue if run_immediately: try: - self._hass.async_run_hass_job(job, event, eager_start=True) + self._hass.async_run_hass_job(job, event) except Exception: # pylint: disable=broad-except _LOGGER.exception("Error running job: %s", job) else: diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index 773fff6ebc6..2fe42e19a67 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -109,7 +109,7 @@ class Debouncer(Generic[_R_co]): assert self._job is not None try: - if task := self.hass.async_run_hass_job(self._job, eager_start=True): + if task := self.hass.async_run_hass_job(self._job): await task finally: self._schedule_timer() @@ -130,7 +130,7 @@ class Debouncer(Generic[_R_co]): return try: - if task := self.hass.async_run_hass_job(self._job, eager_start=True): + if task := self.hass.async_run_hass_job(self._job): await task except Exception: # pylint: disable=broad-except self.logger.exception("Unexpected exception from %s", self.function) diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 1f8ba096a69..98419ae6bf2 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -50,9 +50,7 @@ def async_listen( @core.callback def _async_discovery_event_listener(discovered: DiscoveryDict) -> None: """Listen for discovery events.""" - hass.async_run_hass_job( - job, discovered["service"], discovered["discovered"], eager_start=True - ) + hass.async_run_hass_job(job, discovered["service"], discovered["discovered"]) async_dispatcher_connect( hass, @@ -115,9 +113,7 @@ def async_listen_platform( """Listen for platform discovery events.""" if not (platform := discovered["platform"]): return - hass.async_run_hass_job( - job, platform, discovered.get("discovered"), eager_start=True - ) + hass.async_run_hass_job(job, platform, discovered.get("discovered")) return async_dispatcher_connect( hass, diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index 20781eb8084..cb812ef071b 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -225,4 +225,4 @@ def async_dispatcher_send( if job is None: job = _generate_job(signal, target) target_list[target] = job - hass.async_run_hass_job(job, *args, eager_start=True) + hass.async_run_hass_job(job, *args) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index e9da56bf3af..44fc2356c83 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -328,7 +328,7 @@ def _async_dispatch_entity_id_event( return for job in callbacks_list.copy(): try: - hass.async_run_hass_job(job, event, eager_start=True) + hass.async_run_hass_job(job, event) except Exception: # pylint: disable=broad-except _LOGGER.exception( "Error while dispatching event for %s to %s", @@ -1599,7 +1599,7 @@ class _TrackTimeInterval: self._track_job, hass.loop.time() + self.seconds, ) - hass.async_run_hass_job(self._run_job, now, eager_start=True, background=True) + hass.async_run_hass_job(self._run_job, now, background=True) @callback def async_cancel(self) -> None: @@ -1684,7 +1684,7 @@ class SunListener: """Handle solar event.""" self._unsub_sun = None self._listen_next_sun_event() - self.hass.async_run_hass_job(self.job, eager_start=True, background=True) + self.hass.async_run_hass_job(self.job, background=True) @callback def _handle_config_event(self, _event: Any) -> None: @@ -1770,9 +1770,7 @@ class _TrackUTCTimeChange: # time when the timer was scheduled utc_now = time_tracker_utcnow() localized_now = dt_util.as_local(utc_now) if self.local else utc_now - hass.async_run_hass_job( - self.job, localized_now, eager_start=True, background=True - ) + hass.async_run_hass_job(self.job, localized_now, background=True) if TYPE_CHECKING: assert self._pattern_time_change_listener_job is not None self._cancel_callback = async_track_point_in_utc_time( diff --git a/homeassistant/helpers/integration_platform.py b/homeassistant/helpers/integration_platform.py index 9d98fa24b72..e142f9c2e5a 100644 --- a/homeassistant/helpers/integration_platform.py +++ b/homeassistant/helpers/integration_platform.py @@ -140,7 +140,6 @@ def _process_integration_platforms( hass, integration.domain, platform, - eager_start=True, ) ) ] @@ -250,7 +249,7 @@ async def _async_process_integration_platforms( continue if future := hass.async_run_hass_job( - process_job, hass, integration.domain, platform, eager_start=True + process_job, hass, integration.domain, platform ): futures.append(future) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 951f1c24402..99c15c3412e 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -969,9 +969,9 @@ async def _handle_entity_call( partial(getattr(entity, func), **data), # type: ignore[arg-type] job_type=entity.get_hassjob_type(func), ) - task = hass.async_run_hass_job(job, eager_start=True) + task = hass.async_run_hass_job(job) else: - task = hass.async_run_hass_job(func, entity, data, eager_start=True) + task = hass.async_run_hass_job(func, entity, data) # Guard because callback functions do not return a task when passed to # async_run_job. @@ -1006,7 +1006,7 @@ async def _async_admin_handler( if not user.is_admin: raise Unauthorized(context=call.context) - result = hass.async_run_hass_job(service_job, call, eager_start=True) + result = hass.async_run_hass_job(service_job, call) if result is not None: await result diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py index d09e542e2e0..148b416e087 100644 --- a/homeassistant/helpers/start.py +++ b/homeassistant/helpers/start.py @@ -30,7 +30,7 @@ def _async_at_core_state( """ at_start_job = HassJob(at_start_cb) if check_state(hass): - hass.async_run_hass_job(at_start_job, hass, eager_start=True) + hass.async_run_hass_job(at_start_job, hass) return lambda: None unsub: None | CALLBACK_TYPE = None @@ -38,7 +38,7 @@ def _async_at_core_state( @callback def _matched_event(event: Event) -> None: """Call the callback when Home Assistant started.""" - hass.async_run_hass_job(at_start_job, hass, eager_start=True) + hass.async_run_hass_job(at_start_job, hass) nonlocal unsub unsub = None diff --git a/tests/test_core.py b/tests/test_core.py index 51805e46dba..3d72c7481f5 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -123,9 +123,7 @@ async def test_async_run_hass_job_eager_start_coro_suspends( async def job_that_suspends(): await asyncio.sleep(0) - task = hass.async_run_hass_job( - ha.HassJob(ha.callback(job_that_suspends)), eager_start=True - ) + task = hass.async_run_hass_job(ha.HassJob(ha.callback(job_that_suspends))) assert not task.done() assert task in hass._tasks await task @@ -169,7 +167,7 @@ async def test_async_add_hass_job_eager_background(hass: HomeAssistant) -> None: await asyncio.sleep(0) task = hass.async_add_hass_job( - ha.HassJob(ha.callback(job_that_suspends)), eager_start=True, background=True + ha.HassJob(ha.callback(job_that_suspends)), background=True ) assert not task.done() assert task in hass._background_tasks @@ -184,7 +182,7 @@ async def test_async_run_hass_job_eager_background(hass: HomeAssistant) -> None: await asyncio.sleep(0) task = hass.async_run_hass_job( - ha.HassJob(ha.callback(job_that_suspends)), eager_start=True, background=True + ha.HassJob(ha.callback(job_that_suspends)), background=True ) assert not task.done() assert task in hass._background_tasks @@ -200,7 +198,6 @@ async def test_async_run_hass_job_background_synchronous(hass: HomeAssistant) -> task = hass.async_run_hass_job( ha.HassJob(ha.callback(job_that_does_not_suspends)), - eager_start=True, background=True, ) assert task.done() @@ -217,7 +214,6 @@ async def test_async_run_hass_job_synchronous(hass: HomeAssistant) -> None: task = hass.async_run_hass_job( ha.HassJob(ha.callback(job_that_does_not_suspends)), - eager_start=True, background=False, ) assert task.done() @@ -393,9 +389,7 @@ async def test_async_run_eager_hass_job_calls_callback() -> None: asyncio.get_running_loop() # ensure we are in the event loop calls.append(1) - ha.HomeAssistant.async_run_hass_job( - hass, ha.HassJob(ha.callback(job)), eager_start=True - ) + ha.HomeAssistant.async_run_hass_job(hass, ha.HassJob(ha.callback(job))) assert len(calls) == 1 @@ -406,7 +400,7 @@ async def test_async_run_eager_hass_job_calls_coro_function() -> None: async def job(): pass - ha.HomeAssistant.async_run_hass_job(hass, ha.HassJob(job), eager_start=True) + ha.HomeAssistant.async_run_hass_job(hass, ha.HassJob(job)) assert len(hass.async_add_hass_job.mock_calls) == 1 @@ -2145,6 +2139,20 @@ async def test_async_run_job_starts_tasks_eagerly(hass: HomeAssistant) -> None: await task +async def test_async_run_job_starts_coro_eagerly(hass: HomeAssistant) -> None: + """Test async_run_job starts coros eagerly.""" + runs = [] + + async def _test(): + runs.append(True) + + task = hass.async_run_job(_test()) + # No call to hass.async_block_till_done to ensure the task is run eagerly + assert len(runs) == 1 + assert task.done() + await task + + def test_valid_entity_id() -> None: """Test valid entity ID.""" for invalid in [