1717"""Base executor - this is the base class for all the implemented executors."""
1818import sys
1919from collections import OrderedDict
20- from typing import Any , Dict , List , Optional , Set , Tuple , Union
20+ from typing import Any , Counter , Dict , List , Optional , Set , Tuple , Union
2121
2222from airflow .configuration import conf
2323from airflow .models .taskinstance import TaskInstance , TaskInstanceKey
2929
3030NOT_STARTED_MESSAGE = "The executor should be started first!"
3131
32+ QUEUEING_ATTEMPTS = 5
33+
3234# Command to execute - list of strings
3335# the first element is always "airflow".
3436# It should be result of TaskInstance.generate_command method.q
@@ -63,6 +65,7 @@ def __init__(self, parallelism: int = PARALLELISM):
6365 self .queued_tasks : OrderedDict [TaskInstanceKey , QueuedTaskInstanceType ] = OrderedDict ()
6466 self .running : Set [TaskInstanceKey ] = set ()
6567 self .event_buffer : Dict [TaskInstanceKey , EventBufferValueType ] = {}
68+ self .attempts : Counter [TaskInstanceKey ] = Counter ()
6669
6770 def __repr__ (self ):
6871 return f"{ self .__class__ .__name__ } (parallelism={ self .parallelism } )"
@@ -78,7 +81,7 @@ def queue_command(
7881 queue : Optional [str ] = None ,
7982 ):
8083 """Queues command to task"""
81- if task_instance .key not in self .queued_tasks and task_instance . key not in self . running :
84+ if task_instance .key not in self .queued_tasks :
8285 self .log .info ("Adding to queue: %s" , command )
8386 self .queued_tasks [task_instance .key ] = (command , priority , queue , task_instance )
8487 else :
@@ -183,9 +186,32 @@ def trigger_tasks(self, open_slots: int) -> None:
183186
184187 for _ in range (min ((open_slots , len (self .queued_tasks )))):
185188 key , (command , _ , queue , ti ) = sorted_queue .pop (0 )
186- self .queued_tasks .pop (key )
187- self .running .add (key )
188- self .execute_async (key = key , command = command , queue = queue , executor_config = ti .executor_config )
189+
190+ # If a task makes it here but is still understood by the executor
191+ # to be running, it generally means that the task has been killed
192+ # externally and not yet been marked as failed.
193+ #
194+ # However, when a task is deferred, there is also a possibility of
195+ # a race condition where a task might be scheduled again during
196+ # trigger processing, even before we are able to register that the
197+ # deferred task has completed. In this case and for this reason,
198+ # we make a small number of attempts to see if the task has been
199+ # removed from the running set in the meantime.
200+ if key in self .running :
201+ attempt = self .attempts [key ]
202+ if attempt < QUEUEING_ATTEMPTS - 1 :
203+ self .attempts [key ] = attempt + 1
204+ self .log .info ("task %s is still running" , key )
205+ continue
206+
207+ # We give up and remove the task from the queue.
208+ self .log .error ("could not queue task %s (still running after %d attempts)" , key , attempt )
209+ del self .attempts [key ]
210+ del self .queued_tasks [key ]
211+ else :
212+ del self .queued_tasks [key ]
213+ self .running .add (key )
214+ self .execute_async (key = key , command = command , queue = queue , executor_config = ti .executor_config )
189215
190216 def change_state (self , key : TaskInstanceKey , state : str , info = None ) -> None :
191217 """
0 commit comments