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
4141from airflow .configuration import conf
4242from airflow .dag_processing .bundles .base import BaseDagBundle , BundleVersionLock
4343from airflow .dag_processing .bundles .manager import DagBundlesManager
44- from airflow .exceptions import AirflowInactiveAssetInInletOrOutletException , AirflowTaskTimeout
44+ from airflow .exceptions import (
45+ AirflowInactiveAssetInInletOrOutletException ,
46+ AirflowRescheduleException ,
47+ AirflowTaskTimeout ,
48+ )
4549from airflow .listeners .listener import get_listener_manager
4650from airflow .sdk .api .client import get_hostname , getuser
4751from airflow .sdk .api .datamodels ._generated import (
@@ -604,6 +608,33 @@ def _xcom_push_to_db(ti: RuntimeTaskInstance, key: str, value: Any) -> None:
604608 )
605609
606610
611+ def _maybe_reschedule_startup_failure (
612+ * ,
613+ ti_context : TIRunContext ,
614+ log : Logger ,
615+ ) -> None :
616+ """
617+ Attempt to reschedule the task when a startup failure occurs.
618+
619+ This does not count as a retry. If the reschedule limit is exceeded, this function
620+ returns and the caller should fail the task.
621+ """
622+ missing_dag_retires = conf .getint ("workers" , "missing_dag_retires" , fallback = 3 )
623+ missing_dag_retry_delay = conf .getint ("workers" , "missing_dag_retry_delay" , fallback = 60 )
624+
625+ reschedule_count = int (getattr (ti_context , "task_reschedule_count" , 0 ) or 0 )
626+ if missing_dag_retires > 0 and reschedule_count < missing_dag_retires :
627+ raise AirflowRescheduleException (
628+ reschedule_date = datetime .now (tz = timezone .utc ) + timedelta (seconds = missing_dag_retry_delay )
629+ )
630+
631+ log .error (
632+ "Startup reschedule limit exceeded" ,
633+ reschedule_count = reschedule_count ,
634+ max_reschedules = missing_dag_retires ,
635+ )
636+
637+
607638def parse (what : StartupDetails , log : Logger ) -> RuntimeTaskInstance :
608639 # TODO: Task-SDK:
609640 # Using DagBag here is about 98% wrong, but it'll do for now
@@ -638,6 +669,7 @@ def parse(what: StartupDetails, log: Logger) -> RuntimeTaskInstance:
638669 log .error (
639670 "Dag not found during start up" , dag_id = what .ti .dag_id , bundle = bundle_info , path = what .dag_rel_path
640671 )
672+ _maybe_reschedule_startup_failure (ti_context = what .ti_context , log = log )
641673 sys .exit (1 )
642674
643675 # install_loader()
@@ -652,6 +684,7 @@ def parse(what: StartupDetails, log: Logger) -> RuntimeTaskInstance:
652684 bundle = bundle_info ,
653685 path = what .dag_rel_path ,
654686 )
687+ _maybe_reschedule_startup_failure (ti_context = what .ti_context , log = log )
655688 sys .exit (1 )
656689
657690 if not isinstance (task , (BaseOperator , MappedOperator )):
@@ -1547,7 +1580,17 @@ def main():
15471580 SUPERVISOR_COMMS = CommsDecoder [ToTask , ToSupervisor ](log = log )
15481581
15491582 try :
1550- ti , context , log = startup ()
1583+ try :
1584+ ti , context , log = startup ()
1585+ except AirflowRescheduleException as reschedule :
1586+ log .warning ("Rescheduling task during startup, marking task as UP_FOR_RESCHEDULE" )
1587+ SUPERVISOR_COMMS .send (
1588+ msg = RescheduleTask (
1589+ reschedule_date = reschedule .reschedule_date ,
1590+ end_date = datetime .now (tz = timezone .utc ),
1591+ )
1592+ )
1593+ sys .exit (0 )
15511594 with BundleVersionLock (
15521595 bundle_name = ti .bundle_instance .name ,
15531596 bundle_version = ti .bundle_instance .version ,
@@ -1557,10 +1600,10 @@ def main():
15571600 finalize (ti , state , context , log , error )
15581601 except KeyboardInterrupt :
15591602 log .exception ("Ctrl-c hit" )
1560- exit (2 )
1603+ sys . exit (2 )
15611604 except Exception :
15621605 log .exception ("Top level error" )
1563- exit (1 )
1606+ sys . exit (1 )
15641607 finally :
15651608 # Ensure the request socket is closed on the child side in all circumstances
15661609 # before the process fully terminates.
0 commit comments