18
18
)
19
19
import concurrent .futures
20
20
from contextlib import suppress
21
+ from dataclasses import dataclass
21
22
import datetime
22
23
import enum
23
24
import functools
107
108
from .helpers .entity import StateInfo
108
109
109
110
110
- STAGE_1_SHUTDOWN_TIMEOUT = 100
111
- STAGE_2_SHUTDOWN_TIMEOUT = 60
112
- STAGE_3_SHUTDOWN_TIMEOUT = 30
111
+ STOPPING_STAGE_SHUTDOWN_TIMEOUT = 20
112
+ STOP_STAGE_SHUTDOWN_TIMEOUT = 100
113
+ FINAL_WRITE_STAGE_SHUTDOWN_TIMEOUT = 60
114
+ CLOSE_STAGE_SHUTDOWN_TIMEOUT = 30
113
115
114
116
block_async_io .enable ()
115
117
@@ -299,6 +301,14 @@ def __repr__(self) -> str:
299
301
return f"<Job { self .name } { self .job_type } { self .target } >"
300
302
301
303
304
+ @dataclass (frozen = True )
305
+ class HassJobWithArgs :
306
+ """Container for a HassJob and arguments."""
307
+
308
+ job : HassJob [..., Coroutine [Any , Any , Any ] | Any ]
309
+ args : Iterable [Any ]
310
+
311
+
302
312
def _get_hassjob_callable_job_type (target : Callable [..., Any ]) -> HassJobType :
303
313
"""Determine the job type from the callable."""
304
314
# Check for partials to properly determine if coroutine function
@@ -370,6 +380,7 @@ def __init__(self, config_dir: str) -> None:
370
380
# Timeout handler for Core/Helper namespace
371
381
self .timeout : TimeoutManager = TimeoutManager ()
372
382
self ._stop_future : concurrent .futures .Future [None ] | None = None
383
+ self ._shutdown_jobs : list [HassJobWithArgs ] = []
373
384
374
385
@property
375
386
def is_running (self ) -> bool :
@@ -766,6 +777,42 @@ async def _await_and_log_pending(
766
777
for task in pending :
767
778
_LOGGER .debug ("Waited %s seconds for task: %s" , wait_time , task )
768
779
780
+ @overload
781
+ @callback
782
+ def async_add_shutdown_job (
783
+ self , hassjob : HassJob [..., Coroutine [Any , Any , Any ]], * args : Any
784
+ ) -> CALLBACK_TYPE :
785
+ ...
786
+
787
+ @overload
788
+ @callback
789
+ def async_add_shutdown_job (
790
+ self , hassjob : HassJob [..., Coroutine [Any , Any , Any ] | Any ], * args : Any
791
+ ) -> CALLBACK_TYPE :
792
+ ...
793
+
794
+ @callback
795
+ def async_add_shutdown_job (
796
+ self , hassjob : HassJob [..., Coroutine [Any , Any , Any ] | Any ], * args : Any
797
+ ) -> CALLBACK_TYPE :
798
+ """Add a HassJob which will be executed on shutdown.
799
+
800
+ This method must be run in the event loop.
801
+
802
+ hassjob: HassJob
803
+ args: parameters for method to call.
804
+
805
+ Returns function to remove the job.
806
+ """
807
+ job_with_args = HassJobWithArgs (hassjob , args )
808
+ self ._shutdown_jobs .append (job_with_args )
809
+
810
+ @callback
811
+ def remove_job () -> None :
812
+ self ._shutdown_jobs .remove (job_with_args )
813
+
814
+ return remove_job
815
+
769
816
def stop (self ) -> None :
770
817
"""Stop Home Assistant and shuts down all threads."""
771
818
if self .state == CoreState .not_running : # just ignore
@@ -799,6 +846,26 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None:
799
846
"Stopping Home Assistant before startup has completed may fail"
800
847
)
801
848
849
+ # Stage 1 - Run shutdown jobs
850
+ try :
851
+ async with self .timeout .async_timeout (STOPPING_STAGE_SHUTDOWN_TIMEOUT ):
852
+ tasks : list [asyncio .Future [Any ]] = []
853
+ for job in self ._shutdown_jobs :
854
+ task_or_none = self .async_run_hass_job (job .job , * job .args )
855
+ if not task_or_none :
856
+ continue
857
+ tasks .append (task_or_none )
858
+ if tasks :
859
+ asyncio .gather (* tasks , return_exceptions = True )
860
+ except asyncio .TimeoutError :
861
+ _LOGGER .warning (
862
+ "Timed out waiting for shutdown jobs to complete, the shutdown will"
863
+ " continue"
864
+ )
865
+ self ._async_log_running_tasks ("run shutdown jobs" )
866
+
867
+ # Stage 2 - Stop integrations
868
+
802
869
# Keep holding the reference to the tasks but do not allow them
803
870
# to block shutdown. Only tasks created after this point will
804
871
# be waited for.
@@ -816,33 +883,32 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None:
816
883
817
884
self .exit_code = exit_code
818
885
819
- # stage 1
820
886
self .state = CoreState .stopping
821
887
self .bus .async_fire (EVENT_HOMEASSISTANT_STOP )
822
888
try :
823
- async with self .timeout .async_timeout (STAGE_1_SHUTDOWN_TIMEOUT ):
889
+ async with self .timeout .async_timeout (STOP_STAGE_SHUTDOWN_TIMEOUT ):
824
890
await self .async_block_till_done ()
825
891
except asyncio .TimeoutError :
826
892
_LOGGER .warning (
827
- "Timed out waiting for shutdown stage 1 to complete , the shutdown will"
893
+ "Timed out waiting for integrations to stop , the shutdown will"
828
894
" continue"
829
895
)
830
- self ._async_log_running_tasks (1 )
896
+ self ._async_log_running_tasks ("stop integrations" )
831
897
832
- # stage 2
898
+ # Stage 3 - Final write
833
899
self .state = CoreState .final_write
834
900
self .bus .async_fire (EVENT_HOMEASSISTANT_FINAL_WRITE )
835
901
try :
836
- async with self .timeout .async_timeout (STAGE_2_SHUTDOWN_TIMEOUT ):
902
+ async with self .timeout .async_timeout (FINAL_WRITE_STAGE_SHUTDOWN_TIMEOUT ):
837
903
await self .async_block_till_done ()
838
904
except asyncio .TimeoutError :
839
905
_LOGGER .warning (
840
- "Timed out waiting for shutdown stage 2 to complete, the shutdown will"
906
+ "Timed out waiting for final writes to complete, the shutdown will"
841
907
" continue"
842
908
)
843
- self ._async_log_running_tasks (2 )
909
+ self ._async_log_running_tasks ("final write" )
844
910
845
- # stage 3
911
+ # Stage 4 - Close
846
912
self .state = CoreState .not_running
847
913
self .bus .async_fire (EVENT_HOMEASSISTANT_CLOSE )
848
914
@@ -856,12 +922,12 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None:
856
922
# were awaiting another task
857
923
continue
858
924
_LOGGER .warning (
859
- "Task %s was still running after stage 2 shutdown; "
925
+ "Task %s was still running after final writes shutdown stage ; "
860
926
"Integrations should cancel non-critical tasks when receiving "
861
927
"the stop event to prevent delaying shutdown" ,
862
928
task ,
863
929
)
864
- task .cancel ("Home Assistant stage 2 shutdown" )
930
+ task .cancel ("Home Assistant final writes shutdown stage " )
865
931
try :
866
932
async with asyncio .timeout (0.1 ):
867
933
await task
@@ -870,11 +936,11 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None:
870
936
except asyncio .TimeoutError :
871
937
# Task may be shielded from cancellation.
872
938
_LOGGER .exception (
873
- "Task %s could not be canceled during stage 3 shutdown" , task
939
+ "Task %s could not be canceled during final shutdown stage " , task
874
940
)
875
941
except Exception as exc : # pylint: disable=broad-except
876
942
_LOGGER .exception (
877
- "Task %s error during stage 3 shutdown: %s" , task , exc
943
+ "Task %s error during final shutdown stage : %s" , task , exc
878
944
)
879
945
880
946
# Prevent run_callback_threadsafe from scheduling any additional
@@ -885,14 +951,14 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None:
885
951
shutdown_run_callback_threadsafe (self .loop )
886
952
887
953
try :
888
- async with self .timeout .async_timeout (STAGE_3_SHUTDOWN_TIMEOUT ):
954
+ async with self .timeout .async_timeout (CLOSE_STAGE_SHUTDOWN_TIMEOUT ):
889
955
await self .async_block_till_done ()
890
956
except asyncio .TimeoutError :
891
957
_LOGGER .warning (
892
- "Timed out waiting for shutdown stage 3 to complete , the shutdown will"
958
+ "Timed out waiting for close event to be processed , the shutdown will"
893
959
" continue"
894
960
)
895
- self ._async_log_running_tasks (3 )
961
+ self ._async_log_running_tasks ("close" )
896
962
897
963
self .state = CoreState .stopped
898
964
@@ -912,10 +978,10 @@ def _cancel_cancellable_timers(self) -> None:
912
978
):
913
979
handle .cancel ()
914
980
915
- def _async_log_running_tasks (self , stage : int ) -> None :
981
+ def _async_log_running_tasks (self , stage : str ) -> None :
916
982
"""Log all running tasks."""
917
983
for task in self ._tasks :
918
- _LOGGER .warning ("Shutdown stage %s : still running: %s" , stage , task )
984
+ _LOGGER .warning ("Shutdown stage '%s' : still running: %s" , stage , task )
919
985
920
986
921
987
class Context :
0 commit comments