2727import time
2828from collections .abc import Callable , Iterable , Iterator , Mapping
2929from contextlib import suppress
30- from datetime import datetime , timezone
30+ from datetime import datetime , timedelta , timezone
3131from itertools import product
3232from pathlib import Path
3333from typing import TYPE_CHECKING , Annotated , Any , Literal
6161from airflow .sdk .exceptions import (
6262 AirflowException ,
6363 AirflowInactiveAssetInInletOrOutletException ,
64+ AirflowRescheduleException ,
6465 AirflowRuntimeError ,
6566 AirflowTaskTimeout ,
6667 ErrorType ,
@@ -696,6 +697,33 @@ def _xcom_push_to_db(ti: RuntimeTaskInstance, key: str, value: Any) -> None:
696697 )
697698
698699
700+ def _maybe_reschedule_startup_failure (
701+ * ,
702+ ti_context : TIRunContext ,
703+ log : Logger ,
704+ ) -> None :
705+ """
706+ Attempt to reschedule the task when a startup failure occurs.
707+
708+ This does not count as a retry. If the reschedule limit is exceeded, this function
709+ returns and the caller should fail the task.
710+ """
711+ missing_dag_retires = conf .getint ("workers" , "missing_dag_retires" , fallback = 3 )
712+ missing_dag_retry_delay = conf .getint ("workers" , "missing_dag_retry_delay" , fallback = 60 )
713+
714+ reschedule_count = int (getattr (ti_context , "task_reschedule_count" , 0 ) or 0 )
715+ if missing_dag_retires > 0 and reschedule_count < missing_dag_retires :
716+ raise AirflowRescheduleException (
717+ reschedule_date = datetime .now (tz = timezone .utc ) + timedelta (seconds = missing_dag_retry_delay )
718+ )
719+
720+ log .error (
721+ "Startup reschedule limit exceeded" ,
722+ reschedule_count = reschedule_count ,
723+ max_reschedules = missing_dag_retires ,
724+ )
725+
726+
699727def parse (what : StartupDetails , log : Logger ) -> RuntimeTaskInstance :
700728 # TODO: Task-SDK:
701729 # Using BundleDagBag here is about 98% wrong, but it'll do for now
@@ -726,6 +754,7 @@ def parse(what: StartupDetails, log: Logger) -> RuntimeTaskInstance:
726754 log .error (
727755 "Dag not found during start up" , dag_id = what .ti .dag_id , bundle = bundle_info , path = what .dag_rel_path
728756 )
757+ _maybe_reschedule_startup_failure (ti_context = what .ti_context , log = log )
729758 sys .exit (1 )
730759
731760 # install_loader()
@@ -740,6 +769,7 @@ def parse(what: StartupDetails, log: Logger) -> RuntimeTaskInstance:
740769 bundle = bundle_info ,
741770 path = what .dag_rel_path ,
742771 )
772+ _maybe_reschedule_startup_failure (ti_context = what .ti_context , log = log )
743773 sys .exit (1 )
744774
745775 if not isinstance (task , (BaseOperator , MappedOperator )):
@@ -1721,7 +1751,17 @@ def main():
17211751 )
17221752
17231753 try :
1724- ti , context , log = startup ()
1754+ try :
1755+ ti , context , log = startup ()
1756+ except AirflowRescheduleException as reschedule :
1757+ log .warning ("Rescheduling task during startup, marking task as UP_FOR_RESCHEDULE" )
1758+ SUPERVISOR_COMMS .send (
1759+ msg = RescheduleTask (
1760+ reschedule_date = reschedule .reschedule_date ,
1761+ end_date = datetime .now (tz = timezone .utc ),
1762+ )
1763+ )
1764+ sys .exit (0 )
17251765 with BundleVersionLock (
17261766 bundle_name = ti .bundle_instance .name ,
17271767 bundle_version = ti .bundle_instance .version ,
@@ -1731,10 +1771,10 @@ def main():
17311771 finalize (ti , state , context , log , error )
17321772 except KeyboardInterrupt :
17331773 log .exception ("Ctrl-c hit" )
1734- exit (2 )
1774+ sys . exit (2 )
17351775 except Exception :
17361776 log .exception ("Top level error" )
1737- exit (1 )
1777+ sys . exit (1 )
17381778 finally :
17391779 # Ensure the request socket is closed on the child side in all circumstances
17401780 # before the process fully terminates.
0 commit comments