Merge "Remove threaded and distributed flows"
This commit is contained in:
		| @@ -1,89 +0,0 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||||
|  | ||||
| #    Copyright (C) 2012 Yahoo! Inc. All Rights Reserved. | ||||
| #    Copyright (C) 2013 Rackspace Hosting Inc. All Rights Reserved. | ||||
| # | ||||
| #    Licensed under the Apache License, Version 2.0 (the "License"); you may | ||||
| #    not use this file except in compliance with the License. You may obtain | ||||
| #    a copy of the License at | ||||
| # | ||||
| #         http://www.apache.org/licenses/LICENSE-2.0 | ||||
| # | ||||
| #    Unless required by applicable law or agreed to in writing, software | ||||
| #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
| #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
| #    License for the specific language governing permissions and limitations | ||||
| #    under the License. | ||||
|  | ||||
| import celery | ||||
| import logging | ||||
|  | ||||
| LOG = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class Flow(object): | ||||
|     """A flow that can paralleize task running by using celery. | ||||
|  | ||||
|     This flow backs running tasks (and associated dependencies) by using celery | ||||
|     as the runtime framework to accomplish execution (and status reporting) of | ||||
|     said tasks that compose the flow. It allows for parallel execution where | ||||
|     possible (data/task dependency dependent) without having to worry about how | ||||
|     this is accomplished in celery. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, name, parents=None): | ||||
|         self.name = name | ||||
|         self.root = None | ||||
|         self._tasks = [] | ||||
|  | ||||
|     def chain_listeners(self, context, initial_task, callback_task): | ||||
|         """Register one listener for a task.""" | ||||
|         if self.root is None: | ||||
|             initial_task.name = '%s.%s' % (self.name, initial_task.name) | ||||
|             self.root = initial_task.s(context) | ||||
|             self._tasks.append(initial_task) | ||||
|             LOG.info('WF %s root task set to %s', self.name, initial_task.name) | ||||
|  | ||||
|         callback_task.name = '%s.%s' % (self.name, callback_task.name) | ||||
|         self._tasks.append(callback_task) | ||||
|  | ||||
|         initial_task.link(callback_task.s(context)) | ||||
|  | ||||
|     def split_listeners(self, context, initial_task, callback_tasks): | ||||
|         """Register multiple listeners for one task.""" | ||||
|         if self.root is None: | ||||
|             initial_task.name = '%s.%s' % (self.name, initial_task.name) | ||||
|             self.root = initial_task.s(context) | ||||
|             self._tasks.append(initial_task) | ||||
|             LOG.info('WF %s root task set to %s', self.name, initial_task.name) | ||||
|         for task in callback_tasks: | ||||
|             task.name = '%s.%s' % (self.name, task.name) | ||||
|             self._tasks.append(task) | ||||
|             initial_task.link(task.s(context)) | ||||
|  | ||||
|     def merge_listeners(self, context, initial_tasks, callback_task): | ||||
|         """Register one listener for multiple tasks.""" | ||||
|         header = [] | ||||
|         if self.root is None: | ||||
|             self.root = [] | ||||
|         for task in initial_tasks: | ||||
|             task.name = '%s.%s' % (self.name, task.name) | ||||
|             self._tasks.append(task) | ||||
|             header.append(task.s(context)) | ||||
|             if isinstance(self.root, list): | ||||
|                 self.root.append(task.s(context)) | ||||
|                 LOG.info('WF %s added root task %s' % | ||||
|                          (self.name, task.name)) | ||||
|         callback_task.name = '%s.%s' % (self.name, callback_task.name) | ||||
|         self._tasks.append(callback_task) | ||||
|  | ||||
|         # TODO(jlucci): Need to set up chord so that it's not executed | ||||
|         # immediately. | ||||
|         celery.chord(header, body=callback_task) | ||||
|  | ||||
|     def run(self, context, *args, **kwargs): | ||||
|         """Start root task and kick off workflow.""" | ||||
|         self.root(context) | ||||
|         LOG.info('WF %s has been started' % (self.name,)) | ||||
| @@ -1,637 +0,0 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||||
|  | ||||
| #    Copyright (C) 2012 Yahoo! Inc. All Rights Reserved. | ||||
| # | ||||
| #    Licensed under the Apache License, Version 2.0 (the "License"); you may | ||||
| #    not use this file except in compliance with the License. You may obtain | ||||
| #    a copy of the License at | ||||
| # | ||||
| #         http://www.apache.org/licenses/LICENSE-2.0 | ||||
| # | ||||
| #    Unless required by applicable law or agreed to in writing, software | ||||
| #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
| #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
| #    License for the specific language governing permissions and limitations | ||||
| #    under the License. | ||||
|  | ||||
| from taskflow import exceptions as exc | ||||
| from taskflow import flow | ||||
| from taskflow import states | ||||
| from taskflow.utils import graph_utils | ||||
| from taskflow.utils import misc | ||||
| from taskflow.utils import threading_utils | ||||
|  | ||||
|  | ||||
| import collections | ||||
| import functools | ||||
| import logging | ||||
| import sys | ||||
| import threading | ||||
| import weakref | ||||
|  | ||||
| from networkx.algorithms import cycles | ||||
| from networkx.classes import digraph | ||||
|  | ||||
| LOG = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class DependencyTimeout(exc.InvalidStateException): | ||||
|     """When running in parallel a task has the ability to timeout waiting for | ||||
|     its dependent tasks to finish, this will be raised when that occurs. | ||||
|     """ | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class Flow(flow.Flow): | ||||
|     """This flow pattern establishes tasks into a graph where each task is a | ||||
|     node in the graph and dependencies between tasks are edges in the graph. | ||||
|     When running (in parallel) each task will only be activated when its | ||||
|     dependencies have been satisified. When a graph is split into two or more | ||||
|     segments, both of those segments will be ran in parallel. | ||||
|  | ||||
|     For example lets take this small little *somewhat complicated* graph: | ||||
|  | ||||
|     X--Y--C--D | ||||
|           |  | | ||||
|     A--B--    --G-- | ||||
|           |  |     |--Z(end) | ||||
|     E--F--    --H-- | ||||
|  | ||||
|     In this flow the following will be ran in parallel at start: | ||||
|      1. X--Y | ||||
|      2. A--B | ||||
|      3. E--F | ||||
|     Note the C--D nodes will not be able to run until [Y,B,F] has completed. | ||||
|     After C--D completes the following will be ran in parallel: | ||||
|      1. G | ||||
|      2. H | ||||
|     Then finally Z will run (after [G,H] complete) and the flow will then have | ||||
|     finished executing. | ||||
|     """ | ||||
|     MUTABLE_STATES = set([states.PENDING, states.FAILURE, states.SUCCESS]) | ||||
|     REVERTABLE_STATES = set([states.FAILURE, states.INCOMPLETE]) | ||||
|     CANCELLABLE_STATES = set([states.PENDING, states.RUNNING]) | ||||
|  | ||||
|     def __init__(self, name): | ||||
|         super(Flow, self).__init__(name) | ||||
|         self._graph = digraph.DiGraph(name=name) | ||||
|         self._run_lock = threading.RLock() | ||||
|         self._cancel_lock = threading.RLock() | ||||
|         self._mutate_lock = threading.RLock() | ||||
|         # NOTE(harlowja) The locking order in this list actually matters since | ||||
|         # we need to make sure that users of this list do not get deadlocked | ||||
|         # by out of order lock access. | ||||
|         self._core_locks = [ | ||||
|             self._run_lock, | ||||
|             self._mutate_lock, | ||||
|             self._cancel_lock, | ||||
|         ] | ||||
|         self._run_locks = [ | ||||
|             self._run_lock, | ||||
|             self._mutate_lock, | ||||
|         ] | ||||
|         self._cancel_locks = [ | ||||
|             self._cancel_lock, | ||||
|         ] | ||||
|         self.results = {} | ||||
|         self.resumer = None | ||||
|  | ||||
|     def __str__(self): | ||||
|         lines = ["ParallelFlow: %s" % (self.name)] | ||||
|         lines.append("%s" % (self._graph.number_of_nodes())) | ||||
|         lines.append("%s" % (self.state)) | ||||
|         return "; ".join(lines) | ||||
|  | ||||
|     def soft_reset(self): | ||||
|         # The way this flow works does not allow (at the current moment) for | ||||
|         # you to suspend the threads and then resume them at a later time, | ||||
|         # instead it only supports interruption (which will cancel the threads) | ||||
|         # and then a full reset. | ||||
|         raise NotImplementedError("Threaded flow does not currently support" | ||||
|                                   " soft resetting, please try using" | ||||
|                                   " reset() instead") | ||||
|  | ||||
|     def interrupt(self): | ||||
|         """Currently we can not pause threads and then resume them later, not | ||||
|         really thinking that we should likely ever do this. | ||||
|         """ | ||||
|         raise NotImplementedError("Threaded flow does not currently support" | ||||
|                                   " interruption, please try using" | ||||
|                                   " cancel() instead") | ||||
|  | ||||
|     def reset(self): | ||||
|         # All locks are used so that resets can not happen while running or | ||||
|         # cancelling or modifying. | ||||
|         with threading_utils.MultiLock(self._core_locks): | ||||
|             super(Flow, self).reset() | ||||
|             self.results = {} | ||||
|             self.resumer = None | ||||
|  | ||||
|     def cancel(self): | ||||
|  | ||||
|         def check(): | ||||
|             if self.state not in self.CANCELLABLE_STATES: | ||||
|                 raise exc.InvalidStateException("Can not attempt cancellation" | ||||
|                                                 " when in state %s" % | ||||
|                                                 self.state) | ||||
|  | ||||
|         check() | ||||
|         cancelled = 0 | ||||
|         was_empty = False | ||||
|  | ||||
|         # We don't lock the other locks so that the flow can be cancelled while | ||||
|         # running. Further state management logic is then used while running | ||||
|         # to verify that the flow should still be running when it has been | ||||
|         # cancelled. | ||||
|         with threading_utils.MultiLock(self._cancel_locks): | ||||
|             check() | ||||
|             if len(self._graph) == 0: | ||||
|                 was_empty = True | ||||
|             else: | ||||
|                 for r in self._graph.nodes_iter(): | ||||
|                     try: | ||||
|                         if r.cancel(blocking=False): | ||||
|                             cancelled += 1 | ||||
|                     except exc.InvalidStateException: | ||||
|                         pass | ||||
|             if cancelled or was_empty: | ||||
|                 self._change_state(None, states.CANCELLED) | ||||
|  | ||||
|         return cancelled | ||||
|  | ||||
|     def _find_uuid(self, uuid): | ||||
|         # Finds the runner for the given uuid (or returns none) | ||||
|         for r in self._graph.nodes_iter(): | ||||
|             if r.uuid == uuid: | ||||
|                 return r | ||||
|         return None | ||||
|  | ||||
|     def add(self, task, timeout=None, infer=True): | ||||
|         """Adds a task to the given flow using the given timeout which will be | ||||
|         used a the timeout to wait for dependencies (if any) to be | ||||
|         fulfilled. | ||||
|         """ | ||||
|         def check(): | ||||
|             if self.state not in self.MUTABLE_STATES: | ||||
|                 raise exc.InvalidStateException("Flow is currently in a" | ||||
|                                                 " non-mutable %s state" % | ||||
|                                                 (self.state)) | ||||
|  | ||||
|         # Ensure that we do a quick check to see if we can even perform this | ||||
|         # addition before we go about actually acquiring the lock to perform | ||||
|         # the actual addition. | ||||
|         check() | ||||
|  | ||||
|         # All locks must be acquired so that modifications can not be made | ||||
|         # while running, cancelling or performing a simultaneous mutation. | ||||
|         with threading_utils.MultiLock(self._core_locks): | ||||
|             check() | ||||
|             runner = ThreadRunner(task, self, timeout) | ||||
|             self._graph.add_node(runner, infer=infer) | ||||
|             return runner.uuid | ||||
|  | ||||
|     def _connect(self): | ||||
|         """Infers and connects the edges of the given tasks by examining the | ||||
|         associated tasks provides and requires attributes and connecting tasks | ||||
|         that require items to tasks that produce said items. | ||||
|         """ | ||||
|  | ||||
|         # Disconnect all edges not manually created before we attempt to infer | ||||
|         # them so that we don't retain edges that are invalid. | ||||
|         def disconnect_non_user(u, v, e_data): | ||||
|             if e_data and e_data.get('reason') != 'manual': | ||||
|                 return True | ||||
|             return False | ||||
|  | ||||
|         # Link providers to requirers. | ||||
|         graph_utils.connect(self._graph, | ||||
|                             discard_func=disconnect_non_user) | ||||
|  | ||||
|         # Connect the successors & predecessors and related siblings | ||||
|         for r in self._graph.nodes_iter(): | ||||
|             r._predecessors = [] | ||||
|             r._successors = [] | ||||
|             for (r2, _me) in self._graph.in_edges_iter([r]): | ||||
|                 r._predecessors.append(r2) | ||||
|             for (_me, r2) in self._graph.out_edges_iter([r]): | ||||
|                 r._successors.append(r2) | ||||
|             r.siblings = [] | ||||
|             for r2 in self._graph.nodes_iter(): | ||||
|                 if r2 is r or r2 in r._predecessors or r2 in r._successors: | ||||
|                     continue | ||||
|                 r._siblings.append(r2) | ||||
|  | ||||
|     def add_many(self, tasks): | ||||
|         """Adds a list of tasks to the flow.""" | ||||
|  | ||||
|         def check(): | ||||
|             if self.state not in self.MUTABLE_STATES: | ||||
|                 raise exc.InvalidStateException("Flow is currently in a" | ||||
|                                                 " non-mutable state %s" | ||||
|                                                 % (self.state)) | ||||
|  | ||||
|         # Ensure that we do a quick check to see if we can even perform this | ||||
|         # addition before we go about actually acquiring the lock. | ||||
|         check() | ||||
|  | ||||
|         # All locks must be acquired so that modifications can not be made | ||||
|         # while running, cancelling or performing a simultaneous mutation. | ||||
|         with threading_utils.MultiLock(self._core_locks): | ||||
|             check() | ||||
|             added = [] | ||||
|             for t in tasks: | ||||
|                 added.append(self.add(t)) | ||||
|             return added | ||||
|  | ||||
|     def add_dependency(self, provider_uuid, consumer_uuid): | ||||
|         """Manually adds a dependency between a provider and a consumer.""" | ||||
|  | ||||
|         def check_and_fetch(): | ||||
|             if self.state not in self.MUTABLE_STATES: | ||||
|                 raise exc.InvalidStateException("Flow is currently in a" | ||||
|                                                 " non-mutable state %s" | ||||
|                                                 % (self.state)) | ||||
|             provider = self._find_uuid(provider_uuid) | ||||
|             if not provider or not self._graph.has_node(provider): | ||||
|                 raise exc.InvalidStateException("Can not add a dependency " | ||||
|                                                 "from unknown uuid %s" % | ||||
|                                                 (provider_uuid)) | ||||
|             consumer = self._find_uuid(consumer_uuid) | ||||
|             if not consumer or not self._graph.has_node(consumer): | ||||
|                 raise exc.InvalidStateException("Can not add a dependency " | ||||
|                                                 "to unknown uuid %s" | ||||
|                                                 % (consumer_uuid)) | ||||
|             if provider is consumer: | ||||
|                 raise exc.InvalidStateException("Can not add a dependency " | ||||
|                                                 "to loop via uuid %s" | ||||
|                                                 % (consumer_uuid)) | ||||
|             return (provider, consumer) | ||||
|  | ||||
|         check_and_fetch() | ||||
|  | ||||
|         # All locks must be acquired so that modifications can not be made | ||||
|         # while running, cancelling or performing a simultaneous mutation. | ||||
|         with threading_utils.MultiLock(self._core_locks): | ||||
|             (provider, consumer) = check_and_fetch() | ||||
|             self._graph.add_edge(provider, consumer, reason='manual') | ||||
|             LOG.debug("Connecting %s as a manual provider for %s", | ||||
|                       provider, consumer) | ||||
|  | ||||
|     def run(self, context, *args, **kwargs): | ||||
|         """Executes the given flow using the given context and args/kwargs.""" | ||||
|  | ||||
|         def abort_if(current_state, ok_states): | ||||
|             if current_state in (states.CANCELLED,): | ||||
|                 return False | ||||
|             if current_state not in ok_states: | ||||
|                 return False | ||||
|             return True | ||||
|  | ||||
|         def check(): | ||||
|             if self.state not in self.RUNNABLE_STATES: | ||||
|                 raise exc.InvalidStateException("Flow is currently unable " | ||||
|                                                 "to be ran in state %s" | ||||
|                                                 % (self.state)) | ||||
|  | ||||
|         def connect_and_verify(): | ||||
|             """Do basic sanity tests on the graph structure.""" | ||||
|             if len(self._graph) == 0: | ||||
|                 return | ||||
|             self._connect() | ||||
|             degrees = [g[1] for g in self._graph.in_degree_iter()] | ||||
|             zero_degrees = [d for d in degrees if d == 0] | ||||
|             if not zero_degrees: | ||||
|                 # If every task depends on something else to produce its input | ||||
|                 # then we will be in a deadlock situation. | ||||
|                 raise exc.InvalidStateException("No task has an in-degree" | ||||
|                                                 " of zero") | ||||
|             self_loops = self._graph.nodes_with_selfloops() | ||||
|             if self_loops: | ||||
|                 # A task that has a dependency on itself will never be able | ||||
|                 # to run. | ||||
|                 raise exc.InvalidStateException("%s tasks have been detected" | ||||
|                                                 " with dependencies on" | ||||
|                                                 " themselves" % | ||||
|                                                 len(self_loops)) | ||||
|             simple_cycles = len(cycles.recursive_simple_cycles(self._graph)) | ||||
|             if simple_cycles: | ||||
|                 # A task loop will never be able to run, unless it somehow | ||||
|                 # breaks that loop. | ||||
|                 raise exc.InvalidStateException("%s tasks have been detected" | ||||
|                                                 " with dependency loops" % | ||||
|                                                 simple_cycles) | ||||
|  | ||||
|         def run_it(result_cb, args, kwargs): | ||||
|             check_runnable = functools.partial(abort_if, | ||||
|                                                ok_states=self.RUNNABLE_STATES) | ||||
|             if self._change_state(context, states.RUNNING, | ||||
|                                   check_func=check_runnable): | ||||
|                 self.results = {} | ||||
|                 if len(self._graph) == 0: | ||||
|                     return | ||||
|                 for r in self._graph.nodes_iter(): | ||||
|                     r.reset() | ||||
|                     r._result_cb = result_cb | ||||
|                 executor = threading_utils.ThreadGroupExecutor() | ||||
|                 for r in self._graph.nodes_iter(): | ||||
|                     executor.submit(r, *args, **kwargs) | ||||
|                 executor.await_termination() | ||||
|  | ||||
|         def trigger_rollback(failures): | ||||
|             if not failures: | ||||
|                 return | ||||
|             causes = [] | ||||
|             for r in failures: | ||||
|                 causes.append(misc.FlowFailure(r, self)) | ||||
|             try: | ||||
|                 self.rollback(context, causes) | ||||
|             except exc.InvalidStateException: | ||||
|                 pass | ||||
|             finally: | ||||
|                 if len(failures) > 1: | ||||
|                     exc_infos = [f.exc_info for f in failures] | ||||
|                     raise exc.LinkedException.link(exc_infos) | ||||
|                 else: | ||||
|                     f = failures[0] | ||||
|                     raise f.exc_info[0], f.exc_info[1], f.exc_info[2] | ||||
|  | ||||
|         def handle_results(): | ||||
|             # Isolate each runner state into groups so that we can easily tell | ||||
|             # which ones failed, cancelled, completed... | ||||
|             groups = collections.defaultdict(list) | ||||
|             for r in self._graph.nodes_iter(): | ||||
|                 groups[r.state].append(r) | ||||
|             for r in self._graph.nodes_iter(): | ||||
|                 if r not in groups.get(states.FAILURE, []) and r.has_ran(): | ||||
|                     self.results[r.uuid] = r.result | ||||
|             if groups[states.FAILURE]: | ||||
|                 self._change_state(context, states.FAILURE) | ||||
|                 trigger_rollback(groups[states.FAILURE]) | ||||
|             elif (groups[states.CANCELLED] or groups[states.PENDING] | ||||
|                   or groups[states.TIMED_OUT] or groups[states.STARTED]): | ||||
|                 self._change_state(context, states.INCOMPLETE) | ||||
|             else: | ||||
|                 check_ran = functools.partial(abort_if, | ||||
|                                               ok_states=[states.RUNNING]) | ||||
|                 self._change_state(context, states.SUCCESS, | ||||
|                                    check_func=check_ran) | ||||
|  | ||||
|         def get_resumer_cb(): | ||||
|             if not self.resumer: | ||||
|                 return None | ||||
|             (ran, _others) = self.resumer(self, self._graph.nodes_iter()) | ||||
|  | ||||
|             def fetch_results(runner): | ||||
|                 for (r, metadata) in ran: | ||||
|                     if r is runner: | ||||
|                         return (True, metadata.get('result')) | ||||
|                 return (False, None) | ||||
|  | ||||
|             result_cb = fetch_results | ||||
|             return result_cb | ||||
|  | ||||
|         args = [context] + list(args) | ||||
|         check() | ||||
|  | ||||
|         # Only acquire the run lock (but use further state checking) and the | ||||
|         # mutation lock to stop simultaneous running and simultaneous mutating | ||||
|         # which are not allowed on a running flow. Allow simultaneous cancel | ||||
|         # by performing repeated state checking while running. | ||||
|         with threading_utils.MultiLock(self._run_locks): | ||||
|             check() | ||||
|             connect_and_verify() | ||||
|             try: | ||||
|                 run_it(get_resumer_cb(), args, kwargs) | ||||
|             finally: | ||||
|                 handle_results() | ||||
|  | ||||
|     def rollback(self, context, cause): | ||||
|         """Rolls back all tasks that are *not* still pending or cancelled.""" | ||||
|  | ||||
|         def check(): | ||||
|             if self.state not in self.REVERTABLE_STATES: | ||||
|                 raise exc.InvalidStateException("Flow is currently unable " | ||||
|                                                 "to be rolled back in " | ||||
|                                                 "state %s" % (self.state)) | ||||
|  | ||||
|         check() | ||||
|  | ||||
|         # All locks must be acquired so that modifications can not be made | ||||
|         # while another entity is running, rolling-back, cancelling or | ||||
|         # performing a mutation operation. | ||||
|         with threading_utils.MultiLock(self._core_locks): | ||||
|             check() | ||||
|             accum = misc.RollbackAccumulator() | ||||
|             for r in self._graph.nodes_iter(): | ||||
|                 if r.has_ran(): | ||||
|                     accum.add(misc.Rollback(context, r, | ||||
|                                             self, self.task_notifier)) | ||||
|             try: | ||||
|                 self._change_state(context, states.REVERTING) | ||||
|                 accum.rollback(cause) | ||||
|             finally: | ||||
|                 self._change_state(context, states.FAILURE) | ||||
|  | ||||
|  | ||||
| class ThreadRunner(misc.Runner): | ||||
|     """A helper class that will use a countdown latch to avoid calling its | ||||
|     callable object until said countdown latch has emptied. After it has | ||||
|     been emptied the predecessor tasks will be examined for dependent results | ||||
|     and said results will then be provided to call the runners callable | ||||
|     object. | ||||
|  | ||||
|     TODO(harlowja): this could be a 'future' like object in the future since it | ||||
|     is starting to have the same purpose and usage (in a way). Likely switch | ||||
|     this over to the task details object or a subclass of it??? | ||||
|     """ | ||||
|     RESETTABLE_STATES = set([states.PENDING, states.SUCCESS, states.FAILURE, | ||||
|                              states.CANCELLED]) | ||||
|     RUNNABLE_STATES = set([states.PENDING]) | ||||
|     CANCELABLE_STATES = set([states.PENDING]) | ||||
|     SUCCESS_STATES = set([states.SUCCESS]) | ||||
|     CANCEL_SUCCESSORS_WHEN = set([states.FAILURE, states.CANCELLED, | ||||
|                                   states.TIMED_OUT]) | ||||
|     NO_RAN_STATES = set([states.CANCELLED, states.PENDING, states.TIMED_OUT, | ||||
|                          states.RUNNING]) | ||||
|  | ||||
|     def __init__(self, task, flow, timeout): | ||||
|         super(ThreadRunner, self).__init__(task) | ||||
|         # Use weak references to give the GC a break. | ||||
|         self._flow = weakref.proxy(flow) | ||||
|         self._notifier = flow.task_notifier | ||||
|         self._timeout = timeout | ||||
|         self._state = states.PENDING | ||||
|         self._run_lock = threading.RLock() | ||||
|         # Use the flows state lock so that state notifications are not sent | ||||
|         # simultaneously for a given flow. | ||||
|         self._state_lock = flow._state_lock | ||||
|         self._cancel_lock = threading.RLock() | ||||
|         self._latch = threading_utils.CountDownLatch() | ||||
|         # Any related family. | ||||
|         self._predecessors = [] | ||||
|         self._successors = [] | ||||
|         self._siblings = [] | ||||
|         # This callback will be called before the underlying task is actually | ||||
|         # returned and it should either return a tuple of (has_result, result) | ||||
|         self._result_cb = None | ||||
|  | ||||
|     @property | ||||
|     def state(self): | ||||
|         return self._state | ||||
|  | ||||
|     def has_ran(self): | ||||
|         if self.state in self.NO_RAN_STATES: | ||||
|             return False | ||||
|         return True | ||||
|  | ||||
|     def _change_state(self, context, new_state): | ||||
|         old_state = None | ||||
|         changed = False | ||||
|         with self._state_lock: | ||||
|             if self.state != new_state: | ||||
|                 old_state = self.state | ||||
|                 self._state = new_state | ||||
|                 changed = True | ||||
|         # Don't notify while holding the lock so that the reciever of said | ||||
|         # notifications can actually perform operations on the given runner | ||||
|         # without getting into deadlock. | ||||
|         if changed and self._notifier: | ||||
|             self._notifier.notify(self.state, details={ | ||||
|                 'context': context, | ||||
|                 'flow': self._flow, | ||||
|                 'old_state': old_state, | ||||
|                 'runner': self, | ||||
|             }) | ||||
|  | ||||
|     def cancel(self, blocking=True): | ||||
|  | ||||
|         def check(): | ||||
|             if self.state not in self.CANCELABLE_STATES: | ||||
|                 raise exc.InvalidStateException("Runner not in a cancelable" | ||||
|                                                 " state: %s" % (self.state)) | ||||
|  | ||||
|         # Check before as a quick way out of attempting to acquire the more | ||||
|         # heavy-weight lock. Then acquire the lock (which should not be | ||||
|         # possible if we are currently running) and set the state (if still | ||||
|         # applicable). | ||||
|         check() | ||||
|         acquired = False | ||||
|         cancelled = False | ||||
|         try: | ||||
|             acquired = self._cancel_lock.acquire(blocking=blocking) | ||||
|             if acquired: | ||||
|                 check() | ||||
|                 cancelled = True | ||||
|                 self._change_state(None, states.CANCELLED) | ||||
|         finally: | ||||
|             if acquired: | ||||
|                 self._cancel_lock.release() | ||||
|         return cancelled | ||||
|  | ||||
|     def reset(self): | ||||
|  | ||||
|         def check(): | ||||
|             if self.state not in self.RESETTABLE_STATES: | ||||
|                 raise exc.InvalidStateException("Runner not in a resettable" | ||||
|                                                 " state: %s" % (self.state)) | ||||
|  | ||||
|         def do_reset(): | ||||
|             super(ThreadRunner, self).reset() | ||||
|             self._latch.count = len(self._predecessors) | ||||
|  | ||||
|         def change_state(): | ||||
|             self._change_state(None, states.PENDING) | ||||
|  | ||||
|         # We need to acquire both locks here so that we can not be running | ||||
|         # or being cancelled at the same time we are resetting. | ||||
|         check() | ||||
|         with self._run_lock: | ||||
|             check() | ||||
|             with self._cancel_lock: | ||||
|                 check() | ||||
|                 do_reset() | ||||
|                 change_state() | ||||
|  | ||||
|     @property | ||||
|     def runs_before(self): | ||||
|         # NOTE(harlowja): this list may change, depending on which other | ||||
|         # runners have completed (or are currently actively running), so | ||||
|         # this is why this is a property instead of a semi-static defined list | ||||
|         # like in the AOT class. The list should only get bigger and not | ||||
|         # smaller so it should be fine to filter on runners that have completed | ||||
|         # successfully. | ||||
|         finished_ok = [] | ||||
|         for r in self._siblings: | ||||
|             if r.has_ran() and r.state in self.SUCCESS_STATES: | ||||
|                 finished_ok.append(r) | ||||
|         return finished_ok | ||||
|  | ||||
|     def __call__(self, context, *args, **kwargs): | ||||
|  | ||||
|         def is_runnable(): | ||||
|             if self.state not in self.RUNNABLE_STATES: | ||||
|                 return False | ||||
|             return True | ||||
|  | ||||
|         def run(*args, **kwargs): | ||||
|             try: | ||||
|                 self._change_state(context, states.RUNNING) | ||||
|                 has_result = False | ||||
|                 if self._result_cb: | ||||
|                     has_result, self.result = self._result_cb(self) | ||||
|                 if not has_result: | ||||
|                     super(ThreadRunner, self).__call__(*args, **kwargs) | ||||
|                 self._change_state(context, states.SUCCESS) | ||||
|             except Exception: | ||||
|                 self.result = None | ||||
|                 self.exc_info = sys.exc_info() | ||||
|                 self._change_state(context, states.FAILURE) | ||||
|  | ||||
|         def signal(): | ||||
|             if not self._successors: | ||||
|                 return | ||||
|             if self.state in self.CANCEL_SUCCESSORS_WHEN: | ||||
|                 for r in self._successors: | ||||
|                     try: | ||||
|                         r.cancel(blocking=False) | ||||
|                     except exc.InvalidStateException: | ||||
|                         pass | ||||
|             for r in self._successors: | ||||
|                 try: | ||||
|                     r._latch.countDown() | ||||
|                 except Exception: | ||||
|                     LOG.exception("Failed decrementing %s latch", r) | ||||
|  | ||||
|         # We check before to avoid attempting to acquire the lock when we are | ||||
|         # known to be in a non-runnable state. | ||||
|         if not is_runnable(): | ||||
|             return | ||||
|         args = [context] + list(args) | ||||
|         with self._run_lock: | ||||
|             # We check after we now own the run lock since a previous thread | ||||
|             # could have exited and released that lock and set the state to | ||||
|             # not runnable. | ||||
|             if not is_runnable(): | ||||
|                 return | ||||
|             may_proceed = self._latch.await(self._timeout) | ||||
|             # We now acquire the cancel lock so that we can be assured that | ||||
|             # we have not been cancelled by another entity. | ||||
|             with self._cancel_lock: | ||||
|                 try: | ||||
|                     # If we have been cancelled after awaiting and timing out | ||||
|                     # ensure that we alter the state to show timed out (but | ||||
|                     # not if we have been cancelled, since our state should | ||||
|                     # be cancelled instead). This is done after acquiring the | ||||
|                     # cancel lock so that we will not try to overwrite another | ||||
|                     # entity trying to set the runner to the cancel state. | ||||
|                     if not may_proceed and self.state != states.CANCELLED: | ||||
|                         self._change_state(context, states.TIMED_OUT) | ||||
|                     # We at this point should only have been able to time out | ||||
|                     # or be cancelled, no other state transitions should have | ||||
|                     # been possible. | ||||
|                     if self.state not in (states.CANCELLED, states.TIMED_OUT): | ||||
|                         run(*args, **kwargs) | ||||
|                 finally: | ||||
|                     signal() | ||||
| @@ -102,7 +102,7 @@ MACHINE_GENERATED = ('# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE ' | ||||
|                      'OVERWRITTEN', '') | ||||
|  | ||||
| # FIXME(harlowja): remove these after bugs #1221448 and #1221505 are fixed | ||||
| BLACK_LISTED = ('threaded_flow', 'graph_flow') | ||||
| BLACK_LISTED = ('graph_flow',) | ||||
|  | ||||
|  | ||||
| def _parse_args(argv): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jenkins
					Jenkins