806 lines
27 KiB
Python
806 lines
27 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright (C) 2007 Samuel Abels
|
|
#
|
|
# This library is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU Lesser General Public
|
|
# License as published by the Free Software Foundation; either
|
|
# version 2.1 of the License, or (at your option) any later version.
|
|
#
|
|
# This library is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public
|
|
# License along with this library; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
# 02110-1301 USA
|
|
|
|
import copy
|
|
import logging
|
|
import time
|
|
import warnings
|
|
from uuid import uuid4
|
|
|
|
from .util.deep_merge import DeepMerge
|
|
from .exceptions import WorkflowException
|
|
|
|
logger = logging.getLogger('spiff')
|
|
metrics = logging.getLogger('spiff.metrics')
|
|
data_log = logging.getLogger('spiff.data')
|
|
|
|
|
|
def updateDotDict(dct,dotted_path,value):
|
|
parts = dotted_path.split(".")
|
|
path_len = len(parts)
|
|
root = dct
|
|
for i, key in enumerate(parts):
|
|
if (i + 1) < path_len:
|
|
if key not in dct:
|
|
dct[key] = {}
|
|
dct = dct[key]
|
|
else:
|
|
dct[key] = value
|
|
return root
|
|
|
|
|
|
class TaskState:
|
|
"""
|
|
|
|
The following states may exist:
|
|
|
|
- FUTURE: The task will definitely be reached in the future,
|
|
regardless of which choices the user makes within the workflow.
|
|
|
|
- LIKELY: The task may or may not be reached in the future. It
|
|
is likely because the specification lists it as the default
|
|
option for the ExclusiveChoice.
|
|
|
|
- MAYBE: The task may or may not be reached in the future. It
|
|
is not LIKELY, because the specification does not list it as the
|
|
default choice for the ExclusiveChoice.
|
|
|
|
- WAITING: The task is still waiting for an event before it
|
|
completes. For example, a Join task will be WAITING until all
|
|
predecessors are completed.
|
|
|
|
- READY: The conditions for completing the task are now satisfied.
|
|
Usually this means that all predecessors have completed and the
|
|
task may now be completed using
|
|
:class:`Workflow.complete_task_from_id()`.
|
|
|
|
- CANCELLED: The task was cancelled by a CancelTask or
|
|
CancelWorkflow task.
|
|
|
|
- COMPLETED: The task was regularily completed.
|
|
|
|
Note that the LIKELY and MAYBE tasks are merely predicted/guessed, so
|
|
those tasks may be removed from the tree at runtime later. They are
|
|
created to allow for visualizing the workflow at a time where
|
|
the required decisions have not yet been made.
|
|
"""
|
|
# Note: The states in this list are ordered in the sequence in which
|
|
# they may appear. Do not change.
|
|
MAYBE = 1
|
|
LIKELY = 2
|
|
FUTURE = 4
|
|
WAITING = 8
|
|
READY = 16
|
|
COMPLETED = 32
|
|
CANCELLED = 64
|
|
|
|
FINISHED_MASK = CANCELLED | COMPLETED
|
|
DEFINITE_MASK = FUTURE | WAITING | READY | FINISHED_MASK
|
|
PREDICTED_MASK = FUTURE | LIKELY | MAYBE
|
|
NOT_FINISHED_MASK = PREDICTED_MASK | WAITING | READY
|
|
ANY_MASK = FINISHED_MASK | NOT_FINISHED_MASK
|
|
|
|
|
|
TaskStateNames = {TaskState.FUTURE: 'FUTURE',
|
|
TaskState.WAITING: 'WAITING',
|
|
TaskState.READY: 'READY',
|
|
TaskState.CANCELLED: 'CANCELLED',
|
|
TaskState.COMPLETED: 'COMPLETED',
|
|
TaskState.LIKELY: 'LIKELY',
|
|
TaskState.MAYBE: 'MAYBE'}
|
|
TaskStateMasks = {
|
|
TaskState.FINISHED_MASK: 'FINISHED_MASK',
|
|
TaskState.DEFINITE_MASK: 'DEFINITE_MASK',
|
|
TaskState.PREDICTED_MASK: 'PREDICTED_MASK',
|
|
TaskState.NOT_FINISHED_MASK: 'NOT_FINISHED_MASK',
|
|
TaskState.ANY_MASK: 'ANY_MASK',
|
|
}
|
|
|
|
|
|
class DeprecatedMetaTask(type):
|
|
"""
|
|
Handle deprecated methods that are now moved to TaskState
|
|
"""
|
|
TaskNames = {**TaskStateNames, **TaskStateMasks}
|
|
TaskStateFromNames = {v: k for k, v in TaskNames.items()}
|
|
|
|
def __getattribute__(self, item):
|
|
if item in DeprecatedMetaTask.TaskNames.values():
|
|
warnings.warn(f'Task.{item} is deprecated. '
|
|
f'Please use TaskState.{item}',
|
|
DeprecationWarning, stacklevel=2)
|
|
return DeprecatedMetaTask.TaskStateFromNames[item]
|
|
else:
|
|
return type.__getattribute__(self, item)
|
|
|
|
|
|
class Task(object, metaclass=DeprecatedMetaTask):
|
|
"""
|
|
Used internally for composing a tree that represents the path that
|
|
is taken (or predicted) within the workflow.
|
|
|
|
Each Task has a state. For an explanation, consider the following task
|
|
specification::
|
|
|
|
,-> Simple (default choice)
|
|
StartTask -> ExclusiveChoice
|
|
`-> Simple
|
|
|
|
The initial task tree for this specification looks like so::
|
|
|
|
,-> Simple LIKELY
|
|
StartTask WAITING -> ExclusiveChoice FUTURE
|
|
`-> Simple MAYBE
|
|
|
|
See TaskStates for the available states on a Task.
|
|
"""
|
|
|
|
class Iterator(object):
|
|
|
|
MAX_ITERATIONS = 10000
|
|
|
|
"""
|
|
This is a tree iterator that supports filtering such that a client
|
|
may walk through all tasks that have a specific state.
|
|
"""
|
|
|
|
def __init__(self, current, filter=None):
|
|
"""
|
|
Constructor.
|
|
"""
|
|
self.filter = filter
|
|
self.path = [current]
|
|
self.count = 1
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def _next(self):
|
|
|
|
# Make sure that the end is not yet reached.
|
|
if len(self.path) == 0:
|
|
raise StopIteration()
|
|
|
|
current = self.path[-1]
|
|
|
|
# Assure we don't recurse forever.
|
|
self.count += 1
|
|
if self.count > self.MAX_ITERATIONS:
|
|
raise WorkflowException(current,
|
|
"Task Iterator entered infinite recursion loop" )
|
|
|
|
|
|
# If the current task has children, the first child is the next
|
|
# item. If the current task is LIKELY, and predicted tasks are not
|
|
# specificly searched, we can ignore the children, because
|
|
# predicted tasks should only have predicted children.
|
|
ignore_task = False
|
|
if self.filter is not None:
|
|
search_predicted = self.filter & TaskState.LIKELY != 0
|
|
is_predicted = current.state & TaskState.LIKELY != 0
|
|
ignore_task = is_predicted and not search_predicted
|
|
if current.children and not ignore_task:
|
|
self.path.append(current.children[0])
|
|
if (self.filter is not None and
|
|
current.state & self.filter == 0):
|
|
return None
|
|
return current
|
|
|
|
# Ending up here, this task has no children. Crop the path until we
|
|
# reach a task that has unvisited children, or until we hit the
|
|
# end.
|
|
while True:
|
|
old_child = self.path.pop(-1)
|
|
if len(self.path) == 0:
|
|
break
|
|
|
|
# If this task has a sibling, choose it.
|
|
parent = self.path[-1]
|
|
pos = parent.children.index(old_child)
|
|
if len(parent.children) > pos + 1:
|
|
self.path.append(parent.children[pos + 1])
|
|
break
|
|
if self.filter is not None and current.state & self.filter == 0:
|
|
return None
|
|
return current
|
|
|
|
def __next__(self):
|
|
# By using this loop we avoid an (expensive) recursive call.
|
|
while True:
|
|
next = self._next()
|
|
if next is not None:
|
|
return next
|
|
|
|
# Python 3 iterator protocol
|
|
next = __next__
|
|
|
|
# Pool for assigning a unique thread id to every new Task.
|
|
thread_id_pool = 0
|
|
|
|
def __init__(self, workflow, task_spec, parent=None, state=TaskState.MAYBE):
|
|
"""
|
|
Constructor.
|
|
"""
|
|
assert workflow is not None
|
|
assert task_spec is not None
|
|
self.workflow = workflow
|
|
self.parent = parent
|
|
self.children = []
|
|
self._state = state
|
|
self.triggered = False
|
|
self.task_spec = task_spec
|
|
self.id = uuid4()
|
|
self.thread_id = self.__class__.thread_id_pool
|
|
self.data = {}
|
|
self.terminate_current_loop = False
|
|
self.internal_data = {}
|
|
self.mi_collect_data = {}
|
|
if parent is not None:
|
|
self.parent._child_added_notify(self)
|
|
|
|
# TODO: get rid of this stuff
|
|
self.last_state_change = time.time()
|
|
self.state_history = [state]
|
|
|
|
@property
|
|
def state(self):
|
|
return self._state
|
|
|
|
@state.setter
|
|
def state(self, value):
|
|
if value < self._state:
|
|
raise WorkflowException(
|
|
self.task_spec,
|
|
'state went from %s to %s!' % (self.get_state_name(), TaskStateNames[value])
|
|
)
|
|
self._set_state(value)
|
|
|
|
def _set_state(self, value):
|
|
"""Using the setter method will raise an error on a "backwards" state change.
|
|
Call this method directly to force the state change.
|
|
"""
|
|
if value != self.state:
|
|
logger.info(f'State change to {TaskStateNames[value]}', extra=self.log_info())
|
|
self.last_state_change = time.time()
|
|
self.state_history.append(value)
|
|
self._state = value
|
|
else:
|
|
logger.debug(f'State set to {TaskStateNames[value]}', extra=self.log_info())
|
|
|
|
def __repr__(self):
|
|
return '<Task object (%s) in state %s at %s>' % (
|
|
self.task_spec.name,
|
|
self.get_state_name(),
|
|
hex(id(self)))
|
|
|
|
def log_info(self, dct=None):
|
|
extra = dct or {}
|
|
extra.update({
|
|
'workflow': self.workflow.spec.name,
|
|
'task_spec': self.task_spec.name,
|
|
'task_name': self.task_spec.description,
|
|
'task_id': self.id,
|
|
'task_type': self.task_spec.spec_type,
|
|
'data': self.data if logger.level < 20 else None,
|
|
'internal_data': self.internal_data if logger.level <= 10 else None,
|
|
})
|
|
return extra
|
|
|
|
def update_data_var(self, fieldid, value):
|
|
model = {}
|
|
updateDotDict(model,fieldid, value)
|
|
self.update_data(model)
|
|
|
|
def update_data(self, data):
|
|
"""
|
|
If the task.data needs to be updated from a UserTask form or
|
|
a Script task then use this function rather than updating task.data
|
|
directly. It will handle deeper merges of data,
|
|
and MultiInstance tasks will be updated correctly.
|
|
"""
|
|
self.data = DeepMerge.merge(self.data, data)
|
|
data_log.info('Data update', extra=self.log_info())
|
|
|
|
def task_info(self):
|
|
"""
|
|
Returns a dictionary of information about the current task, so that
|
|
we can give hints to the user about what kind of task we are working
|
|
with such as a looping task or a Parallel MultiInstance task
|
|
:returns: dictionary
|
|
"""
|
|
default = {'is_looping': False,
|
|
'is_sequential_mi': False,
|
|
'is_parallel_mi': False,
|
|
'mi_count': 0,
|
|
'mi_index': 0}
|
|
|
|
miInfo = getattr(self.task_spec, "multiinstance_info", None)
|
|
if callable(miInfo):
|
|
return miInfo(self)
|
|
else:
|
|
return default
|
|
|
|
def terminate_loop(self):
|
|
"""
|
|
Used in the case that we are working with a BPMN 'loop' task.
|
|
The task will loop, repeatedly asking for input until terminate_loop
|
|
is called on the task
|
|
"""
|
|
if self.is_looping():
|
|
self.terminate_current_loop = True
|
|
else:
|
|
raise WorkflowException(self.task_spec,
|
|
'The method terminate_loop should only be called in the case of a BPMN Loop Task')
|
|
|
|
def is_looping(self):
|
|
"""Returns true if this is a looping task."""
|
|
islooping = getattr(self.task_spec, "is_loop_task", None)
|
|
if callable(islooping):
|
|
return self.task_spec.is_loop_task()
|
|
else:
|
|
return False
|
|
|
|
def set_children_future(self):
|
|
"""
|
|
for a parallel gateway, we need to set up our
|
|
children so that the gateway figures out that it needs to join up
|
|
the inputs - otherwise our child process never gets marked as
|
|
'READY'
|
|
"""
|
|
|
|
if not self.task_spec.task_should_set_children_future(self):
|
|
return
|
|
|
|
self.task_spec.task_will_set_children_future(self)
|
|
|
|
# now we set this one to execute
|
|
|
|
self._set_state(TaskState.MAYBE)
|
|
self._sync_children(self.task_spec.outputs)
|
|
for child in self.children:
|
|
child.set_children_future()
|
|
|
|
def find_children_by_name(self,name):
|
|
"""
|
|
for debugging
|
|
"""
|
|
return [x for x in self.workflow.task_tree if x.task_spec.name == name]
|
|
|
|
def reset_token(self, data, reset_data=False):
|
|
"""
|
|
Resets the token to this task. This should allow a trip 'back in time'
|
|
as it were to items that have already been completed.
|
|
:type reset_data: bool
|
|
:param reset_data: Do we want to have the data be where we left of in
|
|
this task or not
|
|
"""
|
|
self.internal_data = {}
|
|
if not reset_data and self.workflow.last_task and self.workflow.last_task.data:
|
|
# This is a little sly, the data that will get inherited should
|
|
# be from the last completed task, but we don't want to alter
|
|
# the tree, so we just set the parent's data to the given data.
|
|
self.parent.data = copy.deepcopy(data)
|
|
self.workflow.last_task = self.parent
|
|
self.set_children_future() # this method actually fixes the problem
|
|
self._set_state(TaskState.FUTURE)
|
|
self.task_spec._update(self)
|
|
|
|
def __iter__(self):
|
|
return Task.Iterator(self)
|
|
|
|
def __setstate__(self, dict):
|
|
self.__dict__.update(dict)
|
|
# If unpickled in the same Python process in which a workflow
|
|
# (Task) is built through the API, we need to make sure
|
|
# that there will not be any ID collisions.
|
|
if dict['thread_id'] >= self.__class__.thread_id_pool:
|
|
self.__class__.thread_id_pool = dict['thread_id']
|
|
|
|
def _get_root(self):
|
|
"""
|
|
Returns the top level parent.
|
|
"""
|
|
if self.parent is None:
|
|
return self
|
|
return self.parent._get_root()
|
|
|
|
def _get_depth(self):
|
|
depth = 0
|
|
task = self.parent
|
|
while task is not None:
|
|
depth += 1
|
|
task = task.parent
|
|
return depth
|
|
|
|
def _child_added_notify(self, child):
|
|
"""
|
|
Called by another Task to let us know that a child was added.
|
|
"""
|
|
assert child is not None
|
|
self.children.append(child)
|
|
|
|
def _drop_children(self, force=False):
|
|
drop = []
|
|
for child in self.children:
|
|
if force or (not child._is_finished()):
|
|
drop.append(child)
|
|
else:
|
|
child._drop_children()
|
|
for task in drop:
|
|
self.children.remove(task)
|
|
|
|
def _has_state(self, state):
|
|
"""
|
|
Returns True if the Task has the given state flag set.
|
|
"""
|
|
return (self.state & state) != 0
|
|
|
|
def _is_finished(self):
|
|
return self._has_state(TaskState.FINISHED_MASK)
|
|
|
|
def _is_predicted(self):
|
|
return self._has_state(TaskState.PREDICTED_MASK)
|
|
|
|
def _is_definite(self):
|
|
return self._has_state(TaskState.DEFINITE_MASK)
|
|
|
|
def _add_child(self, task_spec, state=TaskState.MAYBE):
|
|
"""
|
|
Adds a new child and assigns the given TaskSpec to it.
|
|
|
|
:type task_spec: TaskSpec
|
|
:param task_spec: The task spec that is assigned to the new child.
|
|
:type state: integer
|
|
:param state: The bitmask of states for the new child.
|
|
:rtype: Task
|
|
:returns: The new child task.
|
|
"""
|
|
if task_spec is None:
|
|
raise ValueError(self, '_add_child() requires a TaskSpec')
|
|
if self._is_predicted() and state & TaskState.PREDICTED_MASK == 0:
|
|
msg = 'Attempt to add non-predicted child to predicted task'
|
|
raise WorkflowException(self.task_spec, msg)
|
|
task = Task(self.workflow, task_spec, self, state=state)
|
|
task.thread_id = self.thread_id
|
|
if state == TaskState.READY:
|
|
task._ready()
|
|
return task
|
|
|
|
def _assign_new_thread_id(self, recursive=True):
|
|
"""
|
|
Assigns a new thread id to the task.
|
|
|
|
:type recursive: bool
|
|
:param recursive: Whether to assign the id to children recursively.
|
|
:rtype: bool
|
|
:returns: The new thread id.
|
|
"""
|
|
self.__class__.thread_id_pool += 1
|
|
self.thread_id = self.__class__.thread_id_pool
|
|
if not recursive:
|
|
return self.thread_id
|
|
for child in self:
|
|
child.thread_id = self.thread_id
|
|
return self.thread_id
|
|
|
|
def _sync_children(self, task_specs, state=TaskState.MAYBE):
|
|
"""
|
|
This method syncs up the task's children with the given list of task
|
|
specs. In other words::
|
|
|
|
- Add one child for each given TaskSpec, unless that child already
|
|
exists.
|
|
- Remove all children for which there is no spec in the given list,
|
|
unless it is a "triggered" task.
|
|
- Handle looping back to previous tasks, so we don't end up with
|
|
an infinitely large tree.
|
|
.. note::
|
|
|
|
It is an error if the task has a non-predicted child that is
|
|
not given in the TaskSpecs.
|
|
|
|
:type task_specs: list(TaskSpec)
|
|
:param task_specs: The list of task specs that may become children.
|
|
:type state: integer
|
|
:param state: The bitmask of states for the new children.
|
|
"""
|
|
if task_specs is None:
|
|
raise ValueError('"task_specs" argument is None')
|
|
add = task_specs[:]
|
|
|
|
# If a child task_spec is also an ancestor, we are looping back,
|
|
# replace those specs with a loopReset task.
|
|
root_task = self._get_root()
|
|
for index, task_spec in enumerate(add):
|
|
ancestor_task = self._find_ancestor(task_spec)
|
|
if ancestor_task and ancestor_task != root_task:
|
|
destination = ancestor_task
|
|
new_spec = self.workflow.get_reset_task_spec(destination)
|
|
new_spec.outputs = []
|
|
new_spec.inputs = task_spec.inputs
|
|
add[index] = new_spec
|
|
|
|
# Create a list of all children that are no longer needed.
|
|
remove = []
|
|
for child in self.children:
|
|
# Triggered tasks are never removed.
|
|
if child.triggered:
|
|
continue
|
|
|
|
# Check whether the task needs to be removed.
|
|
if child.task_spec in add:
|
|
add.remove(child.task_spec)
|
|
continue
|
|
|
|
# Non-predicted tasks must not be removed, so they HAVE to be in
|
|
# the given task spec list.
|
|
if child._is_definite():
|
|
raise WorkflowException(self.task_spec,
|
|
'removal of non-predicted child %s' %
|
|
repr(child))
|
|
remove.append(child)
|
|
|
|
|
|
|
|
# Remove and add the children accordingly.
|
|
for child in remove:
|
|
self.children.remove(child)
|
|
for task_spec in add:
|
|
self._add_child(task_spec, state)
|
|
|
|
def _set_likely_task(self, task_specs):
|
|
if not isinstance(task_specs, list):
|
|
task_specs = [task_specs]
|
|
for task_spec in task_specs:
|
|
for child in self.children:
|
|
if child.task_spec != task_spec:
|
|
continue
|
|
if child._is_definite():
|
|
continue
|
|
child._set_state(TaskState.LIKELY)
|
|
return
|
|
|
|
def _is_descendant_of(self, parent):
|
|
"""
|
|
Returns True if parent is in the list of ancestors, returns False
|
|
otherwise.
|
|
|
|
:type parent: Task
|
|
:param parent: The parent that is searched in the ancestors.
|
|
:rtype: bool
|
|
:returns: Whether the parent was found.
|
|
"""
|
|
if self.parent is None:
|
|
return False
|
|
if self.parent == parent:
|
|
return True
|
|
return self.parent._is_descendant_of(parent)
|
|
|
|
def _find_child_of(self, parent_task_spec):
|
|
"""
|
|
Returns the ancestor that has a task with the given task spec
|
|
as a parent.
|
|
If no such ancestor was found, the root task is returned.
|
|
|
|
:type parent_task_spec: TaskSpec
|
|
:param parent_task_spec: The wanted ancestor.
|
|
:rtype: Task
|
|
:returns: The child of the given ancestor.
|
|
"""
|
|
if self.parent is None:
|
|
return self
|
|
if self.parent.task_spec == parent_task_spec:
|
|
return self
|
|
return self.parent._find_child_of(parent_task_spec)
|
|
|
|
def _find_any(self, task_spec):
|
|
"""
|
|
Returns any descendants that have the given task spec assigned.
|
|
|
|
:type task_spec: TaskSpec
|
|
:param task_spec: The wanted task spec.
|
|
:rtype: list(Task)
|
|
:returns: The tasks objects that are attached to the given task spec.
|
|
"""
|
|
tasks = []
|
|
if self.task_spec == task_spec:
|
|
tasks.append(self)
|
|
for child in self:
|
|
if child.task_spec != task_spec:
|
|
continue
|
|
tasks.append(child)
|
|
return tasks
|
|
|
|
def _find_ancestor(self, task_spec):
|
|
"""
|
|
Returns the ancestor that has the given task spec assigned.
|
|
If no such ancestor was found, the root task is returned.
|
|
|
|
:type task_spec: TaskSpec
|
|
:param task_spec: The wanted task spec.
|
|
:rtype: Task
|
|
:returns: The ancestor.
|
|
"""
|
|
if self.parent is None:
|
|
return self
|
|
if self.parent.task_spec == task_spec:
|
|
return self.parent
|
|
return self.parent._find_ancestor(task_spec)
|
|
|
|
def _find_ancestor_from_name(self, name):
|
|
"""
|
|
Returns the ancestor that has a task with the given name assigned.
|
|
Returns None if no such ancestor was found.
|
|
|
|
:type name: str
|
|
:param name: The name of the wanted task.
|
|
:rtype: Task
|
|
:returns: The ancestor.
|
|
"""
|
|
if self.parent is None:
|
|
return None
|
|
if self.parent.get_name() == name:
|
|
return self.parent
|
|
return self.parent._find_ancestor_from_name(name)
|
|
|
|
def _ready(self):
|
|
"""
|
|
Marks the task as ready for execution.
|
|
"""
|
|
if self._has_state(TaskState.COMPLETED) or self._has_state(TaskState.CANCELLED):
|
|
return
|
|
self._set_state(TaskState.READY)
|
|
self.task_spec._on_ready(self)
|
|
|
|
def get_name(self):
|
|
return str(self.task_spec.name)
|
|
|
|
def get_description(self):
|
|
return str(self.task_spec.description)
|
|
|
|
def get_state(self):
|
|
"""
|
|
Returns this Task's state.
|
|
"""
|
|
return self.state
|
|
|
|
def get_state_name(self):
|
|
"""
|
|
Returns a textual representation of this Task's state.
|
|
"""
|
|
state_name = []
|
|
for state, name in list(TaskStateNames.items()):
|
|
if self._has_state(state):
|
|
state_name.append(name)
|
|
return '|'.join(state_name)
|
|
|
|
def get_spec_data(self, name=None, default=None):
|
|
"""
|
|
Returns the value of the spec data with the given name, or the given
|
|
default value if the spec data does not exist.
|
|
|
|
:type name: str
|
|
:param name: The name of the spec data field.
|
|
:type default: obj
|
|
:param default: Return this value if the spec data does not exist.
|
|
:rtype: obj
|
|
:returns: The value of the spec data.
|
|
"""
|
|
return self.task_spec.get_data(name, default)
|
|
|
|
def _set_internal_data(self, **kwargs):
|
|
"""
|
|
Defines the given attribute/value pairs.
|
|
"""
|
|
self.internal_data.update(kwargs)
|
|
|
|
def _get_internal_data(self, name, default=None):
|
|
return self.internal_data.get(name, default)
|
|
|
|
def set_data(self, **kwargs):
|
|
"""
|
|
Defines the given attribute/value pairs.
|
|
"""
|
|
self.data.update(kwargs)
|
|
data_log.info('Set data', extra=self.log_info())
|
|
|
|
def _inherit_data(self):
|
|
"""
|
|
Inherits the data from the parent.
|
|
"""
|
|
self.set_data(**self.parent.data)
|
|
|
|
def get_data(self, name, default=None):
|
|
"""
|
|
Returns the value of the data field with the given name, or the given
|
|
default value if the data field does not exist.
|
|
|
|
:type name: str
|
|
:param name: A data field name.
|
|
:type default: obj
|
|
:param default: Return this value if the data field does not exist.
|
|
:rtype: obj
|
|
:returns: The value of the data field
|
|
"""
|
|
return self.data.get(name, default)
|
|
|
|
def cancel(self):
|
|
"""
|
|
Cancels the item if it was not yet completed, and removes
|
|
any children that are LIKELY.
|
|
"""
|
|
if self._is_finished():
|
|
for child in self.children:
|
|
child.cancel()
|
|
return
|
|
self._set_state(TaskState.CANCELLED)
|
|
self._drop_children()
|
|
self.task_spec._on_cancel(self)
|
|
|
|
def complete(self):
|
|
"""
|
|
Called by the associated task to let us know that its state
|
|
has changed (e.g. from FUTURE to COMPLETED.)
|
|
"""
|
|
self._set_state(TaskState.COMPLETED)
|
|
# WHY on earth do we mark the task completed and THEN attempt to execute it.
|
|
# A sane model would have success and failure states and instead we return
|
|
# a boolean, with no systematic way of dealing with failures. This is just
|
|
# crazy!
|
|
start = time.time()
|
|
retval = self.task_spec._on_complete(self)
|
|
extra = self.log_info({
|
|
'action': 'Complete',
|
|
'elapsed': time.time() - start
|
|
})
|
|
metrics.debug('', extra=extra)
|
|
return retval
|
|
|
|
def trigger(self, *args):
|
|
"""
|
|
If recursive is True, the state is applied to the tree recursively.
|
|
"""
|
|
self.task_spec._on_trigger(self, *args)
|
|
|
|
def get_dump(self, indent=0, recursive=True):
|
|
"""
|
|
Returns the subtree as a string for debugging.
|
|
|
|
:rtype: str
|
|
:returns: The debug information.
|
|
"""
|
|
dbg = (' ' * indent * 2)
|
|
dbg += '%s/' % self.id
|
|
dbg += '%s:' % self.thread_id
|
|
dbg += ' Task of %s' % self.get_name()
|
|
if self.task_spec.description:
|
|
dbg += ' (%s)' % self.get_description()
|
|
dbg += ' State: %s' % self.get_state_name()
|
|
dbg += ' Children: %s' % len(self.children)
|
|
if recursive:
|
|
for child in self.children:
|
|
dbg += '\n' + child.get_dump(indent + 1)
|
|
return dbg
|
|
|
|
def dump(self, indent=0):
|
|
"""
|
|
Prints the subtree as a string for debugging.
|
|
"""
|
|
print(self.get_dump())
|