github.com/apache/beam/sdks/v2@v2.48.2/python/apache_beam/runners/direct/executor.py (about)

     1  #
     2  # Licensed to the Apache Software Foundation (ASF) under one or more
     3  # contributor license agreements.  See the NOTICE file distributed with
     4  # this work for additional information regarding copyright ownership.
     5  # The ASF licenses this file to You under the Apache License, Version 2.0
     6  # (the "License"); you may not use this file except in compliance with
     7  # the License.  You may obtain a copy of the License at
     8  #
     9  #    http://www.apache.org/licenses/LICENSE-2.0
    10  #
    11  # Unless required by applicable law or agreed to in writing, software
    12  # distributed under the License is distributed on an "AS IS" BASIS,
    13  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  # See the License for the specific language governing permissions and
    15  # limitations under the License.
    16  #
    17  
    18  """An executor that schedules and executes applied ptransforms."""
    19  
    20  # pytype: skip-file
    21  
    22  import collections
    23  import itertools
    24  import logging
    25  import queue
    26  import threading
    27  import traceback
    28  from typing import TYPE_CHECKING
    29  from typing import Any
    30  from typing import Dict
    31  from typing import FrozenSet
    32  from typing import Optional
    33  from typing import Set
    34  from weakref import WeakValueDictionary
    35  
    36  from apache_beam.metrics.execution import MetricsContainer
    37  from apache_beam.runners.worker import statesampler
    38  from apache_beam.transforms import sideinputs
    39  from apache_beam.utils import counters
    40  
    41  if TYPE_CHECKING:
    42    from apache_beam import pvalue
    43    from apache_beam.runners.direct.bundle_factory import _Bundle
    44    from apache_beam.runners.direct.evaluation_context import EvaluationContext
    45    from apache_beam.runners.direct.transform_evaluator import TransformEvaluatorRegistry
    46  
    47  _LOGGER = logging.getLogger(__name__)
    48  
    49  
    50  class _ExecutorService(object):
    51    """Thread pool for executing tasks in parallel."""
    52    class CallableTask(object):
    53      def call(self, state_sampler):
    54        pass
    55  
    56      @property
    57      def name(self):
    58        return None
    59  
    60    class _ExecutorServiceWorker(threading.Thread):
    61      """Worker thread for executing a single task at a time."""
    62  
    63      # Amount to block waiting for getting an item from the queue in seconds.
    64      TIMEOUT = 5
    65  
    66      def __init__(
    67          self,
    68          queue,  # type: queue.Queue[_ExecutorService.CallableTask]
    69          index):
    70        super().__init__()
    71        self.queue = queue
    72        self._index = index
    73        self._default_name = 'ExecutorServiceWorker-' + str(index)
    74        self._update_name()
    75        self.shutdown_requested = False
    76  
    77        # Stop worker thread when main thread exits.
    78        self.daemon = True
    79        self.start()
    80  
    81      def _update_name(self, task=None):
    82        if task and task.name:
    83          name = task.name
    84        else:
    85          name = self._default_name
    86        self.name = 'Thread: %d, %s (%s)' % (
    87            self._index, name, 'executing' if task else 'idle')
    88  
    89      def _get_task_or_none(self):
    90        # type: () -> Optional[_ExecutorService.CallableTask]
    91        try:
    92          # Do not block indefinitely, otherwise we may not act for a requested
    93          # shutdown.
    94          return self.queue.get(
    95              timeout=_ExecutorService._ExecutorServiceWorker.TIMEOUT)
    96        except queue.Empty:
    97          return None
    98  
    99      def run(self):
   100        state_sampler = statesampler.StateSampler('', counters.CounterFactory())
   101        statesampler.set_current_tracker(state_sampler)
   102        while not self.shutdown_requested:
   103          task = self._get_task_or_none()
   104          if task:
   105            try:
   106              if not self.shutdown_requested:
   107                self._update_name(task)
   108                task.call(state_sampler)
   109                self._update_name()
   110            finally:
   111              self.queue.task_done()
   112  
   113      def shutdown(self):
   114        self.shutdown_requested = True
   115  
   116    def __init__(self, num_workers):
   117      self.queue = queue.Queue(
   118      )  # type: queue.Queue[_ExecutorService.CallableTask]
   119      self.workers = [
   120          _ExecutorService._ExecutorServiceWorker(self.queue, i)
   121          for i in range(num_workers)
   122      ]
   123      self.shutdown_requested = False
   124  
   125    def submit(self, task):
   126      # type: (_ExecutorService.CallableTask) -> None
   127      assert isinstance(task, _ExecutorService.CallableTask)
   128      if not self.shutdown_requested:
   129        self.queue.put(task)
   130  
   131    def await_completion(self):
   132      for worker in self.workers:
   133        worker.join()
   134  
   135    def shutdown(self):
   136      self.shutdown_requested = True
   137  
   138      for worker in self.workers:
   139        worker.shutdown()
   140  
   141      # Consume all the remaining items in the queue
   142      while not self.queue.empty():
   143        try:
   144          self.queue.get_nowait()
   145          self.queue.task_done()
   146        except queue.Empty:
   147          continue
   148      # All existing threads will eventually terminate (after they complete their
   149      # last task).
   150  
   151  
   152  class _TransformEvaluationState(object):
   153    def __init__(
   154        self,
   155        executor_service,
   156        scheduled  # type: Set[TransformExecutor]
   157    ):
   158      self.executor_service = executor_service
   159      self.scheduled = scheduled
   160  
   161    def schedule(self, work):
   162      self.scheduled.add(work)
   163      self.executor_service.submit(work)
   164  
   165    def complete(self, completed_work):
   166      self.scheduled.remove(completed_work)
   167  
   168  
   169  class _ParallelEvaluationState(_TransformEvaluationState):
   170    """A TransformEvaluationState with unlimited parallelism.
   171  
   172    Any TransformExecutor scheduled will be immediately submitted to the
   173    ExecutorService.
   174  
   175    A principal use of this is for evaluators that can generate output bundles
   176    only using the input bundle (e.g. ParDo).
   177    """
   178    pass
   179  
   180  
   181  class _SerialEvaluationState(_TransformEvaluationState):
   182    """A TransformEvaluationState with a single work queue.
   183  
   184    Any TransformExecutor scheduled will be placed on the work queue. Only one
   185    item of work will be submitted to the ExecutorService at any time.
   186  
   187    A principal use of this is for evaluators that keeps a global state such as
   188    _GroupByKeyOnly.
   189    """
   190    def __init__(self, executor_service, scheduled):
   191      super().__init__(executor_service, scheduled)
   192      self.serial_queue = collections.deque()
   193      self.currently_evaluating = None
   194      self._lock = threading.Lock()
   195  
   196    def complete(self, completed_work):
   197      self._update_currently_evaluating(None, completed_work)
   198      super().complete(completed_work)
   199  
   200    def schedule(self, new_work):
   201      self._update_currently_evaluating(new_work, None)
   202  
   203    def _update_currently_evaluating(self, new_work, completed_work):
   204      with self._lock:
   205        if new_work:
   206          self.serial_queue.append(new_work)
   207        if completed_work:
   208          assert self.currently_evaluating == completed_work
   209          self.currently_evaluating = None
   210        if self.serial_queue and not self.currently_evaluating:
   211          next_work = self.serial_queue.pop()
   212          self.currently_evaluating = next_work
   213          super().schedule(next_work)
   214  
   215  
   216  class _TransformExecutorServices(object):
   217    """Schedules and completes TransformExecutors.
   218  
   219    Controls the concurrency as appropriate for the applied transform the executor
   220    exists for.
   221    """
   222    def __init__(self, executor_service):
   223      # type: (_ExecutorService) -> None
   224      self._executor_service = executor_service
   225      self._scheduled = set()  # type: Set[TransformExecutor]
   226      self._parallel = _ParallelEvaluationState(
   227          self._executor_service, self._scheduled)
   228      self._serial_cache = WeakValueDictionary(
   229      )  # type: WeakValueDictionary[Any, _SerialEvaluationState]
   230  
   231    def parallel(self):
   232      # type: () -> _ParallelEvaluationState
   233      return self._parallel
   234  
   235    def serial(self, step):
   236      # type: (Any) -> _SerialEvaluationState
   237      cached = self._serial_cache.get(step)
   238      if not cached:
   239        cached = _SerialEvaluationState(self._executor_service, self._scheduled)
   240        self._serial_cache[step] = cached
   241      return cached
   242  
   243    @property
   244    def executors(self):
   245      # type: () -> FrozenSet[TransformExecutor]
   246      return frozenset(self._scheduled)
   247  
   248  
   249  class _CompletionCallback(object):
   250    """The default completion callback.
   251  
   252    The default completion callback is used to complete transform evaluations
   253    that are triggered due to the arrival of elements from an upstream transform,
   254    or for a source transform.
   255    """
   256  
   257    def __init__(self,
   258                 evaluation_context,  # type: EvaluationContext
   259                 all_updates,
   260                 timer_firings=None
   261                ):
   262      self._evaluation_context = evaluation_context
   263      self._all_updates = all_updates
   264      self._timer_firings = timer_firings or []
   265  
   266    def handle_result(
   267        self, transform_executor, input_committed_bundle, transform_result):
   268      output_committed_bundles = self._evaluation_context.handle_result(
   269          input_committed_bundle, self._timer_firings, transform_result)
   270      for output_committed_bundle in output_committed_bundles:
   271        self._all_updates.offer(
   272            _ExecutorServiceParallelExecutor._ExecutorUpdate(
   273                transform_executor, committed_bundle=output_committed_bundle))
   274      for unprocessed_bundle in transform_result.unprocessed_bundles:
   275        self._all_updates.offer(
   276            _ExecutorServiceParallelExecutor._ExecutorUpdate(
   277                transform_executor, unprocessed_bundle=unprocessed_bundle))
   278      return output_committed_bundles
   279  
   280    def handle_exception(self, transform_executor, exception):
   281      self._all_updates.offer(
   282          _ExecutorServiceParallelExecutor._ExecutorUpdate(
   283              transform_executor, exception=exception))
   284  
   285  
   286  class TransformExecutor(_ExecutorService.CallableTask):
   287    """For internal use only; no backwards-compatibility guarantees.
   288  
   289    TransformExecutor will evaluate a bundle using an applied ptransform.
   290  
   291    A CallableTask responsible for constructing a TransformEvaluator and
   292    evaluating it on some bundle of input, and registering the result using the
   293    completion callback.
   294    """
   295  
   296    _MAX_RETRY_PER_BUNDLE = 4
   297  
   298    def __init__(self,
   299                 transform_evaluator_registry,  # type: TransformEvaluatorRegistry
   300                 evaluation_context,  # type: EvaluationContext
   301                 input_bundle,  # type: _Bundle
   302                 fired_timers,
   303                 applied_ptransform,
   304                 completion_callback,
   305                 transform_evaluation_state  # type: _TransformEvaluationState
   306                ):
   307      self._transform_evaluator_registry = transform_evaluator_registry
   308      self._evaluation_context = evaluation_context
   309      self._input_bundle = input_bundle
   310      # For non-empty bundles, store the window of the max EOW.
   311      # TODO(mariagh): Move to class _Bundle's inner _StackedWindowedValues
   312      self._latest_main_input_window = None
   313      if input_bundle.has_elements():
   314        self._latest_main_input_window = input_bundle._elements[0].windows[0]
   315        for elem in input_bundle.get_elements_iterable():
   316          if elem.windows[0].end > self._latest_main_input_window.end:
   317            self._latest_main_input_window = elem.windows[0]
   318      self._fired_timers = fired_timers
   319      self._applied_ptransform = applied_ptransform
   320      self._completion_callback = completion_callback
   321      self._transform_evaluation_state = transform_evaluation_state
   322      self._side_input_values = {}  # type: Dict[pvalue.AsSideInput, Any]
   323      self.blocked = False
   324      self._call_count = 0
   325      self._retry_count = 0
   326      self._max_retries_per_bundle = TransformExecutor._MAX_RETRY_PER_BUNDLE
   327  
   328    def call(self, state_sampler):
   329      self._call_count += 1
   330      assert self._call_count <= (1 + len(self._applied_ptransform.side_inputs))
   331      metrics_container = MetricsContainer(self._applied_ptransform.full_label)
   332      start_state = state_sampler.scoped_state(
   333          self._applied_ptransform.full_label,
   334          'start',
   335          metrics_container=metrics_container)
   336      process_state = state_sampler.scoped_state(
   337          self._applied_ptransform.full_label,
   338          'process',
   339          metrics_container=metrics_container)
   340      finish_state = state_sampler.scoped_state(
   341          self._applied_ptransform.full_label,
   342          'finish',
   343          metrics_container=metrics_container)
   344  
   345      with start_state:
   346        # Side input initialization should be accounted for in start_state.
   347        for side_input in self._applied_ptransform.side_inputs:
   348          # Find the projection of main's window onto the side input's window.
   349          window_mapping_fn = side_input._view_options().get(
   350              'window_mapping_fn', sideinputs._global_window_mapping_fn)
   351          main_onto_side_window = window_mapping_fn(
   352              self._latest_main_input_window)
   353          block_until = main_onto_side_window.end
   354  
   355          if side_input not in self._side_input_values:
   356            value = self._evaluation_context.get_value_or_block_until_ready(
   357                side_input, self, block_until)
   358            if not value:
   359              # Monitor task will reschedule this executor once the side input is
   360              # available.
   361              return
   362            self._side_input_values[side_input] = value
   363        side_input_values = [
   364            self._side_input_values[side_input]
   365            for side_input in self._applied_ptransform.side_inputs
   366        ]
   367  
   368      while self._retry_count < self._max_retries_per_bundle:
   369        try:
   370          self.attempt_call(
   371              metrics_container,
   372              side_input_values,
   373              start_state,
   374              process_state,
   375              finish_state)
   376          break
   377        except Exception as e:
   378          self._retry_count += 1
   379          _LOGGER.error(
   380              'Exception at bundle %r, due to an exception.\n %s',
   381              self._input_bundle,
   382              traceback.format_exc())
   383          if self._retry_count == self._max_retries_per_bundle:
   384            _LOGGER.error(
   385                'Giving up after %s attempts.', self._max_retries_per_bundle)
   386            self._completion_callback.handle_exception(self, e)
   387  
   388      self._evaluation_context.metrics().commit_physical(
   389          self._input_bundle, metrics_container.get_cumulative())
   390      self._transform_evaluation_state.complete(self)
   391  
   392    def attempt_call(
   393        self,
   394        metrics_container,
   395        side_input_values,
   396        start_state,
   397        process_state,
   398        finish_state):
   399      """Attempts to run a bundle."""
   400      evaluator = self._transform_evaluator_registry.get_evaluator(
   401          self._applied_ptransform, self._input_bundle, side_input_values)
   402  
   403      with start_state:
   404        evaluator.start_bundle()
   405  
   406      with process_state:
   407        if self._fired_timers:
   408          for timer_firing in self._fired_timers:
   409            evaluator.process_timer_wrapper(timer_firing)
   410  
   411        if self._input_bundle:
   412          for value in self._input_bundle.get_elements_iterable():
   413            evaluator.process_element(value)
   414  
   415      with finish_state:
   416        result = evaluator.finish_bundle()
   417        result.logical_metric_updates = metrics_container.get_cumulative()
   418  
   419      self._completion_callback.handle_result(self, self._input_bundle, result)
   420      return result
   421  
   422  
   423  class Executor(object):
   424    """For internal use only; no backwards-compatibility guarantees."""
   425    def __init__(self, *args, **kwargs):
   426      self._executor = _ExecutorServiceParallelExecutor(*args, **kwargs)
   427  
   428    def start(self, roots):
   429      self._executor.start(roots)
   430  
   431    def await_completion(self):
   432      self._executor.await_completion()
   433  
   434    def shutdown(self):
   435      self._executor.request_shutdown()
   436  
   437  
   438  class _ExecutorServiceParallelExecutor(object):
   439    """An internal implementation for Executor."""
   440  
   441    NUM_WORKERS = 1
   442  
   443    def __init__(
   444        self,
   445        value_to_consumers,
   446        transform_evaluator_registry,
   447        evaluation_context  # type: EvaluationContext
   448    ):
   449      self.executor_service = _ExecutorService(
   450          _ExecutorServiceParallelExecutor.NUM_WORKERS)
   451      self.transform_executor_services = _TransformExecutorServices(
   452          self.executor_service)
   453      self.value_to_consumers = value_to_consumers
   454      self.transform_evaluator_registry = transform_evaluator_registry
   455      self.evaluation_context = evaluation_context
   456      self.all_updates = _ExecutorServiceParallelExecutor._TypedUpdateQueue(
   457          _ExecutorServiceParallelExecutor._ExecutorUpdate)
   458      self.visible_updates = _ExecutorServiceParallelExecutor._TypedUpdateQueue(
   459          _ExecutorServiceParallelExecutor._VisibleExecutorUpdate)
   460      self.default_completion_callback = _CompletionCallback(
   461          evaluation_context, self.all_updates)
   462  
   463    def start(self, roots):
   464      self.root_nodes = frozenset(roots)
   465      self.all_nodes = frozenset(
   466          itertools.chain(
   467              roots, *itertools.chain(self.value_to_consumers.values())))
   468      self.node_to_pending_bundles = {}
   469      for root_node in self.root_nodes:
   470        provider = (
   471            self.transform_evaluator_registry.get_root_bundle_provider(root_node))
   472        self.node_to_pending_bundles[root_node] = provider.get_root_bundles()
   473      self.executor_service.submit(
   474          _ExecutorServiceParallelExecutor._MonitorTask(self))
   475  
   476    def await_completion(self):
   477      update = self.visible_updates.take()
   478      try:
   479        if update.exception:
   480          raise update.exception
   481      finally:
   482        self.executor_service.shutdown()
   483        self.executor_service.await_completion()
   484  
   485    def request_shutdown(self):
   486      self.executor_service.shutdown()
   487      self.executor_service.await_completion()
   488      self.evaluation_context.shutdown()
   489  
   490    def schedule_consumers(self, committed_bundle):
   491      # type: (_Bundle) -> None
   492      if committed_bundle.pcollection in self.value_to_consumers:
   493        consumers = self.value_to_consumers[committed_bundle.pcollection]
   494        for applied_ptransform in consumers:
   495          self.schedule_consumption(
   496              applied_ptransform,
   497              committed_bundle, [],
   498              self.default_completion_callback)
   499  
   500    def schedule_unprocessed_bundle(self, applied_ptransform, unprocessed_bundle):
   501      self.node_to_pending_bundles[applied_ptransform].append(unprocessed_bundle)
   502  
   503    def schedule_consumption(self,
   504                             consumer_applied_ptransform,
   505                             committed_bundle,  # type: _Bundle
   506                             fired_timers,
   507                             on_complete
   508                            ):
   509      """Schedules evaluation of the given bundle with the transform."""
   510      assert consumer_applied_ptransform
   511      assert committed_bundle
   512      assert on_complete
   513      if self.transform_evaluator_registry.should_execute_serially(
   514          consumer_applied_ptransform):
   515        transform_executor_service = self.transform_executor_services.serial(
   516            consumer_applied_ptransform)  # type: _TransformEvaluationState
   517      else:
   518        transform_executor_service = self.transform_executor_services.parallel()
   519  
   520      transform_executor = TransformExecutor(
   521          self.transform_evaluator_registry,
   522          self.evaluation_context,
   523          committed_bundle,
   524          fired_timers,
   525          consumer_applied_ptransform,
   526          on_complete,
   527          transform_executor_service)
   528      transform_executor_service.schedule(transform_executor)
   529  
   530    class _TypedUpdateQueue(object):
   531      """Type checking update queue with blocking and non-blocking operations."""
   532      def __init__(self, item_type):
   533        self._item_type = item_type
   534        self._queue = queue.Queue()
   535  
   536      def poll(self):
   537        try:
   538          item = self._queue.get_nowait()
   539          self._queue.task_done()
   540          return item
   541        except queue.Empty:
   542          return None
   543  
   544      def take(self):
   545        # The implementation of Queue.Queue.get() does not propagate
   546        # KeyboardInterrupts when a timeout is not used.  We therefore use a
   547        # one-second timeout in the following loop to allow KeyboardInterrupts
   548        # to be correctly propagated.
   549        while True:
   550          try:
   551            item = self._queue.get(timeout=1)
   552            self._queue.task_done()
   553            return item
   554          except queue.Empty:
   555            pass
   556  
   557      def offer(self, item):
   558        assert isinstance(item, self._item_type)
   559        self._queue.put_nowait(item)
   560  
   561    class _ExecutorUpdate(object):
   562      """An internal status update on the state of the executor."""
   563      def __init__(
   564          self,
   565          transform_executor,
   566          committed_bundle=None,
   567          unprocessed_bundle=None,
   568          exception=None):
   569        self.transform_executor = transform_executor
   570        # Exactly one of them should be not-None
   571        assert sum(
   572            [bool(committed_bundle), bool(unprocessed_bundle),
   573             bool(exception)]) == 1
   574        self.committed_bundle = committed_bundle
   575        self.unprocessed_bundle = unprocessed_bundle
   576        self.exception = exception
   577  
   578    class _VisibleExecutorUpdate(object):
   579      """An update of interest to the user.
   580  
   581      Used for awaiting the completion to decide whether to return normally or
   582      raise an exception.
   583      """
   584      def __init__(self, exception=None):
   585        self.finished = exception is not None
   586        self.exception = exception
   587  
   588    class _MonitorTask(_ExecutorService.CallableTask):
   589      """MonitorTask continuously runs to ensure that pipeline makes progress."""
   590      def __init__(self, executor):
   591        # type: (_ExecutorServiceParallelExecutor) -> None
   592        self._executor = executor
   593  
   594      @property
   595      def name(self):
   596        return 'monitor'
   597  
   598      def call(self, state_sampler):
   599        try:
   600          update = self._executor.all_updates.poll()
   601          while update:
   602            if update.committed_bundle:
   603              self._executor.schedule_consumers(update.committed_bundle)
   604            elif update.unprocessed_bundle:
   605              self._executor.schedule_unprocessed_bundle(
   606                  update.transform_executor._applied_ptransform,
   607                  update.unprocessed_bundle)
   608            else:
   609              assert update.exception
   610              _LOGGER.warning(
   611                  'A task failed with exception: %s', update.exception)
   612              self._executor.visible_updates.offer(
   613                  _ExecutorServiceParallelExecutor._VisibleExecutorUpdate(
   614                      update.exception))
   615            update = self._executor.all_updates.poll()
   616          self._executor.evaluation_context.schedule_pending_unblocked_tasks(
   617              self._executor.executor_service)
   618          self._add_work_if_necessary(self._fire_timers())
   619        except Exception as e:  # pylint: disable=broad-except
   620          _LOGGER.error('Monitor task died due to exception.\n %s', e)
   621          self._executor.visible_updates.offer(
   622              _ExecutorServiceParallelExecutor._VisibleExecutorUpdate(e))
   623        finally:
   624          if not self._should_shutdown():
   625            self._executor.executor_service.submit(self)
   626  
   627      def _should_shutdown(self):
   628        # type: () -> bool
   629  
   630        """Checks whether the pipeline is completed and should be shut down.
   631  
   632        If there is anything in the queue of tasks to do or
   633        if there are any realtime timers set, do not shut down.
   634  
   635        Otherwise, check if all the transforms' watermarks are complete.
   636        If they are not, the pipeline is not progressing (stall detected).
   637        Whether the pipeline has stalled or not, the executor should shut
   638        down the pipeline.
   639  
   640        Returns:
   641          True only if the pipeline has reached a terminal state and should
   642          be shut down.
   643  
   644        """
   645        if self._is_executing():
   646          # There are some bundles still in progress.
   647          return False
   648  
   649        watermark_manager = self._executor.evaluation_context._watermark_manager
   650        _, any_unfired_realtime_timers = watermark_manager.extract_all_timers()
   651        if any_unfired_realtime_timers:
   652          return False
   653  
   654        else:
   655          if self._executor.evaluation_context.is_done():
   656            self._executor.visible_updates.offer(
   657                _ExecutorServiceParallelExecutor._VisibleExecutorUpdate())
   658          else:
   659            # Nothing is scheduled for execution, but watermarks incomplete.
   660            self._executor.visible_updates.offer(
   661                _ExecutorServiceParallelExecutor._VisibleExecutorUpdate((
   662                    Exception('Monitor task detected a pipeline stall.'),
   663                    None,
   664                    None)))
   665          self._executor.executor_service.shutdown()
   666          return True
   667  
   668      def _fire_timers(self):
   669        """Schedules triggered consumers if any timers fired.
   670  
   671        Returns:
   672          True if timers fired.
   673        """
   674        transform_fired_timers, _ = (
   675            self._executor.evaluation_context.extract_all_timers())
   676        for applied_ptransform, fired_timers in transform_fired_timers:
   677          # Use an empty committed bundle. just to trigger.
   678          empty_bundle = (
   679              self._executor.evaluation_context.create_empty_committed_bundle(
   680                  applied_ptransform.inputs[0]))
   681          timer_completion_callback = _CompletionCallback(
   682              self._executor.evaluation_context,
   683              self._executor.all_updates,
   684              timer_firings=fired_timers)
   685  
   686          self._executor.schedule_consumption(
   687              applied_ptransform,
   688              empty_bundle,
   689              fired_timers,
   690              timer_completion_callback)
   691        return bool(transform_fired_timers)
   692  
   693      def _is_executing(self):
   694        # type: () -> bool
   695  
   696        """Checks whether the job is still executing.
   697  
   698        Returns:
   699          True if there is at least one non-blocked TransformExecutor active."""
   700  
   701        executors = self._executor.transform_executor_services.executors
   702        if not executors:
   703          # Nothing is executing.
   704          return False
   705  
   706        # Ensure that at least one of those executors is not blocked.
   707        for transform_executor in executors:
   708          if not transform_executor.blocked:
   709            return True
   710        return False
   711  
   712      def _add_work_if_necessary(self, timers_fired):
   713        """Adds more work from the roots if pipeline requires more input.
   714  
   715        If all active TransformExecutors are in a blocked state, add more work
   716        from root nodes that may have additional work. This ensures that if a
   717        pipeline has elements available from the root nodes it will add those
   718        elements when necessary.
   719  
   720        Args:
   721          timers_fired: True if any timers fired prior to this call.
   722        """
   723        # If any timers have fired, they will add more work; No need to add more.
   724        if timers_fired:
   725          return
   726  
   727        if self._is_executing():
   728          # We have at least one executor that can proceed without adding
   729          # additional work.
   730          return
   731  
   732        # All current TransformExecutors are blocked; add more work from any
   733        # pending bundles.
   734        for applied_ptransform in self._executor.all_nodes:
   735          if not self._executor.evaluation_context.is_done(applied_ptransform):
   736            pending_bundles = self._executor.node_to_pending_bundles.get(
   737                applied_ptransform, [])
   738            for bundle in pending_bundles:
   739              self._executor.schedule_consumption(
   740                  applied_ptransform,
   741                  bundle, [],
   742                  self._executor.default_completion_callback)
   743            self._executor.node_to_pending_bundles[applied_ptransform] = []