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', '')
|
'OVERWRITTEN', '')
|
||||||
|
|
||||||
# FIXME(harlowja): remove these after bugs #1221448 and #1221505 are fixed
|
# 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):
|
def _parse_args(argv):
|
||||||
|
|||||||
Reference in New Issue
Block a user