Schedule polling as periodic tasks (#112640)
* Schedule periodic coordinator updates as background tasks. Currently, the coordinator's periodic refreshes delay startup because they are not scheduled as background tasks. We will wait if the startup takes long enough for the first planned refresh. Another coordinator's scheduled refresh will be fired on busy systems, further delaying the startup. This chain of events results in the startup taking a long time and hitting the safety timeout because too many coordinators are refreshing. This case can also happen with scheduled entity refreshes, but it's less common. A future PR will address that case. * periodic_tasks * periodic_tasks * periodic_tasks * merge * merge * merge * merge * merge * fix test that call the sync api from async * one more place * cannot chain * async_run_periodic_hass_job * sun and pattern time changes from automations also block startup * Revert "sun and pattern time changes from automations also block startup" This reverts commit6de2defa05
. * make sure polling is cancelled when config entry is unloaded * Revert "Revert "sun and pattern time changes from automations also block startup"" This reverts commite8f12aad55
. * remove DisabledError from homewizard test as it relies on a race * fix race * direct coverage
This commit is contained in:
parent
5da629b3e5
commit
a6b17dbe68
12 changed files with 292 additions and 51 deletions
|
@ -382,6 +382,7 @@ class HomeAssistant:
|
|||
self.loop = asyncio.get_running_loop()
|
||||
self._tasks: set[asyncio.Future[Any]] = set()
|
||||
self._background_tasks: set[asyncio.Future[Any]] = set()
|
||||
self._periodic_tasks: set[asyncio.Future[Any]] = set()
|
||||
self.bus = EventBus(self)
|
||||
self.services = ServiceRegistry(self)
|
||||
self.states = StateMachine(self.bus, self.loop)
|
||||
|
@ -640,6 +641,56 @@ class HomeAssistant:
|
|||
|
||||
return task
|
||||
|
||||
@overload
|
||||
@callback
|
||||
def async_run_periodic_hass_job(
|
||||
self, hassjob: HassJob[..., Coroutine[Any, Any, _R]], *args: Any
|
||||
) -> asyncio.Future[_R] | None:
|
||||
...
|
||||
|
||||
@overload
|
||||
@callback
|
||||
def async_run_periodic_hass_job(
|
||||
self, hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], *args: Any
|
||||
) -> asyncio.Future[_R] | None:
|
||||
...
|
||||
|
||||
@callback
|
||||
def async_run_periodic_hass_job(
|
||||
self, hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], *args: Any
|
||||
) -> asyncio.Future[_R] | None:
|
||||
"""Add a periodic HassJob from within the event loop.
|
||||
|
||||
This method must be run in the event loop.
|
||||
hassjob: HassJob to call.
|
||||
args: parameters for method to call.
|
||||
"""
|
||||
task: asyncio.Future[_R]
|
||||
# This code path is performance sensitive and uses
|
||||
# if TYPE_CHECKING to avoid the overhead of constructing
|
||||
# the type used for the cast. For history see:
|
||||
# https://github.com/home-assistant/core/pull/71960
|
||||
if hassjob.job_type is HassJobType.Coroutinefunction:
|
||||
if TYPE_CHECKING:
|
||||
hassjob.target = cast(
|
||||
Callable[..., Coroutine[Any, Any, _R]], hassjob.target
|
||||
)
|
||||
task = create_eager_task(hassjob.target(*args), name=hassjob.name)
|
||||
elif hassjob.job_type is HassJobType.Callback:
|
||||
if TYPE_CHECKING:
|
||||
hassjob.target = cast(Callable[..., _R], hassjob.target)
|
||||
hassjob.target(*args)
|
||||
return None
|
||||
else:
|
||||
if TYPE_CHECKING:
|
||||
hassjob.target = cast(Callable[..., _R], hassjob.target)
|
||||
task = self.loop.run_in_executor(None, hassjob.target, *args)
|
||||
|
||||
self._periodic_tasks.add(task)
|
||||
task.add_done_callback(self._periodic_tasks.remove)
|
||||
|
||||
return task
|
||||
|
||||
def create_task(
|
||||
self, target: Coroutine[Any, Any, Any], name: str | None = None
|
||||
) -> None:
|
||||
|
@ -681,9 +732,17 @@ class HomeAssistant:
|
|||
) -> asyncio.Task[_R]:
|
||||
"""Create a task from within the event loop.
|
||||
|
||||
This is a background task which will not block startup and will be
|
||||
automatically cancelled on shutdown. If you are using this in your
|
||||
integration, use the create task methods on the config entry instead.
|
||||
This type of task is for background tasks that usually run for
|
||||
the lifetime of Home Assistant or an integration's setup.
|
||||
|
||||
A background task is different from a normal task:
|
||||
|
||||
- Will not block startup
|
||||
- Will be automatically cancelled on shutdown
|
||||
- Calls to async_block_till_done will not wait for completion
|
||||
|
||||
If you are using this in your integration, use the create task
|
||||
methods on the config entry instead.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
|
@ -699,6 +758,37 @@ class HomeAssistant:
|
|||
task.add_done_callback(self._background_tasks.remove)
|
||||
return task
|
||||
|
||||
@callback
|
||||
def async_create_periodic_task(
|
||||
self, target: Coroutine[Any, Any, _R], name: str, eager_start: bool = False
|
||||
) -> asyncio.Task[_R]:
|
||||
"""Create a task from within the event loop.
|
||||
|
||||
This type of task is typically used for polling.
|
||||
|
||||
A periodic task is different from a normal task:
|
||||
|
||||
- Will not block startup
|
||||
- Will be automatically cancelled on shutdown
|
||||
- Calls to async_block_till_done will wait for completion by default
|
||||
|
||||
If you are using this in your integration, use the create task
|
||||
methods on the config entry instead.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
if eager_start:
|
||||
task = create_eager_task(target, name=name, loop=self.loop)
|
||||
if task.done():
|
||||
return task
|
||||
else:
|
||||
# Use loop.create_task
|
||||
# to avoid the extra function call in asyncio.create_task.
|
||||
task = self.loop.create_task(target, name=name)
|
||||
self._periodic_tasks.add(task)
|
||||
task.add_done_callback(self._periodic_tasks.remove)
|
||||
return task
|
||||
|
||||
@callback
|
||||
def async_add_executor_job(
|
||||
self, target: Callable[..., _T], *args: Any
|
||||
|
@ -808,16 +898,19 @@ class HomeAssistant:
|
|||
self.async_block_till_done(), self.loop
|
||||
).result()
|
||||
|
||||
async def async_block_till_done(self) -> None:
|
||||
async def async_block_till_done(self, wait_periodic_tasks: bool = True) -> None:
|
||||
"""Block until all pending work is done."""
|
||||
# To flush out any call_soon_threadsafe
|
||||
await asyncio.sleep(0)
|
||||
start_time: float | None = None
|
||||
current_task = asyncio.current_task()
|
||||
|
||||
while tasks := [
|
||||
task
|
||||
for task in self._tasks
|
||||
for task in (
|
||||
self._tasks | self._periodic_tasks
|
||||
if wait_periodic_tasks
|
||||
else self._tasks
|
||||
)
|
||||
if task is not current_task and not cancelling(task)
|
||||
]:
|
||||
await self._await_and_log_pending(tasks)
|
||||
|
@ -948,7 +1041,7 @@ class HomeAssistant:
|
|||
self._tasks = set()
|
||||
|
||||
# Cancel all background tasks
|
||||
for task in self._background_tasks:
|
||||
for task in self._background_tasks | self._periodic_tasks:
|
||||
self._tasks.add(task)
|
||||
task.add_done_callback(self._tasks.remove)
|
||||
task.cancel("Home Assistant is stopping")
|
||||
|
@ -960,7 +1053,7 @@ class HomeAssistant:
|
|||
self.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||
try:
|
||||
async with self.timeout.async_timeout(STOP_STAGE_SHUTDOWN_TIMEOUT):
|
||||
await self.async_block_till_done()
|
||||
await self.async_block_till_done(wait_periodic_tasks=False)
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"Timed out waiting for integrations to stop, the shutdown will"
|
||||
|
@ -973,7 +1066,7 @@ class HomeAssistant:
|
|||
self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE)
|
||||
try:
|
||||
async with self.timeout.async_timeout(FINAL_WRITE_STAGE_SHUTDOWN_TIMEOUT):
|
||||
await self.async_block_till_done()
|
||||
await self.async_block_till_done(wait_periodic_tasks=False)
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"Timed out waiting for final writes to complete, the shutdown will"
|
||||
|
@ -1025,7 +1118,7 @@ class HomeAssistant:
|
|||
|
||||
try:
|
||||
async with self.timeout.async_timeout(CLOSE_STAGE_SHUTDOWN_TIMEOUT):
|
||||
await self.async_block_till_done()
|
||||
await self.async_block_till_done(wait_periodic_tasks=False)
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"Timed out waiting for close event to be processed, the shutdown will"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue