taskflow(1)

TASKFLOW(1) TaskFlow TASKFLOW(1)

NAME

taskflow - TaskFlow 5.2.0

TaskFlow is a Python library that helps to make task execution easy, consistent and reliable. [1]

NOTE:

If you are just getting started or looking for an overview please visit: https://wiki.openstack.org/wiki/TaskFlow which provides better introductory material, description of high level goals and related content.


USING TASKFLOW

Considerations

Things to consider before (and during) development and integration with TaskFlow into your project:

  • Read over the paradigm shifts and engage the team in IRC (or via the openstack-dev mailing list) if these need more explanation (prefix [Oslo][TaskFlow] to your emails subject to get an even faster response).
  • Follow (or at least attempt to follow) some of the established best practices (feel free to add your own suggested best practices).
  • Keep in touch with the team (see above); we are all friendly and enjoy knowing your use cases and learning how we can help make your lives easier by adding or adjusting functionality in this library.

User Guide

Atoms, tasks and retries

Atom

An atom is the smallest unit in TaskFlow which acts as the base for other classes (its naming was inspired from the similarities between this type and atoms in the physical world). Atoms have a name and may have a version. An atom is expected to name desired input values (requirements) and name outputs (provided values).

NOTE:

For more details about atom inputs and outputs please visit arguments and results.


class taskflow.atom.Atom(name=None, provides=None, requires=None, auto_extract=True, rebind=None, inject=None, ignore_list=None, revert_rebind=None, revert_requires=None)
Bases: object

An unit of work that causes a flow to progress (in some manner).

An atom is a named object that operates with input data to perform some action that furthers the overall flows progress. It usually also produces some of its own named output as a result of this process.

Parameters
  • name -- Meaningful name for this atom, should be something that is distinguishable and understandable for notification, debugging, storing and any other similar purposes.
  • provides -- A set, string or list of items that this will be providing (or could provide) to others, used to correlate and associate the thing/s this atom produces, if it produces anything at all.
  • inject -- An immutable input_name => value dictionary which specifies any initial inputs that should be automatically injected into the atoms scope before the atom execution commences (this allows for providing atom local values that do not need to be provided by other atoms/dependents).
  • rebind -- A dict of key/value pairs used to define argument name conversions for inputs to this atom's execute method.
  • revert_rebind -- The same as rebind but for the revert method. If unpassed, rebind will be used instead.
  • requires -- A set or list of required inputs for this atom's execute method.
  • revert_requires -- A set or list of required inputs for this atom's revert method. If unpassed, requires will be used.

Variables
  • version -- An immutable version that associates version information with this atom. It can be useful in resuming older versions of atoms. Standard major, minor versioning concepts should apply.
  • save_as -- An immutable output resource name OrderedDict this atom produces that other atoms may depend on this atom providing. The format is output index (or key when a dictionary is returned from the execute method) to stored argument name.
  • rebind -- An immutable input resource OrderedDict that can be used to alter the inputs given to this atom. It is typically used for mapping a prior atoms output into the names that this atom expects (in a way this is like remapping a namespace of another atom into the namespace of this atom).
  • revert_rebind -- The same as rebind but for the revert method. This should only differ from rebind if the revert method has a different signature from execute or a different revert_rebind value was received.
  • inject -- See parameter inject.
  • Atom.name -- See parameter name.
  • optional -- A OrderedSet of inputs that are optional for this atom to execute.
  • revert_optional -- The revert version of optional.
  • provides -- A OrderedSet of outputs this atom produces.


priority = 0
A numeric priority that instances of this class will have when running, used when there are multiple parallel candidates to execute and/or revert. During this situation the candidate list will be stably sorted based on this priority attribute which will result in atoms with higher priorities executing (or reverting) before atoms with lower priorities (higher being defined as a number bigger, or greater tha an atom with a lower priority number). By default all atoms have the same priority (zero).

For example when the following is combined into a graph (where each node in the denoted graph is some task):

a -> b
b -> c
b -> e
b -> f


When b finishes there will then be three candidates that can run (c, e, f) and they may run in any order. What this priority does is sort those three by their priority before submitting them to be worked on (so that instead of say a random run order they will now be ran by there sorted order). This is also true when reverting (in that the sort order of the potential nodes will be used to determine the submission order).


requires
A OrderedSet of inputs this atom requires to function.

pre_execute()
Code to be run prior to executing the atom.

A common pattern for initializing the state of the system prior to running atoms is to define some code in a base class that all your atoms inherit from. In that class, you can define a pre_execute method and it will always be invoked just prior to your atoms running.


abstract execute(*args, **kwargs)
Activate a given atom which will perform some operation and return.

This method can be used to perform an action on a given set of input requirements (passed in via *args and **kwargs) to accomplish some type of operation. This operation may provide some named outputs/results as a result of it executing for later reverting (or for other atoms to depend on).

NOTE(harlowja): the result (if any) that is returned should be persistable so that it can be passed back into this atom if reverting is triggered (especially in the case where reverting happens in a different python process or on a remote machine) and so that the result can be transmitted to other atoms (which may be local or remote).

Parameters
  • args -- positional arguments that atom requires to execute.
  • kwargs -- any keyword arguments that atom requires to execute.



post_execute()
Code to be run after executing the atom.

A common pattern for cleaning up global state of the system after the execution of atoms is to define some code in a base class that all your atoms inherit from. In that class, you can define a post_execute method and it will always be invoked just after your atoms execute, regardless of whether they succeeded or not.

This pattern is useful if you have global shared database sessions that need to be cleaned up, for example.


pre_revert()
Code to be run prior to reverting the atom.

This works the same as pre_execute(), but for the revert phase.


revert(*args, **kwargs)
Revert this atom.

This method should undo any side-effects caused by previous execution of the atom using the result of the execute() method and information on the failure which triggered reversion of the flow the atom is contained in (if applicable).

Parameters
  • args -- positional arguments that the atom required to execute.
  • kwargs -- any keyword arguments that the atom required to execute; the special key 'result' will contain the execute() result (if any) and the **kwargs key 'flow_failures' will contain any failure information.



post_revert()
Code to be run after reverting the atom.

This works the same as post_execute(), but for the revert phase.



Task

A task (derived from an atom) is a unit of work that can have an execute & rollback sequence associated with it (they are nearly analogous to functions). Your task objects should all derive from Task which defines what a task must provide in terms of properties and methods.

For example: [image: Task outline.] [image]

Currently the following provided types of task subclasses are:

  • Task: useful for inheriting from and creating your own subclasses.
  • FunctorTask: useful for wrapping existing functions into task objects.

NOTE:

FunctorTask task types can not currently be used with the worker based engine due to the fact that arbitrary functions can not be guaranteed to be correctly located (especially if they are lambda or anonymous functions) on the worker nodes.


Retry

A retry (derived from an atom) is a special unit of work that handles errors, controls flow execution and can (for example) retry other atoms with other parameters if needed. When an associated atom fails, these retry units are consulted to determine what the resolution strategy should be. The goal is that with this consultation the retry atom will suggest a strategy for getting around the failure (perhaps by retrying, reverting a single atom, or reverting everything contained in the retries associated scope).

Currently derivatives of the retry base class must provide a on_failure() method to determine how a failure should be handled. The current enumeration(s) that can be returned from the on_failure() method are defined in an enumeration class described here:

class taskflow.retry.Decision(value)
Bases: StrEnum

Decision results/strategy enumeration.

REVERT = 'REVERT'
Reverts only the surrounding/associated subflow.

This strategy first consults the parent atom before reverting the associated subflow to determine if the parent retry object provides a different reconciliation strategy. This allows for safe nesting of flows with different retry strategies.

If the parent flow has no retry strategy, the default behavior is to just revert the atoms in the associated subflow. This is generally not the desired behavior, but is left as the default in order to keep backwards-compatibility. The defer_reverts engine option will let you change this behavior. If that is set to True, a REVERT will always defer to the parent, meaning that if the parent has no retry strategy, it will be reverted as well.


REVERT_ALL = 'REVERT_ALL'
Reverts the entire flow, regardless of parent strategy.

This strategy will revert every atom that has executed thus far, regardless of whether the parent flow has a separate retry strategy associated with it.


RETRY = 'RETRY'
Retries the surrounding/associated subflow again.


To aid in the reconciliation process the retry base class also mandates execute() and revert() methods (although subclasses are allowed to define these methods as no-ops) that can be used by a retry atom to interact with the runtime execution model (for example, to track the number of times it has been called which is useful for the ForEach retry subclass).

To avoid recreating common retry patterns the following provided retry subclasses are provided:

  • AlwaysRevert: Always reverts subflow.
  • AlwaysRevertAll: Always reverts the whole flow.
  • Times: Retries subflow given number of times.
  • ForEach: Allows for providing different values to subflow atoms each time a failure occurs (making it possibly to resolve the failure by altering subflow atoms inputs).
  • ParameterizedForEach: Same as ForEach but extracts values from storage instead of the ForEach constructor.

NOTE:

They are similar to exception handlers but are made to be more capable due to their ability to dynamically choose a reconciliation strategy, which allows for these atoms to influence subsequent execution(s) and the inputs any associated atoms require.


Area of influence

Each retry atom is associated with a flow and it can influence how the atoms (or nested flows) contained in that flow retry or revert (using the previously mentioned patterns and decision enumerations):

For example: [image: Retry area of influence] [image]

In this diagram retry controller (1) will be consulted if task A, B or C fail and retry controller (2) decides to delegate its retry decision to retry controller (1). If retry controller (2) does not decide to delegate its retry decision to retry controller (1) then retry controller (1) will be oblivious of any decisions. If any of task 1, 2 or 3 fail then only retry controller (1) will be consulted to determine the strategy/pattern to apply to resolve there associated failure.

Usage examples

>>> class EchoTask(task.Task):
...     def execute(self, *args, **kwargs):
...         print(self.name)
...         print(args)
...         print(kwargs)
...
>>> flow = linear_flow.Flow('f1').add(
...     EchoTask('t1'),
...     linear_flow.Flow('f2', retry=retry.ForEach(values=['a', 'b', 'c'], name='r1', provides='value')).add(
...         EchoTask('t2'),
...         EchoTask('t3', requires='value')),
...     EchoTask('t4'))


In this example the flow f2 has a retry controller r1, that is an instance of the default retry controller ForEach, it accepts a collection of values and iterates over this collection when each failure occurs. On each run ForEach retry returns the next value from the collection and stops retrying a subflow if there are no more values left in the collection. For example if tasks t2 or t3 fail, then the flow f2 will be reverted and retry r1 will retry it with the next value from the given collection ['a', 'b', 'c']. But if the task t1 or the task t4 fails, r1 won't retry a flow, because tasks t1 and t4 are in the flow f1 and don't depend on retry r1 (so they will not consult r1 on failure).

>>> class SendMessage(task.Task):
...     def execute(self, message):
...         print("Sending message: %s" % message)
...
>>> flow = linear_flow.Flow('send_message', retry=retry.Times(5)).add(
...      SendMessage('sender'))


In this example the send_message flow will try to execute the SendMessage five times when it fails. When it fails for the sixth time (if it does) the task will be asked to REVERT (in this example task reverting does not cause anything to happen but in other use cases it could).

>>> class ConnectToServer(task.Task):
...     def execute(self, ip):
...         print("Connecting to %s" % ip)
...
>>> server_ips = ['192.168.1.1', '192.168.1.2', '192.168.1.3' ]
>>> flow = linear_flow.Flow('send_message',
...                         retry=retry.ParameterizedForEach(rebind={'values': 'server_ips'},
...                                                          provides='ip')).add(
...     ConnectToServer(requires=['ip']))


In this example the flow tries to connect a server using a list (a tuple can also be used) of possible IP addresses. Each time the retry will return one IP from the list. In case of a failure it will return the next one until it reaches the last one, then the flow will be reverted.

Interfaces

class taskflow.task.Task(name=None, provides=None, requires=None, auto_extract=True, rebind=None, inject=None, ignore_list=None, revert_rebind=None, revert_requires=None)
Bases: Atom

An abstraction that defines a potential piece of work.

This potential piece of work is expected to be able to contain functionality that defines what can be executed to accomplish that work as well as a way of defining what can be executed to reverted/undo that same piece of work.

property notifier
Internal notification dispatcher/registry.

A notification object that will dispatch events that occur related to internal notifications that the task internally emits to listeners (for example for progress status updates, telling others that a task has reached 50% completion...).


copy(retain_listeners=True)
Clone/copy this task.
Parameters
retain_listeners -- retain the attached notification listeners when cloning, when false the listeners will be emptied, when true the listeners will be copied and retained
Returns
the copied task


update_progress(progress)
Update task progress and notify all registered listeners.
Parameters
progress -- task progress float value between 0.0 and 1.0



class taskflow.task.FunctorTask(execute, name=None, provides=None, requires=None, auto_extract=True, rebind=None, revert=None, version=None, inject=None)
Bases: Task

Adaptor to make a task from a callable.

Take any callable pair and make a task from it.

NOTE(harlowja): If a name is not provided the function/method name of the execute callable will be used as the name instead (the name of the revert callable is not used).

execute(*args, **kwargs)
Activate a given atom which will perform some operation and return.

This method can be used to perform an action on a given set of input requirements (passed in via *args and **kwargs) to accomplish some type of operation. This operation may provide some named outputs/results as a result of it executing for later reverting (or for other atoms to depend on).

NOTE(harlowja): the result (if any) that is returned should be persistable so that it can be passed back into this atom if reverting is triggered (especially in the case where reverting happens in a different python process or on a remote machine) and so that the result can be transmitted to other atoms (which may be local or remote).

Parameters
  • args -- positional arguments that atom requires to execute.
  • kwargs -- any keyword arguments that atom requires to execute.



revert(*args, **kwargs)
Revert this atom.

This method should undo any side-effects caused by previous execution of the atom using the result of the execute() method and information on the failure which triggered reversion of the flow the atom is contained in (if applicable).

Parameters
  • args -- positional arguments that the atom required to execute.
  • kwargs -- any keyword arguments that the atom required to execute; the special key 'result' will contain the execute() result (if any) and the **kwargs key 'flow_failures' will contain any failure information.




class taskflow.task.ReduceFunctorTask(functor, requires, name=None, provides=None, auto_extract=True, rebind=None, inject=None)
Bases: Task

General purpose Task to reduce a list by applying a function.

This Task mimics the behavior of Python's built-in reduce function. The Task takes a functor (lambda or otherwise) and a list. The list is specified using the requires argument of the Task. When executed, this task calls reduce with the functor and list as arguments. The resulting value from the call to reduce is then returned after execution.

execute(*args, **kwargs)
Activate a given atom which will perform some operation and return.

This method can be used to perform an action on a given set of input requirements (passed in via *args and **kwargs) to accomplish some type of operation. This operation may provide some named outputs/results as a result of it executing for later reverting (or for other atoms to depend on).

NOTE(harlowja): the result (if any) that is returned should be persistable so that it can be passed back into this atom if reverting is triggered (especially in the case where reverting happens in a different python process or on a remote machine) and so that the result can be transmitted to other atoms (which may be local or remote).

Parameters
  • args -- positional arguments that atom requires to execute.
  • kwargs -- any keyword arguments that atom requires to execute.




class taskflow.task.MapFunctorTask(functor, requires, name=None, provides=None, auto_extract=True, rebind=None, inject=None)
Bases: Task

General purpose Task to map a function to a list.

This Task mimics the behavior of Python's built-in map function. The Task takes a functor (lambda or otherwise) and a list. The list is specified using the requires argument of the Task. When executed, this task calls map with the functor and list as arguments. The resulting list from the call to map is then returned after execution.

Each value of the returned list can be bound to individual names using the provides argument, following taskflow standard behavior. Order is preserved in the returned list.

execute(*args, **kwargs)
Activate a given atom which will perform some operation and return.

This method can be used to perform an action on a given set of input requirements (passed in via *args and **kwargs) to accomplish some type of operation. This operation may provide some named outputs/results as a result of it executing for later reverting (or for other atoms to depend on).

NOTE(harlowja): the result (if any) that is returned should be persistable so that it can be passed back into this atom if reverting is triggered (especially in the case where reverting happens in a different python process or on a remote machine) and so that the result can be transmitted to other atoms (which may be local or remote).

Parameters
  • args -- positional arguments that atom requires to execute.
  • kwargs -- any keyword arguments that atom requires to execute.




class taskflow.retry.Retry(name=None, provides=None, requires=None, auto_extract=True, rebind=None)
Bases: Atom

A class that can decide how to resolve execution failures.

This abstract base class is used to inherit from and provide different strategies that will be activated upon execution failures. Since a retry object is an atom it may also provide execute() and revert() methods to alter the inputs of connected atoms (depending on the desired strategy to be used this can be quite useful).

NOTE(harlowja): the execute() and revert() and on_failure() will automatically be given a history parameter, which contains information about the past decisions and outcomes that have occurred (if available).

abstract execute(history, *args, **kwargs)
Executes the given retry.

This execution activates a given retry which will typically produce data required to start or restart a connected component using previously provided values and a history of prior failures from previous runs. The historical data can be analyzed to alter the resolution strategy that this retry controller will use.

For example, a retry can provide the same values multiple times (after each run), the latest value or some other variation. Old values will be saved to the history of the retry atom automatically, that is a list of tuples (result, failures) are persisted where failures is a dictionary of failures indexed by task names and the result is the execution result returned by this retry during that failure resolution attempt.

Parameters
  • args -- positional arguments that retry requires to execute.
  • kwargs -- any keyword arguments that retry requires to execute.



revert(history, *args, **kwargs)
Reverts this retry.

On revert call all results that had been provided by previous tries and all errors caused during reversion are provided. This method will be called only if a subflow must be reverted without the retry (that is to say that the controller has ran out of resolution options and has either given up resolution or has failed to handle a execution failure).

Parameters
  • args -- positional arguments that the retry required to execute.
  • kwargs -- any keyword arguments that the retry required to execute.



abstract on_failure(history, *args, **kwargs)
Makes a decision about the future.

This method will typically use information about prior failures (if this historical failure information is not available or was not persisted the provided history will be empty).

Returns a retry constant (one of):

  • RETRY: when the controlling flow must be reverted and restarted again (for example with new parameters).
  • REVERT: when this controlling flow must be completely reverted and the parent flow (if any) should make a decision about further flow execution.
  • REVERT_ALL: when this controlling flow and the parent flow (if any) must be reverted and marked as a FAILURE.



class taskflow.retry.History(contents, failure=None)
Bases: object

Helper that simplifies interactions with retry historical contents.

property failure
Returns the retries own failure or none if not existent.

outcomes_iter(index=None)
Iterates over the contained failure outcomes.

If the index is not provided, then all outcomes are iterated over.

NOTE(harlowja): if the retry itself failed, this will not include those types of failures. Use the failure attribute to access that instead (if it exists, aka, non-none).


provided_iter()
Iterates over all the values the retry has attempted (in order).

caused_by(exception_cls, index=None, include_retry=False)
Checks if the exception class provided caused the failures.

If the index is not provided, then all outcomes are iterated over.

NOTE(harlowja): only if include_retry is provided as true (defaults
to false) will the potential retries own failure be checked against as well.



class taskflow.retry.AlwaysRevert(name=None, provides=None, requires=None, auto_extract=True, rebind=None)
Bases: Retry

Retry that always reverts subflow.

on_failure(*args, **kwargs)
Makes a decision about the future.

This method will typically use information about prior failures (if this historical failure information is not available or was not persisted the provided history will be empty).

Returns a retry constant (one of):

  • RETRY: when the controlling flow must be reverted and restarted again (for example with new parameters).
  • REVERT: when this controlling flow must be completely reverted and the parent flow (if any) should make a decision about further flow execution.
  • REVERT_ALL: when this controlling flow and the parent flow (if any) must be reverted and marked as a FAILURE.


execute(*args, **kwargs)
Executes the given retry.

This execution activates a given retry which will typically produce data required to start or restart a connected component using previously provided values and a history of prior failures from previous runs. The historical data can be analyzed to alter the resolution strategy that this retry controller will use.

For example, a retry can provide the same values multiple times (after each run), the latest value or some other variation. Old values will be saved to the history of the retry atom automatically, that is a list of tuples (result, failures) are persisted where failures is a dictionary of failures indexed by task names and the result is the execution result returned by this retry during that failure resolution attempt.

Parameters
  • args -- positional arguments that retry requires to execute.
  • kwargs -- any keyword arguments that retry requires to execute.




class taskflow.retry.AlwaysRevertAll(name=None, provides=None, requires=None, auto_extract=True, rebind=None)
Bases: Retry

Retry that always reverts a whole flow.

on_failure(**kwargs)
Makes a decision about the future.

This method will typically use information about prior failures (if this historical failure information is not available or was not persisted the provided history will be empty).

Returns a retry constant (one of):

  • RETRY: when the controlling flow must be reverted and restarted again (for example with new parameters).
  • REVERT: when this controlling flow must be completely reverted and the parent flow (if any) should make a decision about further flow execution.
  • REVERT_ALL: when this controlling flow and the parent flow (if any) must be reverted and marked as a FAILURE.


execute(**kwargs)
Executes the given retry.

This execution activates a given retry which will typically produce data required to start or restart a connected component using previously provided values and a history of prior failures from previous runs. The historical data can be analyzed to alter the resolution strategy that this retry controller will use.

For example, a retry can provide the same values multiple times (after each run), the latest value or some other variation. Old values will be saved to the history of the retry atom automatically, that is a list of tuples (result, failures) are persisted where failures is a dictionary of failures indexed by task names and the result is the execution result returned by this retry during that failure resolution attempt.

Parameters
  • args -- positional arguments that retry requires to execute.
  • kwargs -- any keyword arguments that retry requires to execute.




class taskflow.retry.Times(attempts=1, name=None, provides=None, requires=None, auto_extract=True, rebind=None, revert_all=False)
Bases: Retry

Retries subflow given number of times. Returns attempt number.

Parameters
  • attempts (int) -- number of attempts to retry the associated subflow before giving up
  • revert_all (bool) -- when provided this will cause the full flow to revert when the number of attempts that have been tried has been reached (when false, it will only locally revert the associated subflow)


Further arguments are interpreted as defined in the Atom constructor.

on_failure(history, *args, **kwargs)
Makes a decision about the future.

This method will typically use information about prior failures (if this historical failure information is not available or was not persisted the provided history will be empty).

Returns a retry constant (one of):

  • RETRY: when the controlling flow must be reverted and restarted again (for example with new parameters).
  • REVERT: when this controlling flow must be completely reverted and the parent flow (if any) should make a decision about further flow execution.
  • REVERT_ALL: when this controlling flow and the parent flow (if any) must be reverted and marked as a FAILURE.


execute(history, *args, **kwargs)
Executes the given retry.

This execution activates a given retry which will typically produce data required to start or restart a connected component using previously provided values and a history of prior failures from previous runs. The historical data can be analyzed to alter the resolution strategy that this retry controller will use.

For example, a retry can provide the same values multiple times (after each run), the latest value or some other variation. Old values will be saved to the history of the retry atom automatically, that is a list of tuples (result, failures) are persisted where failures is a dictionary of failures indexed by task names and the result is the execution result returned by this retry during that failure resolution attempt.

Parameters
  • args -- positional arguments that retry requires to execute.
  • kwargs -- any keyword arguments that retry requires to execute.




class taskflow.retry.ForEach(values, name=None, provides=None, requires=None, auto_extract=True, rebind=None, revert_all=False)
Bases: ForEachBase

Applies a statically provided collection of strategies.

Accepts a collection of decision strategies on construction and returns the next element of the collection on each try.

Parameters
  • values (list) -- values collection to iterate over and provide to atoms other in the flow as a result of this functions execute() method, which other dependent atoms can consume (for example, to alter their own behavior)
  • revert_all (bool) -- when provided this will cause the full flow to revert when the number of attempts that have been tried has been reached (when false, it will only locally revert the associated subflow)


Further arguments are interpreted as defined in the Atom constructor.

on_failure(history, *args, **kwargs)
Makes a decision about the future.

This method will typically use information about prior failures (if this historical failure information is not available or was not persisted the provided history will be empty).

Returns a retry constant (one of):

  • RETRY: when the controlling flow must be reverted and restarted again (for example with new parameters).
  • REVERT: when this controlling flow must be completely reverted and the parent flow (if any) should make a decision about further flow execution.
  • REVERT_ALL: when this controlling flow and the parent flow (if any) must be reverted and marked as a FAILURE.


execute(history, *args, **kwargs)
Executes the given retry.

This execution activates a given retry which will typically produce data required to start or restart a connected component using previously provided values and a history of prior failures from previous runs. The historical data can be analyzed to alter the resolution strategy that this retry controller will use.

For example, a retry can provide the same values multiple times (after each run), the latest value or some other variation. Old values will be saved to the history of the retry atom automatically, that is a list of tuples (result, failures) are persisted where failures is a dictionary of failures indexed by task names and the result is the execution result returned by this retry during that failure resolution attempt.

Parameters
  • args -- positional arguments that retry requires to execute.
  • kwargs -- any keyword arguments that retry requires to execute.




class taskflow.retry.ParameterizedForEach(name=None, provides=None, requires=None, auto_extract=True, rebind=None, revert_all=False)
Bases: ForEachBase

Applies a dynamically provided collection of strategies.

Accepts a collection of decision strategies from a predecessor (or from storage) as a parameter and returns the next element of that collection on each try.

Parameters
revert_all (bool) -- when provided this will cause the full flow to revert when the number of attempts that have been tried has been reached (when false, it will only locally revert the associated subflow)

Further arguments are interpreted as defined in the Atom constructor.

on_failure(values, history, *args, **kwargs)
Makes a decision about the future.

This method will typically use information about prior failures (if this historical failure information is not available or was not persisted the provided history will be empty).

Returns a retry constant (one of):

  • RETRY: when the controlling flow must be reverted and restarted again (for example with new parameters).
  • REVERT: when this controlling flow must be completely reverted and the parent flow (if any) should make a decision about further flow execution.
  • REVERT_ALL: when this controlling flow and the parent flow (if any) must be reverted and marked as a FAILURE.


execute(values, history, *args, **kwargs)
Executes the given retry.

This execution activates a given retry which will typically produce data required to start or restart a connected component using previously provided values and a history of prior failures from previous runs. The historical data can be analyzed to alter the resolution strategy that this retry controller will use.

For example, a retry can provide the same values multiple times (after each run), the latest value or some other variation. Old values will be saved to the history of the retry atom automatically, that is a list of tuples (result, failures) are persisted where failures is a dictionary of failures indexed by task names and the result is the execution result returned by this retry during that failure resolution attempt.

Parameters
  • args -- positional arguments that retry requires to execute.
  • kwargs -- any keyword arguments that retry requires to execute.




Hierarchy

Arguments and results

In TaskFlow, all flow and task state goes to (potentially persistent) storage (see persistence for more details). That includes all the information that atoms (e.g. tasks, retry objects...) in the workflow need when they are executed, and all the information task/retry produces (via serializable results). A developer who implements tasks/retries or flows can specify what arguments a task/retry accepts and what result it returns in several ways. This document will help you understand what those ways are and how to use those ways to accomplish your desired usage pattern.

Task/retry arguments
Set of names of task/retry arguments available as the requires and/or optional property of the task/retry instance. When a task or retry object is about to be executed values with these names are retrieved from storage and passed to the execute method of the task/retry. If any names in the requires property cannot be found in storage, an exception will be thrown. Any names in the optional property that cannot be found are ignored.
Task/retry results
Set of names of task/retry results (what task/retry provides) available as provides property of task or retry instance. After a task/retry finishes successfully, its result(s) (what the execute method returns) are available by these names from storage (see examples below).

Arguments specification

There are different ways to specify the task argument requires set.

Arguments inference

Task/retry arguments can be inferred from arguments of the execute() method of a task (or the execute() of a retry object).

>>> class MyTask(task.Task):
...     def execute(self, spam, eggs, bacon=None):
...         return spam + eggs
...
>>> sorted(MyTask().requires)
['eggs', 'spam']
>>> sorted(MyTask().optional)
['bacon']


Inference from the method signature is the ''simplest'' way to specify arguments. Special arguments like self, *args and **kwargs are ignored during inference (as these names have special meaning/usage in python).

>>> class UniTask(task.Task):
...     def execute(self, *args, **kwargs):
...         pass
...
>>> sorted(UniTask().requires)
[]


Rebinding

Why: There are cases when the value you want to pass to a task/retry is stored with a name other than the corresponding arguments name. That's when the rebind constructor parameter comes in handy. Using it the flow author can instruct the engine to fetch a value from storage by one name, but pass it to a tasks/retries execute method with another name. There are two possible ways of accomplishing this.

The first is to pass a dictionary that maps the argument name to the name of a saved value.

For example, if you have task:

class SpawnVMTask(task.Task):

def execute(self, vm_name, vm_image_id, **kwargs):
pass # TODO(imelnikov): use parameters to spawn vm


and you saved 'vm_name' with 'name' key in storage, you can spawn a vm with such 'name' like this:

SpawnVMTask(rebind={'vm_name': 'name'})


The second way is to pass a tuple/list/dict of argument names. The length of the tuple/list/dict should not be less then number of required parameters.

For example, you can achieve the same effect as the previous example with:

SpawnVMTask(rebind_args=('name', 'vm_image_id'))


This is equivalent to a more elaborate:

SpawnVMTask(rebind=dict(vm_name='name',

vm_image_id='vm_image_id'))


In both cases, if your task (or retry) accepts arbitrary arguments with the **kwargs construct, you can specify extra arguments.

SpawnVMTask(rebind=('name', 'vm_image_id', 'admin_key_name'))


When such task is about to be executed, name, vm_image_id and admin_key_name values are fetched from storage and value from name is passed to execute() method as vm_name, value from vm_image_id is passed as vm_image_id, and value from admin_key_name is passed as admin_key_name parameter in kwargs.

Manually specifying requirements

Why: It is often useful to manually specify the requirements of a task, either by a task author or by the flow author (allowing the flow author to override the task requirements).

To accomplish this when creating your task use the constructor to specify manual requirements. Those manual requirements (if they are not functional arguments) will appear in the kwargs of the execute() method.

>>> class Cat(task.Task):
...     def __init__(self, **kwargs):
...         if 'requires' not in kwargs:
...             kwargs['requires'] = ("food", "milk")
...         super(Cat, self).__init__(**kwargs)
...     def execute(self, food, **kwargs):
...         pass
...
>>> cat = Cat()
>>> sorted(cat.requires)
['food', 'milk']


When constructing a task instance the flow author can also add more requirements if desired. Those manual requirements (if they are not functional arguments) will appear in the kwargs parameter of the execute() method.

>>> class Dog(task.Task):
...     def execute(self, food, **kwargs):
...         pass
>>> dog = Dog(requires=("water", "grass"))
>>> sorted(dog.requires)
['food', 'grass', 'water']


If the flow author desires she can turn the argument inference off and override requirements manually. Use this at your own risk as you must be careful to avoid invalid argument mappings.

>>> class Bird(task.Task):
...     def execute(self, food, **kwargs):
...         pass
>>> bird = Bird(requires=("food", "water", "grass"), auto_extract=False)
>>> sorted(bird.requires)
['food', 'grass', 'water']


Results specification

In python, function results are not named, so we can not infer what a task/retry returns. This is important since the complete result (what the task execute() or retry execute() method returns) is saved in (potentially persistent) storage, and it is typically (but not always) desirable to make those results accessible to others. To accomplish this the task/retry specifies names of those values via its provides constructor parameter or by its default provides attribute.

Examples

Returning one value

If task returns just one value, provides should be string -- the name of the value.

>>> class TheAnswerReturningTask(task.Task):
...    def execute(self):
...        return 42
...
>>> sorted(TheAnswerReturningTask(provides='the_answer').provides)
['the_answer']


Returning a tuple

For a task that returns several values, one option (as usual in python) is to return those values via a tuple.

class BitsAndPiecesTask(task.Task):

def execute(self):
return 'BITs', 'PIECEs'


Then, you can give the value individual names, by passing a tuple or list as provides parameter:

BitsAndPiecesTask(provides=('bits', 'pieces'))


After such task is executed, you (and the engine, which is useful for other tasks) will be able to get those elements from storage by name:

>>> storage.fetch('bits')
'BITs'
>>> storage.fetch('pieces')
'PIECEs'


Provides argument can be shorter then the actual tuple returned by a task -- then extra values are ignored (but, as expected, all those values are saved and passed to the task revert() or retry revert() method).

NOTE:

Provides arguments tuple can also be longer then the actual tuple returned by task -- when this happens the extra parameters are left undefined: a warning is printed to logs and if use of such parameter is attempted a NotFound exception is raised.


Returning a dictionary

Another option is to return several values as a dictionary (aka a dict).

class BitsAndPiecesTask(task.Task):

def execute(self):
return {
'bits': 'BITs',
'pieces': 'PIECEs'
}


TaskFlow expects that a dict will be returned if provides argument is a set:

BitsAndPiecesTask(provides=set(['bits', 'pieces']))


After such task executes, you (and the engine, which is useful for other tasks) will be able to get elements from storage by name:

>>> storage.fetch('bits')
'BITs'
>>> storage.fetch('pieces')
'PIECEs'


NOTE:

If some items from the dict returned by the task are not present in the provides arguments -- then extra values are ignored (but, of course, saved and passed to the revert() method). If the provides argument has some items not present in the actual dict returned by the task -- then extra parameters are left undefined: a warning is printed to logs and if use of such parameter is attempted a NotFound exception is raised.


Default provides

As mentioned above, the default base class provides nothing, which means results are not accessible to other tasks/retries in the flow.

The author can override this and specify default value for provides using the default_provides class/instance variable:

class BitsAndPiecesTask(task.Task):

default_provides = ('bits', 'pieces')
def execute(self):
return 'BITs', 'PIECEs'


Of course, the flow author can override this to change names if needed:

BitsAndPiecesTask(provides=('b', 'p'))


or to change structure -- e.g. this instance will make tuple accessible to other tasks by name 'bnp':

BitsAndPiecesTask(provides='bnp')


or the flow author may want to return default behavior and hide the results of the task from other tasks in the flow (e.g. to avoid naming conflicts):

BitsAndPiecesTask(provides=())


Revert arguments

To revert a task the engine calls the tasks revert() method. This method should accept the same arguments as the execute() method of the task and one more special keyword argument, named result.

For result value, two cases are possible:

  • If the task is being reverted because it failed (an exception was raised from its execute() method), the result value is an instance of a Failure object that holds the exception information.
  • If the task is being reverted because some other task failed, and this task finished successfully, result value is the result fetched from storage: ie, what the execute() method returned.

All other arguments are fetched from storage in the same way it is done for execute() method.

To determine if a task failed you can check whether result is instance of Failure:

from taskflow.types import failure
class RevertingTask(task.Task):

def execute(self, spam, eggs):
return do_something(spam, eggs)
def revert(self, result, spam, eggs):
if isinstance(result, failure.Failure):
print("This task failed, exception: %s"
% result.exception_str)
else:
print("do_something returned %r" % result)


If this task failed (ie do_something raised an exception) it will print "This task failed, exception:" and a exception message on revert. If this task finished successfully, it will print "do_something returned" and a representation of the do_something result.

Retry arguments

A Retry controller works with arguments in the same way as a Task. But it has an additional parameter 'history' that is itself a History object that contains what failed over all the engines attempts (aka the outcomes). The history object can be viewed as a tuple that contains a result of the previous retries run and a table/dict where each key is a failed atoms name and each value is a Failure object.

Consider the following implementation:

class MyRetry(retry.Retry):

default_provides = 'value'
def on_failure(self, history, *args, **kwargs):
print(list(history))
return RETRY
def execute(self, history, *args, **kwargs):
print(list(history))
return 5
def revert(self, history, *args, **kwargs):
print(list(history))


Imagine the above retry had returned a value '5' and then some task 'A' failed with some exception. In this case on_failure method will receive the following history (printed as a list):

[('5', {'A': failure.Failure()})]


At this point (since the implementation returned RETRY) the execute() method will be called again and it will receive the same history and it can then return a value that subsequent tasks can use to alter their behavior.

If instead the execute() method itself raises an exception, the revert() method of the implementation will be called and a Failure object will be present in the history object instead of the typical result.

NOTE:

After a Retry has been reverted, the objects history will be cleaned.


Inputs and outputs

In TaskFlow there are multiple ways to provide inputs for your tasks and flows and get information from them. This document describes one of them, that involves task arguments and results. There are also notifications, which allow you to get notified when a task or flow changes state. You may also opt to use the persistence layer itself directly.

Flow inputs and outputs

Tasks accept inputs via task arguments and provide outputs via task results (see arguments and results for more details). This is the standard and recommended way to pass data from one task to another. Of course not every task argument needs to be provided to some other task of a flow, and not every task result should be consumed by every task.

If some value is required by one or more tasks of a flow, but it is not provided by any task, it is considered to be flow input, and must be put into the storage before the flow is run. A set of names required by a flow can be retrieved via that flow's requires property. These names can be used to determine what names may be applicable for placing in storage ahead of time and which names are not applicable.

All values provided by tasks of the flow are considered to be flow outputs; the set of names of such values is available via the provides property of the flow.

For example:

>>> class MyTask(task.Task):
...     def execute(self, **kwargs):
...         return 1, 2
...
>>> flow = linear_flow.Flow('test').add(
...     MyTask(requires='a', provides=('b', 'c')),
...     MyTask(requires='b', provides='d')
... )
>>> flow.requires
frozenset(['a'])
>>> sorted(flow.provides)
['b', 'c', 'd']


As you can see, this flow does not require b, as it is provided by the fist task.

NOTE:

There is no difference between processing of Task and Retry inputs and outputs.


Engine and storage

The storage layer is how an engine persists flow and task details (for more in-depth details see persistence).

Inputs

As mentioned above, if some value is required by one or more tasks of a flow, but is not provided by any task, it is considered to be flow input, and must be put into the storage before the flow is run. On failure to do so MissingDependencies is raised by the engine prior to running:

>>> class CatTalk(task.Task):
...   def execute(self, meow):
...     print meow
...     return "cat"
...
>>> class DogTalk(task.Task):
...   def execute(self, woof):
...     print woof
...     return "dog"
...
>>> flo = linear_flow.Flow("cat-dog")
>>> flo.add(CatTalk(), DogTalk(provides="dog"))
<taskflow.patterns.linear_flow.Flow object at 0x...>
>>> engines.run(flo)
Traceback (most recent call last):

... taskflow.exceptions.MissingDependencies: 'linear_flow.Flow: cat-dog(len=2)' requires ['meow', 'woof'] but no other entity produces said requirements
MissingDependencies: 'execute' method on '__main__.DogTalk==1.0' requires ['woof'] but no other entity produces said requirements
MissingDependencies: 'execute' method on '__main__.CatTalk==1.0' requires ['meow'] but no other entity produces said requirements


The recommended way to provide flow inputs is to use the store parameter of the engine helpers (run() or load()):

>>> class CatTalk(task.Task):
...   def execute(self, meow):
...     print meow
...     return "cat"
...
>>> class DogTalk(task.Task):
...   def execute(self, woof):
...     print woof
...     return "dog"
...
>>> flo = linear_flow.Flow("cat-dog")
>>> flo.add(CatTalk(), DogTalk(provides="dog"))
<taskflow.patterns.linear_flow.Flow object at 0x...>
>>> result = engines.run(flo, store={'meow': 'meow', 'woof': 'woof'})
meow
woof
>>> pprint(result)
{'dog': 'dog', 'meow': 'meow', 'woof': 'woof'}


You can also directly interact with the engine storage layer to add additional values, note that if this route is used you can't use the helper method run(). Instead, you must activate the engine's run method directly run():

>>> flo = linear_flow.Flow("cat-dog")
>>> flo.add(CatTalk(), DogTalk(provides="dog"))
<taskflow.patterns.linear_flow.Flow object at 0x...>
>>> eng = engines.load(flo, store={'meow': 'meow'})
>>> eng.storage.inject({"woof": "bark"})
>>> eng.run()
meow
bark


Outputs

As you can see from examples above, the run method returns all flow outputs in a dict. This same data can be fetched via fetch_all() method of the engines storage object. You can also get single results using the engines storage objects fetch() method.

For example:

>>> eng = engines.load(flo, store={'meow': 'meow', 'woof': 'woof'})
>>> eng.run()
meow
woof
>>> pprint(eng.storage.fetch_all())
{'dog': 'dog', 'meow': 'meow', 'woof': 'woof'}
>>> print(eng.storage.fetch("dog"))
dog


Patterns

class taskflow.flow.Flow(name, retry=None)
Bases: object

The base abstract class of all flow implementations.

A flow is a structure that defines relationships between tasks. You can add tasks and other flows (as subflows) to the flow, and the flow provides a way to implicitly or explicitly define how they are interdependent. Exact structure of the relationships is defined by concrete implementation, while this class defines common interface and adds human-readable (not necessary unique) name.

NOTE(harlowja): if a flow is placed in another flow as a subflow, a desired way to compose flows together, then it is valid and permissible that during compilation the subflow & parent flow may be flattened into a new flow.

property name
A non-unique name for this flow (human readable).

property retry
The associated flow retry controller.

This retry controller object will affect & control how (and if) this flow and its contained components retry when execution is underway and a failure occurs.


abstract add(*items)
Adds a given item/items to this flow.

abstract iter_links()
Iterates over dependency links between children of the flow.
Iterates over 3-tuples (A, B, meta), where
  • A is a child (atom or subflow) link starts from;
  • B is a child (atom or subflow) link points to; it is said that B depends on A or B requires A;
  • meta is link metadata, a dictionary.



abstract iter_nodes()
Iterate over nodes of the flow.
Iterates over 2-tuples (A, meta), where
  • A is a child (atom or subflow) of current flow;
  • meta is link metadata, a dictionary.



property provides
Set of symbol names provided by the flow.

abstract property requires
Set of unsatisfied symbol names required by the flow.


Linear flow

class taskflow.patterns.linear_flow.Flow(name, retry=None)
Bases: Flow

Linear flow pattern.

A linear (potentially nested) flow of tasks/flows that can be applied in order as one unit and rolled back as one unit using the reverse order that the tasks/flows have been applied in.

add(*items)
Adds a given task/tasks/flow/flows to this flow.

property requires
Set of unsatisfied symbol names required by the flow.

iter_nodes()
Iterate over nodes of the flow.
Iterates over 2-tuples (A, meta), where
  • A is a child (atom or subflow) of current flow;
  • meta is link metadata, a dictionary.



iter_links()
Iterates over dependency links between children of the flow.
Iterates over 3-tuples (A, B, meta), where
  • A is a child (atom or subflow) link starts from;
  • B is a child (atom or subflow) link points to; it is said that B depends on A or B requires A;
  • meta is link metadata, a dictionary.




Unordered flow

class taskflow.patterns.unordered_flow.Flow(name, retry=None)
Bases: Flow

Unordered flow pattern.

A unordered (potentially nested) flow of tasks/flows that can be executed in any order as one unit and rolled back as one unit.

add(*items)
Adds a given task/tasks/flow/flows to this flow.

iter_links()
Iterates over dependency links between children of the flow.
Iterates over 3-tuples (A, B, meta), where
  • A is a child (atom or subflow) link starts from;
  • B is a child (atom or subflow) link points to; it is said that B depends on A or B requires A;
  • meta is link metadata, a dictionary.



iter_nodes()
Iterate over nodes of the flow.
Iterates over 2-tuples (A, meta), where
  • A is a child (atom or subflow) of current flow;
  • meta is link metadata, a dictionary.



property requires
Set of unsatisfied symbol names required by the flow.


Graph flow

class taskflow.patterns.graph_flow.Flow(name, retry=None)
Bases: Flow

Graph flow pattern.

Contained flows/tasks will be executed according to their dependencies which will be resolved by using the flows/tasks provides and requires mappings or by following manually created dependency links.

From dependencies a directed graph is built. If it has edge A -> B, this means B depends on A (and that the execution of B must wait until A has finished executing, on reverting this means that the reverting of A must wait until B has finished reverting).

Note: cyclic dependencies are not allowed.

link(u, v, decider=None, decider_depth=None)
Link existing node u as a runtime dependency of existing node v.

Note that if the addition of these edges creates a cyclic graph then a DependencyFailure will be raised and the provided changes will be discarded. If the nodes that are being requested to link do not exist in this graph than a ValueError will be raised.

Parameters
  • u -- task or flow to create a link from (must exist already)
  • v -- task or flow to create a link to (must exist already)
  • decider -- A callback function that will be expected to decide at runtime whether v should be allowed to execute (or whether the execution of v should be ignored, and therefore not executed). It is expected to take as single keyword argument history which will be the execution results of all u decidable links that have v as a target. It is expected to return a single boolean (True to allow v execution or False to not).
  • decider_depth -- One of the Depth enumerations (or a string version of) that will be used to influence what atoms are ignored when the decider provided results false. If not provided (and a valid decider is provided then this defaults to ALL).



add(*nodes, **kwargs)
Adds a given task/tasks/flow/flows to this flow.

Note that if the addition of these nodes (and any edges) creates a cyclic graph then a DependencyFailure will be raised and the applied changes will be discarded.

Parameters
  • nodes -- node(s) to add to the flow
  • kwargs --

    keyword arguments, the two keyword arguments currently processed are:

  • resolve_requires a boolean that when true (the default) implies that when node(s) are added their symbol requirements will be matched to existing node(s) and links will be automatically made to those providers. If multiple possible providers exist then a AmbiguousDependency exception will be raised and the provided additions will be discarded.
  • resolve_existing, a boolean that when true (the default) implies that on addition of a new node that existing node(s) will have their requirements scanned for symbols that this newly added node can provide. If a match is found a link is automatically created from the newly added node to the requiree.






iter_links()
Iterates over dependency links between children of the flow.
Iterates over 3-tuples (A, B, meta), where
  • A is a child (atom or subflow) link starts from;
  • B is a child (atom or subflow) link points to; it is said that B depends on A or B requires A;
  • meta is link metadata, a dictionary.



iter_nodes()
Iterate over nodes of the flow.
Iterates over 2-tuples (A, meta), where
  • A is a child (atom or subflow) of current flow;
  • meta is link metadata, a dictionary.



property requires
Set of unsatisfied symbol names required by the flow.


class taskflow.patterns.graph_flow.TargetedFlow(*args, **kwargs)
Bases: Flow

Graph flow with a target.

Adds possibility to execute a flow up to certain graph node (task or subflow).

set_target(target_node)
Set target for the flow.

Any node(s) (tasks or subflows) not needed for the target node will not be executed.


reset_target()
Reset target for the flow.

All node(s) of the flow will be executed.


add(*nodes, **kwargs)
Adds a given task/tasks/flow/flows to this flow.

Note that if the addition of these nodes (and any edges) creates a cyclic graph then a DependencyFailure will be raised and the applied changes will be discarded.

Parameters
  • nodes -- node(s) to add to the flow
  • kwargs --

    keyword arguments, the two keyword arguments currently processed are:

  • resolve_requires a boolean that when true (the default) implies that when node(s) are added their symbol requirements will be matched to existing node(s) and links will be automatically made to those providers. If multiple possible providers exist then a AmbiguousDependency exception will be raised and the provided additions will be discarded.
  • resolve_existing, a boolean that when true (the default) implies that on addition of a new node that existing node(s) will have their requirements scanned for symbols that this newly added node can provide. If a match is found a link is automatically created from the newly added node to the requiree.






link(u, v, decider=None, decider_depth=None)
Link existing node u as a runtime dependency of existing node v.

Note that if the addition of these edges creates a cyclic graph then a DependencyFailure will be raised and the provided changes will be discarded. If the nodes that are being requested to link do not exist in this graph than a ValueError will be raised.

Parameters
  • u -- task or flow to create a link from (must exist already)
  • v -- task or flow to create a link to (must exist already)
  • decider -- A callback function that will be expected to decide at runtime whether v should be allowed to execute (or whether the execution of v should be ignored, and therefore not executed). It is expected to take as single keyword argument history which will be the execution results of all u decidable links that have v as a target. It is expected to return a single boolean (True to allow v execution or False to not).
  • decider_depth -- One of the Depth enumerations (or a string version of) that will be used to influence what atoms are ignored when the decider provided results false. If not provided (and a valid decider is provided then this defaults to ALL).




class taskflow.deciders.Depth(value)
Bases: StrEnum

Enumeration of decider(s) area of influence.

ALL = 'ALL'
Default decider depth that affects all successor atoms (including ones that are in successor nested flows).

FLOW = 'FLOW'
Decider depth that affects all successor tasks in the same flow (it will not affect tasks/retries that are in successor nested flows).

WARNING:

While using this kind we are allowed to execute successors of things that have been ignored (for example nested flows and the tasks they contain), this may result in symbol lookup errors during running, user beware.



NEIGHBORS = 'NEIGHBORS'
Decider depth that affects only next successor tasks (and does not traverse past one level of successor tasks).

WARNING:

While using this kind we are allowed to execute successors of things that have been ignored (for example nested flows and the tasks they contain), this may result in symbol lookup errors during running, user beware.



ATOM = 'ATOM'
Decider depth that affects only targeted atom (and does not traverse into any level of successor atoms).

WARNING:

While using this kind we are allowed to execute successors of things that have been ignored (for example nested flows and the tasks they contain), this may result in symbol lookup errors during running, user beware.



classmethod translate(desired_depth)
Translates a string into a depth enumeration.


taskflow.deciders.pick_widest(depths)
Pick from many depths which has the widest area of influence.

Hierarchy

Engines

Overview

Engines are what really runs your atoms.

An engine takes a flow structure (described by patterns) and uses it to decide which atom to run and when.

TaskFlow provides different implementations of engines. Some may be easier to use (ie, require no additional infrastructure setup) and understand; others might require more complicated setup but provide better scalability. The idea and ideal is that deployers or developers of a service that use TaskFlow can select an engine that suites their setup best without modifying the code of said service.

NOTE:

Engines usually have different capabilities and configuration, but all of them must implement the same interface and preserve the semantics of patterns (e.g. parts of a linear_flow.Flow are run one after another, in order, even if the selected engine is capable of running tasks in parallel).


Why they exist

An engine being the core component which actually makes your flows progress is likely a new concept for many programmers so let's describe how it operates in more depth and some of the reasoning behind why it exists. This will hopefully make it more clear on their value add to the TaskFlow library user.

First though let us discuss something most are familiar already with; the difference between declarative and imperative programming models. The imperative model involves establishing statements that accomplish a programs action (likely using conditionals and such other language features to do this). This kind of program embeds the how to accomplish a goal while also defining what the goal actually is (and the state of this is maintained in memory or on the stack while these statements execute). In contrast there is the declarative model which instead of combining the how to accomplish a goal along side the what is to be accomplished splits these two into only declaring what the intended goal is and not the how. In TaskFlow terminology the what is the structure of your flows and the tasks and other atoms you have inside those flows, but the how is not defined (the line becomes blurred since tasks themselves contain imperative code, but for now consider a task as more of a pure function that executes, reverts and may require inputs and provide outputs). This is where engines get involved; they do the execution of the what defined via atoms, tasks, flows and the relationships defined there-in and execute these in a well-defined manner (and the engine is responsible for any state manipulation instead).

This mix of imperative and declarative (with a stronger emphasis on the declarative model) allows for the following functionality to become possible:

  • Enhancing reliability: Decoupling of state alterations from what should be accomplished allows for a natural way of resuming by allowing the engine to track the current state and know at which point a workflow is in and how to get back into that state when resumption occurs.
  • Enhancing scalability: When an engine is responsible for executing your desired work it becomes possible to alter the how in the future by creating new types of execution backends (for example the worker model which does not execute locally). Without the decoupling of the what and the how it is not possible to provide such a feature (since by the very nature of that coupling this kind of functionality is inherently very hard to provide).
  • Enhancing consistency: Since the engine is responsible for executing atoms and the associated workflow, it can be one (if not the only) of the primary entities that is working to keep the execution model in a consistent state. Coupled with atoms which should be immutable and have have limited (if any) internal state the ability to reason about and obtain consistency can be vastly improved.
With future features around locking (using tooz to help) engines can also help ensure that resources being accessed by tasks are reliably obtained and mutated on. This will help ensure that other processes, threads, or other types of entities are also not executing tasks that manipulate those same resources (further increasing consistency).


Of course these kind of features can come with some drawbacks:

  • The downside of decoupling the how and the what is that the imperative model where functions control & manipulate state must start to be shifted away from (and this is likely a mindset change for programmers used to the imperative model). We have worked to make this less of a concern by creating and encouraging the usage of persistence, to help make it possible to have state and transfer that state via a argument input and output mechanism.
  • Depending on how much imperative code exists (and state inside that code) there may be significant rework of that code and converting or refactoring it to these new concepts. We have tried to help here by allowing you to have tasks that internally use regular python code (and internally can be written in an imperative style) as well as by providing examples that show how to use these concepts.
  • Another one of the downsides of decoupling the what from the how is that it may become harder to use traditional techniques to debug failures (especially if remote workers are involved). We try to help here by making it easy to track, monitor and introspect the actions & state changes that are occurring inside an engine (see notifications for how to use some of these capabilities).

Creating

All engines are mere classes that implement the same interface, and of course it is possible to import them and create instances just like with any classes in Python. But the easier (and recommended) way for creating an engine is using the engine helper functions. All of these functions are imported into the taskflow.engines module namespace, so the typical usage of these functions might look like:

from taskflow import engines
...
flow = make_flow()
eng = engines.load(flow, engine='serial', backend=my_persistence_conf)
eng.run()
...


taskflow.engines.helpers.load(flow, store=None, flow_detail=None, book=None, backend=None, namespace='taskflow.engines', engine='default', **kwargs)
Load a flow into an engine.

This function creates and prepares an engine to run the provided flow. All that is left after this returns is to run the engine with the engines run() method.

Which engine to load is specified via the engine parameter. It can be a string that names the engine type to use, or a string that is a URI with a scheme that names the engine type to use and further options contained in the URI's host, port, and query parameters...

Which storage backend to use is defined by the backend parameter. It can be backend itself, or a dictionary that is passed to fetch() to obtain a viable backend.

Parameters
  • flow -- flow to load
  • store -- dict -- data to put to storage to satisfy flow requirements
  • flow_detail -- FlowDetail that holds the state of the flow (if one is not provided then one will be created for you in the provided backend)
  • book -- LogBook to create flow detail in if flow_detail is None
  • backend -- storage backend to use or configuration that defines it
  • namespace -- driver namespace for stevedore (or empty for default)
  • engine -- string engine type or URI string with scheme that contains the engine type and any URI specific components that will become part of the engine options.
  • kwargs -- arbitrary keyword arguments passed as options (merged with any extracted engine), typically used for any engine specific options that do not fit as any of the existing arguments.

Returns
engine


taskflow.engines.helpers.run(flow, store=None, flow_detail=None, book=None, backend=None, namespace='taskflow.engines', engine='default', **kwargs)
Run the flow.

This function loads the flow into an engine (with the load() function) and runs the engine.

The arguments are interpreted as for load().

Returns
dictionary of all named results (see fetch_all())


taskflow.engines.helpers.save_factory_details(flow_detail, flow_factory, factory_args, factory_kwargs, backend=None)
Saves the given factories reimportable attributes into the flow detail.

This function saves the factory name, arguments, and keyword arguments into the given flow details object and if a backend is provided it will also ensure that the backend saves the flow details after being updated.

Parameters
  • flow_detail -- FlowDetail that holds state of the flow to load
  • flow_factory -- function or string: function that creates the flow
  • factory_args -- list or tuple of factory positional arguments
  • factory_kwargs -- dict of factory keyword arguments
  • backend -- storage backend to use or configuration



taskflow.engines.helpers.load_from_factory(flow_factory, factory_args=None, factory_kwargs=None, store=None, book=None, backend=None, namespace='taskflow.engines', engine='default', **kwargs)
Loads a flow from a factory function into an engine.

Gets flow factory function (or name of it) and creates flow with it. Then, the flow is loaded into an engine with the load() function, and the factory function fully qualified name is saved to flow metadata so that it can be later resumed.

Parameters
  • flow_factory -- function or string: function that creates the flow
  • factory_args -- list or tuple of factory positional arguments
  • factory_kwargs -- dict of factory keyword arguments


Further arguments are interpreted as for load().

Returns
engine


taskflow.engines.helpers.flow_from_detail(flow_detail)
Reloads a flow previously saved.

Gets the flow factories name and any arguments and keyword arguments from the flow details metadata, and then calls that factory to recreate the flow.

Parameters
flow_detail -- FlowDetail that holds state of the flow to load


taskflow.engines.helpers.load_from_detail(flow_detail, store=None, backend=None, namespace='taskflow.engines', engine='default', **kwargs)
Reloads an engine previously saved.

This reloads the flow using the flow_from_detail() function and then calls into the load() function to create an engine from that flow.

Parameters
flow_detail -- FlowDetail that holds state of the flow to load

Further arguments are interpreted as for load().

Returns
engine


Usage

To select which engine to use and pass parameters to an engine you should use the engine parameter any engine helper function accepts and for any engine specific options use the kwargs parameter.

Types

Serial

Engine type: 'serial'

Runs all tasks on a single thread -- the same thread run() is called from.

NOTE:

This engine is used by default.


TIP:

If eventlet is used then this engine will not block other threads from running as eventlet automatically creates a implicit co-routine system (using greenthreads and monkey patching). See eventlet and greenlet for more details.


Parallel

Engine type: 'parallel'

A parallel engine schedules tasks onto different threads/processes to allow for running non-dependent tasks simultaneously. See the documentation of ParallelActionEngine for supported arguments that can be used to construct a parallel engine that runs using your desired execution model.

TIP:

Sharing an executor between engine instances provides better scalability by reducing thread/process creation and teardown as well as by reusing existing pools (which is a good practice in general).


WARNING:

Running tasks with a process pool executor is experimentally supported. This is mainly due to the futures backport and the multiprocessing module that exist in older versions of python not being as up to date (with important fixes such as 4892, 6721, 9205, 16284, 22393 and others...) as the most recent python version (which themselves have a variety of ongoing/recent bugs).


Workers

Engine type: 'worker-based' or 'workers'

NOTE:

Since this engine is significantly more complicated (and different) then the others we thought it appropriate to devote a whole documentation section to it.


How they run

To provide a peek into the general process that an engine goes through when running lets break it apart a little and describe what one of the engine types does while executing (for this we will look into the ActionEngine engine type).

Creation

The first thing that occurs is that the user creates an engine for a given flow, providing a flow detail (where results will be saved into a provided persistence backend). This is typically accomplished via the methods described above in creating engines. The engine at this point now will have references to your flow and backends and other internal variables are setup.

Compiling

During this stage (see compile()) the flow will be converted into an internal graph representation using a compiler (the default implementation for patterns is the PatternCompiler). This class compiles/converts the flow objects and contained atoms into a networkx directed graph (and tree structure) that contains the equivalent atoms defined in the flow and any nested flows & atoms as well as the constraints that are created by the application of the different flow patterns. This graph (and tree) are what will be analyzed & traversed during the engines execution. At this point a few helper object are also created and saved to internal engine variables (these object help in execution of atoms, analyzing the graph and performing other internal engine activities). At the finishing of this stage a Runtime object is created which contains references to all needed runtime components and its compile() is called to compile a cache of frequently used execution helper objects.

Preparation

This stage (see prepare()) starts by setting up the storage needed for all atoms in the compiled graph, ensuring that corresponding AtomDetail (or subclass of) objects are created for each node in the graph.

Validation

This stage (see validate()) performs any final validation of the compiled (and now storage prepared) engine. It compares the requirements that are needed to start execution and what is currently provided or will be produced in the future. If there are any atom requirements that are not satisfied (no known current provider or future producer is found) then execution will not be allowed to continue.

Execution

The graph (and helper objects) previously created are now used for guiding further execution (see run()). The flow is put into the RUNNING state and a MachineBuilder state machine object and runner object are built (using the automaton library). That machine and associated runner then starts to take over and begins going through the stages listed below (for a more visual diagram/representation see the engine state diagram).

NOTE:

The engine will respect the constraints imposed by the flow. For example, if an engine is executing a Flow then it is constrained by the dependency graph which is linear in this case, and hence using a parallel engine may not yield any benefits if one is looking for concurrency.


Resumption

One of the first stages is to analyze the state of the tasks in the graph, determining which ones have failed, which one were previously running and determining what the intention of that task should now be (typically an intention can be that it should REVERT, or that it should EXECUTE or that it should be IGNORED). This intention is determined by analyzing the current state of the task; which is determined by looking at the state in the task detail object for that task and analyzing edges of the graph for things like retry atom which can influence what a tasks intention should be (this is aided by the usage of the Selector helper object which was designed to provide helper methods for this analysis). Once these intentions are determined and associated with each task (the intention is also stored in the AtomDetail object) the scheduling stage starts.

Scheduling

This stage selects which atoms are eligible to run by using a Scheduler implementation (the default implementation looks at their intention, checking if predecessor atoms have ran and so-on, using a Selector helper object as needed) and submits those atoms to a previously provided compatible executor for asynchronous execution. This Scheduler will return a future object for each atom scheduled; all of which are collected into a list of not done futures. This will end the initial round of scheduling and at this point the engine enters the waiting stage.

Waiting

In this stage the engine waits for any of the future objects previously submitted to complete. Once one of the future objects completes (or fails) that atoms result will be examined and finalized using a Completer implementation. It typically will persist results to a provided persistence backend (saved into the corresponding AtomDetail and FlowDetail objects via the Storage helper) and reflect the new state of the atom. At this point what typically happens falls into two categories, one for if that atom failed and one for if it did not. If the atom failed it may be set to a new intention such as RETRY or REVERT (other atoms that were predecessors of this failing atom may also have there intention altered). Once this intention adjustment has happened a new round of scheduling occurs and this process repeats until the engine succeeds or fails (if the process running the engine dies the above stages will be restarted and resuming will occur).

NOTE:

If the engine is suspended while the engine is going through the above stages this will stop any further scheduling stages from occurring and all currently executing work will be allowed to finish (see suspension).


Finishing

At this point the machine (and runner) that was built using the MachineBuilder class has now finished successfully, failed, or the execution was suspended. Depending on which one of these occurs will cause the flow to enter a new state (typically one of FAILURE, SUSPENDED, SUCCESS or REVERTED). Notifications will be sent out about this final state change (other state changes also send out notifications) and any failures that occurred will be reraised (the failure objects are wrapped exceptions). If no failures have occurred then the engine will have finished and if so desired the persistence can be used to cleanup any details that were saved for this execution.

Special cases

Suspension

Each engine implements a suspend() method that can be used to externally (or in the future internally) request that the engine stop scheduling new work. By default what this performs is a transition of the flow state from RUNNING into a SUSPENDING state (which will later transition into a SUSPENDED state). Since an engine may be remotely executing atoms (or locally executing them) and there is currently no preemption what occurs is that the engines MachineBuilder state machine will detect this transition into SUSPENDING has occurred and the state machine will avoid scheduling new work (it will though let active work continue). After the current work has finished the engine will transition from SUSPENDING into SUSPENDED and return from its run() method.

NOTE:

When run() is returned from at that point there may (but does not have to be, depending on what was active when suspend() was called) be unfinished work in the flow that was not finished (but which can be resumed at a later point in time).


Scoping

During creation of flows it is also important to understand the lookup strategy (also typically known as scope resolution) that the engine you are using will internally use. For example when a task A provides result 'a' and a task B after A provides a different result 'a' and a task C after A and after B requires 'a' to run, which one will be selected?

Default strategy

When an engine is executing it internally interacts with the Storage class and that class interacts with the a ScopeWalker instance and the Storage class uses the following lookup order to find (or fail) a atoms requirement lookup/request:

1.
Transient injected atom specific arguments.
2.
Non-transient injected atom specific arguments.
3.
Transient injected arguments (flow specific).
4.
Non-transient injected arguments (flow specific).
5.
First scope visited provider that produces the named result; note that if multiple providers are found in the same scope the first (the scope walkers yielded ordering defines what first means) that produced that result and can be extracted without raising an error is selected as the provider of the requested requirement.
6.
Fails with NotFound if unresolved at this point (the cause attribute of this exception may have more details on why the lookup failed).

NOTE:

To examine this information when debugging it is recommended to enable the BLATHER logging level (level 5). At this level the storage and scope code/layers will log what is being searched for and what is being found.


Interfaces

class taskflow.engines.base.Engine(flow, flow_detail, backend, options)
Bases: object

Base for all engines implementations.

Variables
  • Engine.notifier -- A notification object that will dispatch events that occur related to the flow the engine contains.
  • atom_notifier -- A notification object that will dispatch events that occur related to the atoms the engine contains.


property notifier
The flow notifier.

property atom_notifier
The atom notifier.

property options
The options that were passed to this engine on construction.

abstract property storage
The storage unit for this engine.

abstract property statistics
A dictionary of runtime statistics this engine has gathered.

This dictionary will be empty when the engine has never been ran. When it is running or has ran previously it should have (but may not) have useful and/or informational keys and values when running is underway and/or completed.

WARNING:

The keys in this dictionary should be some what stable (not changing), but there existence may change between major releases as new statistics are gathered or removed so before accessing keys ensure that they actually exist and handle when they do not.



abstract compile()
Compiles the contained flow into a internal representation.

This internal representation is what the engine will actually use to run. If this compilation can not be accomplished then an exception is expected to be thrown with a message indicating why the compilation could not be achieved.


abstract reset()
Reset back to the PENDING state.

If a flow had previously ended up (from a prior engine run()) in the FAILURE, SUCCESS or REVERTED states (or for some reason it ended up in an intermediary state) it can be desirable to make it possible to run it again. Calling this method enables that to occur (without causing a state transition failure, which would typically occur if run() is called directly without doing a reset).


abstract prepare()
Performs any pre-run, but post-compilation actions.

NOTE(harlowja): During preparation it is currently assumed that the underlying storage will be initialized, the atoms will be reset and the engine will enter the PENDING state.


abstract validate()
Performs any pre-run, post-prepare validation actions.

NOTE(harlowja): During validation all final dependencies will be verified and ensured. This will by default check that all atoms have satisfiable requirements (satisfied by some other provider).


abstract run()
Runs the flow in the engine to completion (or die trying).

abstract suspend()
Attempts to suspend the engine.

If the engine is currently running atoms then this will attempt to suspend future work from being started (currently active atoms can not currently be preempted) and move the engine into a suspend state which can then later be resumed from.



Implementations

class taskflow.engines.action_engine.engine.ActionEngine(flow, flow_detail, backend, options)
Bases: Engine

Generic action-based engine.

This engine compiles the flow (and any subflows) into a compilation unit which contains the full runtime definition to be executed and then uses this compilation unit in combination with the executor, runtime, machine builder and storage classes to attempt to run your flow (and any subflows & contained atoms) to completion.

NOTE(harlowja): during this process it is permissible and valid to have a task or multiple tasks in the execution graph fail (at the same time even), which will cause the process of reversion or retrying to commence. See the valid states in the states module to learn more about what other states the tasks and flow being ran can go through.

Engine options:

Name/key Description Type Default
defer_reverts This option lets you safely nest flows with retries inside flows without retries and it still behaves as a user would expect (for example if the retry gets exhausted it reverts the outer flow unless the outer flow has a has a separate retry behavior). bool False
never_resolve When true, instead of reverting and trying to resolve a atom failure the engine will skip reverting and abort instead of reverting and/or retrying. bool False
inject_transient When true, values that are local to each atoms scope are injected into storage into a transient location (typically a local dictionary), when false those values are instead persisted into atom details (and saved in a non- transient manner). bool True
NO_RERAISING_STATES = frozenset({'SUCCESS', 'SUSPENDED'})
States that if the engine stops in will not cause any potential failures to be reraised. States not in this list will cause any failure/s that were captured (if any) to get reraised.

IGNORABLE_STATES = frozenset({'ANALYZING', 'GAME_OVER', 'RESUMING', 'SCHEDULING', 'UNDEFINED', 'WAITING'})
Informational states this engines internal machine yields back while running, not useful to have the engine record but useful to provide to end-users when doing execution iterations via run_iter().

MAX_MACHINE_STATES_RETAINED = 10
During run_iter() the last X state machine transitions will be recorded (typically only useful on failure).

suspend()
Attempts to suspend the engine.

If the engine is currently running atoms then this will attempt to suspend future work from being started (currently active atoms can not currently be preempted) and move the engine into a suspend state which can then later be resumed from.


property statistics
A dictionary of runtime statistics this engine has gathered.

This dictionary will be empty when the engine has never been ran. When it is running or has ran previously it should have (but may not) have useful and/or informational keys and values when running is underway and/or completed.

WARNING:

The keys in this dictionary should be some what stable (not changing), but there existence may change between major releases as new statistics are gathered or removed so before accessing keys ensure that they actually exist and handle when they do not.



property compilation
The compilation result.

NOTE(harlowja): Only accessible after compilation has completed (None will be returned when this property is accessed before compilation has completed successfully).


storage
The storage unit for this engine.

NOTE(harlowja): the atom argument lookup strategy will change for this storage unit after compile() has completed (since only after compilation is the actual structure known). Before compile() has completed the atom argument lookup strategy lookup will be restricted to injected arguments only (this will not reflect the actual runtime lookup strategy, which typically will be, but is not always different).


run(timeout=None)
Runs the engine (or die trying).
Parameters
timeout -- timeout to wait for any atoms to complete (this timeout will be used during the waiting period that occurs when unfinished atoms are being waited on).


run_iter(timeout=None)
Runs the engine using iteration (or die trying).
Parameters
timeout -- timeout to wait for any atoms to complete (this timeout will be used during the waiting period that occurs after the waiting state is yielded when unfinished atoms are being waited on).

Instead of running to completion in a blocking manner, this will return a generator which will yield back the various states that the engine is going through (and can be used to run multiple engines at once using a generator per engine). The iterator returned also responds to the send() method from PEP 0342 and will attempt to suspend itself if a truthy value is sent in (the suspend may be delayed until all active atoms have finished).

NOTE(harlowja): using the run_iter method will not retain the engine lock while executing so the user should ensure that there is only one entity using a returned engine iterator (one per engine) at a given time.


validate()
Performs any pre-run, post-prepare validation actions.

NOTE(harlowja): During validation all final dependencies will be verified and ensured. This will by default check that all atoms have satisfiable requirements (satisfied by some other provider).


prepare()
Performs any pre-run, but post-compilation actions.

NOTE(harlowja): During preparation it is currently assumed that the underlying storage will be initialized, the atoms will be reset and the engine will enter the PENDING state.


reset()
Reset back to the PENDING state.

If a flow had previously ended up (from a prior engine run()) in the FAILURE, SUCCESS or REVERTED states (or for some reason it ended up in an intermediary state) it can be desirable to make it possible to run it again. Calling this method enables that to occur (without causing a state transition failure, which would typically occur if run() is called directly without doing a reset).


compile()
Compiles the contained flow into a internal representation.

This internal representation is what the engine will actually use to run. If this compilation can not be accomplished then an exception is expected to be thrown with a message indicating why the compilation could not be achieved.



class taskflow.engines.action_engine.engine.SerialActionEngine(flow, flow_detail, backend, options)
Bases: ActionEngine

Engine that runs tasks in serial manner.


class taskflow.engines.action_engine.engine.ParallelActionEngine(flow, flow_detail, backend, options)
Bases: ActionEngine

Engine that runs tasks in parallel manner.

Additional engine options:
executor: a object that implements a PEP 3148 compatible executor interface; it will be used for scheduling tasks. The following type are applicable (other unknown types passed will cause a type error to be raised).



Type provided Executor used
concurrent.futures.thread.ThreadPoolExecutor ParallelThreadTaskExecutor
concurrent.futures.process.ProcessPoolExecutor ParallelProcessTaskExecutor
concurrent.futures._base.Executor ParallelThreadTaskExecutor
executor: a string that will be used to select a PEP 3148 compatible executor; it will be used for scheduling tasks. The following string are applicable (other unknown strings passed will cause a value error to be raised).



String (case insensitive) Executor used
process ParallelProcessTaskExecutor
processes ParallelProcessTaskExecutor
thread ParallelThreadTaskExecutor
threaded ParallelThreadTaskExecutor
threads ParallelThreadTaskExecutor
greenthread 7.0 ParallelThreadTaskExecutor (greened version) 168u
greedthreaded 7.0 ParallelThreadTaskExecutor (greened version) 168u
greenthreads 7.0 ParallelThreadTaskExecutor (greened version) 168u
  • max_workers: a integer that will affect the number of parallel workers that are used to dispatch tasks into (this number is bounded by the maximum parallelization your workflow can support).
  • wait_timeout: a float (in seconds) that will affect the parallel process task executor (and therefore is only applicable when the executor provided above is of the process variant). This number affects how much time the process task executor waits for messages from child processes (typically indicating they have finished or failed). A lower number will have high granularity but currently involves more polling while a higher number will involve less polling but a slower time for an engine to notice a task has completed.




Components

WARNING:

External usage of internal engine functions, components and modules should be kept to a minimum as they may be altered, refactored or moved to other locations without notice (and without the typical deprecation cycle).


class taskflow.engines.action_engine.builder.MachineMemory
Bases: object

State machine memory.

cancel_futures()
Attempts to cancel any not done futures.


class taskflow.engines.action_engine.builder.MachineBuilder(runtime, waiter)
Bases: object

State machine builder that powers the engine components.

NOTE(harlowja): the machine (states and events that will trigger transitions) that this builds is represented by the following table:

+--------------+------------------+------------+----------+---------+
|    Start     |      Event       |    End     | On Enter | On Exit |
+--------------+------------------+------------+----------+---------+
|  ANALYZING   |    completed     | GAME_OVER  |    .     |    .    |
|  ANALYZING   |  schedule_next   | SCHEDULING |    .     |    .    |
|  ANALYZING   |  wait_finished   |  WAITING   |    .     |    .    |
|  FAILURE[$]  |        .         |     .      |    .     |    .    |
|  GAME_OVER   |      failed      |  FAILURE   |    .     |    .    |
|  GAME_OVER   |     reverted     |  REVERTED  |    .     |    .    |
|  GAME_OVER   |     success      |  SUCCESS   |    .     |    .    |
|  GAME_OVER   |    suspended     | SUSPENDED  |    .     |    .    |
|   RESUMING   |  schedule_next   | SCHEDULING |    .     |    .    |
| REVERTED[$]  |        .         |     .      |    .     |    .    |
|  SCHEDULING  |  wait_finished   |  WAITING   |    .     |    .    |
|  SUCCESS[$]  |        .         |     .      |    .     |    .    |
| SUSPENDED[$] |        .         |     .      |    .     |    .    |
| UNDEFINED[^] |      start       |  RESUMING  |    .     |    .    |
|   WAITING    | examine_finished | ANALYZING  |    .     |    .    |
+--------------+------------------+------------+----------+---------+


Between any of these yielded states (minus GAME_OVER and UNDEFINED) if the engine has been suspended or the engine has failed (due to a non-resolveable task failure or scheduling failure) the machine will stop executing new tasks (currently running tasks will be allowed to complete) and this machines run loop will be broken.

NOTE(harlowja): If the runtimes scheduler component is able to schedule tasks in parallel, this enables parallel running and/or reversion.

build(statistics, timeout=None, gather_statistics=True)
Builds a state-machine (that is used during running).


class taskflow.engines.action_engine.compiler.Terminator(flow)
Bases: object

Flow terminator class.

property flow
The flow which this terminator signifies/marks the end of.

property name
Useful name this end terminator has (derived from flow name).


class taskflow.engines.action_engine.compiler.Compilation(execution_graph, hierarchy)
Bases: object

The result of a compilers compile() is this immutable object.

TASK = 'task'
Task nodes will have a kind metadata key with this value.

RETRY = 'retry'
Retry nodes will have a kind metadata key with this value.

FLOW = 'flow'
Flow entry nodes will have a kind metadata key with this value.

FLOW_END = 'flow_end'
Flow exit nodes will have a kind metadata key with this value (only applicable for compilation execution graph, not currently used in tree hierarchy).

property execution_graph
The execution ordering of atoms (as a graph structure).

property hierarchy
The hierarchy of patterns (as a tree structure).


class taskflow.engines.action_engine.compiler.TaskCompiler
Bases: object

Non-recursive compiler of tasks.


class taskflow.engines.action_engine.compiler.FlowCompiler(deep_compiler_func)
Bases: object

Recursive compiler of flows.

compile(flow, parent=None)
Decomposes a flow into a graph and scope tree hierarchy.


class taskflow.engines.action_engine.compiler.PatternCompiler(root, freeze=True)
Bases: object

Compiles a flow pattern (or task) into a compilation unit.

Let's dive into the basic idea for how this works:

The compiler here is provided a 'root' object via its __init__ method, this object could be a task, or a flow (one of the supported patterns), the end-goal is to produce a Compilation object as the result with the needed components. If this is not possible a CompilationFailure will be raised. In the case where a unknown type is being requested to compile a TypeError will be raised and when a duplicate object (one that has already been compiled) is encountered a ValueError is raised.

The complexity of this comes into play when the 'root' is a flow that contains itself other nested flows (and so-on); to compile this object and its contained objects into a graph that preserves the constraints the pattern mandates we have to go through a recursive algorithm that creates subgraphs for each nesting level, and then on the way back up through the recursion (now with a decomposed mapping from contained patterns or atoms to there corresponding subgraph) we have to then connect the subgraphs (and the atom(s) there-in) that were decomposed for a pattern correctly into a new graph and then ensure the pattern mandated constraints are retained. Finally we then return to the caller (and they will do the same thing up until the root node, which by that point one graph is created with all contained atoms in the pattern/nested patterns mandated ordering).

Also maintained in the Compilation object is a hierarchy of the nesting of items (which is also built up during the above mentioned recusion, via a much simpler algorithm); this is typically used later to determine the prior atoms of a given atom when looking up values that can be provided to that atom for execution (see the scopes.py file for how this works). Note that although you could think that the graph itself could be used for this, which in some ways it can (for limited usage) the hierarchy retains the nested structure (which is useful for scoping analysis/lookup) to be able to provide back a iterator that gives back the scopes visible at each level (the graph does not have this information once flattened).

Let's take an example:

Given the pattern f(a(b, c), d) where f is a Flow with items a(b, c) where a is a Flow composed of tasks (b, c) and task d.

The algorithm that will be performed (mirroring the above described logic) will go through the following steps (the tree hierarchy building is left out as that is more obvious):

Compiling f

- Decomposing flow f with no parent (must be the root)
- Compiling a
- Decomposing flow a with parent f
- Compiling b
- Decomposing task b with parent a
- Decomposed b into:
Name: b
Nodes: 1
- b
Edges: 0
- Compiling c
- Decomposing task c with parent a
- Decomposed c into:
Name: c
Nodes: 1
- c
Edges: 0
- Relinking decomposed b -> decomposed c
- Decomposed a into:
Name: a
Nodes: 2
- b
- c
Edges: 1
b -> c ({'invariant': True})
- Compiling d
- Decomposing task d with parent f
- Decomposed d into:
Name: d
Nodes: 1
- d
Edges: 0
- Relinking decomposed a -> decomposed d
- Decomposed f into:
Name: f
Nodes: 3
- c
- b
- d
Edges: 2
c -> d ({'invariant': True})
b -> c ({'invariant': True})


compile()
Compiles the contained item into a compiled equivalent.


class taskflow.engines.action_engine.completer.Strategy(runtime)
Bases: object

Failure resolution strategy base class.

abstract apply()
Applies some algorithm to resolve some detected failure.


class taskflow.engines.action_engine.completer.RevertAndRetry(runtime, retry)
Bases: Strategy

Sets the associated subflow for revert to be later retried.

apply()
Applies some algorithm to resolve some detected failure.


class taskflow.engines.action_engine.completer.RevertAll(runtime)
Bases: Strategy

Sets all nodes/atoms to the REVERT intention.

apply()
Applies some algorithm to resolve some detected failure.


class taskflow.engines.action_engine.completer.Revert(runtime, atom)
Bases: Strategy

Sets atom and associated nodes to the REVERT intention.

apply()
Applies some algorithm to resolve some detected failure.


class taskflow.engines.action_engine.completer.Completer(runtime)
Bases: object

Completes atoms using actions to complete them.

resume()
Resumes atoms in the contained graph.

This is done to allow any previously completed or failed atoms to be analyzed, there results processed and any potential atoms affected to be adjusted as needed.

This should return a set of atoms which should be the initial set of atoms that were previously not finished (due to a RUNNING or REVERTING attempt not previously finishing).


complete_failure(node, outcome, failure)
Performs post-execution completion of a nodes failure.

Returns whether the result should be saved into an accumulator of failures or whether this should not be done.


complete(node, outcome, result)
Performs post-execution completion of a node result.


class taskflow.engines.action_engine.deciders.Decider
Bases: object

Base class for deciders.

Provides interface to be implemented by sub-classes.

Deciders check whether next atom in flow should be executed or not.

abstract tally(runtime)
Tally edge deciders on whether this decider should allow running.

The returned value is a list of edge deciders that voted 'nay' (do not allow running).


abstract affect(runtime, nay_voters)
Affects associated atoms due to at least one 'nay' edge decider.

This will alter the associated atom + some set of successor atoms by setting there state and intention to IGNORE so that they are ignored in future runtime activities.


check_and_affect(runtime)
Handles tally() + affect() in right order.

NOTE(harlowja): If there are zero 'nay' edge deciders then it is assumed this decider should allow running.

Returns boolean of whether this decider allows for running (or not).



class taskflow.engines.action_engine.deciders.IgnoreDecider(atom, edge_deciders)
Bases: Decider

Checks any provided edge-deciders and determines if ok to run.

tally(runtime)
Tally edge deciders on whether this decider should allow running.

The returned value is a list of edge deciders that voted 'nay' (do not allow running).


affect(runtime, nay_voters)
Affects associated atoms due to at least one 'nay' edge decider.

This will alter the associated atom + some set of successor atoms by setting there state and intention to IGNORE so that they are ignored in future runtime activities.



class taskflow.engines.action_engine.deciders.NoOpDecider
Bases: Decider

No-op decider that says it is always ok to run & has no effect(s).

tally(runtime)
Always good to go.

affect(runtime, nay_voters)
Does nothing.


class taskflow.engines.action_engine.executor.SerialRetryExecutor
Bases: object

Executes and reverts retries.

start()
Prepare to execute retries.

stop()
Finalize retry executor.

execute_retry(retry, arguments)
Schedules retry execution.

revert_retry(retry, arguments)
Schedules retry reversion.


class taskflow.engines.action_engine.executor.TaskExecutor
Bases: object

Executes and reverts tasks.

This class takes task and its arguments and executes or reverts it. It encapsulates knowledge on how task should be executed or reverted: right now, on separate thread, on another machine, etc.

abstract execute_task(task, task_uuid, arguments, progress_callback=None)
Schedules task execution.

abstract revert_task(task, task_uuid, arguments, result, failures, progress_callback=None)
Schedules task reversion.

start()
Prepare to execute tasks.

stop()
Finalize task executor.


class taskflow.engines.action_engine.executor.SerialTaskExecutor
Bases: TaskExecutor

Executes tasks one after another.

start()
Prepare to execute tasks.

stop()
Finalize task executor.

execute_task(task, task_uuid, arguments, progress_callback=None)
Schedules task execution.

revert_task(task, task_uuid, arguments, result, failures, progress_callback=None)
Schedules task reversion.


class taskflow.engines.action_engine.executor.ParallelTaskExecutor(executor=None, max_workers=None)
Bases: TaskExecutor

Executes tasks in parallel.

Submits tasks to an executor which should provide an interface similar to concurrent.Futures.Executor.

constructor_options = [('max_workers', <function ParallelTaskExecutor.<lambda>>)]
Optional constructor keyword arguments this executor supports. These will typically be passed via engine options (by a engine user) and converted into the correct type before being sent into this classes __init__ method.

execute_task(task, task_uuid, arguments, progress_callback=None)
Schedules task execution.

revert_task(task, task_uuid, arguments, result, failures, progress_callback=None)
Schedules task reversion.

start()
Prepare to execute tasks.

stop()
Finalize task executor.


class taskflow.engines.action_engine.executor.ParallelThreadTaskExecutor(executor=None, max_workers=None)
Bases: ParallelTaskExecutor

Executes tasks in parallel using a thread pool executor.


class taskflow.engines.action_engine.executor.ParallelGreenThreadTaskExecutor(executor=None, max_workers=None)
Bases: ParallelThreadTaskExecutor

Executes tasks in parallel using a greenthread pool executor.

DEFAULT_WORKERS = 1000
Default number of workers when None is passed; being that greenthreads don't map to native threads or processors very well this is more of a guess/somewhat arbitrary, but it does match what the eventlet greenpool default size is (so at least it's consistent with what eventlet does).


exception taskflow.engines.action_engine.process_executor.UnknownSender
Bases: Exception

Exception raised when message from unknown sender is recvd.


exception taskflow.engines.action_engine.process_executor.ChallengeIgnored
Bases: Exception

Exception raised when challenge has not been responded to.


class taskflow.engines.action_engine.process_executor.Reader(auth_key, dispatch_func, msg_limit=-1)
Bases: object

Reader machine that streams & parses messages that it then dispatches.

TODO(harlowja): Use python-suitcase in the future when the following are addressed/resolved and released:

  • https://github.com/digidotcom/python-suitcase/issues/28
  • https://github.com/digidotcom/python-suitcase/issues/29

Binary format format is the following (no newlines in actual format):

<magic-header> (4 bytes)
<mac-header-length> (4 bytes)
<mac> (1 or more variable bytes)
<identity-header-length> (4 bytes)
<identity> (1 or more variable bytes)
<msg-header-length> (4 bytes)
<msg> (1 or more variable bytes)



exception taskflow.engines.action_engine.process_executor.BadHmacValueError
Bases: ValueError

Value error raised when an invalid hmac is discovered.


class taskflow.engines.action_engine.process_executor.Channel(port, identity, auth_key)
Bases: object

Object that workers use to communicate back to their creator.


class taskflow.engines.action_engine.process_executor.EventSender(channel)
Bases: object

Sends event information from a child worker process to its creator.


class taskflow.engines.action_engine.process_executor.DispatcherHandler(sock, addr, dispatcher)
Bases: dispatcher

Dispatches from a single connection into a target.

CHUNK_SIZE = 8192
Read/write chunk size.


class taskflow.engines.action_engine.process_executor.Dispatcher(map, auth_key, identity)
Bases: dispatcher

Accepts messages received from child worker processes.

MAX_BACKLOG = 5
See https://docs.python.org/2/library/socket.html#socket.socket.listen


class taskflow.engines.action_engine.process_executor.ParallelProcessTaskExecutor(executor=None, max_workers=None, wait_timeout=None)
Bases: ParallelTaskExecutor

Executes tasks in parallel using a process pool executor.

NOTE(harlowja): this executor executes tasks in external processes, so that implies that tasks that are sent to that external process are pickleable since this is how the multiprocessing works (sending pickled objects back and forth) and that the bound handlers (for progress updating in particular) are proxied correctly from that external process to the one that is alive in the parent process to ensure that callbacks registered in the parent are executed on events in the child.

WAIT_TIMEOUT = 0.01
Default timeout used by asyncore io loop (and eventually select/poll).

constructor_options = [('max_workers', <function ParallelProcessTaskExecutor.<lambda>>), ('wait_timeout', <function ParallelProcessTaskExecutor.<lambda>>)]
Optional constructor keyword arguments this executor supports. These will typically be passed via engine options (by a engine user) and converted into the correct type before being sent into this classes __init__ method.

start()
Prepare to execute tasks.

stop()
Finalize task executor.


class taskflow.engines.action_engine.runtime.Runtime(compilation, storage, atom_notifier, task_executor, retry_executor, options=None)
Bases: object

A aggregate of runtime objects, properties, ... used during execution.

This object contains various utility methods and properties that represent the collection of runtime components and functionality needed for an action engine to run to completion.

compile()
Compiles & caches frequently used execution helper objects.

Build out a cache of commonly used item that are associated with the contained atoms (by name), and are useful to have for quick lookup on (for example, the change state handler function for each atom, the scope walker object for each atom, the task or retry specific scheduler and so-on).


check_atom_transition(atom, current_state, target_state)
Checks if the atom can transition to the provided target state.

fetch_edge_deciders(atom)
Fetches the edge deciders for the given atom.

fetch_scheduler(atom)
Fetches the cached specific scheduler for the given atom.

fetch_action(atom)
Fetches the cached action handler for the given atom.

fetch_scopes_for(atom_name)
Fetches a walker of the visible scopes for the given atom.

iterate_retries(state=None)
Iterates retry atoms that match the provided state.

If no state is provided it will yield back all retry atoms.


iterate_nodes(allowed_kinds)
Yields back all nodes of specified kinds in the execution graph.

is_success()
Checks if all atoms in the execution graph are in 'happy' state.

find_retry(node)
Returns the retry atom associated to the given node (or none).

reset_atoms(atoms, state='PENDING', intention='EXECUTE')
Resets all the provided atoms to the given state and intention.

reset_all(state='PENDING', intention='EXECUTE')
Resets all atoms to the given state and intention.

reset_subgraph(atom, state='PENDING', intention='EXECUTE')
Resets a atoms subgraph to the given state and intention.

The subgraph is contained of all of the atoms successors.


retry_subflow(retry)
Prepares a retrys + its subgraph for execution.

This sets the retrys intention to EXECUTE and resets all of its subgraph (its successors) to the PENDING state with an EXECUTE intention.



class taskflow.engines.action_engine.scheduler.RetryScheduler(runtime)
Bases: object

Schedules retry atoms.

schedule(retry)
Schedules the given retry atom for future completion.

Depending on the atoms stored intention this may schedule the retry atom for reversion or execution.



class taskflow.engines.action_engine.scheduler.TaskScheduler(runtime)
Bases: object

Schedules task atoms.

schedule(task)
Schedules the given task atom for future completion.

Depending on the atoms stored intention this may schedule the task atom for reversion or execution.



class taskflow.engines.action_engine.scheduler.Scheduler(runtime)
Bases: object

Safely schedules atoms using a runtime fetch_scheduler routine.

schedule(atoms)
Schedules the provided atoms for future completion.

This method should schedule a future for each atom provided and return a set of those futures to be waited on (or used for other similar purposes). It should also return any failure objects that represented scheduling failures that may have occurred during this scheduling process.



class taskflow.engines.action_engine.selector.Selector(runtime)
Bases: object

Selector that uses a compilation and aids in execution processes.

Its primary purpose is to get the next atoms for execution or reversion by utilizing the compilations underlying structures (graphs, nodes and edge relations...) and using this information along with the atom state/states stored in storage to provide other useful functionality to the rest of the runtime system.

iter_next_atoms(atom=None)
Iterate next atoms to run (originating from atom or all atoms).


class taskflow.engines.action_engine.scopes.ScopeWalker(compilation, atom, names_only=False)
Bases: object

Walks through the scopes of a atom using a engines compilation.

NOTE(harlowja): for internal usage only.

This will walk the visible scopes that are accessible for the given atom, which can be used by some external entity in some meaningful way, for example to find dependent values...

__iter__()
Iterates over the visible scopes.

How this works is the following:

We first grab all the predecessors of the given atom (lets call it Y) by using the Compilation execution graph (and doing a reverse breadth-first expansion to gather its predecessors), this is useful since we know they always will exist (and execute) before this atom but it does not tell us the corresponding scope level (flow, nested flow...) that each predecessor was created in, so we need to find this information.

For that information we consult the location of the atom Y in the Compilation hierarchy/tree. We lookup in a reverse order the parent X of Y and traverse backwards from the index in the parent where Y exists to all siblings (and children of those siblings) in X that we encounter in this backwards search (if a sibling is a flow itself, its atom(s) will be recursively expanded and included). This collection will then be assumed to be at the same scope. This is what is called a potential single scope, to make an actual scope we remove the items from the potential scope that are not predecessors of Y to form the actual scope which we then yield back.

Then for additional scopes we continue up the tree, by finding the parent of X (lets call it Z) and perform the same operation, going through the children in a reverse manner from the index in parent Z where X was located. This forms another potential scope which we provide back as an actual scope after reducing the potential set to only include predecessors previously gathered. We then repeat this process until we no longer have any parent nodes (aka we have reached the top of the tree) or we run out of predecessors.



class taskflow.engines.action_engine.traversal.Direction(value)
Bases: Enum

Traversal direction enum.

FORWARD = 1
Go through successors.

BACKWARD = 2
Go through predecessors.


taskflow.engines.action_engine.traversal.breadth_first_iterate(execution_graph, starting_node, direction, through_flows=True, through_retries=True, through_tasks=True)
Iterates connected nodes in execution graph (from starting node).

Does so in a breadth first manner.

Jumps over nodes with noop attribute (does not yield them back).


taskflow.engines.action_engine.traversal.depth_first_iterate(execution_graph, starting_node, direction, through_flows=True, through_retries=True, through_tasks=True)
Iterates connected nodes in execution graph (from starting node).

Does so in a depth first manner.

Jumps over nodes with noop attribute (does not yield them back).


taskflow.engines.action_engine.traversal.depth_first_reverse_iterate(node, start_from_idx=-1)
Iterates connected (in reverse) tree nodes (from starting node).

Jumps through nodes with noop attribute (does not yield them back).


Hierarchy

Overview

This is engine that schedules tasks to workers -- separate processes dedicated for certain atoms execution, possibly running on other machines, connected via amqp (or other supported kombu transports).

NOTE:

This engine is under active development and is usable and does work but is missing some features (please check the blueprint page for known issues and plans) that will make it more production ready.


Terminology

Client
Code or program or service (or user) that uses this library to define flows and run them via engines.
Transport + protocol
Mechanism (and protocol on top of that mechanism) used to pass information between the client and worker (for example amqp as a transport and a json encoded message format as the protocol).
Executor
Part of the worker-based engine and is used to publish task requests, so these requests can be accepted and processed by remote workers.
Worker
Workers are started on remote hosts and each has a list of tasks it can perform (on request). Workers accept and process task requests that are published by an executor. Several requests can be processed simultaneously in separate threads (or processes...). For example, an executor can be passed to the worker and configured to run in as many threads (green or not) as desired.
Proxy
Executors interact with workers via a proxy. The proxy maintains the underlying transport and publishes messages (and invokes callbacks on message reception).

Requirements

  • Transparent: it should work as ad-hoc replacement for existing (local) engines with minimal, if any refactoring (e.g. it should be possible to run the same flows on it without changing client code if everything is set up and configured properly).
  • Transport-agnostic: the means of transport should be abstracted so that we can use oslo.messaging, gearmand, amqp, zookeeper, marconi, websockets or anything else that allows for passing information between a client and a worker.
  • Simple: it should be simple to write and deploy.
  • Non-uniformity: it should support non-uniform workers which allows different workers to execute different sets of atoms depending on the workers published capabilities.

Design

There are two communication sides, the executor (and associated engine derivative) and worker that communicate using a proxy component. The proxy is designed to accept/publish messages from/into a named exchange.

High level architecture

[image]

Executor and worker communication

Let's consider how communication between an executor and a worker happens. First of all an engine resolves all atoms dependencies and schedules atoms that can be performed at the moment. This uses the same scheduling and dependency resolution logic that is used for every other engine type. Then the atoms which can be executed immediately (ones that are dependent on outputs of other tasks will be executed when that output is ready) are executed by the worker-based engine executor in the following manner:

1.
The executor initiates task execution/reversion using a proxy object.
2.
Proxy publishes task request (format is described below) into a named exchange using a routing key that is used to deliver request to particular workers topic. The executor then waits for the task requests to be accepted and confirmed by workers. If the executor doesn't get a task confirmation from workers within the given timeout the task is considered as timed-out and a timeout exception is raised.
3.
A worker receives a request message and starts a new thread for processing it.
1.
The worker dispatches the request (gets desired endpoint that actually executes the task).
2.
If dispatched succeeded then the worker sends a confirmation response to the executor otherwise the worker sends a failed response along with a serialized failure object that contains what has failed (and why).
3.
The worker executes the task and once it is finished sends the result back to the originating executor (every time a task progress event is triggered it sends progress notification to the executor where it is handled by the engine, dispatching to listeners and so-on).

4.
The executor gets the task request confirmation from the worker and the task request state changes from the PENDING to the RUNNING state. Once a task request is in the RUNNING state it can't be timed-out (considering that the task execution process may take an unpredictable amount of time).
5.
The executor gets the task execution result from the worker and passes it back to the executor and worker-based engine to finish task processing (this repeats for subsequent tasks).

NOTE:

Failure objects are not directly json-serializable (they contain references to tracebacks which are not serializable), so they are converted to dicts before sending and converted from dicts after receiving on both executor & worker sides (this translation is lossy since the traceback can't be fully retained, due to its contents containing internal interpreter references and details).


Protocol

taskflow.engines.worker_based.protocol.make_an_event(new_state)
Turns a new/target state into an event name.

taskflow.engines.worker_based.protocol.build_a_machine(freeze=True)
Builds a state machine that requests are allowed to go through.

taskflow.engines.worker_based.protocol.failure_to_dict(failure)
Attempts to convert a failure object into a jsonifyable dictionary.

class taskflow.engines.worker_based.protocol.Message
Bases: object

Base class for all message types.

abstract to_dict()
Return json-serializable message representation.


class taskflow.engines.worker_based.protocol.Notify(**data)
Bases: Message

Represents notify message type.

TYPE = 'NOTIFY'
String constant representing this message type.

RESPONSE_SCHEMA = {'additionalProperties': False, 'properties': {'tasks': {'items': {'type': 'string'}, 'type': 'array'}, 'topic': {'type': 'string'}}, 'required': ['topic', 'tasks'], 'type': 'object'}
Expected notify response message schema (in json schema format).

SENDER_SCHEMA = {'additionalProperties': False, 'type': 'object'}
Expected sender request message schema (in json schema format).

to_dict()
Return json-serializable message representation.


class taskflow.engines.worker_based.protocol.Request(task, uuid, action, arguments, timeout=60, result=<object object>, failures=None)
Bases: Message

Represents request with execution results.

Every request is created in the WAITING state and is expired within the given timeout if it does not transition out of the (WAITING, PENDING) states.

State machine a request goes through as it progresses (or expires):

+------------+------------+---------+----------+---------+
|   Start    |   Event    |   End   | On Enter | On Exit |
+------------+------------+---------+----------+---------+
| FAILURE[$] |     .      |    .    |    .     |    .    |
|  PENDING   | on_failure | FAILURE |    .     |    .    |
|  PENDING   | on_running | RUNNING |    .     |    .    |
|  RUNNING   | on_failure | FAILURE |    .     |    .    |
|  RUNNING   | on_success | SUCCESS |    .     |    .    |
| SUCCESS[$] |     .      |    .    |    .     |    .    |
| WAITING[^] | on_failure | FAILURE |    .     |    .    |
| WAITING[^] | on_pending | PENDING |    .     |    .    |
+------------+------------+---------+----------+---------+


TYPE = 'REQUEST'
String constant representing this message type.

SCHEMA = {'properties': {'action': {'enum': ['execute', 'revert'], 'type': 'string'}, 'arguments': {'type': 'object'}, 'failures': {'type': 'object'}, 'result': {}, 'task_cls': {'type': 'string'}, 'task_name': {'type': 'string'}, 'task_version': {'oneOf': [{'type': 'string'}, {'type': 'array'}]}}, 'required': ['task_cls', 'task_name', 'task_version', 'action'], 'type': 'object'}
Expected message schema (in json schema format).

property current_state
Current state the request is in.

set_result(result)
Sets the responses futures result.

property expired
Check if request has expired.

When new request is created its state is set to the WAITING, creation time is stored and timeout is given via constructor arguments.

Request is considered to be expired when it is in the WAITING/PENDING state for more then the given timeout (it is not considered to be expired in any other state).


to_dict()
Return json-serializable request.

To convert requests that have failed due to some exception this will convert all failure.Failure objects into dictionaries (which will then be reconstituted by the receiver).


transition_and_log_error(new_state, logger=None)
Transitions and logs an error if that transitioning raises.

This overlays the transition function and performs nearly the same functionality but instead of raising if the transition was not valid it logs a warning to the provided logger and returns False to indicate that the transition was not performed (note that this is different from the transition function where False means ignored).


transition(new_state)
Transitions the request to a new state.

If transition was performed, it returns True. If transition was ignored, it returns False. If transition was not valid (and will not be performed), it raises an InvalidState exception.


static from_dict(data, task_uuid=None)
Parses validated data into a work unit.

All Failure objects that have been converted to dict(s) on the remote side will now converted back to py:class:~taskflow.types.failure.Failure objects.



class taskflow.engines.worker_based.protocol.Response(state, **data)
Bases: Message

Represents response message type.

TYPE = 'RESPONSE'
String constant representing this message type.

SCHEMA = {'additionalProperties': False, 'definitions': {'completion': {'additionalProperties': False, 'properties': {'result': {}}, 'required': ['result'], 'type': 'object'}, 'empty': {'additionalProperties': False, 'type': 'object'}, 'event': {'additionalProperties': False, 'properties': {'details': {'type': 'object'}, 'event_type': {'type': 'string'}}, 'required': ['event_type', 'details'], 'type': 'object'}}, 'properties': {'data': {'anyOf': [{'$ref': '#/definitions/event'}, {'$ref': '#/definitions/completion'}, {'$ref': '#/definitions/empty'}]}, 'state': {'enum': ['WAITING', 'PENDING', 'RUNNING', 'SUCCESS', 'FAILURE', 'EVENT'], 'type': 'string'}}, 'required': ['state', 'data'], 'type': 'object'}
Expected message schema (in json schema format).

to_dict()
Return json-serializable message representation.


Examples

Request (execute)

  • task_name - full task name to be performed
  • task_cls - full task class name to be performed
  • action - task action to be performed (e.g. execute, revert)
  • arguments - arguments the task action to be called with
  • result - task execution result (result or Failure) [passed to revert only]

Additionally, the following parameters are added to the request message:

  • reply_to - executor named exchange workers will send responses back to
  • correlation_id - executor request id (since there can be multiple request being processed simultaneously)

Example:

{

"action": "execute",
"arguments": {
"x": 111
},
"task_cls": "taskflow.tests.utils.TaskOneArgOneReturn",
"task_name": "taskflow.tests.utils.TaskOneArgOneReturn",
"task_version": [
1,
0
] }


Request (revert)

When reverting:

{

"action": "revert",
"arguments": {},
"failures": {
"taskflow.tests.utils.TaskWithFailure": {
"exc_type_names": [
"RuntimeError",
"StandardError",
"Exception"
],
"exception_str": "Woot!",
"traceback_str": " File \"/homes/harlowja/dev/os/taskflow/taskflow/engines/action_engine/executor.py\", line 56, in _execute_task\n result = task.execute(**arguments)\n File \"/homes/harlowja/dev/os/taskflow/taskflow/tests/utils.py\", line 165, in execute\n raise RuntimeError('Woot!')\n",
"version": 1
}
},
"result": [
"failure",
{
"exc_type_names": [
"RuntimeError",
"StandardError",
"Exception"
],
"exception_str": "Woot!",
"traceback_str": " File \"/homes/harlowja/dev/os/taskflow/taskflow/engines/action_engine/executor.py\", line 56, in _execute_task\n result = task.execute(**arguments)\n File \"/homes/harlowja/dev/os/taskflow/taskflow/tests/utils.py\", line 165, in execute\n raise RuntimeError('Woot!')\n",
"version": 1
}
],
"task_cls": "taskflow.tests.utils.TaskWithFailure",
"task_name": "taskflow.tests.utils.TaskWithFailure",
"task_version": [
1,
0
] }


Worker response(s)

When running:

{

"data": {},
"state": "RUNNING" }


When progressing:

{

"details": {
"progress": 0.5
},
"event_type": "update_progress",
"state": "EVENT" }


When succeeded:

{

"data": {
"result": 666
},
"state": "SUCCESS" }


When failed:

{

"data": {
"result": {
"exc_type_names": [
"RuntimeError",
"StandardError",
"Exception"
],
"exception_str": "Woot!",
"traceback_str": " File \"/homes/harlowja/dev/os/taskflow/taskflow/engines/action_engine/executor.py\", line 56, in _execute_task\n result = task.execute(**arguments)\n File \"/homes/harlowja/dev/os/taskflow/taskflow/tests/utils.py\", line 165, in execute\n raise RuntimeError('Woot!')\n",
"version": 1
}
},
"state": "FAILURE" }


Request state transitions

[image: WBE request state transitions] [image]

WAITING - Request placed on queue (or other kombu message bus/transport) but not yet consumed.

PENDING - Worker accepted request and is pending to run using its executor (threads, processes, or other).

FAILURE - Worker failed after running request (due to task exception) or no worker moved/started executing (by placing the request into RUNNING state) with-in specified time span (this defaults to 60 seconds unless overridden).

RUNNING - Workers executor (using threads, processes...) has started to run requested task (once this state is transitioned to any request timeout no longer becomes applicable; since at this point it is unknown how long a task will run since it can not be determined if a task is just taking a long time or has failed).

SUCCESS - Worker finished running task without exception.

NOTE:

During the WAITING and PENDING stages the engine keeps track of how long the request has been alive for and if a timeout is reached the request will automatically transition to FAILURE and any further transitions from a worker will be disallowed (for example, if a worker accepts the request in the future and sets the task to PENDING this transition will be logged and ignored). This timeout can be adjusted and/or removed by setting the engine transition_timeout option to a higher/lower value or by setting it to None (to remove the timeout completely). In the future this will be improved to be more dynamic by implementing the blueprints associated with failover and info/resilence.


Usage

Workers

To use the worker based engine a set of workers must first be established on remote machines. These workers must be provided a list of task objects, task names, modules names (or entrypoints that can be examined for valid tasks) they can respond to (this is done so that arbitrary code execution is not possible).

For complete parameters and object usage please visit Worker.

Example:

from taskflow.engines.worker_based import worker as w
config = {

'url': 'amqp://guest:guest@localhost:5672//',
'exchange': 'test-exchange',
'topic': 'test-tasks',
'tasks': ['tasks:TestTask1', 'tasks:TestTask2'], } worker = w.Worker(**config) worker.run()


Engines

To use the worker based engine a flow must be constructed (which contains tasks that are visible on remote machines) and the specific worker based engine entrypoint must be selected. Certain configuration options must also be provided so that the transport backend can be configured and initialized correctly. Otherwise the usage should be mostly transparent (and is nearly identical to using any other engine type).

For complete parameters and object usage please see WorkerBasedActionEngine.

Example with amqp transport:

flow = lf.Flow('simple-linear').add(...)
eng = taskflow.engines.load(flow, engine='worker-based',

url='amqp://guest:guest@localhost:5672//',
exchange='test-exchange',
topics=['topic1', 'topic2']) eng.run()


Example with filesystem transport:

flow = lf.Flow('simple-linear').add(...)
eng = taskflow.engines.load(flow, engine='worker-based',

exchange='test-exchange',
topics=['topic1', 'topic2'],
transport='filesystem',
transport_options={
'data_folder_in': '/tmp/in',
'data_folder_out': '/tmp/out',
}) eng.run()


Additional supported keyword arguments:

executor: a class that provides a WorkerTaskExecutor interface; it will be used for executing, reverting and waiting for remote tasks.

Limitations

  • Atoms inside a flow must receive and accept parameters only from the ways defined in persistence. In other words, the task that is created when a workflow is constructed will not be the same task that is executed on a remote worker (and any internal state not passed via the input and output mechanism can not be transferred). This means resource objects (database handles, file descriptors, sockets, ...) can not be directly sent across to remote workers (instead the configuration that defines how to fetch/create these objects must be instead).
  • Worker-based engines will in the future be able to run lightweight tasks locally to avoid transport overhead for very simple tasks (currently it will run even lightweight tasks remotely, which may be non-performant).
  • Fault detection, currently when a worker acknowledges a task the engine will wait for the task result indefinitely (a task may take an indeterminate amount of time to finish). In the future there needs to be a way to limit the duration of a remote workers execution (and track their liveness) and possibly spawn the task on a secondary worker if a timeout is reached (aka the first worker has died or has stopped responding).

Implementations

Components

WARNING:

External usage of internal engine functions, components and modules should be kept to a minimum as they may be altered, refactored or moved to other locations without notice (and without the typical deprecation cycle).


class taskflow.engines.worker_based.endpoint.Endpoint(task_cls)
Bases: object

Represents a single task with execute/revert methods.


class taskflow.engines.worker_based.types.TopicWorker(topic, tasks, identity=<object object>)
Bases: object

A (read-only) worker and its relevant information + useful methods.


class taskflow.engines.worker_based.types.ProxyWorkerFinder(uuid, proxy, topics, beat_periodicity=5, worker_expiry=60)
Bases: object

Requests and receives responses about workers topic+task details.

property total_workers
Number of workers currently known.

wait_for_workers(workers=1, timeout=None)
Waits for geq workers to notify they are ready to do work.

NOTE(harlowja): if a timeout is provided this function will wait until that timeout expires, if the amount of workers does not reach the desired amount of workers before the timeout expires then this will return how many workers are still needed, otherwise it will return zero.


property messages_processed
How many notify response messages have been processed.

maybe_publish()
Periodically called to publish notify message to each topic.

These messages (especially the responses) are how this find learns about workers and what tasks they can perform (so that we can then match workers to tasks to run).


process_response(data, message)
Process notify message sent from remote side.

clean()
Cleans out any dead/expired/not responding workers.

Returns how many workers were removed.


reset()
Resets finders internal state.

get_worker_for_task(task)
Gets a worker that can perform a given task.


Notifications and listeners

Overview

Engines provide a way to receive notification on task and flow state transitions (see states), which is useful for monitoring, logging, metrics, debugging and plenty of other tasks.

To receive these notifications you should register a callback with an instance of the Notifier class that is attached to Engine attributes atom_notifier and notifier.

TaskFlow also comes with a set of predefined listeners, and provides means to write your own listeners, which can be more convenient than using raw callbacks.

Receiving notifications with callbacks

Flow notifications

To receive notification on flow state changes use the Notifier instance available as the notifier property of an engine.

A basic example is:

>>> class CatTalk(task.Task):
...   def execute(self, meow):
...     print(meow)
...     return "cat"
...
>>> class DogTalk(task.Task):
...   def execute(self, woof):
...     print(woof)
...     return 'dog'
...
>>> def flow_transition(state, details):
...     print("Flow '%s' transition to state %s" % (details['flow_name'], state))
...
>>>
>>> flo = linear_flow.Flow("cat-dog").add(
...   CatTalk(), DogTalk(provides="dog"))
>>> eng = engines.load(flo, store={'meow': 'meow', 'woof': 'woof'})
>>> eng.notifier.register(ANY, flow_transition)
>>> eng.run()
Flow 'cat-dog' transition to state RUNNING
meow
woof
Flow 'cat-dog' transition to state SUCCESS


Task notifications

To receive notification on task state changes use the Notifier instance available as the atom_notifier property of an engine.

A basic example is:

>>> class CatTalk(task.Task):
...   def execute(self, meow):
...     print(meow)
...     return "cat"
...
>>> class DogTalk(task.Task):
...   def execute(self, woof):
...     print(woof)
...     return 'dog'
...
>>> def task_transition(state, details):
...     print("Task '%s' transition to state %s" % (details['task_name'], state))
...
>>>
>>> flo = linear_flow.Flow("cat-dog")
>>> flo.add(CatTalk(), DogTalk(provides="dog"))
<taskflow.patterns.linear_flow.Flow object at 0x...>
>>> eng = engines.load(flo, store={'meow': 'meow', 'woof': 'woof'})
>>> eng.atom_notifier.register(ANY, task_transition)
>>> eng.run()
Task 'CatTalk' transition to state RUNNING
meow
Task 'CatTalk' transition to state SUCCESS
Task 'DogTalk' transition to state RUNNING
woof
Task 'DogTalk' transition to state SUCCESS


Listeners

TaskFlow comes with a set of predefined listeners -- helper classes that can be used to do various actions on flow and/or tasks transitions. You can also create your own listeners easily, which may be more convenient than using raw callbacks for some use cases.

For example, this is how you can use PrintingListener:

>>> from taskflow.listeners import printing
>>> class CatTalk(task.Task):
...   def execute(self, meow):
...     print(meow)
...     return "cat"
...
>>> class DogTalk(task.Task):
...   def execute(self, woof):
...     print(woof)
...     return 'dog'
...
>>>
>>> flo = linear_flow.Flow("cat-dog").add(
...   CatTalk(), DogTalk(provides="dog"))
>>> eng = engines.load(flo, store={'meow': 'meow', 'woof': 'woof'})
>>> with printing.PrintingListener(eng):
...   eng.run()
...
<taskflow.engines.action_engine.engine.SerialActionEngine object at ...> has moved flow 'cat-dog' (...) into state 'RUNNING' from state 'PENDING'
<taskflow.engines.action_engine.engine.SerialActionEngine object at ...> has moved task 'CatTalk' (...) into state 'RUNNING' from state 'PENDING'
meow
<taskflow.engines.action_engine.engine.SerialActionEngine object at ...> has moved task 'CatTalk' (...) into state 'SUCCESS' from state 'RUNNING' with result 'cat' (failure=False)
<taskflow.engines.action_engine.engine.SerialActionEngine object at ...> has moved task 'DogTalk' (...) into state 'RUNNING' from state 'PENDING'
woof
<taskflow.engines.action_engine.engine.SerialActionEngine object at ...> has moved task 'DogTalk' (...) into state 'SUCCESS' from state 'RUNNING' with result 'dog' (failure=False)
<taskflow.engines.action_engine.engine.SerialActionEngine object at ...> has moved flow 'cat-dog' (...) into state 'SUCCESS' from state 'RUNNING'


Interfaces

taskflow.listeners.base.FINISH_STATES = ('FAILURE', 'SUCCESS', 'REVERTED', 'REVERT_FAILURE')
These states will results be usable, other states do not produce results.

taskflow.listeners.base.DEFAULT_LISTEN_FOR = ('*',)
What is listened for by default...

class taskflow.listeners.base.Listener(engine, task_listen_for=('*',), flow_listen_for=('*',), retry_listen_for=('*',))
Bases: object

Base class for listeners.

A listener can be attached to an engine to do various actions on flow and atom state transitions. It implements the context manager protocol to be able to register and unregister with a given engine automatically when a context is entered and when it is exited.

To implement a listener, derive from this class and override _flow_receiver and/or _task_receiver and/or _retry_receiver methods (in this class, they do nothing).


class taskflow.listeners.base.DumpingListener(engine, task_listen_for=('*',), flow_listen_for=('*',), retry_listen_for=('*',))
Bases: Listener

Abstract base class for dumping listeners.

This provides a simple listener that can be attached to an engine which can be derived from to dump task and/or flow state transitions to some target backend.

To implement your own dumping listener derive from this class and override the _dump method.


Implementations

Printing and logging listeners

class taskflow.listeners.logging.LoggingListener(engine, task_listen_for=('*',), flow_listen_for=('*',), retry_listen_for=('*',), log=None, level=10)
Bases: DumpingListener

Listener that logs notifications it receives.

It listens for task and flow notifications and writes those notifications to a provided logger, or logger of its module (taskflow.listeners.logging) if none is provided (and no class attribute is overridden). The log level can also be configured, logging.DEBUG is used by default when none is provided.


class taskflow.listeners.logging.DynamicLoggingListener(engine, task_listen_for=('*',), flow_listen_for=('*',), retry_listen_for=('*',), log=None, failure_level=30, level=10, hide_inputs_outputs_of=(), fail_formatter=None)
Bases: Listener

Listener that logs notifications it receives.

It listens for task and flow notifications and writes those notifications to a provided logger, or logger of its module (taskflow.listeners.logging) if none is provided (and no class attribute is overridden). The log level can slightly be configured and logging.DEBUG or logging.WARNING (unless overridden via a constructor parameter) will be selected automatically based on the execution state and results produced.

The following flow states cause logging.WARNING (or provided level) to be used:

  • states.FAILURE
  • states.REVERTED

The following task states cause logging.WARNING (or provided level) to be used:

  • states.FAILURE
  • states.RETRYING
  • states.REVERTING
  • states.REVERT_FAILURE

When a task produces a Failure object as its result (typically this happens when a task raises an exception) this will always switch the logger to use logging.WARNING (if the failure object contains a exc_info tuple this will also be logged to provide a meaningful traceback).


class taskflow.listeners.printing.PrintingListener(engine, task_listen_for=('*',), flow_listen_for=('*',), retry_listen_for=('*',), stderr=False)
Bases: DumpingListener

Writes the task and flow notifications messages to stdout or stderr.


Timing listeners

class taskflow.listeners.timing.DurationListener(engine)
Bases: Listener

Listener that captures task duration.

It records how long a task took to execute (or fail) to storage. It saves the duration in seconds as float value to task metadata with key 'duration'.


class taskflow.listeners.timing.PrintingDurationListener(engine, printer=None)
Bases: DurationListener

Listener that prints the duration as well as recording it.


class taskflow.listeners.timing.EventTimeListener(engine, task_listen_for=('*',), flow_listen_for=('*',), retry_listen_for=('*',))
Bases: Listener

Listener that captures task, flow, and retry event timestamps.

It records how when an event is received (using unix time) to storage. It saves the timestamps under keys (in atom or flow details metadata) of the format {event}-timestamp where event is the state/event name that has been received.

This information can be later extracted/examined to derive durations...


Claim listener

class taskflow.listeners.claims.CheckingClaimListener(engine, job, board, owner, on_job_loss=None)
Bases: Listener

Listener that interacts [engine, job, jobboard]; ensures claim is valid.

This listener (or a derivative) can be associated with an engines notification system after the job has been claimed (so that the jobs work can be worked on by that engine). This listener (after associated) will check that the job is still claimed whenever the engine notifies of a task or flow state change. If the job is not claimed when a state change occurs, a associated handler (or the default) will be activated to determine how to react to this hopefully exceptional case.

NOTE(harlowja): this may create more traffic than desired to the jobboard backend (zookeeper or other), since the amount of state change per task and flow is non-zero (and checking during each state change will result in quite a few calls to that management system to check the jobs claim status); this could be later optimized to check less (or only check on a smaller set of states)

NOTE(harlowja): if a custom on_job_loss callback is provided it must accept three positional arguments, the first being the current engine being ran, the second being the 'task/flow' state and the third being the details that were sent from the engine to listeners for inspection.


Capturing listener

class taskflow.listeners.capturing.CaptureListener(engine, task_listen_for=('*',), flow_listen_for=('*',), retry_listen_for=('*',), capture_flow=True, capture_task=True, capture_retry=True, skip_tasks=None, skip_retries=None, skip_flows=None, values=None)
Bases: Listener

A listener that captures transitions and saves them locally.

NOTE(harlowja): this listener is mainly useful for testing (where it is useful to test the appropriate/expected transitions, produced results... occurred after engine running) but it could have other usages as well.

Variables
values -- Captured transitions + details (the result of the _format_capture() method) are stored into this list (a previous list to append to may be provided using the constructor keyword argument of the same name); by default this stores tuples of the format (kind, state, details).

FLOW = 'flow'
Kind that denotes a 'flow' capture.

TASK = 'task'
Kind that denotes a 'task' capture.

RETRY = 'retry'
Kind that denotes a 'retry' capture.


Formatters

class taskflow.formatters.FailureFormatter(engine, hide_inputs_outputs_of=())
Bases: object

Formats a failure and connects it to associated atoms & engine.

format(fail, atom_matcher)
Returns a (exc_info, details) tuple about the failure.

The exc_info tuple should be a standard three element (exctype, value, traceback) tuple that will be used for further logging. A non-empty string is typically returned for details; it should contain any string info about the failure (with any specific details the exc_info may not have/contain).



Hierarchy

Persistence

Overview

In order to be able to receive inputs and create outputs from atoms (or other engine processes) in a fault-tolerant way, there is a need to be able to place what atoms output in some kind of location where it can be re-used by other atoms (or used for other purposes). To accommodate this type of usage TaskFlow provides an abstraction (provided by pluggable stevedore backends) that is similar in concept to a running programs memory.

This abstraction serves the following major purposes:

  • Tracking of what was done (introspection).
  • Saving memory which allows for restarting from the last saved state which is a critical feature to restart and resume workflows (checkpointing).
  • Associating additional metadata with atoms while running (without having those atoms need to save this data themselves). This makes it possible to add-on new metadata in the future without having to change the atoms themselves. For example the following can be saved:
  • Timing information (how long a task took to run).
  • User information (who the task ran as).
  • When a atom/workflow was ran (and why).

  • Saving historical data (failures, successes, intermediary results...) to allow for retry atoms to be able to decide if they should should continue vs. stop.
  • Something you create...

How it is used

On engine construction typically a backend (it can be optional) will be provided which satisfies the Backend abstraction. Along with providing a backend object a FlowDetail object will also be created and provided (this object will contain the details about the flow to be ran) to the engine constructor (or associated load() helper functions). Typically a FlowDetail object is created from a LogBook object (the book object acts as a type of container for FlowDetail and AtomDetail objects).

Preparation: Once an engine starts to run it will create a Storage object which will act as the engines interface to the underlying backend storage objects (it provides helper functions that are commonly used by the engine, avoiding repeating code when interacting with the provided FlowDetail and Backend objects). As an engine initializes it will extract (or create) AtomDetail objects for each atom in the workflow the engine will be executing.

Execution: When an engine beings to execute (see engine for more of the details about how an engine goes about this process) it will examine any previously existing AtomDetail objects to see if they can be used for resuming; see resumption for more details on this subject. For atoms which have not finished (or did not finish correctly from a previous run) they will begin executing only after any dependent inputs are ready. This is done by analyzing the execution graph and looking at predecessor AtomDetail outputs and states (which may have been persisted in a past run). This will result in either using their previous information or by running those predecessors and saving their output to the FlowDetail and Backend objects. This execution, analysis and interaction with the storage objects continues (what is described here is a simplification of what really happens; which is quite a bit more complex) until the engine has finished running (at which point the engine will have succeeded or failed in its attempt to run the workflow).

Post-execution: Typically when an engine is done running the logbook would be discarded (to avoid creating a stockpile of useless data) and the backend storage would be told to delete any contents for a given execution. For certain use-cases though it may be advantageous to retain logbooks and their contents.

A few scenarios come to mind:

  • Post runtime failure analysis and triage (saving what failed and why).
  • Metrics (saving timing information associated with each atom and using it to perform offline performance analysis, which enables tuning tasks and/or isolating and fixing slow tasks).
  • Data mining logbooks to find trends (in failures for example).
  • Saving logbooks for further forensics analysis.
  • Exporting logbooks to hdfs (or other no-sql storage) and running some type of map-reduce jobs on them.

NOTE:

It should be emphasized that logbook is the authoritative, and, preferably, the only (see inputs and outputs) source of run-time state information (breaking this principle makes it hard/impossible to restart or resume in any type of automated fashion). When an atom returns a result, it should be written directly to a logbook. When atom or flow state changes in any way, logbook is first to know (see notifications for how a user may also get notified of those same state changes). The logbook and a backend and associated storage helper class are responsible to store the actual data. These components used together specify the persistence mechanism (how data is saved and where -- memory, database, whatever...) and the persistence policy (when data is saved -- every time it changes or at some particular moments or simply never).


Usage

To select which persistence backend to use you should use the fetch() function which uses entrypoints (internally using stevedore) to fetch and configure your backend. This makes it simpler than accessing the backend data types directly and provides a common function from which a backend can be fetched.

Using this function to fetch a backend might look like:

from taskflow.persistence import backends
...
persistence = backends.fetch(conf={

"connection": "mysql",
"user": ...,
"password": ..., }) book = make_and_save_logbook(persistence) ...


As can be seen from above the conf parameter acts as a dictionary that is used to fetch and configure your backend. The restrictions on it are the following:

a dictionary (or dictionary like type), holding backend type with key 'connection' and possibly type-specific backend parameters as other keys.

Types

Memory

Connection: 'memory'

Retains all data in local memory (not persisted to reliable storage). Useful for scenarios where persistence is not required (and also in unit tests).

NOTE:

See MemoryBackend for implementation details.


Files

Connection: 'dir' or 'file'

Retains all data in a directory & file based structure on local disk. Will be persisted locally in the case of system failure (allowing for resumption from the same local machine only). Useful for cases where a more reliable persistence is desired along with the simplicity of files and directories (a concept everyone is familiar with).

NOTE:

See DirBackend for implementation details.


SQLAlchemy

Connection: 'mysql' or 'postgres' or 'sqlite'

Retains all data in a ACID compliant database using the sqlalchemy library for schemas, connections, and database interaction functionality. Useful when you need a higher level of durability than offered by the previous solutions. When using these connection types it is possible to resume a engine from a peer machine (this does not apply when using sqlite).

Schema

Logbooks

Name Type Primary Key
created_at DATETIME False
updated_at DATETIME False
uuid VARCHAR True
name VARCHAR False
meta TEXT False

Flow details

Name Type Primary Key
created_at DATETIME False
updated_at DATETIME False
uuid VARCHAR True
name VARCHAR False
meta TEXT False
state VARCHAR False
parent_uuid VARCHAR False

Atom details

Name Type Primary Key
created_at DATETIME False
updated_at DATETIME False
uuid VARCHAR True
name VARCHAR False
meta TEXT False
atom_type VARCHAR False
state VARCHAR False
intention VARCHAR False
results TEXT False
failure TEXT False
version TEXT False
parent_uuid VARCHAR False

NOTE:

See SQLAlchemyBackend for implementation details.


WARNING:

Currently there is a size limit (not applicable for sqlite) that the results will contain. This size limit will restrict how many prior failures a retry atom can contain. More information and a future fix will be posted to bug 1416088 (for the meantime try to ensure that your retry units history does not grow beyond ~80 prior results). This truncation can also be avoided by providing mysql_sql_mode as traditional when selecting your mysql + sqlalchemy based backend (see the mysql modes documentation for what this implies).


Zookeeper

Connection: 'zookeeper'

Retains all data in a zookeeper backend (zookeeper exposes operations on files and directories, similar to the above 'dir' or 'file' connection types). Internally the kazoo library is used to interact with zookeeper to perform reliable, distributed and atomic operations on the contents of a logbook represented as znodes. Since zookeeper is also distributed it is also able to resume a engine from a peer machine (having similar functionality as the database connection types listed previously).

NOTE:

See ZkBackend for implementation details.


Interfaces

taskflow.persistence.backends.fetch(conf, namespace='taskflow.persistence', **kwargs)
Fetch a persistence backend with the given configuration.

This fetch method will look for the entrypoint name in the entrypoint namespace, and then attempt to instantiate that entrypoint using the provided configuration and any persistence backend specific kwargs.

NOTE(harlowja): to aid in making it easy to specify configuration and options to a backend the configuration (which is typical just a dictionary) can also be a URI string that identifies the entrypoint name and any configuration specific to that backend.

For example, given the following configuration URI:

mysql://<not-used>/?a=b&c=d


This will look for the entrypoint named 'mysql' and will provide a configuration object composed of the URI's components, in this case that is {'a': 'b', 'c': 'd'} to the constructor of that persistence backend instance.


taskflow.persistence.backends.backend(conf, namespace='taskflow.persistence', **kwargs)
Fetches a backend, connects, upgrades, then closes it on completion.

This allows a backend instance to be fetched, connected to, have its schema upgraded (if the schema is already up to date this is a no-op) and then used in a context manager statement with the backend being closed upon context manager exit.


class taskflow.persistence.base.Backend(conf)
Bases: object

Base class for persistence backends.

abstract get_connection()
Return a Connection instance based on the configuration settings.

abstract close()
Closes any resources this backend has open.


class taskflow.persistence.base.Connection
Bases: object

Base class for backend connections.

abstract property backend
Returns the backend this connection is associated with.

abstract close()
Closes any resources this connection has open.

abstract upgrade()
Migrate the persistence backend to the most recent version.

abstract clear_all()
Clear all entries from this backend.

abstract validate()
Validates that a backend is still ok to be used.

The semantics of this may vary depending on the backend. On failure a backend specific exception should be raised that will indicate why the failure occurred.


abstract update_atom_details(atom_detail)
Updates a given atom details and returns the updated version.

NOTE(harlowja): the details that is to be updated must already have been created by saving a flow details with the given atom detail inside of it.


abstract update_flow_details(flow_detail)
Updates a given flow details and returns the updated version.

NOTE(harlowja): the details that is to be updated must already have been created by saving a logbook with the given flow detail inside of it.


abstract save_logbook(book)
Saves a logbook, and all its contained information.

abstract destroy_logbook(book_uuid)
Deletes/destroys a logbook matching the given uuid.

abstract get_logbook(book_uuid, lazy=False)
Fetches a logbook object matching the given uuid.

abstract get_logbooks(lazy=False)
Return an iterable of logbook objects.

abstract get_flows_for_book(book_uuid)
Return an iterable of flowdetails for a given logbook uuid.

abstract get_flow_details(fd_uuid, lazy=False)
Fetches a flowdetails object matching the given uuid.

abstract get_atom_details(ad_uuid)
Fetches a atomdetails object matching the given uuid.

abstract get_atoms_for_flow(fd_uuid)
Return an iterable of atomdetails for a given flowdetails uuid.


class taskflow.persistence.path_based.PathBasedBackend(conf)
Bases: Backend

Base class for persistence backends that address data by path

Subclasses of this backend write logbooks, flow details, and atom details to a provided base path in some filesystem-like storage. They will create and store those objects in three key directories (one for logbooks, one for flow details and one for atom details). They create those associated directories and then create files inside those directories that represent the contents of those objects for later reading and writing.

DEFAULT_PATH = None
Default path used when none is provided.


class taskflow.persistence.path_based.PathBasedConnection(backend)
Bases: Connection

Base class for path based backend connections.

property backend
Returns the backend this connection is associated with.

get_logbooks(lazy=False)
Return an iterable of logbook objects.

get_logbook(book_uuid, lazy=False)
Fetches a logbook object matching the given uuid.

save_logbook(book)
Saves a logbook, and all its contained information.

get_flows_for_book(book_uuid, lazy=False)
Return an iterable of flowdetails for a given logbook uuid.

get_flow_details(flow_uuid, lazy=False)
Fetches a flowdetails object matching the given uuid.

update_flow_details(flow_detail, ignore_missing=False)
Updates a given flow details and returns the updated version.

NOTE(harlowja): the details that is to be updated must already have been created by saving a logbook with the given flow detail inside of it.


get_atoms_for_flow(flow_uuid)
Return an iterable of atomdetails for a given flowdetails uuid.

get_atom_details(atom_uuid)
Fetches a atomdetails object matching the given uuid.

update_atom_details(atom_detail, ignore_missing=False)
Updates a given atom details and returns the updated version.

NOTE(harlowja): the details that is to be updated must already have been created by saving a flow details with the given atom detail inside of it.


destroy_logbook(book_uuid)
Deletes/destroys a logbook matching the given uuid.

clear_all()
Clear all entries from this backend.

upgrade()
Migrate the persistence backend to the most recent version.

close()
Closes any resources this connection has open.


Models

class taskflow.persistence.models.LogBook(name, uuid=None)
Bases: object

A collection of flow details and associated metadata.

Typically this class contains a collection of flow detail entries for a given engine (or job) so that those entities can track what 'work' has been completed for resumption, reverting and miscellaneous tracking purposes.

The data contained within this class need not be persisted to the backend storage in real time. The data in this class will only be guaranteed to be persisted when a save occurs via some backend connection.

NOTE(harlowja): the naming of this class is analogous to a ship's log or a similar type of record used in detailing work that has been completed (or work that has not been completed).

Variables
  • created_at -- A datetime.datetime object of when this logbook was created.
  • updated_at -- A datetime.datetime object of when this logbook was last updated at.
  • meta -- A dictionary of meta-data associated with this logbook.


pformat(indent=0, linesep='\n')
Pretty formats this logbook into a string.

>>> from taskflow.persistence import models
>>> tmp = models.LogBook("example")
>>> print(tmp.pformat())
LogBook: 'example'

- uuid = ...
- created_at = ...

add(fd)
Adds a new flow detail into this logbook.

NOTE(harlowja): if an existing flow detail exists with the same uuid the existing one will be overwritten with the newly provided one.

Does not guarantee that the details will be immediately saved.


find(flow_uuid)
Locate the flow detail corresponding to the given uuid.
Returns
the flow detail with that uuid
Return type
FlowDetail (or None if not found)


merge(lb, deep_copy=False)
Merges the current object state with the given ones state.

If deep_copy is provided as truthy then the local object will use copy.deepcopy to replace this objects local attributes with the provided objects attributes (only if there is a difference between this objects attributes and the provided attributes). If deep_copy is falsey (the default) then a reference copy will occur instead when a difference is detected.

NOTE(harlowja): If the provided object is this object itself then no merging is done. Also note that this does not merge the flow details contained in either.

Returns
this logbook (freshly merged with the incoming object)
Return type
LogBook


to_dict(marshal_time=False)
Translates the internal state of this object to a dict.

NOTE(harlowja): The returned dict does not include any contained flow details.

Returns
this logbook in dict form


classmethod from_dict(data, unmarshal_time=False)
Translates the given dict into an instance of this class.

NOTE(harlowja): the dict provided should come from a prior call to to_dict().

Returns
a new logbook
Return type
LogBook


property uuid
The unique identifer of this logbook.

property name
The name of this logbook.

copy(retain_contents=True)
Copies this logbook.

Creates a shallow copy of this logbook. If this logbook contains flow details and retain_contents is truthy (the default) then the flow details container will be shallow copied (the flow details contained there-in will not be copied). If retain_contents is falsey then the copied logbook will have no contained flow details (but it will have the rest of the local objects attributes copied).

Returns
a new logbook
Return type
LogBook



class taskflow.persistence.models.FlowDetail(name, uuid)
Bases: object

A collection of atom details and associated metadata.

Typically this class contains a collection of atom detail entries that represent the atoms in a given flow structure (along with any other needed metadata relevant to that flow).

The data contained within this class need not be persisted to the backend storage in real time. The data in this class will only be guaranteed to be persisted when a save (or update) occurs via some backend connection.

Variables
meta -- A dictionary of meta-data associated with this flow detail.

state
The state of the flow associated with this flow detail.

update(fd)
Updates the objects state to be the same as the given one.

This will assign the private and public attributes of the given flow detail directly to this object (replacing any existing attributes in this object; even if they are the same).

NOTE(harlowja): If the provided object is this object itself then no update is done.

Returns
this flow detail
Return type
FlowDetail


pformat(indent=0, linesep='\n')
Pretty formats this flow detail into a string.

>>> from oslo_utils import uuidutils
>>> from taskflow.persistence import models
>>> flow_detail = models.FlowDetail("example",
...                                 uuid=uuidutils.generate_uuid())
>>> print(flow_detail.pformat())
FlowDetail: 'example'

- uuid = ...
- state = ...

merge(fd, deep_copy=False)
Merges the current object state with the given one's state.

If deep_copy is provided as truthy then the local object will use copy.deepcopy to replace this objects local attributes with the provided objects attributes (only if there is a difference between this objects attributes and the provided attributes). If deep_copy is falsey (the default) then a reference copy will occur instead when a difference is detected.

NOTE(harlowja): If the provided object is this object itself then no merging is done. Also this does not merge the atom details contained in either.

Returns
this flow detail (freshly merged with the incoming object)
Return type
FlowDetail


copy(retain_contents=True)
Copies this flow detail.

Creates a shallow copy of this flow detail. If this detail contains flow details and retain_contents is truthy (the default) then the atom details container will be shallow copied (the atom details contained there-in will not be copied). If retain_contents is falsey then the copied flow detail will have no contained atom details (but it will have the rest of the local objects attributes copied).

Returns
a new flow detail
Return type
FlowDetail


to_dict()
Translates the internal state of this object to a dict.

NOTE(harlowja): The returned dict does not include any contained atom details.

Returns
this flow detail in dict form


classmethod from_dict(data)
Translates the given dict into an instance of this class.

NOTE(harlowja): the dict provided should come from a prior call to to_dict().

Returns
a new flow detail
Return type
FlowDetail


add(ad)
Adds a new atom detail into this flow detail.

NOTE(harlowja): if an existing atom detail exists with the same uuid the existing one will be overwritten with the newly provided one.

Does not guarantee that the details will be immediately saved.


find(ad_uuid)
Locate the atom detail corresponding to the given uuid.
Returns
the atom detail with that uuid
Return type
AtomDetail (or None if not found)


property uuid
The unique identifer of this flow detail.

property name
The name of this flow detail.


class taskflow.persistence.models.AtomDetail(name, uuid)
Bases: object

A collection of atom specific runtime information and metadata.

This is a base abstract class that contains attributes that are used to connect a atom to the persistence layer before, during, or after it is running. It includes any results it may have produced, any state that it may be in (for example FAILURE), any exception that occurred when running, and any associated stacktrace that may have occurring during an exception being thrown. It may also contain any other metadata that should also be stored along-side the details about the connected atom.

The data contained within this class need not be persisted to the backend storage in real time. The data in this class will only be guaranteed to be persisted when a save (or update) occurs via some backend connection.

Variables
  • intention -- The execution strategy of the atom associated with this atom detail (used by an engine/others to determine if the associated atom needs to be executed, reverted, retried and so-on).
  • meta -- A dictionary of meta-data associated with this atom detail.
  • version -- A version tuple or string that represents the atom version this atom detail is associated with (typically used for introspection and any data migration strategies).
  • results -- Any results the atom produced from either its execute method or from other sources.
  • revert_results -- Any results the atom produced from either its revert method or from other sources.
  • AtomDetail.failure -- If the atom failed (due to its execute method raising) this will be a Failure object that represents that failure (if there was no failure this will be set to none).
  • revert_failure -- If the atom failed (possibly due to its revert method raising) this will be a Failure object that represents that failure (if there was no failure this will be set to none).


state
The state of the atom associated with this atom detail.

property last_results
Gets the atoms last result.

If the atom has produced many results (for example if it has been retried, reverted, executed and ...) this returns the last one of many results.


update(ad)
Updates the object's state to be the same as the given one.

This will assign the private and public attributes of the given atom detail directly to this object (replacing any existing attributes in this object; even if they are the same).

NOTE(harlowja): If the provided object is this object itself then no update is done.

Returns
this atom detail
Return type
AtomDetail


abstract merge(other, deep_copy=False)
Merges the current object state with the given ones state.

If deep_copy is provided as truthy then the local object will use copy.deepcopy to replace this objects local attributes with the provided objects attributes (only if there is a difference between this objects attributes and the provided attributes). If deep_copy is falsey (the default) then a reference copy will occur instead when a difference is detected.

NOTE(harlowja): If the provided object is this object itself then no merging is done. Do note that no results are merged in this method. That operation must to be the responsibilty of subclasses to implement and override this abstract method and provide that merging themselves as they see fit.

Returns
this atom detail (freshly merged with the incoming object)
Return type
AtomDetail


abstract put(state, result)
Puts a result (acquired in the given state) into this detail.

to_dict()
Translates the internal state of this object to a dict.
Returns
this atom detail in dict form


classmethod from_dict(data)
Translates the given dict into an instance of this class.

NOTE(harlowja): the dict provided should come from a prior call to to_dict().

Returns
a new atom detail
Return type
AtomDetail


property uuid
The unique identifer of this atom detail.

property name
The name of this atom detail.

abstract reset(state)
Resets this atom detail and sets state attribute value.

abstract copy()
Copies this atom detail.

pformat(indent=0, linesep='\n')
Pretty formats this atom detail into a string.


class taskflow.persistence.models.TaskDetail(name, uuid)
Bases: AtomDetail

A task detail (an atom detail typically associated with a Task atom).

reset(state)
Resets this task detail and sets state attribute value.

This sets any previously set results, failure, and revert_results attributes back to None and sets the state to the provided one, as well as setting this task details intention attribute to EXECUTE.


put(state, result)
Puts a result (acquired in the given state) into this detail.

Returns whether this object was modified (or whether it was not).


merge(other, deep_copy=False)
Merges the current task detail with the given one.

NOTE(harlowja): This merge does not copy and replace the results or revert_results if it differs. Instead the current objects results and revert_results attributes directly becomes (via assignment) the other objects attributes. Also note that if the provided object is this object itself then no merging is done.

See: https://bugs.launchpad.net/taskflow/+bug/1452978 for what happens if this is copied at a deeper level (for example by using copy.deepcopy or by using copy.copy).

Returns
this task detail (freshly merged with the incoming object)
Return type
TaskDetail


copy()
Copies this task detail.

Creates a shallow copy of this task detail (any meta-data and version information that this object maintains is shallow copied via copy.copy).

NOTE(harlowja): This copy does not copy and replace the results or revert_results attribute if it differs. Instead the current objects results and revert_results attributes directly becomes (via assignment) the cloned objects attributes.

See: https://bugs.launchpad.net/taskflow/+bug/1452978 for what happens if this is copied at a deeper level (for example by using copy.deepcopy or by using copy.copy).

Returns
a new task detail
Return type
TaskDetail



class taskflow.persistence.models.RetryDetail(name, uuid)
Bases: AtomDetail

A retry detail (an atom detail typically associated with a Retry atom).

reset(state)
Resets this retry detail and sets state attribute value.

This sets any previously added results back to an empty list and resets the failure and revert_failure and revert_results attributes back to None and sets the state to the provided one, as well as setting this retry details intention attribute to EXECUTE.


copy()
Copies this retry detail.

Creates a shallow copy of this retry detail (any meta-data and version information that this object maintains is shallow copied via copy.copy).

NOTE(harlowja): This copy does not copy the incoming objects results or revert_results attributes. Instead this objects results attribute list is iterated over and a new list is constructed with each (data, failures) element in that list having its failures (a dictionary of each named Failure object that occured) copied but its data is left untouched. After this is done that new list becomes (via assignment) the cloned objects results attribute. The revert_results is directly assigned to the cloned objects revert_results attribute.

See: https://bugs.launchpad.net/taskflow/+bug/1452978 for what happens if the data in results is copied at a deeper level (for example by using copy.deepcopy or by using copy.copy).

Returns
a new retry detail
Return type
RetryDetail


property last_results
The last result that was produced.

property last_failures
The last failure dictionary that was produced.

NOTE(harlowja): This is not the same as the local failure attribute as the obtained failure dictionary in the results attribute (which is what this returns) is from associated atom failures (which is different from the directly related failure of the retry unit associated with this atom detail).


put(state, result)
Puts a result (acquired in the given state) into this detail.

Returns whether this object was modified (or whether it was not).


classmethod from_dict(data)
Translates the given dict into an instance of this class.

to_dict()
Translates the internal state of this object to a dict.

merge(other, deep_copy=False)
Merges the current retry detail with the given one.

NOTE(harlowja): This merge does not deep copy the incoming objects results attribute (if it differs). Instead the incoming objects results attribute list is always iterated over and a new list is constructed with each (data, failures) element in that list having its failures (a dictionary of each named Failure objects that occurred) copied but its data is left untouched. After this is done that new list becomes (via assignment) this objects results attribute. Also note that if the provided object is this object itself then no merging is done.

See: https://bugs.launchpad.net/taskflow/+bug/1452978 for what happens if the data in results is copied at a deeper level (for example by using copy.deepcopy or by using copy.copy).

Returns
this retry detail (freshly merged with the incoming object)
Return type
RetryDetail



Implementations

Memory

class taskflow.persistence.backends.impl_memory.FakeInode(item, path, value=None)
Bases: Node

A in-memory filesystem inode-like object.


class taskflow.persistence.backends.impl_memory.FakeFilesystem(deep_copy=True)
Bases: object

An in-memory filesystem-like structure.

This filesystem uses posix style paths only so users must be careful to use the posixpath module instead of the os.path one which will vary depending on the operating system which the active python is running in (the decision to use posixpath was to avoid the path variations which are not relevant in an implementation of a in-memory fake filesystem).

Not thread-safe when a single filesystem is mutated at the same time by multiple threads. For example having multiple threads call into clear() at the same time could potentially end badly. It is thread-safe when only get() or other read-only actions (like calling into ls()) are occurring at the same time.

Example usage:

>>> from taskflow.persistence.backends import impl_memory
>>> fs = impl_memory.FakeFilesystem()
>>> fs.ensure_path('/a/b/c')
>>> fs['/a/b/c'] = 'd'
>>> print(fs['/a/b/c'])
d
>>> del fs['/a/b/c']
>>> fs.ls("/a/b")
[]
>>> fs.get("/a/b/c", 'blob')
'blob'
    
root_path = '/'
Root path of the in-memory filesystem.

classmethod normpath(path)
Return a normalized absolutized version of the pathname path.

static split(p)
Split a pathname into a tuple of (head, tail).

static join(*pieces)
Join many path segments together.

ensure_path(path)
Ensure the path (and parents) exists.

get(path, default=None)
Fetch the value of given path (and return default if not found).

ls_r(path, absolute=False)
Return list of all children of the given path (recursively).

ls(path, absolute=False)
Return list of all children of the given path (not recursive).

clear()
Remove all nodes (except the root) from this filesystem.

delete(path, recursive=False)
Deletes a node (optionally its children) from this filesystem.

pformat()
Pretty format this in-memory filesystem.

symlink(src_path, dest_path)
Link the destionation path to the source path.


class taskflow.persistence.backends.impl_memory.MemoryBackend(conf=None)
Bases: PathBasedBackend

A in-memory (non-persistent) backend.

This backend writes logbooks, flow details, and atom details to a in-memory filesystem-like structure (rooted by the memory instance variable).

This backend does not provide true transactional semantics. It does guarantee that there will be no inter-thread race conditions when writing and reading by using a read/write locks.

DEFAULT_PATH = '/'
Default path used when none is provided.

get_connection()
Return a Connection instance based on the configuration settings.

close()
Closes any resources this backend has open.


class taskflow.persistence.backends.impl_memory.Connection(backend)
Bases: PathBasedConnection
validate()
Validates that a backend is still ok to be used.

The semantics of this may vary depending on the backend. On failure a backend specific exception should be raised that will indicate why the failure occurred.



Files

class taskflow.persistence.backends.impl_dir.DirBackend(conf)
Bases: PathBasedBackend

A directory and file based backend.

This backend does not provide true transactional semantics. It does guarantee that there will be no interprocess race conditions when writing and reading by using a consistent hierarchy of file based locks.

Example configuration:

conf = {

"path": "/tmp/taskflow", # save data to this root directory
"max_cache_size": 1024, # keep up-to 1024 entries in memory }


DEFAULT_FILE_ENCODING = 'utf-8'
Default encoding used when decoding or encoding files into or from text/unicode into binary or binary into text/unicode.

get_connection()
Return a Connection instance based on the configuration settings.

close()
Closes any resources this backend has open.


class taskflow.persistence.backends.impl_dir.Connection(backend)
Bases: PathBasedConnection
validate()
Validates that a backend is still ok to be used.

The semantics of this may vary depending on the backend. On failure a backend specific exception should be raised that will indicate why the failure occurred.



SQLAlchemy

Zookeeper

Storage

class taskflow.storage.Storage(flow_detail, backend=None, scope_fetcher=None)
Bases: object

Interface between engines and logbook and its backend (if any).

This class provides a simple interface to save atoms of a given flow and associated activity and results to persistence layer (logbook, atom_details, flow_details) for use by engines. This makes it easier to interact with the underlying storage & backend mechanism through this interface rather than accessing those objects directly.

NOTE(harlowja): if no backend is provided then a in-memory backend will be automatically used and the provided flow detail object will be placed into it for the duration of this objects existence.

injector_name = '_TaskFlow_INJECTOR'
Injector task detail name.

This task detail is a special detail that will be automatically created and saved to store persistent injected values (name conflicts with it must be avoided) that are global to the flow being executed.


ensure_atoms(atoms)
Ensure there is an atomdetail for each of the given atoms.

Returns list of atomdetail uuids for each atom processed.


property lock
Reader/writer lock used to ensure multi-thread safety.

This does not protect against the same storage objects being used by multiple engines/users across multiple processes (or different machines); certain backends handle that situation better than others (for example by using sequence identifiers) and it's a ongoing work in progress to make that better).


ensure_atom(atom)
Ensure there is an atomdetail for the given atom.

Returns the uuid for the atomdetail that corresponds to the given atom.


property flow_name
The flow detail name this storage unit is associated with.

property flow_uuid
The flow detail uuid this storage unit is associated with.

property flow_meta
The flow detail metadata this storage unit is associated with.

property backend
The backend this storage unit is associated with.

get_atom_uuid(atom_name)
Gets an atoms uuid given a atoms name.

set_atom_state(atom_name, state)
Sets an atoms state.

get_atom_state(atom_name)
Gets the state of an atom given an atoms name.

set_atom_intention(atom_name, intention)
Sets the intention of an atom given an atoms name.

get_atom_intention(atom_name)
Gets the intention of an atom given an atoms name.

get_atoms_states(atom_names)
Gets a dict of atom name => (state, intention) given atom names.

update_atom_metadata(atom_name, update_with)
Updates a atoms associated metadata.

This update will take a provided dictionary or a list of (key, value) pairs to include in the updated metadata (newer keys will overwrite older keys) and after merging saves the updated data into the underlying persistence layer.


set_task_progress(task_name, progress, details=None)
Set a tasks progress.
Parameters
  • task_name -- task name
  • progress -- tasks progress (0.0 <-> 1.0)
  • details -- any task specific progress details



get_task_progress(task_name)
Get the progress of a task given a tasks name.
Parameters
task_name -- tasks name
Returns
current task progress value


get_task_progress_details(task_name)
Get the progress details of a task given a tasks name.
Parameters
task_name -- task name
Returns
None if progress_details not defined, else progress_details dict


save(atom_name, result, state='SUCCESS')
Put result for atom with provided name to storage.

save_retry_failure(retry_name, failed_atom_name, failure)
Save subflow failure to retry controller history.

cleanup_retry_history(retry_name, state)
Cleanup history of retry atom with given name.

get_execute_result(atom_name)
Gets the execute results for an atom from storage.

get_execute_failures()
Get all execute failures that happened with this flow.

get(atom_name)
Gets the execute results for an atom from storage.

get_failures()
Get all execute failures that happened with this flow.

get_revert_result(atom_name)
Gets the revert results for an atom from storage.

get_revert_failures()
Get all revert failures that happened with this flow.

has_failures()
Returns true if there are any failures in storage.

reset(atom_name, state='PENDING')
Reset atom with given name (if the atom is not in a given state).

inject_atom_args(atom_name, pairs, transient=True)
Add values into storage for a specific atom only.
Parameters
transient -- save the data in-memory only instead of persisting the data to backend storage (useful for resource-like objects or similar objects which can not be persisted)

This method injects a dictionary/pairs of arguments for an atom so that when that atom is scheduled for execution it will have immediate access to these arguments.

NOTE:

Injected atom arguments take precedence over arguments provided by predecessor atoms or arguments provided by injecting into the flow scope (using the inject() method).


WARNING:

It should be noted that injected atom arguments (that are scoped to the atom with the given name) should be serializable whenever possible. This is a requirement for the worker based engine which must serialize (typically using json) all atom execute() and revert() arguments to be able to transmit those arguments to the target worker(s). If the use-case being applied/desired is to later use the worker based engine then it is highly recommended to ensure all injected atoms (even transient ones) are serializable to avoid issues that may appear later (when a object turned out to not actually be serializable).



inject(pairs, transient=False)
Add values into storage.

This method should be used to put flow parameters (requirements that are not satisfied by any atom in the flow) into storage.

Parameters
transient -- save the data in-memory only instead of persisting the data to backend storage (useful for resource-like objects or similar objects which can not be persisted)

WARNING:

It should be noted that injected flow arguments (that are scoped to all atoms in this flow) should be serializable whenever possible. This is a requirement for the worker based engine which must serialize (typically using json) all atom execute() and revert() arguments to be able to transmit those arguments to the target worker(s). If the use-case being applied/desired is to later use the worker based engine then it is highly recommended to ensure all injected atoms (even transient ones) are serializable to avoid issues that may appear later (when a object turned out to not actually be serializable).



fetch(name, many_handler=None)
Fetch a named execute result.

fetch_unsatisfied_args(atom_name, args_mapping, scope_walker=None, optional_args=None)
Fetch unsatisfied execute arguments using an atoms args mapping.

NOTE(harlowja): this takes into account the provided scope walker atoms who should produce the required value at runtime, as well as the transient/persistent flow and atom specific injected arguments. It does not check if the providers actually have produced the needed values; it just checks that they are registered to produce it in the future.


fetch_all(many_handler=None)
Fetch all named execute results known so far.

fetch_mapped_args(args_mapping, atom_name=None, scope_walker=None, optional_args=None)
Fetch execute arguments for an atom using its args mapping.

set_flow_state(state)
Set flow details state and save it.

update_flow_metadata(update_with)
Update flowdetails metadata and save it.

change_flow_state(state)
Transition flow from old state to new state.

Returns (True, old_state) if transition was performed, or (False, old_state) if it was ignored, or raises a InvalidState exception if transition is invalid.


get_flow_state()
Get state from flow details.

get_retry_history(retry_name)
Fetch a single retrys history.

get_retry_histories()
Fetch all retrys histories.


Hierarchy

Resumption

Overview

Question: How can we persist the flow so that it can be resumed, restarted or rolled-back on engine failure?

Answer: Since a flow is a set of atoms and relations between atoms we need to create a model and corresponding information that allows us to persist the right amount of information to preserve, resume, and rollback a flow on software or hardware failure.

To allow for resumption TaskFlow must be able to re-create the flow and re-connect the links between atom (and between atoms->atom details and so on) in order to revert those atoms or resume those atoms in the correct ordering. TaskFlow provides a pattern that can help in automating this process (it does not prohibit the user from creating their own strategies for doing this).

Factories

The default provided way is to provide a factory function which will create (or recreate your workflow). This function can be provided when loading a flow and corresponding engine via the provided load_from_factory() method. This factory function is expected to be a function (or staticmethod) which is reimportable (aka has a well defined name that can be located by the __import__ function in python, this excludes lambda style functions and instance methods). The factory function name will be saved into the logbook and it will be imported and called to create the workflow objects (or recreate it if resumption happens). This allows for the flow to be recreated if and when that is needed (even on remote machines, as long as the reimportable name can be located).

Names

When a flow is created it is expected that each atom has a unique name, this name serves a special purpose in the resumption process (as well as serving a useful purpose when running, allowing for atom identification in the notification process). The reason for having names is that an atom in a flow needs to be somehow matched with (a potentially) existing AtomDetail during engine resumption & subsequent running.

The match should be:

  • stable if atoms are added or removed
  • should not change when service is restarted, upgraded...
  • should be the same across all server instances in HA setups

Names provide this although they do have weaknesses:

  • the names of atoms must be unique in flow
  • it becomes hard to change the name of atom since a name change causes other side-effects

NOTE:

Even though these weaknesses names were selected as a good enough solution for the above matching requirements (until something better is invented/created that can satisfy those same requirements).


Scenarios

When new flow is loaded into engine, there is no persisted data for it yet, so a corresponding FlowDetail object will be created, as well as a AtomDetail object for each atom that is contained in it. These will be immediately saved into the persistence backend that is configured. If no persistence backend is configured, then as expected nothing will be saved and the atoms and flow will be ran in a non-persistent manner.

Subsequent run: When we resume the flow from a persistent backend (for example, if the flow was interrupted and engine destroyed to save resources or if the service was restarted), we need to re-create the flow. For that, we will call the function that was saved on first-time loading that builds the flow for us (aka; the flow factory function described above) and the engine will run. The following scenarios explain some expected structural changes and how they can be accommodated (and what the effect will be when resuming & running).

Same atoms

When the factory function mentioned above returns the exact same the flow and atoms (no changes are performed).

Runtime change: Nothing should be done -- the engine will re-associate atoms with AtomDetail objects by name and then the engine resumes.

Atom was added

When the factory function mentioned above alters the flow by adding a new atom in (for example for changing the runtime structure of what was previously ran in the first run).

Runtime change: By default when the engine resumes it will notice that a corresponding AtomDetail does not exist and one will be created and associated.

Atom was removed

When the factory function mentioned above alters the flow by removing a new atom in (for example for changing the runtime structure of what was previously ran in the first run).

Runtime change: Nothing should be done -- flow structure is reloaded from factory function, and removed atom is not in it -- so, flow will be ran as if it was not there, and any results it returned if it was completed before will be ignored.

Atom code was changed

When the factory function mentioned above alters the flow by deciding that a newer version of a previously existing atom should be ran (possibly to perform some kind of upgrade or to fix a bug in a prior atoms code).

Factory change: The atom name & version will have to be altered. The factory should replace this name where it was being used previously.

Runtime change: This will fall under the same runtime adjustments that exist when a new atom is added. In the future TaskFlow could make this easier by providing a upgrade() function that can be used to give users the ability to upgrade atoms before running (manual introspection & modification of a LogBook can be done before engine loading and running to accomplish this in the meantime).

Atom was split in two atoms or merged

When the factory function mentioned above alters the flow by deciding that a previously existing atom should be split into N atoms or the factory function decides that N atoms should be merged in <N atoms (typically occurring during refactoring).

Runtime change: This will fall under the same runtime adjustments that exist when a new atom is added or removed. In the future TaskFlow could make this easier by providing a migrate() function that can be used to give users the ability to migrate atoms previous data before running (manual introspection & modification of a LogBook can be done before engine loading and running to accomplish this in the meantime).

Flow structure was changed

If manual links were added or removed from graph, or task requirements were changed, or flow was refactored (atom moved into or out of subflows, linear flow was replaced with graph flow, tasks were reordered in linear flow, etc).

Runtime change: Nothing should be done.

Jobs

Overview

Jobs and jobboards are a novel concept that TaskFlow provides to allow for automatic ownership transfer of workflows between capable owners (those owners usually then use engines to complete the workflow). They provide the necessary semantics to be able to atomically transfer a job from a producer to a consumer in a reliable and fault tolerant manner. They are modeled off the concept used to post and acquire work in the physical world (typically a job listing in a newspaper or online website serves a similar role).

TLDR: It's similar to a queue, but consumers lock items on the queue when claiming them, and only remove them from the queue when they're done with the work. If the consumer fails, the lock is automatically released and the item is back on the queue for further consumption.

NOTE:

For more information, please visit the paradigm shift page for more details.


Definitions

Jobs
A job consists of a unique identifier, name, and a reference to a logbook which contains the details of the work that has been or should be/will be completed to finish the work that has been created for that job.
Jobboards
A jobboard is responsible for managing the posting, ownership, and delivery of jobs. It acts as the location where jobs can be posted, claimed and searched for; typically by iteration or notification. Jobboards may be backed by different capable implementations (each with potentially differing configuration) but all jobboards implement the same interface and semantics so that the backend usage is as transparent as possible. This allows deployers or developers of a service that uses TaskFlow to select a jobboard implementation that fits their setup (and their intended usage) best.

High level architecture

[image] Note: This diagram shows the high-level diagram (and further parts of this documentation also refer to it as well) of the zookeeper implementation (other implementations will typically have different architectures)..UNINDENT

Features

High availability
Guarantees workflow forward progress by transferring partially complete work or work that has not been started to entities which can either resume the previously partially completed work or begin initial work to ensure that the workflow as a whole progresses (where progressing implies transitioning through the workflow patterns and atoms and completing their associated states transitions).

Atomic transfer and single ownership
  • Ensures that only one workflow is managed (aka owned) by a single owner at a time in an atomic manner (including when the workflow is transferred to a owner that is resuming some other failed owners work). This avoids contention and ensures a workflow is managed by one and only one entity at a time.
  • Note: this does not mean that the owner needs to run the workflow itself but instead said owner could use an engine that runs the work in a distributed manner to ensure that the workflow progresses.

Separation of workflow construction and execution
Jobs can be created with logbooks that contain a specification of the work to be done by a entity (such as an API server). The job then can be completed by a entity that is watching that jobboard (not necessarily the API server itself). This creates a disconnection between work formation and work completion that is useful for scaling out horizontally.

Asynchronous completion
When for example a API server posts a job for completion to a jobboard that API server can return a tracking identifier to the user calling the API service. This tracking identifier can be used by the user to poll for status (similar in concept to a shipping tracking identifier created by fedex or UPS).


Usage

All jobboards are mere classes that implement same interface, and of course it is possible to import them and create instances of them just like with any other class in Python. But the easier (and recommended) way for creating jobboards is by using the fetch() function which uses entrypoints (internally using stevedore) to fetch and configure your backend.

Using this function the typical creation of a jobboard (and an example posting of a job) might look like:

from taskflow.persistence import backends as persistence_backends
from taskflow.jobs import backends as job_backends
...
persistence = persistence_backends.fetch({

"connection': "mysql",
"user": ...,
"password": ..., }) book = make_and_save_logbook(persistence) board = job_backends.fetch('my-board', {
"board": "zookeeper", }, persistence=persistence) job = board.post("my-first-job", book) ...


Consumption of jobs is similarly achieved by creating a jobboard and using the iteration functionality to find and claim jobs (and eventually consume them). The typical usage of a jobboard for consumption (and work completion) might look like:

import time
from taskflow import exceptions as exc
from taskflow.persistence import backends as persistence_backends
from taskflow.jobs import backends as job_backends
...
my_name = 'worker-1'
coffee_break_time = 60
persistence = persistence_backends.fetch({

"connection': "mysql",
"user": ...,
"password": ..., }) board = job_backends.fetch('my-board', {
"board": "zookeeper", }, persistence=persistence) while True:
my_job = None
for job in board.iterjobs(only_unclaimed=True):
try:
board.claim(job, my_name)
except exc.UnclaimableJob:
pass
else:
my_job = job
break
if my_job is not None:
try:
perform_job(my_job)
except Exception:
LOG.exception("I failed performing job: %s", my_job)
board.abandon(my_job, my_name)
else:
# I finished it, now cleanup.
board.consume(my_job)
persistence.get_connection().destroy_logbook(my_job.book.uuid)
time.sleep(coffee_break_time) ...


There are a few ways to provide arguments to the flow. The first option is to add a store to the flowdetail object in the logbook.

You can also provide a store in the job itself when posting it to the job board. If both store values are found, they will be combined, with the job store overriding the logbook store.

from oslo_utils import uuidutils
from taskflow import engines
from taskflow.persistence import backends as persistence_backends
from taskflow.persistence import models
from taskflow.jobs import backends as job_backends
...
persistence = persistence_backends.fetch({

"connection': "mysql",
"user": ...,
"password": ..., }) board = job_backends.fetch('my-board', {
"board": "zookeeper", }, persistence=persistence) book = models.LogBook('my-book', uuidutils.generate_uuid()) flow_detail = models.FlowDetail('my-job', uuidutils.generate_uuid()) book.add(flow_detail) connection = persistence.get_connection() connection.save_logbook(book) flow_detail.meta['store'] = {'a': 1, 'c': 3} job_details = {
"flow_uuid": flow_detail.uuid,
"store": {'a': 2, 'b': 1} } engines.save_factory_details(flow_detail, flow_factory,
factory_args=[],
factory_kwargs={},
backend=persistence) jobboard = get_jobboard(zk_client) jobboard.connect() job = jobboard.post('my-job', book=book, details=job_details) # the flow global parameters are now the combined store values # {'a': 2, 'b': 1', 'c': 3} ...


Types

Zookeeper

Board type: 'zookeeper'

Uses zookeeper to provide the jobboard capabilities and semantics by using a zookeeper directory, ephemeral, non-ephemeral nodes and watches.

Additional kwarg parameters:

  • client: a class that provides kazoo.client.KazooClient-like interface; it will be used for zookeeper interactions, sharing clients between jobboard instances will likely provide better scalability and can help avoid creating to many open connections to a set of zookeeper servers.
  • persistence: a class that provides a persistence backend interface; it will be used for loading jobs logbooks for usage at runtime or for usage before a job is claimed for introspection.

Additional configuration parameters:

  • path: the root zookeeper path to store job information (defaults to /taskflow/jobs)
  • hosts: the list of zookeeper hosts to connect to (defaults to localhost:2181); only used if a client is not provided.
  • timeout: the timeout used when performing operations with zookeeper; only used if a client is not provided.
  • handler: a class that provides kazoo.handlers-like interface; it will be used internally by kazoo to perform asynchronous operations, useful when your program uses eventlet and you want to instruct kazoo to use an eventlet compatible handler.

NOTE:

See ZookeeperJobBoard for implementation details.


Redis

Board type: 'redis'

Uses redis to provide the jobboard capabilities and semantics by using a redis hash data structure and individual job ownership keys (that can optionally expire after a given amount of time).

NOTE:

See RedisJobBoard for implementation details.


Considerations

Some usage considerations should be used when using a jobboard to make sure it's used in a safe and reliable manner. Eventually we hope to make these non-issues but for now they are worth mentioning.

Dual-engine jobs

What: Since atoms and engines are not currently preemptable we can not force an engine (or the threads/remote workers... it is using to run) to stop working on an atom (it is general bad behavior to force code to stop without its consent anyway) if it has already started working on an atom (short of doing a kill -9 on the running interpreter). This could cause problems since the points an engine can notice that it no longer owns a claim is at any state change that occurs (transitioning to a new atom or recording a result for example), where upon noticing the claim has been lost the engine can immediately stop doing further work. The effect that this causes is that when a claim is lost another engine can immediately attempt to acquire the claim that was previously lost and it could begin working on the unfinished tasks that the later engine may also still be executing (since that engine is not yet aware that it has lost the claim).

TLDR: not preemptable, possible to become aware of losing a claim after the fact (at the next state change), another engine could have acquired the claim by then, therefore both would be working on a job.

Alleviate by:

1.
Ensure your atoms are idempotent, this will cause an engine that may be executing the same atom to be able to continue executing without causing any conflicts/problems (idempotency guarantees this).
2.
On claiming jobs that have been claimed previously enforce a policy that happens before the jobs workflow begins to execute (possibly prior to an engine beginning the jobs work) that ensures that any prior work has been rolled back before continuing rolling forward. For example:
  • Rolling back the last atom/set of atoms that finished.
  • Rolling back the last state change that occurred.

3.
Delay claiming partially completed work by adding a wait period (to allow the previous engine to coalesce) before working on a partially completed job (combine this with the prior suggestions and most dual-engine issues should be avoided).

Interfaces

class taskflow.jobs.base.JobPriority(value)
Bases: Enum

Enum of job priorities (modeled after hadoop job priorities).

VERY_HIGH = 'VERY_HIGH'
Extremely urgent job priority.

HIGH = 'HIGH'
Mildly urgent job priority.

NORMAL = 'NORMAL'
Default job priority.

LOW = 'LOW'
Not needed anytime soon job priority.

VERY_LOW = 'VERY_LOW'
Very much not needed anytime soon job priority.

classmethod reorder(*values)
Reorders (priority, value) tuples -> priority ordered values.


class taskflow.jobs.base.Job(board, name, uuid=None, details=None, backend=None, book=None, book_data=None)
Bases: object

A abstraction that represents a named and trackable unit of work.

A job connects a logbook, a owner, a priority, last modified and created on dates and any associated state that the job has. Since it is a connected to a logbook, which are each associated with a set of factories that can create set of flows, it is the current top-level container for a piece of work that can be owned by an entity (typically that entity will read those logbooks and run any contained flows).

Only one entity will be allowed to own and operate on the flows contained in a job at a given time (for the foreseeable future).

NOTE(harlowja): It is the object that will be transferred to another entity on failure so that the contained flows ownership can be transferred to the secondary entity/owner for resumption, continuation, reverting...

abstract property last_modified
The datetime the job was last modified.

abstract property created_on
The datetime the job was created on.

property board
The board this job was posted on or was created from.

abstract property state
Access the current state of this job.

abstract property priority
The JobPriority of this job.

wait(timeout=None, delay=0.01, delay_multiplier=2.0, max_delay=60.0, sleep_func=<built-in function sleep>)
Wait for job to enter completion state.

If the job has not completed in the given timeout, then return false, otherwise return true (a job failure exception may also be raised if the job information can not be read, for whatever reason). Periodic state checks will happen every delay seconds where delay will be multiplied by the given multipler after a state is found that is not complete.

Note that if no timeout is given this is equivalent to blocking until the job has completed. Also note that if a jobboard backend can optimize this method then its implementation may not use delays (and backoffs) at all. In general though no matter what optimizations are applied implementations must always respect the given timeout value.


property book
Logbook associated with this job.

If no logbook is associated with this job, this property is None.


property book_uuid
UUID of logbook associated with this job.

If no logbook is associated with this job, this property is None.


property book_name
Name of logbook associated with this job.

If no logbook is associated with this job, this property is None.


property uuid
The uuid of this job.

property details
A dictionary of any details associated with this job.

property name
The non-uniquely identifying name of this job.


class taskflow.jobs.base.JobBoardIterator(board, logger, board_fetch_func=None, board_removal_func=None, only_unclaimed=False, ensure_fresh=False)
Bases: object

Iterator over a jobboard that iterates over potential jobs.

It provides the following attributes:

  • only_unclaimed: boolean that indicates whether to only iterate over unclaimed jobs
  • ensure_fresh: boolean that requests that during every fetch of a new set of jobs this will cause the iterator to force the backend to refresh (ensuring that the jobboard has the most recent job listings)
  • board: the board this iterator was created from

property board
The board this iterator was created from.


class taskflow.jobs.base.JobBoard(name, conf)
Bases: object

A place where jobs can be posted, reposted, claimed and transferred.

There can be multiple implementations of this job board, depending on the desired semantics and capabilities of the underlying jobboard implementation.

NOTE(harlowja): the name is meant to be an analogous to a board/posting system that is used in newspapers, or elsewhere to solicit jobs that people can interview and apply for (and then work on & complete).

abstract iterjobs(only_unclaimed=False, ensure_fresh=False)
Returns an iterator of jobs that are currently on this board.

NOTE(harlowja): the ordering of this iteration should be by posting order (oldest to newest) with higher priority jobs being provided before lower priority jobs, but it is left up to the backing implementation to provide the order that best suits it..

NOTE(harlowja): the iterator that is returned may support other attributes which can be used to further customize how iteration can be accomplished; check with the backends iterator object to determine what other attributes are supported.

Parameters
  • only_unclaimed -- boolean that indicates whether to only iteration over unclaimed jobs.
  • ensure_fresh -- boolean that requests to only iterate over the most recent jobs available, where the definition of what is recent is backend specific. It is allowable that a backend may ignore this value if the backends internal semantics/capabilities can not support this argument.



abstract wait(timeout=None)
Waits a given amount of time for any jobs to be posted.

When jobs are found then an iterator will be returned that can be used to iterate over those jobs.

NOTE(harlowja): since a jobboard can be mutated on by multiple external entities at the same time the iterator that can be returned may still be empty due to other entities removing those jobs after the iterator has been created (be aware of this when using it).

Parameters
timeout -- float that indicates how long to wait for a job to appear (if None then waits forever).


abstract property job_count
Returns how many jobs are on this jobboard.

NOTE(harlowja): this count may change as jobs appear or are removed so the accuracy of this count should not be used in a way that requires it to be exact & absolute.


abstract find_owner(job)
Gets the owner of the job if one exists.

property name
The non-uniquely identifying name of this jobboard.

abstract consume(job, who)
Permanently (and atomically) removes a job from the jobboard.

Consumption signals to the board (and any others examining the board) that this job has been completed by the entity that previously claimed that job.

Only the entity that has claimed that job is able to consume the job.

A job that has been consumed can not be reclaimed or reposted by another entity (job postings are immutable). Any entity consuming a unclaimed job (or a job they do not have a claim on) will cause an exception.

Parameters
  • job -- a job on this jobboard that can be consumed (if it does not exist then a NotFound exception will be raised).
  • who -- string that names the entity performing the consumption, this must be the same name that was used for claiming this job.



abstract post(name, book=None, details=None, priority=JobPriority.NORMAL)
Atomically creates and posts a job to the jobboard.

This posting allowing others to attempt to claim that job (and subsequently work on that job). The contents of the provided logbook, details dictionary, or name (or a mix of these) must provide enough information for consumers to reference to construct and perform that jobs contained work (whatever it may be).

Once a job has been posted it can only be removed by consuming that job (after that job is claimed). Any entity can post/propose jobs to the jobboard (in the future this may be restricted).

Returns a job object representing the information that was posted.


abstract claim(job, who)
Atomically attempts to claim the provided job.

If a job is claimed it is expected that the entity that claims that job will at sometime in the future work on that jobs contents and either fail at completing them (resulting in a reposting) or consume that job from the jobboard (signaling its completion). If claiming fails then a corresponding exception will be raised to signal this to the claim attempter.

Parameters
  • job -- a job on this jobboard that can be claimed (if it does not exist then a NotFound exception will be raised).
  • who -- string that names the claiming entity.



abstract abandon(job, who)
Atomically attempts to abandon the provided job.

This abandonment signals to others that the job may now be reclaimed. This would typically occur if the entity that has claimed the job has failed or is unable to complete the job or jobs it had previously claimed.

Only the entity that has claimed that job can abandon a job. Any entity abandoning a unclaimed job (or a job they do not own) will cause an exception.

Parameters
  • job -- a job on this jobboard that can be abandoned (if it does not exist then a NotFound exception will be raised).
  • who -- string that names the entity performing the abandoning, this must be the same name that was used for claiming this job.



abstract trash(job, who)
Trash the provided job.

Trashing a job signals to others that the job is broken and should not be reclaimed. This is provided as an option for users to be able to remove jobs from the board externally. The trashed job details should be kept around in an alternate location to be reviewed, if desired.

Only the entity that has claimed that job can trash a job. Any entity trashing a unclaimed job (or a job they do not own) will cause an exception.

Parameters
  • job -- a job on this jobboard that can be trashed (if it does not exist then a NotFound exception will be raised).
  • who -- string that names the entity performing the trashing, this must be the same name that was used for claiming this job.



abstract register_entity(entity)
Register an entity to the jobboard('s backend), e.g: a conductor.
Parameters
entity (Entity) -- entity to register as being associated with the jobboard('s backend)


abstract property connected
Returns if this jobboard is connected.

abstract connect()
Opens the connection to any backend system.

abstract close()
Close the connection to any backend system.

Once closed the jobboard can no longer be used (unless reconnection occurs).



class taskflow.jobs.base.NotifyingJobBoard(name, conf)
Bases: JobBoard

A jobboard subclass that can notify others about board events.

Implementers are expected to notify at least about jobs being posted and removed.

NOTE(harlowja): notifications that are emitted may be emitted on a separate dedicated thread when they occur, so ensure that all callbacks registered are thread safe (and block for as little time as possible).


taskflow.jobs.backends.fetch(name, conf, namespace='taskflow.jobboards', **kwargs)
Fetch a jobboard backend with the given configuration.

This fetch method will look for the entrypoint name in the entrypoint namespace, and then attempt to instantiate that entrypoint using the provided name, configuration and any board specific kwargs.

NOTE(harlowja): to aid in making it easy to specify configuration and options to a board the configuration (which is typical just a dictionary) can also be a URI string that identifies the entrypoint name and any configuration specific to that board.

For example, given the following configuration URI:

zookeeper://<not-used>/?a=b&c=d


This will look for the entrypoint named 'zookeeper' and will provide a configuration object composed of the URI's components, in this case that is {'a': 'b', 'c': 'd'} to the constructor of that board instance (also including the name specified).


taskflow.jobs.backends.backend(name, conf, namespace='taskflow.jobboards', **kwargs)
Fetches a jobboard, connects to it and closes it on completion.

This allows a board instance to fetched, connected to, and then used in a context manager statement with the board being closed upon context manager exit.


Implementations

Zookeeper

Redis

class taskflow.jobs.backends.impl_redis.RedisJob(board, name, sequence, key, uuid=None, details=None, created_on=None, backend=None, book=None, book_data=None, priority=JobPriority.NORMAL)
Bases: Job

A redis job.

property key
Key (in board listings/trash hash) the job data is stored under.

property priority
The JobPriority of this job.

property last_modified_key
Key the job last modified data is stored under.

property owner_key
Key the job claim + data of the owner is stored under.

property sequence
Sequence number of the current job.

expires_in()
How many seconds until the claim expires.

Returns the number of seconds until the ownership entry expires or DOES_NOT_EXPIRE or KEY_NOT_FOUND if it does not expire or if the expiry can not be determined (perhaps the owner_key expired at/before time of inquiry?).


extend_expiry(expiry)
Extends the owner key (aka the claim) expiry for this job.

NOTE(harlowja): if the claim for this job did not previously have an expiry associated with it, calling this method will create one (and after that time elapses the claim on this job will cease to exist).

Returns True if the expiry request was performed otherwise False.


property created_on
The datetime the job was created on.

property last_modified
The datetime the job was last modified.

property state
Access the current state of this job.


class taskflow.jobs.backends.impl_redis.RedisJobBoard(name, conf, client=None, persistence=None)
Bases: JobBoard

A jobboard backed by redis.

Powered by the redis-py library.

This jobboard creates job entries by listing jobs in a redis hash. This hash contains jobs that can be actively worked on by (and examined/claimed by) some set of eligible consumers. Job posting is typically performed using the post() method (this creates a hash entry with job contents/details encoded in msgpack). The users of these jobboard(s) (potentially on disjoint sets of machines) can then iterate over the available jobs and decide if they want to attempt to claim one of the jobs they have iterated over. If so they will then attempt to contact redis and they will attempt to create a key in redis (using a embedded lua script to perform this atomically) to claim a desired job. If the entity trying to use the jobboard to claim() the job is able to create that lock/owner key then it will be allowed (and expected) to perform whatever work the contents of that job described. Once the claiming entity is finished the lock/owner key and the hash entry will be deleted (if successfully completed) in a single request (also using a embedded lua script to perform this atomically). If the claiming entity is not successful (or the entity that claimed the job dies) the lock/owner key can be released automatically (by optional usage of a claim expiry) or by using abandon() to manually abandon the job so that it can be consumed/worked on by others.

NOTE(harlowja): by default the claim() has no expiry (which means claims will be persistent, even under claiming entity failure). To ensure a expiry occurs pass a numeric value for the expiry keyword argument to the claim() method that defines how many seconds the claim should be retained for. When an expiry is used ensure that that claim is kept alive while it is being worked on by using the extend_expiry() method periodically.

CLIENT_CONF_TRANSFERS = (('host', <class 'str'>), ('port', <class 'int'>), ('password', <class 'str'>), ('encoding', <class 'str'>), ('encoding_errors', <class 'str'>), ('socket_timeout', <class 'float'>), ('socket_connect_timeout', <class 'float'>), ('unix_socket_path', <class 'str'>), ('ssl', <function bool_from_string>), ('ssl_keyfile', <class 'str'>), ('ssl_certfile', <class 'str'>), ('ssl_cert_reqs', <class 'str'>), ('ssl_ca_certs', <class 'str'>), ('db', <class 'int'>))
Keys (and value type converters) that we allow to proxy from the jobboard configuration into the redis client (used to configure the redis client internals if no explicit client is provided via the client keyword argument).

See: http://redis-py.readthedocs.org/en/latest/#redis.Redis

See: https://github.com/andymccurdy/redis-py/blob/2.10.3/redis/client.py


OWNED_POSTFIX = b'.owned'
Postfix (combined with job key) used to make a jobs owner key.

LAST_MODIFIED_POSTFIX = b'.last_modified'
Postfix (combined with job key) used to make a jobs last modified key.

DEFAULT_NAMESPACE = b'taskflow'
Default namespace for keys when none is provided.

MIN_REDIS_VERSION = (2, 6)
Minimum redis version this backend requires.

This version is required since we need the built-in server-side lua scripting support that is included in 2.6 and newer.


NAMESPACE_SEP = b':'
Separator that is used to combine a key with the namespace (to get the actual key that will be used).

KEY_PIECE_SEP = b'.'
Separator that is used to combine a bunch of key pieces together (to get the actual key that will be used).

SCRIPT_STATUS_OK = 'ok'
Expected lua response status field when call is ok.

SCRIPT_STATUS_ERROR = 'error'
Expected lua response status field when call is not ok.

SCRIPT_NOT_EXPECTED_OWNER = 'Not expected owner!'
Expected lua script error response when the owner is not as expected.

SCRIPT_UNKNOWN_OWNER = 'Unknown owner!'
Expected lua script error response when the owner is not findable.

SCRIPT_UNKNOWN_JOB = 'Unknown job!'
Expected lua script error response when the job is not findable.

SCRIPT_ALREADY_CLAIMED = 'Job already claimed!'
Expected lua script error response when the job is already claimed.

SCRIPT_TEMPLATES = {'abandon': '\n-- Extract *all* the variables (so we can easily know what they are)...\nlocal owner_key = KEYS[1]\nlocal listings_key = KEYS[2]\nlocal last_modified_key = KEYS[3]\n\nlocal expected_owner = ARGV[1]\nlocal job_key = ARGV[2]\nlocal last_modified_blob = ARGV[3]\nlocal result = {}\nif redis.call("hexists", listings_key, job_key) == 1 then\n if redis.call("exists", owner_key) == 1 then\n local owner = redis.call("get", owner_key)\n if owner ~= expected_owner then\n result["status"] = "${error}"\n result["reason"] = "${not_expected_owner}"\n result["owner"] = owner\n else\n redis.call("del", owner_key)\n redis.call("set", last_modified_key, last_modified_blob)\n result["status"] = "${ok}"\n end\n else\n result["status"] = "${error}"\n result["reason"] = "${unknown_owner}"\n end\nelse\n result["status"] = "${error}"\n result["reason"] = "${unknown_job}"\nend\nreturn cmsgpack.pack(result)\n', 'claim': '\nlocal function apply_ttl(key, ms_expiry)\n if ms_expiry ~= nil then\n redis.call("pexpire", key, ms_expiry)\n end\nend\n\n-- Extract *all* the variables (so we can easily know what they are)...\nlocal owner_key = KEYS[1]\nlocal listings_key = KEYS[2]\nlocal last_modified_key = KEYS[3]\n\nlocal expected_owner = ARGV[1]\nlocal job_key = ARGV[2]\nlocal last_modified_blob = ARGV[3]\n\n-- If this is non-numeric (which it may be) this becomes nil\nlocal ms_expiry = nil\nif ARGV[4] ~= "none" then\n ms_expiry = tonumber(ARGV[4])\nend\nlocal result = {}\nif redis.call("hexists", listings_key, job_key) == 1 then\n if redis.call("exists", owner_key) == 1 then\n local owner = redis.call("get", owner_key)\n if owner == expected_owner then\n -- Owner is the same, leave it alone...\n redis.call("set", last_modified_key, last_modified_blob)\n apply_ttl(owner_key, ms_expiry)\n end\n result["status"] = "${error}"\n result["reason"] = "${already_claimed}"\n result["owner"] = owner\n else\n redis.call("set", owner_key, expected_owner)\n redis.call("set", last_modified_key, last_modified_blob)\n apply_ttl(owner_key, ms_expiry)\n result["status"] = "${ok}"\n end\nelse\n result["status"] = "${error}"\n result["reason"] = "${unknown_job}"\nend\nreturn cmsgpack.pack(result)\n', 'consume': '\n-- Extract *all* the variables (so we can easily know what they are)...\nlocal owner_key = KEYS[1]\nlocal listings_key = KEYS[2]\nlocal last_modified_key = KEYS[3]\n\nlocal expected_owner = ARGV[1]\nlocal job_key = ARGV[2]\nlocal result = {}\nif redis.call("hexists", listings_key, job_key) == 1 then\n if redis.call("exists", owner_key) == 1 then\n local owner = redis.call("get", owner_key)\n if owner ~= expected_owner then\n result["status"] = "${error}"\n result["reason"] = "${not_expected_owner}"\n result["owner"] = owner\n else\n -- The order is important here, delete the owner first (and if\n -- that blows up, the job data will still exist so it can be\n -- worked on again, instead of the reverse)...\n redis.call("del", owner_key, last_modified_key)\n redis.call("hdel", listings_key, job_key)\n result["status"] = "${ok}"\n end\n else\n result["status"] = "${error}"\n result["reason"] = "${unknown_owner}"\n end\nelse\n result["status"] = "${error}"\n result["reason"] = "${unknown_job}"\nend\nreturn cmsgpack.pack(result)\n', 'trash': '\n-- Extract *all* the variables (so we can easily know what they are)...\nlocal owner_key = KEYS[1]\nlocal listings_key = KEYS[2]\nlocal last_modified_key = KEYS[3]\nlocal trash_listings_key = KEYS[4]\n\nlocal expected_owner = ARGV[1]\nlocal job_key = ARGV[2]\nlocal last_modified_blob = ARGV[3]\nlocal result = {}\nif redis.call("hexists", listings_key, job_key) == 1 then\n local raw_posting = redis.call("hget", listings_key, job_key)\n if redis.call("exists", owner_key) == 1 then\n local owner = redis.call("get", owner_key)\n if owner ~= expected_owner then\n result["status"] = "${error}"\n result["reason"] = "${not_expected_owner}"\n result["owner"] = owner\n else\n -- This ordering is important (try to first move the value\n -- and only if that works do we try to do any deletions)...\n redis.call("hset", trash_listings_key, job_key, raw_posting)\n redis.call("set", last_modified_key, last_modified_blob)\n redis.call("del", owner_key)\n redis.call("hdel", listings_key, job_key)\n result["status"] = "${ok}"\n end\n else\n result["status"] = "${error}"\n result["reason"] = "${unknown_owner}"\n end\nelse\n result["status"] = "${error}"\n result["reason"] = "${unknown_job}"\nend\nreturn cmsgpack.pack(result)\n'}
Lua template scripts that will be used by various methods (they are turned into real scripts and loaded on call into the connect() method).

Some things to note:

  • The lua script is ran serially, so when this runs no other command will be mutating the backend (and redis also ensures that no other script will be running) so atomicity of these scripts are guaranteed by redis.
  • Transactions were considered (and even mostly implemented) but ultimately rejected since redis does not support rollbacks and transactions can not be interdependent (later operations can not depend on the results of earlier operations). Both of these issues limit our ability to correctly report errors (with useful messages) and to maintain consistency under failure/contention (due to the inability to rollback). A third and final blow to using transactions was to correctly use them we would have to set a watch on a very contentious key (the listings key) which would under load cause clients to retry more often then would be desired (this also increases network load, CPU cycles used, transactions failures triggered and so on).
  • Partial transaction execution is possible due to pre/post EXEC failures (and the lack of rollback makes this worse).

So overall after thinking, it seemed like having little lua scripts was not that bad (even if it is somewhat convoluted) due to the above and public mentioned issues with transactions. In general using lua scripts for this purpose seems to be somewhat common practice and it solves the issues that came up when transactions were considered & implemented.

Some links about redis (and redis + lua) that may be useful to look over:

  • Atomicity of scripts
  • Scripting and transactions
  • Why redis does not support rollbacks
  • Intro to lua for redis programmers
  • Five key takeaways for developing with redis
  • Everything you always wanted to know about redis (slides)


join(key_piece, *more_key_pieces)
Create and return a namespaced key from many segments.

NOTE(harlowja): all pieces that are text/unicode are converted into their binary equivalent (if they are already binary no conversion takes place) before being joined (as redis expects binary keys and not unicode/text ones).


property namespace
The namespace all keys will be prefixed with (or none).

trash_key
Key where a hash will be stored with trashed jobs in it.

sequence_key
Key where a integer will be stored (used to sequence jobs).

listings_key
Key where a hash will be stored with active jobs in it.

property job_count
Returns how many jobs are on this jobboard.

NOTE(harlowja): this count may change as jobs appear or are removed so the accuracy of this count should not be used in a way that requires it to be exact & absolute.


property connected
Returns if this jobboard is connected.

connect()
Opens the connection to any backend system.

close()
Close the connection to any backend system.

Once closed the jobboard can no longer be used (unless reconnection occurs).


find_owner(job)
Gets the owner of the job if one exists.

post(name, book=None, details=None, priority=JobPriority.NORMAL)
Atomically creates and posts a job to the jobboard.

This posting allowing others to attempt to claim that job (and subsequently work on that job). The contents of the provided logbook, details dictionary, or name (or a mix of these) must provide enough information for consumers to reference to construct and perform that jobs contained work (whatever it may be).

Once a job has been posted it can only be removed by consuming that job (after that job is claimed). Any entity can post/propose jobs to the jobboard (in the future this may be restricted).

Returns a job object representing the information that was posted.


wait(timeout=None, initial_delay=0.005, max_delay=1.0, sleep_func=<built-in function sleep>)
Waits a given amount of time for any jobs to be posted.

When jobs are found then an iterator will be returned that can be used to iterate over those jobs.

NOTE(harlowja): since a jobboard can be mutated on by multiple external entities at the same time the iterator that can be returned may still be empty due to other entities removing those jobs after the iterator has been created (be aware of this when using it).

Parameters
timeout -- float that indicates how long to wait for a job to appear (if None then waits forever).


iterjobs(only_unclaimed=False, ensure_fresh=False)
Returns an iterator of jobs that are currently on this board.

NOTE(harlowja): the ordering of this iteration should be by posting order (oldest to newest) with higher priority jobs being provided before lower priority jobs, but it is left up to the backing implementation to provide the order that best suits it..

NOTE(harlowja): the iterator that is returned may support other attributes which can be used to further customize how iteration can be accomplished; check with the backends iterator object to determine what other attributes are supported.

Parameters
  • only_unclaimed -- boolean that indicates whether to only iteration over unclaimed jobs.
  • ensure_fresh -- boolean that requests to only iterate over the most recent jobs available, where the definition of what is recent is backend specific. It is allowable that a backend may ignore this value if the backends internal semantics/capabilities can not support this argument.



register_entity(entity)
Register an entity to the jobboard('s backend), e.g: a conductor.
Parameters
entity (Entity) -- entity to register as being associated with the jobboard('s backend)


consume(job, who)
Permanently (and atomically) removes a job from the jobboard.

Consumption signals to the board (and any others examining the board) that this job has been completed by the entity that previously claimed that job.

Only the entity that has claimed that job is able to consume the job.

A job that has been consumed can not be reclaimed or reposted by another entity (job postings are immutable). Any entity consuming a unclaimed job (or a job they do not have a claim on) will cause an exception.

Parameters
  • job -- a job on this jobboard that can be consumed (if it does not exist then a NotFound exception will be raised).
  • who -- string that names the entity performing the consumption, this must be the same name that was used for claiming this job.



claim(job, who, expiry=None)
Atomically attempts to claim the provided job.

If a job is claimed it is expected that the entity that claims that job will at sometime in the future work on that jobs contents and either fail at completing them (resulting in a reposting) or consume that job from the jobboard (signaling its completion). If claiming fails then a corresponding exception will be raised to signal this to the claim attempter.

Parameters
  • job -- a job on this jobboard that can be claimed (if it does not exist then a NotFound exception will be raised).
  • who -- string that names the claiming entity.



abandon(job, who)
Atomically attempts to abandon the provided job.

This abandonment signals to others that the job may now be reclaimed. This would typically occur if the entity that has claimed the job has failed or is unable to complete the job or jobs it had previously claimed.

Only the entity that has claimed that job can abandon a job. Any entity abandoning a unclaimed job (or a job they do not own) will cause an exception.

Parameters
  • job -- a job on this jobboard that can be abandoned (if it does not exist then a NotFound exception will be raised).
  • who -- string that names the entity performing the abandoning, this must be the same name that was used for claiming this job.



trash(job, who)
Trash the provided job.

Trashing a job signals to others that the job is broken and should not be reclaimed. This is provided as an option for users to be able to remove jobs from the board externally. The trashed job details should be kept around in an alternate location to be reviewed, if desired.

Only the entity that has claimed that job can trash a job. Any entity trashing a unclaimed job (or a job they do not own) will cause an exception.

Parameters
  • job -- a job on this jobboard that can be trashed (if it does not exist then a NotFound exception will be raised).
  • who -- string that names the entity performing the trashing, this must be the same name that was used for claiming this job.




Hierarchy

Conductors

[image: Conductor] [image]

Overview

Conductors provide a mechanism that unifies the various concepts under a single easy to use (as plug-and-play as we can make it) construct.

They are responsible for the following:

  • Interacting with jobboards (examining and claiming jobs).
  • Creating engines from the claimed jobs (using factories to reconstruct the contained tasks and flows to be executed).
  • Dispatching the engine using the provided persistence layer and engine configuration.
  • Completing or abandoning the claimed job (depending on dispatching and execution outcome).
  • Rinse and repeat.

NOTE:

They are inspired by and have similar responsibilities as railroad conductors or musical conductors.


Considerations

Some usage considerations should be used when using a conductor to make sure it's used in a safe and reliable manner. Eventually we hope to make these non-issues but for now they are worth mentioning.

Endless cycling

What: Jobs that fail (due to some type of internal error) on one conductor will be abandoned by that conductor and then another conductor may experience those same errors and abandon it (and repeat). This will create a job abandonment cycle that will continue for as long as the job exists in an claimable state.

Example: [image: Conductor cycling] [image]

Alleviate by:

1.
Forcefully delete jobs that have been failing continuously after a given number of conductor attempts. This can be either done manually or automatically via scripts (or other associated monitoring) or via the jobboards trash() method.
2.
Resolve the internal error's cause (storage backend failure, other...).

Interfaces

class taskflow.conductors.base.Conductor(name, jobboard, persistence=None, engine=None, engine_options=None)
Bases: object

Base for all conductor implementations.

Conductors act as entities which extract jobs from a jobboard, assign there work to some engine (using some desired configuration) and then wait for that work to complete. If the work fails then they abandon the claimed work (or if the process they are running in crashes or dies this abandonment happens automatically) and then another conductor at a later period of time will finish up the prior failed conductors work.

ENTITY_KIND = 'conductor'
Entity kind used when creating new entity objects

conductor
Entity object that represents this conductor.

property notifier
The conductor actions (or other state changes) notifier.

NOTE(harlowja): different conductor implementations may emit different events + event details at different times, so refer to your conductor documentation to know exactly what can and what can not be subscribed to.


connect()
Ensures the jobboard is connected (noop if it is already).

close()
Closes the contained jobboard, disallowing further use.

abstract run(max_dispatches=None)
Continuously claims, runs, and consumes jobs (and repeat).
Parameters
max_dispatches -- An upper bound on the number of jobs that will be dispatched, if none or negative this implies there is no limit to the number of jobs that will be dispatched, otherwise if positive this run method will return when that amount of jobs has been dispatched (instead of running forever and/or until stopped).



taskflow.conductors.backends.fetch(kind, name, jobboard, namespace='taskflow.conductors', **kwargs)
Fetch a conductor backend with the given options.

This fetch method will look for the entrypoint 'kind' in the entrypoint namespace, and then attempt to instantiate that entrypoint using the provided name, jobboard and any board specific kwargs.


class taskflow.conductors.backends.impl_executor.ExecutorConductor(name, jobboard, persistence=None, engine=None, engine_options=None, wait_timeout=None, log=None, max_simultaneous_jobs=-1)
Bases: Conductor

Dispatches jobs from blocking run() method to some executor.

This conductor iterates over jobs in the provided jobboard (waiting for the given timeout if no jobs exist) and attempts to claim them, work on those jobs using an executor (potentially blocking further work from being claimed and consumed) and then consume those work units after completion. This process will repeat until the conductor has been stopped or other critical error occurs.

NOTE(harlowja): consumption occurs even if a engine fails to run due to a atom failure. This is only skipped when an execution failure or a storage failure occurs which are usually correctable by re-running on a different conductor (storage failures and execution failures may be transient issues that can be worked around by later execution). If a job after completing can not be consumed or abandoned the conductor relies upon the jobboard capabilities to automatically abandon these jobs.

LOG = None
Logger that will be used for listening to events (if none then the module level logger will be used instead).

REFRESH_PERIODICITY = 30
Every 30 seconds the jobboard will be resynced (if for some reason a watch or set of watches was not received) using the ensure_fresh option to ensure this (for supporting jobboard backends only).

WAIT_TIMEOUT = 0.5
Default timeout used to idle/wait when no jobs have been found.

MAX_SIMULTANEOUS_JOBS = -1
Default maximum number of jobs that can be in progress at the same time.

Negative or zero values imply no limit (do note that if a executor is used that is built on a queue, as most are, that this will imply that the queue will contain a potentially large & unfinished backlog of submitted jobs). This may get better someday if https://bugs.python.org/issue22737 is ever implemented and released.


NO_CONSUME_EXCEPTIONS = (<class 'taskflow.exceptions.ExecutionFailure'>, <class 'taskflow.exceptions.StorageFailure'>)
Exceptions that will not cause consumption to occur.

EVENTS_EMITTED = ('compilation_start', 'compilation_end', 'preparation_start', 'preparation_end', 'validation_start', 'validation_end', 'running_start', 'running_end', 'job_consumed', 'job_abandoned')
Events will be emitted for each of the events above. The event is emitted to listeners registered with the conductor.

stop()
Requests the conductor to stop dispatching.

This method can be used to request that a conductor stop its consumption & dispatching loop.

The method returns immediately regardless of whether the conductor has been stopped.


property dispatching
Whether or not the dispatching loop is still dispatching.

run(max_dispatches=None)
Continuously claims, runs, and consumes jobs (and repeat).
Parameters
max_dispatches -- An upper bound on the number of jobs that will be dispatched, if none or negative this implies there is no limit to the number of jobs that will be dispatched, otherwise if positive this run method will return when that amount of jobs has been dispatched (instead of running forever and/or until stopped).


wait(timeout=None)
Waits for the conductor to gracefully exit.

This method waits for the conductor to gracefully exit. An optional timeout can be provided, which will cause the method to return within the specified timeout. If the timeout is reached, the returned value will be False, otherwise it will be True.

Parameters
timeout -- Maximum number of seconds that the wait() method should block for.



Implementations

Blocking

class taskflow.conductors.backends.impl_blocking.BlockingConductor(name, jobboard, persistence=None, engine=None, engine_options=None, wait_timeout=None, log=None, max_simultaneous_jobs=1)
Bases: ExecutorConductor

Blocking conductor that processes job(s) in a blocking manner.

MAX_SIMULTANEOUS_JOBS = 1
Default maximum number of jobs that can be in progress at the same time.


Non-blocking

class taskflow.conductors.backends.impl_nonblocking.NonBlockingConductor(name, jobboard, persistence=None, engine=None, engine_options=None, wait_timeout=None, log=None, max_simultaneous_jobs=33, executor_factory=None)
Bases: ExecutorConductor

Non-blocking conductor that processes job(s) using a thread executor.

NOTE(harlowja): A custom executor factory can be provided via keyword
argument executor_factory, if provided it will be invoked at run() time with one positional argument (this conductor) and it must return a compatible executor which can be used to submit jobs to. If None is a provided a thread pool backed executor is selected by default (it will have an equivalent number of workers as this conductors simultaneous job count).

MAX_SIMULTANEOUS_JOBS = 33
Default maximum number of jobs that can be in progress at the same time.


Hierarchy

Examples

While developing TaskFlow the team has worked hard to make sure the various concepts are explained by relevant examples. Here are a few selected examples to get started (ordered by perceived complexity):

To explore more of these examples please check out the examples directory in the TaskFlow source tree.

NOTE:

If the examples provided are not satisfactory (or up to your standards) contributions are welcome and very much appreciated to help improve them. The higher the quality and the clearer the examples are the better and more useful they are for everyone.


Hello world

NOTE:

Full source located at hello_world.


import logging
import os
import sys
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) from taskflow import engines from taskflow.patterns import linear_flow as lf from taskflow.patterns import unordered_flow as uf from taskflow import task # INTRO: This is the defacto hello world equivalent for taskflow; it shows how # an overly simplistic workflow can be created that runs using different # engines using different styles of execution (all can be used to run in # parallel if a workflow is provided that is parallelizable). class PrinterTask(task.Task):
def __init__(self, name, show_name=True, inject=None):
super(PrinterTask, self).__init__(name, inject=inject)
self._show_name = show_name
def execute(self, output):
if self._show_name:
print("%s: %s" % (self.name, output))
else:
print(output) # This will be the work that we want done, which for this example is just to # print 'hello world' (like a song) using different tasks and different # execution models. song = lf.Flow("beats") # Unordered flows when ran can be ran in parallel; and a chorus is everyone # singing at once of course! hi_chorus = uf.Flow('hello') world_chorus = uf.Flow('world') for (name, hello, world) in [('bob', 'hello', 'world'),
('joe', 'hellooo', 'worllllld'),
('sue', "helloooooo!", 'wooorllld!')]:
hi_chorus.add(PrinterTask("%s@hello" % name,
# This will show up to the execute() method of
# the task as the argument named 'output' (which
# will allow us to print the character we want).
inject={'output': hello}))
world_chorus.add(PrinterTask("%s@world" % name,
inject={'output': world})) # The composition starts with the conductor and then runs in sequence with # the chorus running in parallel, but no matter what the 'hello' chorus must # always run before the 'world' chorus (otherwise the world will fall apart). song.add(PrinterTask("conductor@begin",
show_name=False, inject={'output': "*ding*"}),
hi_chorus,
world_chorus,
PrinterTask("conductor@end",
show_name=False, inject={'output': "*dong*"})) # Run in parallel using eventlet green threads... try:
import eventlet as _eventlet # noqa except ImportError:
# No eventlet currently active, skip running with it...
pass else:
print("-- Running in parallel using eventlet --")
e = engines.load(song, executor='greenthreaded', engine='parallel',
max_workers=1)
e.run() # Run in parallel using real threads... print("-- Running in parallel using threads --") e = engines.load(song, executor='threaded', engine='parallel',
max_workers=1) e.run() # Run in parallel using external processes... print("-- Running in parallel using processes --") e = engines.load(song, executor='processes', engine='parallel',
max_workers=1) e.run() # Run serially (aka, if the workflow could have been ran in parallel, it will # not be when ran in this mode)... print("-- Running serially --") e = engines.load(song, engine='serial') e.run() print("-- Statistics gathered --") print(e.statistics)


Passing values from and to tasks

NOTE:

Full source located at simple_linear_pass.


import logging
import os
import sys
logging.basicConfig(level=logging.ERROR)
self_dir = os.path.abspath(os.path.dirname(__file__))
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) sys.path.insert(0, self_dir) from taskflow import engines from taskflow.patterns import linear_flow from taskflow import task # INTRO: This example shows how a task (in a linear/serial workflow) can # produce an output that can be then consumed/used by a downstream task. class TaskA(task.Task):
default_provides = 'a'
def execute(self):
print("Executing '%s'" % (self.name))
return 'a' class TaskB(task.Task):
def execute(self, a):
print("Executing '%s'" % (self.name))
print("Got input '%s'" % (a)) print("Constructing...") wf = linear_flow.Flow("pass-from-to") wf.add(TaskA('a'), TaskB('b')) print("Loading...") e = engines.load(wf) print("Compiling...") e.compile() print("Preparing...") e.prepare() print("Running...") e.run() print("Done...")


Using listeners

NOTE:

Full source located at echo_listener.


import logging
import os
import sys
logging.basicConfig(level=logging.DEBUG)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) from taskflow import engines from taskflow.listeners import logging as logging_listener from taskflow.patterns import linear_flow as lf from taskflow import task # INTRO: This example walks through a miniature workflow which will do a # simple echo operation; during this execution a listener is associated with # the engine to receive all notifications about what the flow has performed, # this example dumps that output to the stdout for viewing (at debug level # to show all the information which is possible). class Echo(task.Task):
def execute(self):
print(self.name) # Generate the work to be done (but don't do it yet). wf = lf.Flow('abc') wf.add(Echo('a')) wf.add(Echo('b')) wf.add(Echo('c')) # This will associate the listener with the engine (the listener # will automatically register for notifications with the engine and deregister # when the context is exited). e = engines.load(wf) with logging_listener.DynamicLoggingListener(e):
e.run()


Using listeners (to watch a phone call)

NOTE:

Full source located at simple_linear_listening.


import logging
import os
import sys
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) import taskflow.engines from taskflow.patterns import linear_flow as lf from taskflow import task from taskflow.types import notifier ANY = notifier.Notifier.ANY # INTRO: In this example we create two tasks (this time as functions instead # of task subclasses as in the simple_linear.py example), each of which ~calls~ # a given ~phone~ number (provided as a function input) in a linear fashion # (one after the other). # # For a workflow which is serial this shows an extremely simple way # of structuring your tasks (the code that does the work) into a linear # sequence (the flow) and then passing the work off to an engine, with some # initial data to be ran in a reliable manner. # # This example shows a basic usage of the taskflow structures without involving # the complexity of persistence. Using the structures that taskflow provides # via tasks and flows makes it possible for you to easily at a later time # hook in a persistence layer (and then gain the functionality that offers) # when you decide the complexity of adding that layer in is 'worth it' for your # applications usage pattern (which some applications may not need). # # It **also** adds on to the simple_linear.py example by adding a set of # callback functions which the engine will call when a flow state transition # or task state transition occurs. These types of functions are useful for # updating task or flow progress, or for debugging, sending notifications to # external systems, or for other yet unknown future usage that you may create! def call_jim(context):
print("Calling jim.")
print("Context = %s" % (sorted(context.items(), key=lambda x: x[0]))) def call_joe(context):
print("Calling joe.")
print("Context = %s" % (sorted(context.items(), key=lambda x: x[0]))) def flow_watch(state, details):
print('Flow => %s' % state) def task_watch(state, details):
print('Task %s => %s' % (details.get('task_name'), state)) # Wrap your functions into a task type that knows how to treat your functions # as tasks. There was previous work done to just allow a function to be # directly passed, but in python 3.0 there is no easy way to capture an # instance method, so this wrapping approach was decided upon instead which # can attach to instance methods (if that's desired). flow = lf.Flow("Call-them") flow.add(task.FunctorTask(execute=call_jim)) flow.add(task.FunctorTask(execute=call_joe)) # Now load (but do not run) the flow using the provided initial data. engine = taskflow.engines.load(flow, store={
'context': {
"joe_number": 444,
"jim_number": 555,
} }) # This is where we attach our callback functions to the 2 different # notification objects that an engine exposes. The usage of a ANY (kleene star) # here means that we want to be notified on all state changes, if you want to # restrict to a specific state change, just register that instead. engine.notifier.register(ANY, flow_watch) engine.atom_notifier.register(ANY, task_watch) # And now run! engine.run()


Dumping a in-memory backend

NOTE:

Full source located at dump_memory_backend.


import logging
import os
import sys
logging.basicConfig(level=logging.ERROR)
self_dir = os.path.abspath(os.path.dirname(__file__))
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) sys.path.insert(0, self_dir) from taskflow import engines from taskflow.patterns import linear_flow as lf from taskflow import task # INTRO: in this example we create a dummy flow with a dummy task, and run # it using a in-memory backend and pre/post run we dump out the contents # of the in-memory backends tree structure (which can be quite useful to # look at for debugging or other analysis). class PrintTask(task.Task):
def execute(self):
print("Running '%s'" % self.name) # Make a little flow and run it... f = lf.Flow('root') for alpha in ['a', 'b', 'c']:
f.add(PrintTask(alpha)) e = engines.load(f) e.compile() e.prepare() # After prepare the storage layer + backend can now be accessed safely... backend = e.storage.backend print("----------") print("Before run") print("----------") print(backend.memory.pformat()) print("----------") e.run() print("---------") print("After run") print("---------") for path in backend.memory.ls_r(backend.memory.root_path, absolute=True):
value = backend.memory[path]
if value:
print("%s -> %s" % (path, value))
else:
print("%s" % (path))


Making phone calls

NOTE:

Full source located at simple_linear.


import logging
import os
import sys
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) import taskflow.engines from taskflow.patterns import linear_flow as lf from taskflow import task # INTRO: In this example we create two tasks, each of which ~calls~ a given # ~phone~ number (provided as a function input) in a linear fashion (one after # the other). For a workflow which is serial this shows a extremely simple way # of structuring your tasks (the code that does the work) into a linear # sequence (the flow) and then passing the work off to an engine, with some # initial data to be ran in a reliable manner. # # NOTE(harlowja): This example shows a basic usage of the taskflow structures # without involving the complexity of persistence. Using the structures that # taskflow provides via tasks and flows makes it possible for you to easily at # a later time hook in a persistence layer (and then gain the functionality # that offers) when you decide the complexity of adding that layer in # is 'worth it' for your application's usage pattern (which certain # applications may not need). class CallJim(task.Task):
def execute(self, jim_number, *args, **kwargs):
print("Calling jim %s." % jim_number) class CallJoe(task.Task):
def execute(self, joe_number, *args, **kwargs):
print("Calling joe %s." % joe_number) # Create your flow and associated tasks (the work to be done). flow = lf.Flow('simple-linear').add(
CallJim(),
CallJoe() ) # Now run that flow using the provided initial data (store below). taskflow.engines.run(flow, store=dict(joe_number=444,
jim_number=555))


Making phone calls (automatically reverting)

NOTE:

Full source located at reverting_linear.


import logging
import os
import sys
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) import taskflow.engines from taskflow.patterns import linear_flow as lf from taskflow import task # INTRO: In this example we create three tasks, each of which ~calls~ a given # number (provided as a function input), one of those tasks *fails* calling a # given number (the suzzie calling); this causes the workflow to enter the # reverting process, which activates the revert methods of the previous two # phone ~calls~. # # This simulated calling makes it appear like all three calls occur or all # three don't occur (transaction-like capabilities). No persistence layer is # used here so reverting and executing will *not* be tolerant of process # failure. class CallJim(task.Task):
def execute(self, jim_number, *args, **kwargs):
print("Calling jim %s." % jim_number)
def revert(self, jim_number, *args, **kwargs):
print("Calling %s and apologizing." % jim_number) class CallJoe(task.Task):
def execute(self, joe_number, *args, **kwargs):
print("Calling joe %s." % joe_number)
def revert(self, joe_number, *args, **kwargs):
print("Calling %s and apologizing." % joe_number) class CallSuzzie(task.Task):
def execute(self, suzzie_number, *args, **kwargs):
raise IOError("Suzzie not home right now.") # Create your flow and associated tasks (the work to be done). flow = lf.Flow('simple-linear').add(
CallJim(),
CallJoe(),
CallSuzzie() ) try:
# Now run that flow using the provided initial data (store below).
taskflow.engines.run(flow, store=dict(joe_number=444,
jim_number=555,
suzzie_number=666)) except Exception as e:
# NOTE(harlowja): This exception will be the exception that came out of the
# 'CallSuzzie' task instead of a different exception, this is useful since
# typically surrounding code wants to handle the original exception and not
# a wrapped or altered one.
#
# *WARNING* If this flow was multi-threaded and multiple active tasks threw
# exceptions then the above exception would be wrapped into a combined
# exception (the object has methods to iterate over the contained
# exceptions). See: exceptions.py and the class 'WrappedFailure' to look at
# how to deal with multiple tasks failing while running.
#
# You will also note that this is not a problem in this case since no
# parallelism is involved; this is ensured by the usage of a linear flow
# and the default engine type which is 'serial' vs being 'parallel'.
print("Flow failed: %s" % e)


Building a car

NOTE:

Full source located at build_a_car.


import logging
import os
import sys
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) import taskflow.engines from taskflow.patterns import graph_flow as gf from taskflow.patterns import linear_flow as lf from taskflow import task from taskflow.types import notifier ANY = notifier.Notifier.ANY import example_utils as eu # noqa # INTRO: This example shows how a graph flow and linear flow can be used # together to execute dependent & non-dependent tasks by going through the # steps required to build a simplistic car (an assembly line if you will). It # also shows how raw functions can be wrapped into a task object instead of # being forced to use the more *heavy* task base class. This is useful in # scenarios where pre-existing code has functions that you easily want to # plug-in to taskflow, without requiring a large amount of code changes. def build_frame():
return 'steel' def build_engine():
return 'honda' def build_doors():
return '2' def build_wheels():
return '4' # These just return true to indiciate success, they would in the real work # do more than just that. def install_engine(frame, engine):
return True def install_doors(frame, windows_installed, doors):
return True def install_windows(frame, doors):
return True def install_wheels(frame, engine, engine_installed, wheels):
return True def trash(**kwargs):
eu.print_wrapped("Throwing away pieces of car!") def startup(**kwargs):
# If you want to see the rollback function being activated try uncommenting
# the following line.
#
# raise ValueError("Car not verified")
return True def verify(spec, **kwargs):
# If the car is not what we ordered throw away the car (trigger reversion).
for key, value in kwargs.items():
if spec[key] != value:
raise Exception("Car doesn't match spec!")
return True # These two functions connect into the state transition notification emission # points that the engine outputs, they can be used to log state transitions # that are occurring, or they can be used to suspend the engine (or perform # other useful activities). def flow_watch(state, details):
print('Flow => %s' % state) def task_watch(state, details):
print('Task %s => %s' % (details.get('task_name'), state)) flow = lf.Flow("make-auto").add(
task.FunctorTask(startup, revert=trash, provides='ran'),
# A graph flow allows automatic dependency based ordering, the ordering
# is determined by analyzing the symbols required and provided and ordering
# execution based on a functioning order (if one exists).
gf.Flow("install-parts").add(
task.FunctorTask(build_frame, provides='frame'),
task.FunctorTask(build_engine, provides='engine'),
task.FunctorTask(build_doors, provides='doors'),
task.FunctorTask(build_wheels, provides='wheels'),
# These *_installed outputs allow for other tasks to depend on certain
# actions being performed (aka the components were installed), another
# way to do this is to link() the tasks manually instead of creating
# an 'artificial' data dependency that accomplishes the same goal the
# manual linking would result in.
task.FunctorTask(install_engine, provides='engine_installed'),
task.FunctorTask(install_doors, provides='doors_installed'),
task.FunctorTask(install_windows, provides='windows_installed'),
task.FunctorTask(install_wheels, provides='wheels_installed')),
task.FunctorTask(verify, requires=['frame',
'engine',
'doors',
'wheels',
'engine_installed',
'doors_installed',
'windows_installed',
'wheels_installed'])) # This dictionary will be provided to the tasks as a specification for what # the tasks should produce, in this example this specification will influence # what those tasks do and what output they create. Different tasks depend on # different information from this specification, all of which will be provided # automatically by the engine to those tasks. spec = {
"frame": 'steel',
"engine": 'honda',
"doors": '2',
"wheels": '4',
# These are used to compare the result product, a car without the pieces
# installed is not a car after all.
"engine_installed": True,
"doors_installed": True,
"windows_installed": True,
"wheels_installed": True, } engine = taskflow.engines.load(flow, store={'spec': spec.copy()}) # This registers all (ANY) state transitions to trigger a call to the # flow_watch function for flow state transitions, and registers the # same all (ANY) state transitions for task state transitions. engine.notifier.register(ANY, flow_watch) engine.atom_notifier.register(ANY, task_watch) eu.print_wrapped("Building a car") engine.run() # Alter the specification and ensure that the reverting logic gets triggered # since the resultant car that will be built by the build_wheels function will # build a car with 4 doors only (not 5), this will cause the verification # task to mark the car that is produced as not matching the desired spec. spec['doors'] = 5 engine = taskflow.engines.load(flow, store={'spec': spec.copy()}) engine.notifier.register(ANY, flow_watch) engine.atom_notifier.register(ANY, task_watch) eu.print_wrapped("Building a wrong car that doesn't match specification") try:
engine.run() except Exception as e:
eu.print_wrapped("Flow failed: %s" % e)


Iterating over the alphabet (using processes)

NOTE:

Full source located at alphabet_soup.


import fractions
import functools
import logging
import os
import string
import sys
import time
logging.basicConfig(level=logging.ERROR)
self_dir = os.path.abspath(os.path.dirname(__file__))
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) sys.path.insert(0, self_dir) from taskflow import engines from taskflow import exceptions from taskflow.patterns import linear_flow from taskflow import task # In this example we show how a simple linear set of tasks can be executed # using local processes (and not threads or remote workers) with minimal (if # any) modification to those tasks to make them safe to run in this mode. # # This is useful since it allows further scaling up your workflows when thread # execution starts to become a bottleneck (which it can start to be due to the # GIL in python). It also offers a intermediary scalable runner that can be # used when the scale and/or setup of remote workers is not desirable. def progress_printer(task, event_type, details):
# This callback, attached to each task will be called in the local
# process (not the child processes)...
progress = details.pop('progress')
progress = int(progress * 100.0)
print("Task '%s' reached %d%% completion" % (task.name, progress)) class AlphabetTask(task.Task):
# Second delay between each progress part.
_DELAY = 0.1
# This task will run in X main stages (each with a different progress
# report that will be delivered back to the running process...). The
# initial 0% and 100% are triggered automatically by the engine when
# a task is started and finished (so that's why those are not emitted
# here).
_PROGRESS_PARTS = [fractions.Fraction("%s/5" % x) for x in range(1, 5)]
def execute(self):
for p in self._PROGRESS_PARTS:
self.update_progress(p)
time.sleep(self._DELAY) print("Constructing...") soup = linear_flow.Flow("alphabet-soup") for letter in string.ascii_lowercase:
abc = AlphabetTask(letter)
abc.notifier.register(task.EVENT_UPDATE_PROGRESS,
functools.partial(progress_printer, abc))
soup.add(abc) try:
print("Loading...")
e = engines.load(soup, engine='parallel', executor='processes')
print("Compiling...")
e.compile()
print("Preparing...")
e.prepare()
print("Running...")
e.run()
print("Done: %s" % e.statistics) except exceptions.NotImplementedError as e:
print(e)


Watching execution timing

NOTE:

Full source located at timing_listener.


import logging
import os
import random
import sys
import time
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) from taskflow import engines from taskflow.listeners import timing from taskflow.patterns import linear_flow as lf from taskflow import task # INTRO: in this example we will attach a listener to an engine # and have variable run time tasks run and show how the listener will print # out how long those tasks took (when they started and when they finished). # # This shows how timing metrics can be gathered (or attached onto an engine) # after a workflow has been constructed, making it easy to gather metrics # dynamically for situations where this kind of information is applicable (or # even adding this information on at a later point in the future when your # application starts to slow down). class VariableTask(task.Task):
def __init__(self, name):
super(VariableTask, self).__init__(name)
self._sleepy_time = random.random()
def execute(self):
time.sleep(self._sleepy_time) f = lf.Flow('root') f.add(VariableTask('a'), VariableTask('b'), VariableTask('c')) e = engines.load(f) with timing.PrintingDurationListener(e):
e.run()


Distance calculator

NOTE:

Full source located at distance_calculator


import collections
import math
import os
import sys
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) from taskflow import engines from taskflow.patterns import linear_flow from taskflow import task # INTRO: This shows how to use a tasks/atoms ability to take requirements from # its execute functions default parameters and shows how to provide those # via different methods when needed, to influence those parameters to in # this case calculate the distance between two points in 2D space. # A 2D point. Point = collections.namedtuple("Point", "x,y") def is_near(val, expected, tolerance=0.001):
# Floats don't really provide equality...
if val > (expected + tolerance):
return False
if val < (expected - tolerance):
return False
return True class DistanceTask(task.Task):
# See: http://en.wikipedia.org/wiki/Distance#Distance_in_Euclidean_space
default_provides = 'distance'
def execute(self, a=Point(0, 0), b=Point(0, 0)):
return math.sqrt(math.pow(b.x - a.x, 2) + math.pow(b.y - a.y, 2)) if __name__ == '__main__':
# For these we rely on the execute() methods points by default being
# at the origin (and we override it with store values when we want) at
# execution time (which then influences what is calculated).
any_distance = linear_flow.Flow("origin").add(DistanceTask())
results = engines.run(any_distance)
print(results)
print("%s is near-enough to %s: %s" % (results['distance'],
0.0,
is_near(results['distance'], 0.0)))
results = engines.run(any_distance, store={'a': Point(1, 1)})
print(results)
print("%s is near-enough to %s: %s" % (results['distance'],
1.4142,
is_near(results['distance'],
1.4142)))
results = engines.run(any_distance, store={'a': Point(10, 10)})
print(results)
print("%s is near-enough to %s: %s" % (results['distance'],
14.14199,
is_near(results['distance'],
14.14199)))
results = engines.run(any_distance,
store={'a': Point(5, 5), 'b': Point(10, 10)})
print(results)
print("%s is near-enough to %s: %s" % (results['distance'],
7.07106,
is_near(results['distance'],
7.07106)))
# For this we use the ability to override at task creation time the
# optional arguments so that we don't need to continue to send them
# in via the 'store' argument like in the above (and we fix the new
# starting point 'a' at (10, 10) instead of (0, 0)...
ten_distance = linear_flow.Flow("ten")
ten_distance.add(DistanceTask(inject={'a': Point(10, 10)}))
results = engines.run(ten_distance, store={'b': Point(10, 10)})
print(results)
print("%s is near-enough to %s: %s" % (results['distance'],
0.0,
is_near(results['distance'], 0.0)))
results = engines.run(ten_distance)
print(results)
print("%s is near-enough to %s: %s" % (results['distance'],
14.14199,
is_near(results['distance'],
14.14199)))


Table multiplier (in parallel)

NOTE:

Full source located at parallel_table_multiply


import csv
import logging
import os
import random
import sys
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) import futurist from taskflow import engines from taskflow.patterns import unordered_flow as uf from taskflow import task # INTRO: This example walks through a miniature workflow which does a parallel # table modification where each row in the table gets adjusted by a thread, or # green thread (if eventlet is available) in parallel and then the result # is reformed into a new table and some verifications are performed on it # to ensure everything went as expected. MULTIPLER = 10 class RowMultiplier(task.Task):
"""Performs a modification of an input row, creating a output row."""
def __init__(self, name, index, row, multiplier):
super(RowMultiplier, self).__init__(name=name)
self.index = index
self.multiplier = multiplier
self.row = row
def execute(self):
return [r * self.multiplier for r in self.row] def make_flow(table):
# This creation will allow for parallel computation (since the flow here
# is specifically unordered; and when things are unordered they have
# no dependencies and when things have no dependencies they can just be
# ran at the same time, limited in concurrency by the executor or max
# workers of that executor...)
f = uf.Flow("root")
for i, row in enumerate(table):
f.add(RowMultiplier("m-%s" % i, i, row, MULTIPLER))
# NOTE(harlowja): at this point nothing has ran, the above is just
# defining what should be done (but not actually doing it) and associating
# an ordering dependencies that should be enforced (the flow pattern used
# forces this), the engine in the later main() function will actually
# perform this work...
return f def main():
if len(sys.argv) == 2:
tbl = []
with open(sys.argv[1], 'rb') as fh:
reader = csv.reader(fh)
for row in reader:
tbl.append([float(r) if r else 0.0 for r in row])
else:
# Make some random table out of thin air...
tbl = []
cols = random.randint(1, 100)
rows = random.randint(1, 100)
for _i in range(0, rows):
row = []
for _j in range(0, cols):
row.append(random.random())
tbl.append(row)
# Generate the work to be done.
f = make_flow(tbl)
# Now run it (using the specified executor)...
try:
executor = futurist.GreenThreadPoolExecutor(max_workers=5)
except RuntimeError:
# No eventlet currently active, use real threads instead.
executor = futurist.ThreadPoolExecutor(max_workers=5)
try:
e = engines.load(f, engine='parallel', executor=executor)
for st in e.run_iter():
print(st)
finally:
executor.shutdown()
# Find the old rows and put them into place...
#
# TODO(harlowja): probably easier just to sort instead of search...
computed_tbl = []
for i in range(0, len(tbl)):
for t in f:
if t.index == i:
computed_tbl.append(e.storage.get(t.name))
# Do some basic validation (which causes the return code of this process
# to be different if things were not as expected...)
if len(computed_tbl) != len(tbl):
return 1
else:
return 0 if __name__ == "__main__":
sys.exit(main())


Linear equation solver (explicit dependencies)

NOTE:

Full source located at calculate_linear.


import logging
import os
import sys
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) import taskflow.engines from taskflow.patterns import linear_flow as lf from taskflow import task # INTRO: In this example a linear flow is used to group four tasks to calculate # a value. A single added task is used twice, showing how this can be done # and the twice added task takes in different bound values. In the first case # it uses default parameters ('x' and 'y') and in the second case arguments # are bound with ('z', 'd') keys from the engines internal storage mechanism. # # A multiplier task uses a binding that another task also provides, but this # example explicitly shows that 'z' parameter is bound with 'a' key # This shows that if a task depends on a key named the same as a key provided # from another task the name can be remapped to take the desired key from a # different origin. # This task provides some values from as a result of execution, this can be # useful when you want to provide values from a static set to other tasks that # depend on those values existing before those tasks can run. # # NOTE(harlowja): this usage is *depreciated* in favor of a simpler mechanism # that just provides those values on engine running by prepopulating the # storage backend before your tasks are ran (which accomplishes a similar goal # in a more uniform manner). class Provider(task.Task):
def __init__(self, name, *args, **kwargs):
super(Provider, self).__init__(name=name, **kwargs)
self._provide = args
def execute(self):
return self._provide # This task adds two input variables and returns the result. # # Note that since this task does not have a revert() function (since addition # is a stateless operation) there are no side-effects that this function needs # to undo if some later operation fails. class Adder(task.Task):
def execute(self, x, y):
return x + y # This task multiplies an input variable by a multiplier and returns the # result. # # Note that since this task does not have a revert() function (since # multiplication is a stateless operation) and there are no side-effects that # this function needs to undo if some later operation fails. class Multiplier(task.Task):
def __init__(self, name, multiplier, provides=None, rebind=None):
super(Multiplier, self).__init__(name=name, provides=provides,
rebind=rebind)
self._multiplier = multiplier
def execute(self, z):
return z * self._multiplier # Note here that the ordering is established so that the correct sequences # of operations occurs where the adding and multiplying is done according # to the expected and typical mathematical model. A graph flow could also be # used here to automatically infer & ensure the correct ordering. flow = lf.Flow('root').add(
# Provide the initial values for other tasks to depend on.
#
# x = 2, y = 3, d = 5
Provider("provide-adder", 2, 3, 5, provides=('x', 'y', 'd')),
# z = x+y = 5
Adder("add-1", provides='z'),
# a = z+d = 10
Adder("add-2", provides='a', rebind=['z', 'd']),
# Calculate 'r = a*3 = 30'
#
# Note here that the 'z' argument of the execute() function will not be
# bound to the 'z' variable provided from the above 'provider' object but
# instead the 'z' argument will be taken from the 'a' variable provided
# by the second add-2 listed above.
Multiplier("multi", 3, provides='r', rebind={'z': 'a'}) ) # The result here will be all results (from all tasks) which is stored in an # in-memory storage location that backs this engine since it is not configured # with persistence storage. results = taskflow.engines.run(flow) print(results)


Linear equation solver (inferred dependencies)

Source: graph_flow.py

import logging
import os
import sys
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) import taskflow.engines from taskflow.patterns import graph_flow as gf from taskflow.patterns import linear_flow as lf from taskflow import task # In this example there are complex *inferred* dependencies between tasks that # are used to perform a simple set of linear equations. # # As you will see below the tasks just define what they require as input # and produce as output (named values). Then the user doesn't care about # ordering the tasks (in this case the tasks calculate pieces of the overall # equation). # # As you will notice a graph flow resolves dependencies automatically using the # tasks symbol requirements and provided symbol values and no orderin # dependency has to be manually created. # # Also notice that flows of any types can be nested into a graph flow; showing # that subflow dependencies (and associated ordering) will be inferred too. class Adder(task.Task):
def execute(self, x, y):
return x + y flow = gf.Flow('root').add(
lf.Flow('nested_linear').add(
# x2 = y3+y4 = 12
Adder("add2", provides='x2', rebind=['y3', 'y4']),
# x1 = y1+y2 = 4
Adder("add1", provides='x1', rebind=['y1', 'y2'])
),
# x5 = x1+x3 = 20
Adder("add5", provides='x5', rebind=['x1', 'x3']),
# x3 = x1+x2 = 16
Adder("add3", provides='x3', rebind=['x1', 'x2']),
# x4 = x2+y5 = 21
Adder("add4", provides='x4', rebind=['x2', 'y5']),
# x6 = x5+x4 = 41
Adder("add6", provides='x6', rebind=['x5', 'x4']),
# x7 = x6+x6 = 82
Adder("add7", provides='x7', rebind=['x6', 'x6'])) # Provide the initial variable inputs using a storage dictionary. store = {
"y1": 1,
"y2": 3,
"y3": 5,
"y4": 7,
"y5": 9, } # This is the expected values that should be created. unexpected = 0 expected = [
('x1', 4),
('x2', 12),
('x3', 16),
('x4', 21),
('x5', 20),
('x6', 41),
('x7', 82), ] result = taskflow.engines.run(
flow, engine='serial', store=store) print("Single threaded engine result %s" % result) for (name, value) in expected:
actual = result.get(name)
if actual != value:
sys.stderr.write("%s != %s\n" % (actual, value))
unexpected += 1 result = taskflow.engines.run(
flow, engine='parallel', store=store) print("Multi threaded engine result %s" % result) for (name, value) in expected:
actual = result.get(name)
if actual != value:
sys.stderr.write("%s != %s\n" % (actual, value))
unexpected += 1 if unexpected:
sys.exit(1)


Linear equation solver (in parallel)

NOTE:

Full source located at calculate_in_parallel


import logging
import os
import sys
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) import taskflow.engines from taskflow.patterns import linear_flow as lf from taskflow.patterns import unordered_flow as uf from taskflow import task # INTRO: These examples show how a linear flow and an unordered flow can be # used together to execute calculations in parallel and then use the # result for the next task/s. The adder task is used for all calculations # and argument bindings are used to set correct parameters for each task. # This task provides some values from as a result of execution, this can be # useful when you want to provide values from a static set to other tasks that # depend on those values existing before those tasks can run. # # NOTE(harlowja): this usage is *depreciated* in favor of a simpler mechanism # that provides those values on engine running by prepopulating the storage # backend before your tasks are ran (which accomplishes a similar goal in a # more uniform manner). class Provider(task.Task):
def __init__(self, name, *args, **kwargs):
super(Provider, self).__init__(name=name, **kwargs)
self._provide = args
def execute(self):
return self._provide # This task adds two input variables and returns the result of that addition. # # Note that since this task does not have a revert() function (since addition # is a stateless operation) there are no side-effects that this function needs # to undo if some later operation fails. class Adder(task.Task):
def execute(self, x, y):
return x + y flow = lf.Flow('root').add(
# Provide the initial values for other tasks to depend on.
#
# x1 = 2, y1 = 3, x2 = 5, x3 = 8
Provider("provide-adder", 2, 3, 5, 8,
provides=('x1', 'y1', 'x2', 'y2')),
# Note here that we define the flow that contains the 2 adders to be an
# unordered flow since the order in which these execute does not matter,
# another way to solve this would be to use a graph_flow pattern, which
# also can run in parallel (since they have no ordering dependencies).
uf.Flow('adders').add(
# Calculate 'z1 = x1+y1 = 5'
#
# Rebind here means that the execute() function x argument will be
# satisfied from a previous output named 'x1', and the y argument
# of execute() will be populated from the previous output named 'y1'
#
# The output (result of adding) will be mapped into a variable named
# 'z1' which can then be refereed to and depended on by other tasks.
Adder(name="add", provides='z1', rebind=['x1', 'y1']),
# z2 = x2+y2 = 13
Adder(name="add-2", provides='z2', rebind=['x2', 'y2']),
),
# r = z1+z2 = 18
Adder(name="sum-1", provides='r', rebind=['z1', 'z2'])) # The result here will be all results (from all tasks) which is stored in an # in-memory storage location that backs this engine since it is not configured # with persistence storage. result = taskflow.engines.run(flow, engine='parallel') print(result)


Creating a volume (in parallel)

NOTE:

Full source located at create_parallel_volume


import contextlib
import logging
import os
import random
import sys
import time
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) from oslo_utils import reflection from taskflow import engines from taskflow.listeners import printing from taskflow.patterns import unordered_flow as uf from taskflow import task # INTRO: These examples show how unordered_flow can be used to create a large # number of fake volumes in parallel (or serially, depending on a constant that # can be easily changed). @contextlib.contextmanager def show_time(name):
start = time.time()
yield
end = time.time()
print(" -- %s took %0.3f seconds" % (name, end - start)) # This affects how many volumes to create and how much time to *simulate* # passing for that volume to be created. MAX_CREATE_TIME = 3 VOLUME_COUNT = 5 # This will be used to determine if all the volumes are created in parallel # or whether the volumes are created serially (in an undefined ordered since # a unordered flow is used). Note that there is a disconnection between the # ordering and the concept of parallelism (since unordered items can still be # ran in a serial ordering). A typical use-case for offering both is to allow # for debugging using a serial approach, while when running at a larger scale # one would likely want to use the parallel approach. # # If you switch this flag from serial to parallel you can see the overall # time difference that this causes. SERIAL = False if SERIAL:
engine = 'serial' else:
engine = 'parallel' class VolumeCreator(task.Task):
def __init__(self, volume_id):
# Note here that the volume name is composed of the name of the class
# along with the volume id that is being created, since a name of a
# task uniquely identifies that task in storage it is important that
# the name be relevant and identifiable if the task is recreated for
# subsequent resumption (if applicable).
#
# UUIDs are *not* used as they can not be tied back to a previous tasks
# state on resumption (since they are unique and will vary for each
# task that is created). A name based off the volume id that is to be
# created is more easily tied back to the original task so that the
# volume create can be resumed/revert, and is much easier to use for
# audit and tracking purposes.
base_name = reflection.get_callable_name(self)
super(VolumeCreator, self).__init__(name="%s-%s" % (base_name,
volume_id))
self._volume_id = volume_id
def execute(self):
print("Making volume %s" % (self._volume_id))
time.sleep(random.random() * MAX_CREATE_TIME)
print("Finished making volume %s" % (self._volume_id)) # Assume there is no ordering dependency between volumes. flow = uf.Flow("volume-maker") for i in range(0, VOLUME_COUNT):
flow.add(VolumeCreator(volume_id="vol-%s" % (i))) # Show how much time the overall engine loading and running takes. with show_time(name=flow.name.title()):
eng = engines.load(flow, engine=engine)
# This context manager automatically adds (and automatically removes) a
# helpful set of state transition notification printing helper utilities
# that show you exactly what transitions the engine is going through
# while running the various volume create tasks.
with printing.PrintingListener(eng):
eng.run()


Summation mapper(s) and reducer (in parallel)

NOTE:

Full source located at simple_map_reduce


import logging
import os
import sys
logging.basicConfig(level=logging.ERROR)
self_dir = os.path.abspath(os.path.dirname(__file__))
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) sys.path.insert(0, self_dir) # INTRO: These examples show a simplistic map/reduce implementation where # a set of mapper(s) will sum a series of input numbers (in parallel) and # return their individual summed result. A reducer will then use those # produced values and perform a final summation and this result will then be # printed (and verified to ensure the calculation was as expected). from taskflow import engines from taskflow.patterns import linear_flow from taskflow.patterns import unordered_flow from taskflow import task class SumMapper(task.Task):
def execute(self, inputs):
# Sums some set of provided inputs.
return sum(inputs) class TotalReducer(task.Task):
def execute(self, *args, **kwargs):
# Reduces all mapped summed outputs into a single value.
total = 0
for (k, v) in kwargs.items():
# If any other kwargs was passed in, we don't want to use those
# in the calculation of the total...
if k.startswith('reduction_'):
total += v
return total def chunk_iter(chunk_size, upperbound):
"""Yields back chunk size pieces from zero to upperbound - 1."""
chunk = []
for i in range(0, upperbound):
chunk.append(i)
if len(chunk) == chunk_size:
yield chunk
chunk = [] # Upper bound of numbers to sum for example purposes... UPPER_BOUND = 10000 # How many mappers we want to have. SPLIT = 10 # How big of a chunk we want to give each mapper. CHUNK_SIZE = UPPER_BOUND // SPLIT # This will be the workflow we will compose and run. w = linear_flow.Flow("root") # The mappers will run in parallel. store = {} provided = [] mappers = unordered_flow.Flow('map') for i, chunk in enumerate(chunk_iter(CHUNK_SIZE, UPPER_BOUND)):
mapper_name = 'mapper_%s' % i
# Give that mapper some information to compute.
store[mapper_name] = chunk
# The reducer uses all of the outputs of the mappers, so it needs
# to be recorded that it needs access to them (under a specific name).
provided.append("reduction_%s" % i)
mappers.add(SumMapper(name=mapper_name,
rebind={'inputs': mapper_name},
provides=provided[-1])) w.add(mappers) # The reducer will run last (after all the mappers). w.add(TotalReducer('reducer', requires=provided)) # Now go! e = engines.load(w, engine='parallel', store=store, max_workers=4) print("Running a parallel engine with options: %s" % e.options) e.run() # Now get the result the reducer created. total = e.storage.get('reducer') print("Calculated result = %s" % total) # Calculate it manually to verify that it worked... calc_total = sum(range(0, UPPER_BOUND)) if calc_total != total:
sys.exit(1)


Sharing a thread pool executor (in parallel)

NOTE:

Full source located at share_engine_thread


import logging
import os
import random
import sys
import time
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) import futurist from taskflow import engines from taskflow.patterns import unordered_flow as uf from taskflow import task from taskflow.utils import threading_utils as tu # INTRO: in this example we create 2 dummy flow(s) with a 2 dummy task(s), and # run it using a shared thread pool executor to show how a single executor can # be used with more than one engine (sharing the execution thread pool between # them); this allows for saving resources and reusing threads in situations # where this is benefical. class DelayedTask(task.Task):
def __init__(self, name):
super(DelayedTask, self).__init__(name=name)
self._wait_for = random.random()
def execute(self):
print("Running '%s' in thread '%s'" % (self.name, tu.get_ident()))
time.sleep(self._wait_for) f1 = uf.Flow("f1") f1.add(DelayedTask("f1-1")) f1.add(DelayedTask("f1-2")) f2 = uf.Flow("f2") f2.add(DelayedTask("f2-1")) f2.add(DelayedTask("f2-2")) # Run them all using the same futures (thread-pool based) executor... with futurist.ThreadPoolExecutor() as ex:
e1 = engines.load(f1, engine='parallel', executor=ex)
e2 = engines.load(f2, engine='parallel', executor=ex)
iters = [e1.run_iter(), e2.run_iter()]
# Iterate over a copy (so we can remove from the source list).
cloned_iters = list(iters)
while iters:
# Run a single 'step' of each iterator, forcing each engine to perform
# some work, then yield, and repeat until each iterator is consumed
# and there is no more engine work to be done.
for it in cloned_iters:
try:
next(it)
except StopIteration:
try:
iters.remove(it)
except ValueError:
pass


Storing & emitting a bill

NOTE:

Full source located at fake_billing


import json
import logging
import os
import sys
import time
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) from oslo_utils import uuidutils from taskflow import engines from taskflow.listeners import printing from taskflow.patterns import graph_flow as gf from taskflow.patterns import linear_flow as lf from taskflow import task from taskflow.utils import misc # INTRO: This example walks through a miniature workflow which simulates # the reception of an API request, creation of a database entry, driver # activation (which invokes a 'fake' webservice) and final completion. # # This example also shows how a function/object (in this class the url sending) # that occurs during driver activation can update the progress of a task # without being aware of the internals of how to do this by associating a # callback that the url sending can update as the sending progresses from 0.0% # complete to 100% complete. class DB(object):
def query(self, sql):
print("Querying with: %s" % (sql)) class UrlCaller(object):
def __init__(self):
self._send_time = 0.5
self._chunks = 25
def send(self, url, data, status_cb=None):
sleep_time = float(self._send_time) / self._chunks
for i in range(0, len(data)):
time.sleep(sleep_time)
# As we send the data, each chunk we 'fake' send will progress
# the sending progress that much further to 100%.
if status_cb:
status_cb(float(i) / len(data)) # Since engines save the output of tasks to a optional persistent storage # backend resources have to be dealt with in a slightly different manner since # resources are transient and can *not* be persisted (or serialized). For tasks # that require access to a set of resources it is a common pattern to provide # a object (in this case this object) on construction of those tasks via the # task constructor. class ResourceFetcher(object):
def __init__(self):
self._db_handle = None
self._url_handle = None
@property
def db_handle(self):
if self._db_handle is None:
self._db_handle = DB()
return self._db_handle
@property
def url_handle(self):
if self._url_handle is None:
self._url_handle = UrlCaller()
return self._url_handle class ExtractInputRequest(task.Task):
def __init__(self, resources):
super(ExtractInputRequest, self).__init__(provides="parsed_request")
self._resources = resources
def execute(self, request):
return {
'user': request.user,
'user_id': misc.as_int(request.id),
'request_id': uuidutils.generate_uuid(),
} class MakeDBEntry(task.Task):
def __init__(self, resources):
super(MakeDBEntry, self).__init__()
self._resources = resources
def execute(self, parsed_request):
db_handle = self._resources.db_handle
db_handle.query("INSERT %s INTO mydb" % (parsed_request))
def revert(self, result, parsed_request):
db_handle = self._resources.db_handle
db_handle.query("DELETE %s FROM mydb IF EXISTS" % (parsed_request)) class ActivateDriver(task.Task):
def __init__(self, resources):
super(ActivateDriver, self).__init__(provides='sent_to')
self._resources = resources
self._url = "http://blahblah.com"
def execute(self, parsed_request):
print("Sending billing data to %s" % (self._url))
url_sender = self._resources.url_handle
# Note that here we attach our update_progress function (which is a
# function that the engine also 'binds' to) to the progress function
# that the url sending helper class uses. This allows the task progress
# to be tied to the url sending progress, which is very useful for
# downstream systems to be aware of what a task is doing at any time.
url_sender.send(self._url, json.dumps(parsed_request),
status_cb=self.update_progress)
return self._url
def update_progress(self, progress, **kwargs):
# Override the parent method to also print out the status.
super(ActivateDriver, self).update_progress(progress, **kwargs)
print("%s is %0.2f%% done" % (self.name, progress * 100)) class DeclareSuccess(task.Task):
def execute(self, sent_to):
print("Done!")
print("All data processed and sent to %s" % (sent_to)) class DummyUser(object):
def __init__(self, user, id_):
self.user = user
self.id = id_ # Resources (db handles and similar) of course can *not* be persisted so we # need to make sure that we pass this resource fetcher to the tasks constructor # so that the tasks have access to any needed resources (the resources are # lazily loaded so that they are only created when they are used). resources = ResourceFetcher() flow = lf.Flow("initialize-me") # 1. First we extract the api request into a usable format. # 2. Then we go ahead and make a database entry for our request. flow.add(ExtractInputRequest(resources), MakeDBEntry(resources)) # 3. Then we activate our payment method and finally declare success. sub_flow = gf.Flow("after-initialize") sub_flow.add(ActivateDriver(resources), DeclareSuccess()) flow.add(sub_flow) # Initially populate the storage with the following request object, # prepopulating this allows the tasks that dependent on the 'request' variable # to start processing (in this case this is the ExtractInputRequest task). store = {
'request': DummyUser(user="bob", id_="1.35"), } eng = engines.load(flow, engine='serial', store=store) # This context manager automatically adds (and automatically removes) a # helpful set of state transition notification printing helper utilities # that show you exactly what transitions the engine is going through # while running the various billing related tasks. with printing.PrintingListener(eng):
eng.run()


Suspending a workflow & resuming

NOTE:

Full source located at resume_from_backend


import contextlib
import logging
import os
import sys
logging.basicConfig(level=logging.ERROR)
self_dir = os.path.abspath(os.path.dirname(__file__))
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) sys.path.insert(0, self_dir) from oslo_utils import uuidutils import taskflow.engines from taskflow.patterns import linear_flow as lf from taskflow.persistence import models from taskflow import task import example_utils as eu # noqa # INTRO: In this example linear_flow is used to group three tasks, one which # will suspend the future work the engine may do. This suspend engine is then # discarded and the workflow is reloaded from the persisted data and then the # workflow is resumed from where it was suspended. This allows you to see how # to start an engine, have a task stop the engine from doing future work (if # a multi-threaded engine is being used, then the currently active work is not # preempted) and then resume the work later. # # Usage: # # With a filesystem directory as backend # # python taskflow/examples/resume_from_backend.py # # With ZooKeeper as backend # # python taskflow/examples/resume_from_backend.py \ # zookeeper://127.0.0.1:2181/taskflow/resume_from_backend/ # UTILITY FUNCTIONS ######################################### def print_task_states(flowdetail, msg):
eu.print_wrapped(msg)
print("Flow '%s' state: %s" % (flowdetail.name, flowdetail.state))
# Sort by these so that our test validation doesn't get confused by the
# order in which the items in the flow detail can be in.
items = sorted((td.name, td.version, td.state, td.results)
for td in flowdetail)
for item in items:
print(" %s==%s: %s, result=%s" % item) def find_flow_detail(backend, lb_id, fd_id):
conn = backend.get_connection()
lb = conn.get_logbook(lb_id)
return lb.find(fd_id) # CREATE FLOW ############################################### class InterruptTask(task.Task):
def execute(self):
# DO NOT TRY THIS AT HOME
engine.suspend() class TestTask(task.Task):
def execute(self):
print('executing %s' % self)
return 'ok' def flow_factory():
return lf.Flow('resume from backend example').add(
TestTask(name='first'),
InterruptTask(name='boom'),
TestTask(name='second')) # INITIALIZE PERSISTENCE #################################### with eu.get_backend() as backend:
# Create a place where the persistence information will be stored.
book = models.LogBook("example")
flow_detail = models.FlowDetail("resume from backend example",
uuid=uuidutils.generate_uuid())
book.add(flow_detail)
with contextlib.closing(backend.get_connection()) as conn:
conn.save_logbook(book)
# CREATE AND RUN THE FLOW: FIRST ATTEMPT ####################
flow = flow_factory()
engine = taskflow.engines.load(flow, flow_detail=flow_detail,
book=book, backend=backend)
print_task_states(flow_detail, "At the beginning, there is no state")
eu.print_wrapped("Running")
engine.run()
print_task_states(flow_detail, "After running")
# RE-CREATE, RESUME, RUN ####################################
eu.print_wrapped("Resuming and running again")
# NOTE(harlowja): reload the flow detail from backend, this will allow us
# to resume the flow from its suspended state, but first we need to search
# for the right flow details in the correct logbook where things are
# stored.
#
# We could avoid re-loading the engine and just do engine.run() again, but
# this example shows how another process may unsuspend a given flow and
# start it again for situations where this is useful to-do (say the process
# running the above flow crashes).
flow2 = flow_factory()
flow_detail_2 = find_flow_detail(backend, book.uuid, flow_detail.uuid)
engine2 = taskflow.engines.load(flow2,
flow_detail=flow_detail_2,
backend=backend, book=book)
engine2.run()
print_task_states(flow_detail_2, "At the end")


Creating a virtual machine (resumable)

NOTE:

Full source located at resume_vm_boot


import contextlib
import hashlib
import logging
import os
import random
import sys
import time
logging.basicConfig(level=logging.ERROR)
self_dir = os.path.abspath(os.path.dirname(__file__))
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) sys.path.insert(0, self_dir) import futurist from oslo_utils import uuidutils from taskflow import engines from taskflow import exceptions as exc from taskflow.patterns import graph_flow as gf from taskflow.patterns import linear_flow as lf from taskflow.persistence import models from taskflow import task import example_utils as eu # noqa # INTRO: These examples show how a hierarchy of flows can be used to create a # vm in a reliable & resumable manner using taskflow + a miniature version of # what nova does while booting a vm. @contextlib.contextmanager def slow_down(how_long=0.5):
try:
yield how_long
finally:
if len(sys.argv) > 1:
# Only both to do this if user input provided.
print("** Ctrl-c me please!!! **")
time.sleep(how_long) class PrintText(task.Task):
"""Just inserts some text print outs in a workflow."""
def __init__(self, print_what, no_slow=False):
content_hash = hashlib.md5(print_what.encode('utf-8')).hexdigest()[0:8]
super(PrintText, self).__init__(name="Print: %s" % (content_hash))
self._text = print_what
self._no_slow = no_slow
def execute(self):
if self._no_slow:
eu.print_wrapped(self._text)
else:
with slow_down():
eu.print_wrapped(self._text) class DefineVMSpec(task.Task):
"""Defines a vm specification to be."""
def __init__(self, name):
super(DefineVMSpec, self).__init__(provides='vm_spec', name=name)
def execute(self):
return {
'type': 'kvm',
'disks': 2,
'vcpu': 1,
'ips': 1,
'volumes': 3,
} class LocateImages(task.Task):
"""Locates where the vm images are."""
def __init__(self, name):
super(LocateImages, self).__init__(provides='image_locations',
name=name)
def execute(self, vm_spec):
image_locations = {}
for i in range(0, vm_spec['disks']):
url = "http://www.yahoo.com/images/%s" % (i)
image_locations[url] = "/tmp/%s.img" % (i)
return image_locations class DownloadImages(task.Task):
"""Downloads all the vm images."""
def __init__(self, name):
super(DownloadImages, self).__init__(provides='download_paths',
name=name)
def execute(self, image_locations):
for src, loc in image_locations.items():
with slow_down(1):
print("Downloading from %s => %s" % (src, loc))
return sorted(image_locations.values()) class CreateNetworkTpl(task.Task):
"""Generates the network settings file to be placed in the images."""
SYSCONFIG_CONTENTS = """DEVICE=eth%s BOOTPROTO=static IPADDR=%s ONBOOT=yes"""
def __init__(self, name):
super(CreateNetworkTpl, self).__init__(provides='network_settings',
name=name)
def execute(self, ips):
settings = []
for i, ip in enumerate(ips):
settings.append(self.SYSCONFIG_CONTENTS % (i, ip))
return settings class AllocateIP(task.Task):
"""Allocates the ips for the given vm."""
def __init__(self, name):
super(AllocateIP, self).__init__(provides='ips', name=name)
def execute(self, vm_spec):
ips = []
for _i in range(0, vm_spec.get('ips', 0)):
ips.append("192.168.0.%s" % (random.randint(1, 254)))
return ips class WriteNetworkSettings(task.Task):
"""Writes all the network settings into the downloaded images."""
def execute(self, download_paths, network_settings):
for j, path in enumerate(download_paths):
with slow_down(1):
print("Mounting %s to /tmp/%s" % (path, j))
for i, setting in enumerate(network_settings):
filename = ("/tmp/etc/sysconfig/network-scripts/"
"ifcfg-eth%s" % (i))
with slow_down(1):
print("Writing to %s" % (filename))
print(setting) class BootVM(task.Task):
"""Fires off the vm boot operation."""
def execute(self, vm_spec):
print("Starting vm!")
with slow_down(1):
print("Created: %s" % (vm_spec)) class AllocateVolumes(task.Task):
"""Allocates the volumes for the vm."""
def execute(self, vm_spec):
volumes = []
for i in range(0, vm_spec['volumes']):
with slow_down(1):
volumes.append("/dev/vda%s" % (i + 1))
print("Allocated volume %s" % volumes[-1])
return volumes class FormatVolumes(task.Task):
"""Formats the volumes for the vm."""
def execute(self, volumes):
for v in volumes:
print("Formatting volume %s" % v)
with slow_down(1):
pass
print("Formatted volume %s" % v) def create_flow():
# Setup the set of things to do (mini-nova).
flow = lf.Flow("root").add(
PrintText("Starting vm creation.", no_slow=True),
lf.Flow('vm-maker').add(
# First create a specification for the final vm to-be.
DefineVMSpec("define_spec"),
# This does all the image stuff.
gf.Flow("img-maker").add(
LocateImages("locate_images"),
DownloadImages("download_images"),
),
# This does all the network stuff.
gf.Flow("net-maker").add(
AllocateIP("get_my_ips"),
CreateNetworkTpl("fetch_net_settings"),
WriteNetworkSettings("write_net_settings"),
),
# This does all the volume stuff.
gf.Flow("volume-maker").add(
AllocateVolumes("allocate_my_volumes", provides='volumes'),
FormatVolumes("volume_formatter"),
),
# Finally boot it all.
BootVM("boot-it"),
),
# Ya it worked!
PrintText("Finished vm create.", no_slow=True),
PrintText("Instance is running!", no_slow=True))
return flow eu.print_wrapped("Initializing") # Setup the persistence & resumption layer. with eu.get_backend() as backend:
# Try to find a previously passed in tracking id...
try:
book_id, flow_id = sys.argv[2].split("+", 1)
if not uuidutils.is_uuid_like(book_id):
book_id = None
if not uuidutils.is_uuid_like(flow_id):
flow_id = None
except (IndexError, ValueError):
book_id = None
flow_id = None
# Set up how we want our engine to run, serial, parallel...
try:
executor = futurist.GreenThreadPoolExecutor(max_workers=5)
except RuntimeError:
# No eventlet installed, just let the default be used instead.
executor = None
# Create/fetch a logbook that will track the workflows work.
book = None
flow_detail = None
if all([book_id, flow_id]):
# Try to find in a prior logbook and flow detail...
with contextlib.closing(backend.get_connection()) as conn:
try:
book = conn.get_logbook(book_id)
flow_detail = book.find(flow_id)
except exc.NotFound:
pass
if book is None and flow_detail is None:
book = models.LogBook("vm-boot")
with contextlib.closing(backend.get_connection()) as conn:
conn.save_logbook(book)
engine = engines.load_from_factory(create_flow,
backend=backend, book=book,
engine='parallel',
executor=executor)
print("!! Your tracking id is: '%s+%s'" % (book.uuid,
engine.storage.flow_uuid))
print("!! Please submit this on later runs for tracking purposes")
else:
# Attempt to load from a previously partially completed flow.
engine = engines.load_from_detail(flow_detail, backend=backend,
engine='parallel', executor=executor)
# Make me my vm please!
eu.print_wrapped('Running')
engine.run() # How to use. # # 1. $ python me.py "sqlite:////tmp/nova.db" # 2. ctrl-c before this finishes # 3. Find the tracking id (search for 'Your tracking id is') # 4. $ python me.py "sqlite:////tmp/cinder.db" "$tracking_id" # 5. Watch it pick up where it left off. # 6. Profit!


Creating a volume (resumable)

NOTE:

Full source located at resume_volume_create


import contextlib
import hashlib
import logging
import os
import random
import sys
import time
logging.basicConfig(level=logging.ERROR)
self_dir = os.path.abspath(os.path.dirname(__file__))
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) sys.path.insert(0, self_dir) from oslo_utils import uuidutils from taskflow import engines from taskflow.patterns import graph_flow as gf from taskflow.patterns import linear_flow as lf from taskflow.persistence import models from taskflow import task import example_utils # noqa # INTRO: These examples show how a hierarchy of flows can be used to create a # pseudo-volume in a reliable & resumable manner using taskflow + a miniature # version of what cinder does while creating a volume (very miniature). @contextlib.contextmanager def slow_down(how_long=0.5):
try:
yield how_long
finally:
print("** Ctrl-c me please!!! **")
time.sleep(how_long) def find_flow_detail(backend, book_id, flow_id):
# NOTE(harlowja): this is used to attempt to find a given logbook with
# a given id and a given flow details inside that logbook, we need this
# reference so that we can resume the correct flow (as a logbook tracks
# flows and a flow detail tracks a individual flow).
#
# Without a reference to the logbook and the flow details in that logbook
# we will not know exactly what we should resume and that would mean we
# can't resume what we don't know.
with contextlib.closing(backend.get_connection()) as conn:
lb = conn.get_logbook(book_id)
return lb.find(flow_id) class PrintText(task.Task):
def __init__(self, print_what, no_slow=False):
content_hash = hashlib.md5(print_what.encode('utf-8')).hexdigest()[0:8]
super(PrintText, self).__init__(name="Print: %s" % (content_hash))
self._text = print_what
self._no_slow = no_slow
def execute(self):
if self._no_slow:
print("-" * (len(self._text)))
print(self._text)
print("-" * (len(self._text)))
else:
with slow_down():
print("-" * (len(self._text)))
print(self._text)
print("-" * (len(self._text))) class CreateSpecForVolumes(task.Task):
def execute(self):
volumes = []
for i in range(0, random.randint(1, 10)):
volumes.append({
'type': 'disk',
'location': "/dev/vda%s" % (i + 1),
})
return volumes class PrepareVolumes(task.Task):
def execute(self, volume_specs):
for v in volume_specs:
with slow_down():
print("Dusting off your hard drive %s" % (v))
with slow_down():
print("Taking a well deserved break.")
print("Your drive %s has been certified." % (v)) # Setup the set of things to do (mini-cinder). flow = lf.Flow("root").add(
PrintText("Starting volume create", no_slow=True),
gf.Flow('maker').add(
CreateSpecForVolumes("volume_specs", provides='volume_specs'),
PrintText("I need a nap, it took me a while to build those specs."),
PrepareVolumes(),
),
PrintText("Finished volume create", no_slow=True)) # Setup the persistence & resumption layer. with example_utils.get_backend() as backend:
try:
book_id, flow_id = sys.argv[2].split("+", 1)
except (IndexError, ValueError):
book_id = None
flow_id = None
if not all([book_id, flow_id]):
# If no 'tracking id' (think a fedex or ups tracking id) is provided
# then we create one by creating a logbook (where flow details are
# stored) and creating a flow detail (where flow and task state is
# stored). The combination of these 2 objects unique ids (uuids) allows
# the users of taskflow to reassociate the workflows that were
# potentially running (and which may have partially completed) back
# with taskflow so that those workflows can be resumed (or reverted)
# after a process/thread/engine has failed in someway.
book = models.LogBook('resume-volume-create')
flow_detail = models.FlowDetail("root", uuid=uuidutils.generate_uuid())
book.add(flow_detail)
with contextlib.closing(backend.get_connection()) as conn:
conn.save_logbook(book)
print("!! Your tracking id is: '%s+%s'" % (book.uuid,
flow_detail.uuid))
print("!! Please submit this on later runs for tracking purposes")
else:
flow_detail = find_flow_detail(backend, book_id, flow_id)
# Load and run.
engine = engines.load(flow,
flow_detail=flow_detail,
backend=backend, engine='serial')
engine.run() # How to use. # # 1. $ python me.py "sqlite:////tmp/cinder.db" # 2. ctrl-c before this finishes # 3. Find the tracking id (search for 'Your tracking id is') # 4. $ python me.py "sqlite:////tmp/cinder.db" "$tracking_id" # 5. Profit!


Running engines via iteration

NOTE:

Full source located at run_by_iter


import logging
import os
import sys
logging.basicConfig(level=logging.ERROR)
self_dir = os.path.abspath(os.path.dirname(__file__))
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) sys.path.insert(0, self_dir) from taskflow import engines from taskflow.patterns import linear_flow as lf from taskflow import task # INTRO: This example shows how to run a set of engines at the same time, each # running in different engines using a single thread of control to iterate over # each engine (which causes that engine to advanced to its next state during # each iteration). class EchoTask(task.Task):
def execute(self, value):
print(value)
return chr(ord(value) + 1) def make_alphabet_flow(i):
f = lf.Flow("alphabet_%s" % (i))
start_value = 'A'
end_value = 'Z'
curr_value = start_value
while ord(curr_value) <= ord(end_value):
next_value = chr(ord(curr_value) + 1)
if curr_value != end_value:
f.add(EchoTask(name="echoer_%s" % curr_value,
rebind={'value': curr_value},
provides=next_value))
else:
f.add(EchoTask(name="echoer_%s" % curr_value,
rebind={'value': curr_value}))
curr_value = next_value
return f # Adjust this number to change how many engines/flows run at once. flow_count = 1 flows = [] for i in range(0, flow_count):
f = make_alphabet_flow(i + 1)
flows.append(make_alphabet_flow(i + 1)) engine_iters = [] for f in flows:
e = engines.load(f)
e.compile()
e.storage.inject({'A': 'A'})
e.prepare()
engine_iters.append(e.run_iter()) while engine_iters:
for it in list(engine_iters):
try:
print(next(it))
except StopIteration:
engine_iters.remove(it)


Controlling retries using a retry controller

NOTE:

Full source located at retry_flow


import logging
import os
import sys
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) import taskflow.engines from taskflow.patterns import linear_flow as lf from taskflow import retry from taskflow import task # INTRO: In this example we create a retry controller that receives a phone # directory and tries different phone numbers. The next task tries to call Jim # using the given number. If it is not a Jim's number, the task raises an # exception and retry controller takes the next number from the phone # directory and retries the call. # # This example shows a basic usage of retry controllers in a flow. # Retry controllers allows to revert and retry a failed subflow with new # parameters. class CallJim(task.Task):
def execute(self, jim_number):
print("Calling jim %s." % jim_number)
if jim_number != 555:
raise Exception("Wrong number!")
else:
print("Hello Jim!")
def revert(self, jim_number, **kwargs):
print("Wrong number, apologizing.") # Create your flow and associated tasks (the work to be done). flow = lf.Flow('retrying-linear',
retry=retry.ParameterizedForEach(
rebind=['phone_directory'],
provides='jim_number')).add(CallJim()) # Now run that flow using the provided initial data (store below). taskflow.engines.run(flow, store={'phone_directory': [333, 444, 555, 666]})


Distributed execution (simple)

NOTE:

Full source located at wbe_simple_linear


import json
import logging
import os
import sys
import tempfile
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) from taskflow import engines from taskflow.engines.worker_based import worker from taskflow.patterns import linear_flow as lf from taskflow.tests import utils from taskflow.utils import threading_utils import example_utils # noqa # INTRO: This example walks through a miniature workflow which shows how to # start up a number of workers (these workers will process task execution and # reversion requests using any provided input data) and then use an engine # that creates a set of *capable* tasks and flows (the engine can not create # tasks that the workers are not able to run, this will end in failure) that # those workers will run and then executes that workflow seamlessly using the # workers to perform the actual execution. # # NOTE(harlowja): this example simulates the expected larger number of workers # by using a set of threads (which in this example simulate the remote workers # that would typically be running on other external machines). # A filesystem can also be used as the queue transport (useful as simple # transport type that does not involve setting up a larger mq system). If this # is false then the memory transport is used instead, both work in standalone # setups. USE_FILESYSTEM = False BASE_SHARED_CONF = {
'exchange': 'taskflow', } # Until https://github.com/celery/kombu/issues/398 is resolved it is not # recommended to run many worker threads in this example due to the types # of errors mentioned in that issue. MEMORY_WORKERS = 2 FILE_WORKERS = 1 WORKER_CONF = {
# These are the tasks the worker can execute, they *must* be importable,
# typically this list is used to restrict what workers may execute to
# a smaller set of *allowed* tasks that are known to be safe (one would
# not want to allow all python code to be executed).
'tasks': [
'taskflow.tests.utils:TaskOneArgOneReturn',
'taskflow.tests.utils:TaskMultiArgOneReturn'
], } def run(engine_options):
flow = lf.Flow('simple-linear').add(
utils.TaskOneArgOneReturn(provides='result1'),
utils.TaskMultiArgOneReturn(provides='result2')
)
eng = engines.load(flow,
store=dict(x=111, y=222, z=333),
engine='worker-based', **engine_options)
eng.run()
return eng.storage.fetch_all() if __name__ == "__main__":
logging.basicConfig(level=logging.ERROR)
# Setup our transport configuration and merge it into the worker and
# engine configuration so that both of those use it correctly.
shared_conf = dict(BASE_SHARED_CONF)
tmp_path = None
if USE_FILESYSTEM:
worker_count = FILE_WORKERS
tmp_path = tempfile.mkdtemp(prefix='wbe-example-')
shared_conf.update({
'transport': 'filesystem',
'transport_options': {
'data_folder_in': tmp_path,
'data_folder_out': tmp_path,
'polling_interval': 0.1,
},
})
else:
worker_count = MEMORY_WORKERS
shared_conf.update({
'transport': 'memory',
'transport_options': {
'polling_interval': 0.1,
},
})
worker_conf = dict(WORKER_CONF)
worker_conf.update(shared_conf)
engine_options = dict(shared_conf)
workers = []
worker_topics = []
try:
# Create a set of workers to simulate actual remote workers.
print('Running %s workers.' % (worker_count))
for i in range(0, worker_count):
worker_conf['topic'] = 'worker-%s' % (i + 1)
worker_topics.append(worker_conf['topic'])
w = worker.Worker(**worker_conf)
runner = threading_utils.daemon_thread(w.run)
runner.start()
w.wait()
workers.append((runner, w.stop))
# Now use those workers to do something.
print('Executing some work.')
engine_options['topics'] = worker_topics
result = run(engine_options)
print('Execution finished.')
# This is done so that the test examples can work correctly
# even when the keys change order (which will happen in various
# python versions).
print("Result = %s" % json.dumps(result, sort_keys=True))
finally:
# And cleanup.
print('Stopping workers.')
while workers:
r, stopper = workers.pop()
stopper()
r.join()
if tmp_path:
example_utils.rm_path(tmp_path)


Distributed notification (simple)

NOTE:

Full source located at wbe_event_sender


import logging
import os
import string
import sys
import time
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) from taskflow import engines from taskflow.engines.worker_based import worker from taskflow.patterns import linear_flow as lf from taskflow import task from taskflow.types import notifier from taskflow.utils import threading_utils ANY = notifier.Notifier.ANY # INTRO: These examples show how to use a remote worker's event notification # attribute to proxy back task event notifications to the controlling process. # # In this case a simple set of events is triggered by a worker running a # task (simulated to be remote by using a kombu memory transport and threads). # Those events that the 'remote worker' produces will then be proxied back to # the task that the engine is running 'remotely', and then they will be emitted # back to the original callbacks that exist in the originating engine # process/thread. This creates a one-way *notification* channel that can # transparently be used in-process, outside-of-process using remote workers and # so-on that allows tasks to signal to its controlling process some sort of # action that has occurred that the task may need to tell others about (for # example to trigger some type of response when the task reaches 50% done...). def event_receiver(event_type, details):
"""This is the callback that (in this example) doesn't do much..."""
print("Recieved event '%s'" % event_type)
print("Details = %s" % details) class EventReporter(task.Task):
"""This is the task that will be running 'remotely' (not really remote)."""
EVENTS = tuple(string.ascii_uppercase)
EVENT_DELAY = 0.1
def execute(self):
for i, e in enumerate(self.EVENTS):
details = {
'leftover': self.EVENTS[i:],
}
self.notifier.notify(e, details)
time.sleep(self.EVENT_DELAY) BASE_SHARED_CONF = {
'exchange': 'taskflow',
'transport': 'memory',
'transport_options': {
'polling_interval': 0.1,
}, } # Until https://github.com/celery/kombu/issues/398 is resolved it is not # recommended to run many worker threads in this example due to the types # of errors mentioned in that issue. MEMORY_WORKERS = 1 WORKER_CONF = {
'tasks': [
# Used to locate which tasks we can run (we don't want to allow
# arbitrary code/tasks to be ran by any worker since that would
# open up a variety of vulnerabilities).
'%s:EventReporter' % (__name__),
], } def run(engine_options):
reporter = EventReporter()
reporter.notifier.register(ANY, event_receiver)
flow = lf.Flow('event-reporter').add(reporter)
eng = engines.load(flow, engine='worker-based', **engine_options)
eng.run() if __name__ == "__main__":
logging.basicConfig(level=logging.ERROR)
# Setup our transport configuration and merge it into the worker and
# engine configuration so that both of those objects use it correctly.
worker_conf = dict(WORKER_CONF)
worker_conf.update(BASE_SHARED_CONF)
engine_options = dict(BASE_SHARED_CONF)
workers = []
# These topics will be used to request worker information on; those
# workers will respond with their capabilities which the executing engine
# will use to match pending tasks to a matched worker, this will cause
# the task to be sent for execution, and the engine will wait until it
# is finished (a response is received) and then the engine will either
# continue with other tasks, do some retry/failure resolution logic or
# stop (and potentially re-raise the remote workers failure)...
worker_topics = []
try:
# Create a set of worker threads to simulate actual remote workers...
print('Running %s workers.' % (MEMORY_WORKERS))
for i in range(0, MEMORY_WORKERS):
# Give each one its own unique topic name so that they can
# correctly communicate with the engine (they will all share the
# same exchange).
worker_conf['topic'] = 'worker-%s' % (i + 1)
worker_topics.append(worker_conf['topic'])
w = worker.Worker(**worker_conf)
runner = threading_utils.daemon_thread(w.run)
runner.start()
w.wait()
workers.append((runner, w.stop))
# Now use those workers to do something.
print('Executing some work.')
engine_options['topics'] = worker_topics
result = run(engine_options)
print('Execution finished.')
finally:
# And cleanup.
print('Stopping workers.')
while workers:
r, stopper = workers.pop()
stopper()
r.join()


Distributed mandelbrot (complex)

NOTE:

Full source located at wbe_mandelbrot


Output

[image: Generated mandelbrot fractal] [image]

Code

import logging
import math
import os
import sys
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) from taskflow import engines from taskflow.engines.worker_based import worker from taskflow.patterns import unordered_flow as uf from taskflow import task from taskflow.utils import threading_utils # INTRO: This example walks through a workflow that will in parallel compute # a mandelbrot result set (using X 'remote' workers) and then combine their # results together to form a final mandelbrot fractal image. It shows a usage # of taskflow to perform a well-known embarrassingly parallel problem that has # the added benefit of also being an elegant visualization. # # NOTE(harlowja): this example simulates the expected larger number of workers # by using a set of threads (which in this example simulate the remote workers # that would typically be running on other external machines). # # NOTE(harlowja): to have it produce an image run (after installing pillow): # # $ python taskflow/examples/wbe_mandelbrot.py output.png BASE_SHARED_CONF = {
'exchange': 'taskflow', } WORKERS = 2 WORKER_CONF = {
# These are the tasks the worker can execute, they *must* be importable,
# typically this list is used to restrict what workers may execute to
# a smaller set of *allowed* tasks that are known to be safe (one would
# not want to allow all python code to be executed).
'tasks': [
'%s:MandelCalculator' % (__name__),
], } ENGINE_CONF = {
'engine': 'worker-based', } # Mandelbrot & image settings... IMAGE_SIZE = (512, 512) CHUNK_COUNT = 8 MAX_ITERATIONS = 25 class MandelCalculator(task.Task):
def execute(self, image_config, mandelbrot_config, chunk):
"""Returns the number of iterations before the computation "escapes".
Given the real and imaginary parts of a complex number, determine if it
is a candidate for membership in the mandelbrot set given a fixed
number of iterations.
"""
# Parts borrowed from (credit to mark harris and benoît mandelbrot).
#
# http://nbviewer.ipython.org/gist/harrism/f5707335f40af9463c43
def mandelbrot(x, y, max_iters):
c = complex(x, y)
z = 0.0j
for i in range(max_iters):
z = z * z + c
if (z.real * z.real + z.imag * z.imag) >= 4:
return i
return max_iters
min_x, max_x, min_y, max_y, max_iters = mandelbrot_config
height, width = image_config['size']
pixel_size_x = (max_x - min_x) / width
pixel_size_y = (max_y - min_y) / height
block = []
for y in range(chunk[0], chunk[1]):
row = []
imag = min_y + y * pixel_size_y
for x in range(0, width):
real = min_x + x * pixel_size_x
row.append(mandelbrot(real, imag, max_iters))
block.append(row)
return block def calculate(engine_conf):
# Subdivide the work into X pieces, then request each worker to calculate
# one of those chunks and then later we will write these chunks out to
# an image bitmap file.
# And unordered flow is used here since the mandelbrot calculation is an
# example of an embarrassingly parallel computation that we can scatter
# across as many workers as possible.
flow = uf.Flow("mandelbrot")
# These symbols will be automatically given to tasks as input to their
# execute method, in this case these are constants used in the mandelbrot
# calculation.
store = {
'mandelbrot_config': [-2.0, 1.0, -1.0, 1.0, MAX_ITERATIONS],
'image_config': {
'size': IMAGE_SIZE,
}
}
# We need the task names to be in the right order so that we can extract
# the final results in the right order (we don't care about the order when
# executing).
task_names = []
# Compose our workflow.
height, _width = IMAGE_SIZE
chunk_size = int(math.ceil(height / float(CHUNK_COUNT)))
for i in range(0, CHUNK_COUNT):
chunk_name = 'chunk_%s' % i
task_name = "calculation_%s" % i
# Break the calculation up into chunk size pieces.
rows = [i * chunk_size, i * chunk_size + chunk_size]
flow.add(
MandelCalculator(task_name,
# This ensures the storage symbol with name
# 'chunk_name' is sent into the tasks local
# symbol 'chunk'. This is how we give each
# calculator its own correct sequence of rows
# to work on.
rebind={'chunk': chunk_name}))
store[chunk_name] = rows
task_names.append(task_name)
# Now execute it.
eng = engines.load(flow, store=store, engine_conf=engine_conf)
eng.run()
# Gather all the results and order them for further processing.
gather = []
for name in task_names:
gather.extend(eng.storage.get(name))
points = []
for y, row in enumerate(gather):
for x, color in enumerate(row):
points.append(((x, y), color))
return points def write_image(results, output_filename=None):
print("Gathered %s results that represents a mandelbrot"
" image (using %s chunks that are computed jointly"
" by %s workers)." % (len(results), CHUNK_COUNT, WORKERS))
if not output_filename:
return
# Pillow (the PIL fork) saves us from writing our own image writer...
try:
from PIL import Image
except ImportError as e:
# To currently get this (may change in the future),
# $ pip install Pillow
raise RuntimeError("Pillow is required to write image files: %s" % e)
# Limit to 255, find the max and normalize to that...
color_max = 0
for _point, color in results:
color_max = max(color, color_max)
# Use gray scale since we don't really have other colors.
img = Image.new('L', IMAGE_SIZE, "black")
pixels = img.load()
for (x, y), color in results:
if color_max == 0:
color = 0
else:
color = int((float(color) / color_max) * 255.0)
pixels[x, y] = color
img.save(output_filename) def create_fractal():
logging.basicConfig(level=logging.ERROR)
# Setup our transport configuration and merge it into the worker and
# engine configuration so that both of those use it correctly.
shared_conf = dict(BASE_SHARED_CONF)
shared_conf.update({
'transport': 'memory',
'transport_options': {
'polling_interval': 0.1,
},
})
if len(sys.argv) >= 2:
output_filename = sys.argv[1]
else:
output_filename = None
worker_conf = dict(WORKER_CONF)
worker_conf.update(shared_conf)
engine_conf = dict(ENGINE_CONF)
engine_conf.update(shared_conf)
workers = []
worker_topics = []
print('Calculating your mandelbrot fractal of size %sx%s.' % IMAGE_SIZE)
try:
# Create a set of workers to simulate actual remote workers.
print('Running %s workers.' % (WORKERS))
for i in range(0, WORKERS):
worker_conf['topic'] = 'calculator_%s' % (i + 1)
worker_topics.append(worker_conf['topic'])
w = worker.Worker(**worker_conf)
runner = threading_utils.daemon_thread(w.run)
runner.start()
w.wait()
workers.append((runner, w.stop))
# Now use those workers to do something.
engine_conf['topics'] = worker_topics
results = calculate(engine_conf)
print('Execution finished.')
finally:
# And cleanup.
print('Stopping workers.')
while workers:
r, stopper = workers.pop()
stopper()
r.join()
print("Writing image...")
write_image(results, output_filename=output_filename) if __name__ == "__main__":
create_fractal()


Jobboard producer/consumer (simple)

NOTE:

Full source located at jobboard_produce_consume_colors


import collections
import contextlib
import logging
import os
import random
import sys
import threading
import time
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) from zake import fake_client from taskflow import exceptions as excp from taskflow.jobs import backends from taskflow.utils import threading_utils # In this example we show how a jobboard can be used to post work for other # entities to work on. This example creates a set of jobs using one producer # thread (typically this would be split across many machines) and then having # other worker threads with their own jobboards select work using a given # filters [red/blue] and then perform that work (and consuming or abandoning # the job after it has been completed or failed). # Things to note: # - No persistence layer is used (or logbook), just the job details are used # to determine if a job should be selected by a worker or not. # - This example runs in a single process (this is expected to be atypical # but this example shows that it can be done if needed, for testing...) # - The iterjobs(), claim(), consume()/abandon() worker workflow. # - The post() producer workflow. SHARED_CONF = {
'path': "/taskflow/jobs",
'board': 'zookeeper', } # How many workers and producers of work will be created (as threads). PRODUCERS = 3 WORKERS = 5 # How many units of work each producer will create. PRODUCER_UNITS = 10 # How many units of work are expected to be produced (used so workers can # know when to stop running and shutdown, typically this would not be a # a value but we have to limit this example's execution time to be less than # infinity). EXPECTED_UNITS = PRODUCER_UNITS * PRODUCERS # Delay between producing/consuming more work. WORKER_DELAY, PRODUCER_DELAY = (0.5, 0.5) # To ensure threads don't trample other threads output. STDOUT_LOCK = threading.Lock() def dispatch_work(job):
# This is where the jobs contained work *would* be done
time.sleep(1.0) def safe_print(name, message, prefix=""):
with STDOUT_LOCK:
if prefix:
print("%s %s: %s" % (prefix, name, message))
else:
print("%s: %s" % (name, message)) def worker(ident, client, consumed):
# Create a personal board (using the same client so that it works in
# the same process) and start looking for jobs on the board that we want
# to perform.
name = "W-%s" % (ident)
safe_print(name, "started")
claimed_jobs = 0
consumed_jobs = 0
abandoned_jobs = 0
with backends.backend(name, SHARED_CONF.copy(), client=client) as board:
while len(consumed) != EXPECTED_UNITS:
favorite_color = random.choice(['blue', 'red'])
for job in board.iterjobs(ensure_fresh=True, only_unclaimed=True):
# See if we should even bother with it...
if job.details.get('color') != favorite_color:
continue
safe_print(name, "'%s' [attempting claim]" % (job))
try:
board.claim(job, name)
claimed_jobs += 1
safe_print(name, "'%s' [claimed]" % (job))
except (excp.NotFound, excp.UnclaimableJob):
safe_print(name, "'%s' [claim unsuccessful]" % (job))
else:
try:
dispatch_work(job)
board.consume(job, name)
safe_print(name, "'%s' [consumed]" % (job))
consumed_jobs += 1
consumed.append(job)
except Exception:
board.abandon(job, name)
abandoned_jobs += 1
safe_print(name, "'%s' [abandoned]" % (job))
time.sleep(WORKER_DELAY)
safe_print(name,
"finished (claimed %s jobs, consumed %s jobs,"
" abandoned %s jobs)" % (claimed_jobs, consumed_jobs,
abandoned_jobs), prefix=">>>") def producer(ident, client):
# Create a personal board (using the same client so that it works in
# the same process) and start posting jobs on the board that we want
# some entity to perform.
name = "P-%s" % (ident)
safe_print(name, "started")
with backends.backend(name, SHARED_CONF.copy(), client=client) as board:
for i in range(0, PRODUCER_UNITS):
job_name = "%s-%s" % (name, i)
details = {
'color': random.choice(['red', 'blue']),
}
job = board.post(job_name, book=None, details=details)
safe_print(name, "'%s' [posted]" % (job))
time.sleep(PRODUCER_DELAY)
safe_print(name, "finished", prefix=">>>") def main():
# TODO(harlowja): Hack to make eventlet work right, remove when the
# following is fixed: https://github.com/eventlet/eventlet/issues/230
from taskflow.utils import eventlet_utils as _eu # noqa
try:
import eventlet as _eventlet # noqa
except ImportError:
pass
with contextlib.closing(fake_client.FakeClient()) as c:
created = []
for i in range(0, PRODUCERS):
p = threading_utils.daemon_thread(producer, i + 1, c)
created.append(p)
p.start()
consumed = collections.deque()
for i in range(0, WORKERS):
w = threading_utils.daemon_thread(worker, i + 1, c, consumed)
created.append(w)
w.start()
while created:
t = created.pop()
t.join()
# At the end there should be nothing leftover, let's verify that.
board = backends.fetch('verifier', SHARED_CONF.copy(), client=c)
board.connect()
with contextlib.closing(board):
if board.job_count != 0 or len(consumed) != EXPECTED_UNITS:
return 1
return 0 if __name__ == "__main__":
sys.exit(main())


Conductor simulating a CI pipeline

NOTE:

Full source located at tox_conductor


import contextlib
import itertools
import logging
import os
import shutil
import socket
import sys
import tempfile
import threading
import time
logging.basicConfig(level=logging.ERROR)
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) from oslo_utils import timeutils from oslo_utils import uuidutils from zake import fake_client from taskflow.conductors import backends as conductors from taskflow import engines from taskflow.jobs import backends as boards from taskflow.patterns import linear_flow from taskflow.persistence import backends as persistence from taskflow.persistence import models from taskflow import task from taskflow.utils import threading_utils # INTRO: This examples shows how a worker/producer can post desired work (jobs) # to a jobboard and a conductor can consume that work (jobs) from that jobboard # and execute those jobs in a reliable & async manner (for example, if the # conductor were to crash then the job will be released back onto the jobboard # and another conductor can attempt to finish it, from wherever that job last # left off). # # In this example a in-memory jobboard (and in-memory storage) is created and # used that simulates how this would be done at a larger scale (it is an # example after all). # Restrict how long this example runs for... RUN_TIME = 5 REVIEW_CREATION_DELAY = 0.5 SCAN_DELAY = 0.1 NAME = "%s_%s" % (socket.getfqdn(), os.getpid()) # This won't really use zookeeper but will use a local version of it using # the zake library that mimics an actual zookeeper cluster using threads and # an in-memory data structure. JOBBOARD_CONF = {
'board': 'zookeeper://localhost?path=/taskflow/tox/jobs', } class RunReview(task.Task):
# A dummy task that clones the review and runs tox...
def _clone_review(self, review, temp_dir):
print("Cloning review '%s' into %s" % (review['id'], temp_dir))
def _run_tox(self, temp_dir):
print("Running tox in %s" % temp_dir)
def execute(self, review, temp_dir):
self._clone_review(review, temp_dir)
self._run_tox(temp_dir) class MakeTempDir(task.Task):
# A task that creates and destroys a temporary dir (on failure).
#
# It provides the location of the temporary dir for other tasks to use
# as they see fit.
default_provides = 'temp_dir'
def execute(self):
return tempfile.mkdtemp()
def revert(self, *args, **kwargs):
temp_dir = kwargs.get(task.REVERT_RESULT)
if temp_dir:
shutil.rmtree(temp_dir) class CleanResources(task.Task):
# A task that cleans up any workflow resources.
def execute(self, temp_dir):
print("Removing %s" % temp_dir)
shutil.rmtree(temp_dir) def review_iter():
"""Makes reviews (never-ending iterator/generator)."""
review_id_gen = itertools.count(0)
while True:
review_id = next(review_id_gen)
review = {
'id': review_id,
}
yield review # The reason this is at the module namespace level is important, since it must # be accessible from a conductor dispatching an engine, if it was a lambda # function for example, it would not be reimportable and the conductor would # be unable to reference it when creating the workflow to run. def create_review_workflow():
"""Factory method used to create a review workflow to run."""
f = linear_flow.Flow("tester")
f.add(
MakeTempDir(name="maker"),
RunReview(name="runner"),
CleanResources(name="cleaner")
)
return f def generate_reviewer(client, saver, name=NAME):
"""Creates a review producer thread with the given name prefix."""
real_name = "%s_reviewer" % name
no_more = threading.Event()
jb = boards.fetch(real_name, JOBBOARD_CONF,
client=client, persistence=saver)
def make_save_book(saver, review_id):
# Record what we want to happen (sometime in the future).
book = models.LogBook("book_%s" % review_id)
detail = models.FlowDetail("flow_%s" % review_id,
uuidutils.generate_uuid())
book.add(detail)
# Associate the factory method we want to be called (in the future)
# with the book, so that the conductor will be able to call into
# that factory to retrieve the workflow objects that represent the
# work.
#
# These args and kwargs *can* be used to save any specific parameters
# into the factory when it is being called to create the workflow
# objects (typically used to tell a factory how to create a unique
# workflow that represents this review).
factory_args = ()
factory_kwargs = {}
engines.save_factory_details(detail, create_review_workflow,
factory_args, factory_kwargs)
with contextlib.closing(saver.get_connection()) as conn:
conn.save_logbook(book)
return book
def run():
"""Periodically publishes 'fake' reviews to analyze."""
jb.connect()
review_generator = review_iter()
with contextlib.closing(jb):
while not no_more.is_set():
review = next(review_generator)
details = {
'store': {
'review': review,
},
}
job_name = "%s_%s" % (real_name, review['id'])
print("Posting review '%s'" % review['id'])
jb.post(job_name,
book=make_save_book(saver, review['id']),
details=details)
time.sleep(REVIEW_CREATION_DELAY)
# Return the unstarted thread, and a callback that can be used
# shutdown that thread (to avoid running forever).
return (threading_utils.daemon_thread(target=run), no_more.set) def generate_conductor(client, saver, name=NAME):
"""Creates a conductor thread with the given name prefix."""
real_name = "%s_conductor" % name
jb = boards.fetch(name, JOBBOARD_CONF,
client=client, persistence=saver)
conductor = conductors.fetch("blocking", real_name, jb,
engine='parallel', wait_timeout=SCAN_DELAY)
def run():
jb.connect()
with contextlib.closing(jb):
conductor.run()
# Return the unstarted thread, and a callback that can be used
# shutdown that thread (to avoid running forever).
return (threading_utils.daemon_thread(target=run), conductor.stop) def main():
# Need to share the same backend, so that data can be shared...
persistence_conf = {
'connection': 'memory',
}
saver = persistence.fetch(persistence_conf)
with contextlib.closing(saver.get_connection()) as conn:
# This ensures that the needed backend setup/data directories/schema
# upgrades and so on... exist before they are attempted to be used...
conn.upgrade()
fc1 = fake_client.FakeClient()
# Done like this to share the same client storage location so the correct
# zookeeper features work across clients...
fc2 = fake_client.FakeClient(storage=fc1.storage)
entities = [
generate_reviewer(fc1, saver),
generate_conductor(fc2, saver),
]
for t, stopper in entities:
t.start()
try:
watch = timeutils.StopWatch(duration=RUN_TIME)
watch.start()
while not watch.expired():
time.sleep(0.1)
finally:
for t, stopper in reversed(entities):
stopper()
t.join() if __name__ == '__main__':
main()


Conductor running 99 bottles of beer song requests

NOTE:

Full source located at 99_bottles


import contextlib
import functools
import logging
import os
import sys
import time
import traceback
from kazoo import client
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),

os.pardir,
os.pardir)) sys.path.insert(0, top_dir) from taskflow.conductors import backends as conductor_backends from taskflow import engines from taskflow.jobs import backends as job_backends from taskflow import logging as taskflow_logging from taskflow.patterns import linear_flow as lf from taskflow.persistence import backends as persistence_backends from taskflow.persistence import models from taskflow import task from oslo_utils import timeutils from oslo_utils import uuidutils # Instructions! # # 1. Install zookeeper (or change host listed below) # 2. Download this example, place in file '99_bottles.py' # 3. Run `python 99_bottles.py p` to place a song request onto the jobboard # 4. Run `python 99_bottles.py c` a few times (in different shells) # 5. On demand kill previously listed processes created in (4) and watch # the work resume on another process (and repeat) # 6. Keep enough workers alive to eventually finish the song (if desired). ME = os.getpid() ZK_HOST = "localhost:2181" JB_CONF = {
'hosts': ZK_HOST,
'board': 'zookeeper',
'path': '/taskflow/99-bottles-demo', } PERSISTENCE_URI = r"sqlite:////tmp/bottles.db" TAKE_DOWN_DELAY = 1.0 PASS_AROUND_DELAY = 3.0 HOW_MANY_BOTTLES = 99 class TakeABottleDown(task.Task):
def execute(self, bottles_left):
sys.stdout.write('Take one down, ')
sys.stdout.flush()
time.sleep(TAKE_DOWN_DELAY)
return bottles_left - 1 class PassItAround(task.Task):
def execute(self):
sys.stdout.write('pass it around, ')
sys.stdout.flush()
time.sleep(PASS_AROUND_DELAY) class Conclusion(task.Task):
def execute(self, bottles_left):
sys.stdout.write('%s bottles of beer on the wall...\n' % bottles_left)
sys.stdout.flush() def make_bottles(count):
# This is the function that will be called to generate the workflow
# and will also be called to regenerate it on resumption so that work
# can continue from where it last left off...
s = lf.Flow("bottle-song")
take_bottle = TakeABottleDown("take-bottle-%s" % count,
inject={'bottles_left': count},
provides='bottles_left')
pass_it = PassItAround("pass-%s-around" % count)
next_bottles = Conclusion("next-bottles-%s" % (count - 1))
s.add(take_bottle, pass_it, next_bottles)
for bottle in reversed(list(range(1, count))):
take_bottle = TakeABottleDown("take-bottle-%s" % bottle,
provides='bottles_left')
pass_it = PassItAround("pass-%s-around" % bottle)
next_bottles = Conclusion("next-bottles-%s" % (bottle - 1))
s.add(take_bottle, pass_it, next_bottles)
return s def run_conductor(only_run_once=False):
# This continuously runs consumers until its stopped via ctrl-c or other
# kill signal...
event_watches = {}
# This will be triggered by the conductor doing various activities
# with engines, and is quite nice to be able to see the various timing
# segments (which is useful for debugging, or watching, or figuring out
# where to optimize).
def on_conductor_event(cond, event, details):
print("Event '%s' has been received..." % event)
print("Details = %s" % details)
if event.endswith("_start"):
w = timeutils.StopWatch()
w.start()
base_event = event[0:-len("_start")]
event_watches[base_event] = w
if event.endswith("_end"):
base_event = event[0:-len("_end")]
try:
w = event_watches.pop(base_event)
w.stop()
print("It took %0.3f seconds for event '%s' to finish"
% (w.elapsed(), base_event))
except KeyError:
pass
if event == 'running_end' and only_run_once:
cond.stop()
print("Starting conductor with pid: %s" % ME)
my_name = "conductor-%s" % ME
persist_backend = persistence_backends.fetch(PERSISTENCE_URI)
with contextlib.closing(persist_backend):
with contextlib.closing(persist_backend.get_connection()) as conn:
conn.upgrade()
job_backend = job_backends.fetch(my_name, JB_CONF,
persistence=persist_backend)
job_backend.connect()
with contextlib.closing(job_backend):
cond = conductor_backends.fetch('blocking', my_name, job_backend,
persistence=persist_backend)
on_conductor_event = functools.partial(on_conductor_event, cond)
cond.notifier.register(cond.notifier.ANY, on_conductor_event)
# Run forever, and kill -9 or ctrl-c me...
try:
cond.run()
finally:
cond.stop()
cond.wait() def run_poster():
# This just posts a single job and then ends...
print("Starting poster with pid: %s" % ME)
my_name = "poster-%s" % ME
persist_backend = persistence_backends.fetch(PERSISTENCE_URI)
with contextlib.closing(persist_backend):
with contextlib.closing(persist_backend.get_connection()) as conn:
conn.upgrade()
job_backend = job_backends.fetch(my_name, JB_CONF,
persistence=persist_backend)
job_backend.connect()
with contextlib.closing(job_backend):
# Create information in the persistence backend about the
# unit of work we want to complete and the factory that
# can be called to create the tasks that the work unit needs
# to be done.
lb = models.LogBook("post-from-%s" % my_name)
fd = models.FlowDetail("song-from-%s" % my_name,
uuidutils.generate_uuid())
lb.add(fd)
with contextlib.closing(persist_backend.get_connection()) as conn:
conn.save_logbook(lb)
engines.save_factory_details(fd, make_bottles,
[HOW_MANY_BOTTLES], {},
backend=persist_backend)
# Post, and be done with it!
jb = job_backend.post("song-from-%s" % my_name, book=lb)
print("Posted: %s" % jb)
print("Goodbye...") def main_local():
# Run locally typically this is activating during unit testing when all
# the examples are made sure to still function correctly...
global TAKE_DOWN_DELAY
global PASS_AROUND_DELAY
global JB_CONF
# Make everything go much faster (so that this finishes quickly).
PASS_AROUND_DELAY = 0.01
TAKE_DOWN_DELAY = 0.01
JB_CONF['path'] = JB_CONF['path'] + "-" + uuidutils.generate_uuid()
run_poster()
run_conductor(only_run_once=True) def check_for_zookeeper(timeout=1):
sys.stderr.write("Testing for the existence of a zookeeper server...\n")
sys.stderr.write("Please wait....\n")
with contextlib.closing(client.KazooClient()) as test_client:
try:
test_client.start(timeout=timeout)
except test_client.handler.timeout_exception:
sys.stderr.write("Zookeeper is needed for running this example!\n")
traceback.print_exc()
return False
else:
test_client.stop()
return True def main():
if not check_for_zookeeper():
return
if len(sys.argv) == 1:
main_local()
elif sys.argv[1] in ('p', 'c'):
if sys.argv[-1] == "v":
logging.basicConfig(level=taskflow_logging.TRACE)
else:
logging.basicConfig(level=logging.ERROR)
if sys.argv[1] == 'p':
run_poster()
else:
run_conductor()
else:
sys.stderr.write("%s p|c (v?)\n" % os.path.basename(sys.argv[0])) if __name__ == '__main__':
main()


Miscellaneous

Exceptions

taskflow.exceptions.raise_with_cause(exc_cls, message, *args, **kwargs)
Helper to raise + chain exceptions (when able) and associate a cause.

NOTE(harlowja): Since in py3.x exceptions can be chained (due to PEP 3134) we should try to raise the desired exception with the given cause (or extract a cause from the current stack if able) so that the exception formats nicely in old and new versions of python. Since py2.x does not support exception chaining (or formatting) our root exception class has a pformat() method that can be used to get similar information instead (and this function makes sure to retain the cause in that case as well so that the pformat() method shows them).

Parameters
  • exc_cls -- the TaskFlowException class to raise.
  • message -- the text/str message that will be passed to the exceptions constructor as its first positional argument.
  • args -- any additional positional arguments to pass to the exceptions constructor.
  • kwargs -- any additional keyword arguments to pass to the exceptions constructor.



exception taskflow.exceptions.TaskFlowException(message, cause=None)
Bases: Exception

Base class for most exceptions emitted from this library.

NOTE(harlowja): in later versions of python we can likely remove the need to have a cause here as PY3+ have implemented PEP 3134 which handles chaining in a much more elegant manner.

Parameters
  • message -- the exception message, typically some string that is useful for consumers to view when debugging or analyzing failures.
  • cause -- the cause of the exception being raised, when provided this should itself be an exception instance, this is useful for creating a chain of exceptions for versions of python where this is not yet implemented/supported natively.


pformat(indent=2, indent_text=' ', show_root_class=False)
Pretty formats a taskflow exception + any connected causes.


exception taskflow.exceptions.StorageFailure(message, cause=None)
Bases: TaskFlowException

Raised when storage backends can not be read/saved/deleted.


exception taskflow.exceptions.ConductorFailure(message, cause=None)
Bases: TaskFlowException

Errors related to conducting activities.


exception taskflow.exceptions.JobFailure(message, cause=None)
Bases: TaskFlowException

Errors related to jobs or operations on jobs.


exception taskflow.exceptions.UnclaimableJob(message, cause=None)
Bases: JobFailure

Raised when a job can not be claimed.


exception taskflow.exceptions.ExecutionFailure(message, cause=None)
Bases: TaskFlowException

Errors related to engine execution.


exception taskflow.exceptions.RequestTimeout(message, cause=None)
Bases: ExecutionFailure

Raised when a worker request was not finished within allotted time.


exception taskflow.exceptions.InvalidState(message, cause=None)
Bases: ExecutionFailure

Raised when a invalid state transition is attempted while executing.


exception taskflow.exceptions.DependencyFailure(message, cause=None)
Bases: TaskFlowException

Raised when some type of dependency problem occurs.


exception taskflow.exceptions.AmbiguousDependency(message, cause=None)
Bases: DependencyFailure

Raised when some type of ambiguous dependency problem occurs.


exception taskflow.exceptions.MissingDependencies(who, requirements, cause=None, method=None)
Bases: DependencyFailure

Raised when a entity has dependencies that can not be satisfied.

Parameters
  • who -- the entity that caused the missing dependency to be triggered.
  • requirements -- the dependency which were not satisfied.


Further arguments are interpreted as for in TaskFlowException.

MESSAGE_TPL = "'%(who)s' requires %(requirements)s but no other entity produces said requirements"
Exception message template used when creating an actual message.


exception taskflow.exceptions.CompilationFailure(message, cause=None)
Bases: TaskFlowException

Raised when some type of compilation issue is found.


exception taskflow.exceptions.IncompatibleVersion(message, cause=None)
Bases: TaskFlowException

Raised when some type of version incompatibility is found.


exception taskflow.exceptions.Duplicate(message, cause=None)
Bases: TaskFlowException

Raised when a duplicate entry is found.


exception taskflow.exceptions.NotFound(message, cause=None)
Bases: TaskFlowException

Raised when some entry in some object doesn't exist.


exception taskflow.exceptions.Empty(message, cause=None)
Bases: TaskFlowException

Raised when some object is empty when it shouldn't be.


exception taskflow.exceptions.MultipleChoices(message, cause=None)
Bases: TaskFlowException

Raised when some decision can't be made due to many possible choices.


exception taskflow.exceptions.InvalidFormat(message, cause=None)
Bases: TaskFlowException

Raised when some object/entity is not in the expected format.


exception taskflow.exceptions.DisallowedAccess(message, cause=None, state=None)
Bases: TaskFlowException

Raised when storage access is not possible due to state limitations.


exception taskflow.exceptions.NotImplementedError
Bases: NotImplementedError

Exception for when some functionality really isn't implemented.

This is typically useful when the library itself needs to distinguish internal features not being made available from users features not being made available/implemented (and to avoid misinterpreting the two).


exception taskflow.exceptions.WrappedFailure(causes)
Bases: Exception

Wraps one or several failure objects.

When exception/s cannot be re-raised (for example, because the value and traceback are lost in serialization) or there are several exceptions active at the same time (due to more than one thread raising exceptions), we will wrap the corresponding failure objects into this exception class and may reraise this exception type to allow users to handle the contained failures/causes as they see fit...

See the failure class documentation for a more comprehensive set of reasons why this object may be reraised instead of the original exception.

Parameters
causes -- the Failure objects that caused this exception to be raised.

check(*exc_classes)
Check if any of exception classes caused the failure/s.
Parameters
exc_classes -- exception types/exception type names to search for.

If any of the contained failures were caused by an exception of a given type, the corresponding argument that matched is returned. If not then none is returned.



States

NOTE:

The code contains explicit checks during transitions using the models described below. These checks ensure that a transition is valid, if the transition is determined to be invalid the transitioning code will raise a InvalidState exception. This exception being triggered usually means there is some kind of bug in the code or some type of misuse/state violation is occurring, and should be reported as such.


Engine

[image: Action engine state transitions] [image]

RESUMING - Prepares flow & atoms to be resumed.

SCHEDULING - Schedules and submits atoms to be worked on.

WAITING - Wait for atoms to finish executing.

ANALYZING - Analyzes and processes result/s of atom completion.

SUCCESS - Completed successfully.

FAILURE - Completed unsuccessfully.

REVERTED - Reverting was induced and all atoms were not completed successfully.

SUSPENDED - Suspended while running.

UNDEFINED - Internal state.

GAME_OVER - Internal state.

Flow

[image: Flow state transitions] [image]

PENDING - A flow starts (or via reset()) its execution lifecycle in this state (it has no state prior to being ran by an engine, since flow(s) are just pattern(s) that define the semantics and ordering of their contents and flows gain state only when they are executed).

RUNNING - In this state the engine running a flow progresses through the flow.

SUCCESS - Transitioned to once all of the flows atoms have finished successfully.

REVERTED - Transitioned to once all of the flows atoms have been reverted successfully after a failure.

FAILURE - The engine will transition the flow to this state when it can not be reverted after a single failure or after multiple failures (greater than one failure may occur when running in parallel).

SUSPENDING - In the RUNNING state the engine running the flow can be suspended. When this happens, the engine attempts to transition the flow to the SUSPENDING state immediately. In that state the engine running the flow waits for running atoms to finish (since the engine can not preempt atoms that are actively running).

SUSPENDED - When no atoms are running and all results received so far have been saved, the engine transitions the flow from the SUSPENDING state to the SUSPENDED state.

NOTE:

The engine may transition the flow to the SUCCESS state (from the SUSPENDING state) if all atoms were in fact running (and completed) before the suspension request was able to be honored (this is due to the lack of preemption) or to the REVERTED state if the engine was reverting and all atoms were reverted while the engine was waiting for running atoms to finish or to the FAILURE state if atoms were running or reverted and some of them had failed.


RESUMING - When the engine running a flow is interrupted 'in a hard way' (e.g. server crashed), it can be loaded from storage in any state (this is required since it is can not be known what state was last successfully saved). If the loaded state is not PENDING (aka, the flow was never ran) or SUCCESS, FAILURE or REVERTED (in which case the flow has already finished), the flow gets set to the RESUMING state for the short time period while it is being loaded from backend storage [a database, a filesystem...] (this transition is not shown on the diagram). When the flow is finally loaded, it goes to the SUSPENDED state.

From the SUCCESS, FAILURE or REVERTED states the flow can be ran again; therefore it is allowable to go back into the RUNNING state immediately. One of the possible use cases for this transition is to allow for alteration of a flow or flow details associated with a previously ran flow after the flow has finished, and client code wants to ensure that each atom from this new (potentially updated) flow has its chance to run.

Task

[image: Task state transitions] [image]

PENDING - A task starts its execution lifecycle in this state (it has no state prior to being ran by an engine, since tasks(s) are just objects that represent how to accomplish a piece of work). Once it has been transitioned to the PENDING state by the engine this means it can be executed immediately or if needed will wait for all of the atoms it depends on to complete.

NOTE:

An engine running a task also transitions the task to the PENDING state after it was reverted and its containing flow was restarted or retried.


IGNORE - When a conditional decision has been made to skip (not execute) the task the engine will transition the task to the IGNORE state.

RUNNING - When an engine running the task starts to execute the task, the engine will transition the task to the RUNNING state, and the task will stay in this state until the tasks execute() method returns.

SUCCESS - The engine running the task transitions the task to this state after the task has finished successfully (ie no exception/s were raised during running its execute() method).

FAILURE - The engine running the task transitions the task to this state after it has finished with an error (ie exception/s were raised during running its execute() method).

REVERT_FAILURE - The engine running the task transitions the task to this state after it has finished with an error (ie exception/s were raised during running its revert() method).

REVERTING - The engine running a task transitions the task to this state when the containing flow the engine is running starts to revert and its revert() method is called. Only tasks in the SUCCESS or FAILURE state can be reverted. If this method fails (ie raises an exception), the task goes to the REVERT_FAILURE state.

REVERTED - The engine running the task transitions the task to this state after it has successfully reverted the task (ie no exception/s were raised during running its revert() method).

Retry

NOTE:

A retry has the same states as a task and one additional state.


[image: Retry state transitions] [image]

PENDING - A retry starts its execution lifecycle in this state (it has no state prior to being ran by an engine, since retry(s) are just objects that represent how to retry an associated flow). Once it has been transitioned to the PENDING state by the engine this means it can be executed immediately or if needed will wait for all of the atoms it depends on to complete (in the retry case the retry object will also be consulted when failures occur in the flow that the retry is associated with by consulting its on_failure() method).

NOTE:

An engine running a retry also transitions the retry to the PENDING state after it was reverted and its associated flow was restarted or retried.


IGNORE - When a conditional decision has been made to skip (not execute) the retry the engine will transition the retry to the IGNORE state.

RUNNING - When an engine starts to execute the retry, the engine transitions the retry to the RUNNING state, and the retry stays in this state until its execute() method returns.

SUCCESS - The engine running the retry transitions it to this state after it was finished successfully (ie no exception/s were raised during execution).

FAILURE - The engine running the retry transitions the retry to this state after it has finished with an error (ie exception/s were raised during running its execute() method).

REVERT_FAILURE - The engine running the retry transitions the retry to this state after it has finished with an error (ie exception/s were raised during its revert() method).

REVERTING - The engine running the retry transitions to this state when the associated flow the engine is running starts to revert it and its revert() method is called. Only retries in SUCCESS or FAILURE state can be reverted. If this method fails (ie raises an exception), the retry goes to the REVERT_FAILURE state.

REVERTED - The engine running the retry transitions the retry to this state after it has successfully reverted the retry (ie no exception/s were raised during running its revert() method).

RETRYING - If flow that is associated with the current retry was failed and reverted, the engine prepares the flow for the next run and transitions the retry to the RETRYING state.

Jobs

[image: Job state transitions] [image]

UNCLAIMED - A job (with details about what work is to be completed) has been initially posted (by some posting entity) for work on by some other entity (for example a conductor). This can also be a state that is entered when some owning entity has manually abandoned (or lost ownership of) a previously claimed job.

CLAIMED - A job that is actively owned by some entity; typically that ownership is tied to jobs persistent data via some ephemeral connection so that the job ownership is lost (typically automatically or after some timeout) if that ephemeral connection is lost.

COMPLETE - The work defined in the job has been finished by its owning entity and the job can no longer be processed (and it may be removed at some/any point in the future).

Types

NOTE:

Even though these types are made for public consumption and usage should be encouraged/easily possible it should be noted that these may be moved out to new libraries at various points in the future. If you are using these types without using the rest of this library it is strongly encouraged that you be a vocal proponent of getting these made into isolated libraries (as using these types in this manner is not the expected and/or desired usage).


Entity

class taskflow.types.entity.Entity(kind, name, metadata)
Bases: object

Entity object that identifies some resource/item/other.

Variables
  • kind -- immutable type/kind that identifies this entity (typically unique to a library/application)
  • Entity.name -- immutable name that can be used to uniquely identify this entity among many other entities
  • metadata -- immutable dictionary of metadata that is associated with this entity (and typically has keys/values that further describe this entity)



Failure

class taskflow.types.failure.Failure(exc_info=None, **kwargs)
Bases: object

An immutable object that represents failure.

Failure objects encapsulate exception information so that they can be re-used later to re-raise, inspect, examine, log, print, serialize, deserialize...

One example where they are depended upon is in the WBE engine. When a remote worker throws an exception, the WBE based engine will receive that exception and desire to reraise it to the user/caller of the WBE based engine for appropriate handling (this matches the behavior of non-remote engines). To accomplish this a failure object (or a to_dict() form) would be sent over the WBE channel and the WBE based engine would deserialize it and use this objects reraise() method to cause an exception that contains similar/equivalent information as the original exception to be reraised, allowing the user (or the WBE engine itself) to then handle the worker failure/exception as they desire.

For those who are curious, here are a few reasons why the original exception itself may not be reraised and instead a reraised wrapped failure exception object will be instead. These explanations are only applicable when a failure object is serialized and deserialized (when it is retained inside the python process that the exception was created in the the original exception can be reraised correctly without issue).

  • Traceback objects are not serializable/recreatable, since they contain references to stack frames at the location where the exception was raised. When a failure object is serialized and sent across a channel and recreated it is not possible to restore the original traceback and originating stack frames.
  • The original exception type can not be guaranteed to be found, workers can run code that is not accessible/available when the failure is being deserialized. Even if it was possible to use pickle safely it would not be possible to find the originating exception or associated code in this situation.
  • The original exception type can not be guaranteed to be constructed in a correct manner. At the time of failure object creation the exception has already been created and the failure object can not assume it has knowledge (or the ability) to recreate the original type of the captured exception (this is especially hard if the original exception was created via a complex process via some custom exception constructor).
  • The original exception type can not be guaranteed to be constructed in a safe manner. Importing foreign exception types dynamically can be problematic when not done correctly and in a safe manner; since failure objects can capture any exception it would be unsafe to try to import those exception types namespaces and modules on the receiver side dynamically (this would create similar issues as the pickle module in python has where foreign modules can be imported, causing those modules to have code ran when this happens, and this can cause issues and side-effects that the receiver would not have intended to have caused).

TODO(harlowja): use parts of 17911 and the backport at https://pypi.org/project/traceback2/ to (hopefully) simplify the methods and contents of this object...

BASE_EXCEPTIONS = ('BaseException', 'Exception')
Root exceptions of all other python exceptions.

See: https://docs.python.org/2/library/exceptions.html


SCHEMA = {'$ref': '#/definitions/cause', 'definitions': {'cause': {'additionalProperties': True, 'properties': {'causes': {'items': {'$ref': '#/definitions/cause'}, 'type': 'array'}, 'exc_args': {'minItems': 0, 'type': 'array'}, 'exc_type_names': {'items': {'type': 'string'}, 'minItems': 1, 'type': 'array'}, 'exception_str': {'type': 'string'}, 'traceback_str': {'type': 'string'}, 'version': {'minimum': 0, 'type': 'integer'}}, 'required': ['exception_str', 'traceback_str', 'exc_type_names'], 'type': 'object'}}}
Expected failure schema (in json schema format).

classmethod from_exception(exception)
Creates a failure object from a exception instance.

classmethod validate(data)
Validate input data matches expected failure dict format.

matches(other)
Checks if another object is equivalent to this object.
Returns
checks if another object is equivalent to this object
Return type
boolean


property exception
Exception value, or none if exception value is not present.

Exception value may be lost during serialization.


property exception_str
String representation of exception.

property exception_args
Tuple of arguments given to the exception constructor.

property exc_info
Exception info tuple or none.
See: https://docs.python.org/2/library/sys.html#sys.exc_info for what
the contents of this tuple are (if none, then no contents can be examined).


property traceback_str
Exception traceback as string.

static reraise_if_any(failures)
Re-raise exceptions if argument is not empty.

If argument is empty list/tuple/iterator, this method returns None. If argument is converted into a list with a single Failure object in it, that failure is reraised. Else, a WrappedFailure exception is raised with the failure list as causes.


reraise()
Re-raise captured exception.

check(*exc_classes)
Check if any of exc_classes caused the failure.

Arguments of this method can be exception types or type names (stings). If captured exception is instance of exception of given type, the corresponding argument is returned. Else, None is returned.


property causes
Tuple of all inner failure causes of this failure.

NOTE(harlowja): Does not include the current failure (only returns connected causes of this failure, if any). This property is really only useful on 3.x or newer versions of python as older versions do not have associated causes (the tuple will always be empty on 2.x versions of python).

Refer to PEP 3134 and PEP 409 and PEP 415 for what this is examining to find failure causes.


pformat(traceback=False)
Pretty formats the failure object into a string.

classmethod from_dict(data)
Converts this from a dictionary to a object.

to_dict(include_args=True)
Converts this object to a dictionary.
Parameters
include_args -- boolean indicating whether to include the exception args in the output.


copy()
Copies this object.


Graph

class taskflow.types.graph.Graph(incoming_graph_data=None, name='')
Bases: Graph

A graph subclass with useful utility functions.

freeze()
Freezes the graph so that no more mutations can occur.

export_to_dot()
Exports the graph to a dot format (requires pydot library).

pformat()
Pretty formats your graph into a string.

add_edge(u, v, attr_dict=None, **attr)
Add an edge between u and v.

add_node(n, attr_dict=None, **attr)
Add a single node n and update node attributes.

fresh_copy()
Return a fresh copy graph with the same data structure.

A fresh copy has no nodes, edges or graph attributes. It is the same data structure as the current graph. This method is typically used to create an empty version of the graph.



class taskflow.types.graph.DiGraph(incoming_graph_data=None, name='')
Bases: DiGraph

A directed graph subclass with useful utility functions.

freeze()
Freezes the graph so that no more mutations can occur.

get_edge_data(u, v, default=None)
Returns a copy of the edge attribute dictionary between (u, v).

NOTE(harlowja): this differs from the networkx get_edge_data() as that function does not return a copy (but returns a reference to the actual edge data).


topological_sort()
Return a list of nodes in this graph in topological sort order.

pformat()
Pretty formats your graph into a string.

This pretty formatted string representation includes many useful details about your graph, including; name, type, frozeness, node count, nodes, edge count, edges, graph density and graph cycles (if any).


export_to_dot()
Exports the graph to a dot format (requires pydot library).

is_directed_acyclic()
Returns if this graph is a DAG or not.

no_successors_iter()
Returns an iterator for all nodes with no successors.

no_predecessors_iter()
Returns an iterator for all nodes with no predecessors.

bfs_predecessors_iter(n)
Iterates breadth first over all predecessors of a given node.

This will go through the nodes predecessors, then the predecessor nodes predecessors and so on until no more predecessors are found.

NOTE(harlowja): predecessor cycles (if they exist) will not be iterated over more than once (this prevents infinite iteration).


add_edge(u, v, attr_dict=None, **attr)
Add an edge between u and v.

add_node(n, attr_dict=None, **attr)
Add a single node n and update node attributes.

fresh_copy()
Return a fresh copy graph with the same data structure.

A fresh copy has no nodes, edges or graph attributes. It is the same data structure as the current graph. This method is typically used to create an empty version of the graph.



class taskflow.types.graph.OrderedDiGraph(incoming_graph_data=None, name='')
Bases: DiGraph

A directed graph subclass with useful utility functions.

This derivative retains node, edge, insertion and iteration ordering (so that the iteration order matches the insertion order).

node_dict_factory
alias of OrderedDict

adjlist_outer_dict_factory
alias of OrderedDict

adjlist_inner_dict_factory
alias of OrderedDict

edge_attr_dict_factory
alias of OrderedDict

fresh_copy()
Return a fresh copy graph with the same data structure.

A fresh copy has no nodes, edges or graph attributes. It is the same data structure as the current graph. This method is typically used to create an empty version of the graph.



class taskflow.types.graph.OrderedGraph(incoming_graph_data=None, name='')
Bases: Graph

A graph subclass with useful utility functions.

This derivative retains node, edge, insertion and iteration ordering (so that the iteration order matches the insertion order).

node_dict_factory
alias of OrderedDict

adjlist_outer_dict_factory
alias of OrderedDict

adjlist_inner_dict_factory
alias of OrderedDict

edge_attr_dict_factory
alias of OrderedDict

fresh_copy()
Return a fresh copy graph with the same data structure.

A fresh copy has no nodes, edges or graph attributes. It is the same data structure as the current graph. This method is typically used to create an empty version of the graph.



taskflow.types.graph.merge_graphs(graph, *graphs, **kwargs)
Merges a bunch of graphs into a new graph.

If no additional graphs are provided the first graph is returned unmodified otherwise the merged graph is returned.


Notifier

class taskflow.types.notifier.Listener(callback, args=None, kwargs=None, details_filter=None)
Bases: object

Immutable helper that represents a notification listener/target.

property callback
Callback (can not be none) to call with event + details.

property details_filter
Callback (may be none) to call to discard events + details.

property kwargs
Dictionary of keyword arguments to use in future calls.

property args
Tuple of positional arguments to use in future calls.

__call__(event_type, details)
Activate the target callback with the given event + details.

NOTE(harlowja): if a details filter callback exists and it returns a falsey value when called with the provided details, then the target callback will not be called.


is_equivalent(callback, details_filter=None)
Check if the callback is same
Parameters
  • callback -- callback used for comparison
  • details_filter -- callback used for comparison

Returns
false if not the same callback, otherwise true
Return type
boolean



class taskflow.types.notifier.Notifier
Bases: object

A notification (pub/sub like) helper class.

It is intended to be used to subscribe to notifications of events occurring as well as allow a entity to post said notifications to any associated subscribers without having either entity care about how this notification occurs.

Not thread-safe when a single notifier is mutated at the same time by multiple threads. For example having multiple threads call into register() or reset() at the same time could potentially end badly. It is thread-safe when only notify() calls or other read-only actions (like calling into is_registered()) are occurring at the same time.

RESERVED_KEYS = ('details',)
Keys that can not be used in callbacks arguments

ANY = '*'
Kleene star constant that is used to receive all notifications

is_registered(event_type, callback, details_filter=None)
Check if a callback is registered.
Returns
checks if the callback is registered
Return type
boolean


reset()
Forget all previously registered callbacks.

notify(event_type, details)
Notify about event occurrence.

All callbacks registered to receive notifications about given event type will be called. If the provided event type can not be used to emit notifications (this is checked via the can_be_registered() method) then it will silently be dropped (notification failures are not allowed to cause or raise exceptions).

Parameters
  • event_type -- event type that occurred
  • details (dictionary) -- additional event details dictionary passed to callback keyword argument with the same name



register(event_type, callback, args=None, kwargs=None, details_filter=None)
Register a callback to be called when event of a given type occurs.

Callback will be called with provided args and kwargs and when event type occurs (or on any event if event_type equals to ANY). It will also get additional keyword argument, details, that will hold event details provided to the notify() method (if a details filter callback is provided then the target callback will only be triggered if the details filter callback returns a truthy value).

Parameters
  • event_type -- event type input
  • callback -- function callback to be registered.
  • args (list) -- non-keyworded arguments
  • kwargs (dictionary) -- key-value pair arguments



deregister(event_type, callback, details_filter=None)
Remove a single listener bound to event event_type.
Parameters
event_type -- deregister listener bound to event_type


deregister_event(event_type)
Remove a group of listeners bound to event event_type.
Parameters
event_type -- deregister listeners bound to event_type


listeners_iter()
Return an iterator over the mapping of event => listeners bound.

NOTE(harlowja): Each listener in the yielded (event, listeners) tuple is an instance of the Listener type, which itself wraps a provided callback (and its details filter callback, if any).


can_be_registered(event_type)
Checks if the event can be registered/subscribed to.

can_trigger_notification(event_type)
Checks if the event can trigger a notification.
Parameters
event_type -- event that needs to be verified
Returns
whether the event can trigger a notification
Return type
boolean



class taskflow.types.notifier.RestrictedNotifier(watchable_events, allow_any=True)
Bases: Notifier

A notification class that restricts events registered/triggered.

NOTE(harlowja): This class unlike Notifier restricts and disallows registering callbacks for event types that are not declared when constructing the notifier.

events_iter()
Returns iterator of events that can be registered/subscribed to.

NOTE(harlowja): does not include back the ANY event type as that meta-type is not a specific event but is a capture-all that does not imply the same meaning as specific event types.


can_be_registered(event_type)
Checks if the event can be registered/subscribed to.
Parameters
event_type -- event that needs to be verified
Returns
whether the event can be registered/subscribed to
Return type
boolean



taskflow.types.notifier.register_deregister(notifier, event_type, callback=None, args=None, kwargs=None, details_filter=None)
Context manager that registers a callback, then deregisters on exit.
NOTE(harlowja): if the callback is none, then this registers nothing, which
is different from the behavior of the register method which will not accept none as it is not callable...


Sets

class taskflow.types.sets.OrderedSet(iterable=None)
Bases: Set, Hashable

A read-only hashable set that retains insertion/initial ordering.

It should work in all existing places that frozenset is used.

See: https://mail.python.org/pipermail/python-ideas/2009-May/004567.html for an idea thread that may eventually (someday) result in this (or similar) code being included in the mainline python codebase (although the end result of that thread is somewhat discouraging in that regard).

copy()
Return a shallow copy of a set.

intersection(*sets)
Return the intersection of two or more sets as a new set.

(i.e. elements that are common to all of the sets.)


issuperset(other)
Report whether this set contains another set.

issubset(other)
Report whether another set contains this set.

difference(*sets)
Return the difference of two or more sets as a new set.

(i.e. all elements that are in this set but not the others.)


union(*sets)
Return the union of sets as a new set.

(i.e. all elements that are in either set.)



Timing

class taskflow.types.timing.Timeout(value, event_factory=<class 'threading.Event'>)
Bases: object

An object which represents a timeout.

This object has the ability to be interrupted before the actual timeout is reached.

property value
Immutable value of the internally used timeout.

interrupt()
Forcefully set the timeout (releases any waiters).

is_stopped()
Returns if the timeout has been interrupted.

wait()
Block current thread (up to timeout) and wait until interrupted.

reset()
Reset so that interruption (and waiting) can happen again.


taskflow.types.timing.convert_to_timeout(value=None, default_value=None, event_factory=<class 'threading.Event'>)
Converts a given value to a timeout instance (and returns it).

Does nothing if the value provided is already a timeout instance.


Tree

exception taskflow.types.tree.FrozenNode
Bases: Exception

Exception raised when a frozen node is modified.


class taskflow.types.tree.Node(item, **kwargs)
Bases: object

A n-ary node class that can be used to create tree structures.

STARTING_PREFIX = ''
Default string prefix used in pformat().

EMPTY_SPACE_SEP = ' '
Default string used to create empty space used in pformat().

HORIZONTAL_CONN = '__'
Default string used to horizontally connect a node to its parent (used in pformat().).

VERTICAL_CONN = '|'
Default string used to vertically connect a node to its parent (used in pformat()).

LINE_SEP = '\n'
Default line separator used in pformat().

add(child)
Adds a child to this node (appends to left of existing children).

NOTE(harlowja): this will also set the childs parent to be this node.


empty()
Returns if the node is a leaf node.

path_iter(include_self=True)
Yields back the path from this node to the root node.

find_first_match(matcher, only_direct=False, include_self=True)
Finds the first node that matching callback returns true.

This will search not only this node but also any children nodes (in depth first order, from right to left) and finally if nothing is matched then None is returned instead of a node object.

Parameters
  • matcher -- callback that takes one positional argument (a node) and returns true if it matches desired node or false if not.
  • only_direct -- only look at current node and its direct children (implies that this does not search using depth first).
  • include_self -- include the current node during searching.

Returns
the node that matched (or None)


find(item, only_direct=False, include_self=True)
Returns the first node for an item if it exists in this node.

This will search not only this node but also any children nodes (in depth first order, from right to left) and finally if nothing is matched then None is returned instead of a node object.

Parameters
  • item -- item to look for.
  • only_direct -- only look at current node and its direct children (implies that this does not search using depth first).
  • include_self -- include the current node during searching.

Returns
the node that matched provided item (or None)


disassociate()
Removes this node from its parent (if any).
Returns
occurrences of this node that were removed from its parent.


remove(item, only_direct=False, include_self=True)
Removes a item from this nodes children.

This will search not only this node but also any children nodes and finally if nothing is found then a value error is raised instead of the normally returned removed node object.

Parameters
  • item -- item to lookup.
  • only_direct -- only look at current node and its direct children (implies that this does not search using depth first).
  • include_self -- include the current node during searching.



pformat(stringify_node=None, linesep='\n', vertical_conn='|', horizontal_conn='__', empty_space=' ', starting_prefix='')
Formats this node + children into a nice string representation.

Example:

>>> from taskflow.types import tree
>>> yahoo = tree.Node("CEO")
>>> yahoo.add(tree.Node("Infra"))
>>> yahoo[0].add(tree.Node("Boss"))
>>> yahoo[0][0].add(tree.Node("Me"))
>>> yahoo.add(tree.Node("Mobile"))
>>> yahoo.add(tree.Node("Mail"))
>>> print(yahoo.pformat())
CEO
|__Infra
|  |__Boss
|     |__Me
|__Mobile
|__Mail



child_count(only_direct=True)
Returns how many children this node has.

This can be either only the direct children of this node or inclusive of all children nodes of this node (children of children and so-on).

NOTE(harlowja): it does not account for the current node in this count.


reverse_iter()
Iterates over the direct children of this node (left->right).

index(item)
Finds the child index of a given item, searches in added order.

dfs_iter(include_self=False, right_to_left=True)
Depth first iteration (non-recursive) over the child nodes.

bfs_iter(include_self=False, right_to_left=False)
Breadth first iteration (non-recursive) over the child nodes.

to_digraph()
Converts this node + its children into a ordered directed graph.

The graph returned will have the same structure as the this node and its children (and tree node metadata will be translated into graph node metadata).

Returns
a directed graph
Return type
taskflow.types.graph.OrderedDiGraph



Utilities

WARNING:

External usage of internal utility functions and modules should be kept to a minimum as they may be altered, refactored or moved to other locations without notice (and without the typical deprecation cycle).


Async

taskflow.utils.async_utils.make_completed_future(result)
Make and return a future completed with a given result.

taskflow.utils.banner.make_banner(what, chapters)
Makes a taskflow banner string.

For example:

>>> from taskflow.utils import banner
>>> chapters = {

'Connection details': {
'Topic': 'hello',
},
'Powered by': {
'Executor': 'parallel',
}, } >>> print(banner.make_banner('Worker', chapters))


This will output:

___    __

| |_
|ask |low v1.26.1 *Worker* Connection details:
Topic => hello Powered by:
Executor => parallel



Eventlet

taskflow.utils.eventlet_utils.check_for_eventlet(exc=None)
Check if eventlet is available and if not raise a runtime error.
Parameters
exc (exception) -- exception to raise instead of raising a runtime error


Iterators

taskflow.utils.iter_utils.fill(it, desired_len, filler=None)
Iterates over a provided iterator up to the desired length.

If the source iterator does not have enough values then the filler value is yielded until the desired length is reached.


taskflow.utils.iter_utils.count(it)
Returns how many values in the iterator (depletes the iterator).

taskflow.utils.iter_utils.generate_delays(delay, max_delay, multiplier=2)
Generator/iterator that provides back delays values.

The values it generates increments by a given multiple after each iteration (using the max delay as a upper bound). Negative values will never be generated... and it will iterate forever (ie it will never stop generating values).


taskflow.utils.iter_utils.unique_seen(its, seen_selector=None)
Yields unique values from iterator(s) (and retains order).

taskflow.utils.iter_utils.find_first_match(it, matcher, not_found_value=None)
Searches iterator for first value that matcher callback returns true.

taskflow.utils.iter_utils.while_is_not(it, stop_value)
Yields given values from iterator until stop value is passed.

This uses the is operator to determine equivalency (and not the == operator).


taskflow.utils.iter_utils.iter_forever(limit)
Yields values from iterator until a limit is reached.

if limit is negative, we iterate forever.


Kazoo

Kombu

class taskflow.utils.kombu_utils.DelayedPretty(message)
Bases: object

Wraps a message and delays prettifying it until requested.

TODO(harlowja): remove this when https://github.com/celery/kombu/pull/454/ is merged and a release is made that contains it (since that pull request is equivalent and/or better than this).


Miscellaneous

class taskflow.utils.misc.StrEnum(value)
Bases: str, Enum

An enumeration that is also a string and can be compared to strings.


class taskflow.utils.misc.StringIO(initial_value='', newline='\n')
Bases: StringIO

String buffer with some small additions.


class taskflow.utils.misc.BytesIO(initial_bytes=b'')
Bases: BytesIO

Byte buffer with some small additions.


taskflow.utils.misc.get_hostname(unknown_hostname='<unknown>')
Gets the machines hostname; if not able to returns an invalid one.

taskflow.utils.misc.match_type(obj, matchers)
Matches a given object using the given matchers list/iterable.

NOTE(harlowja): each element of the provided list/iterable must be tuple of (valid types, result).

Returns the result (the second element of the provided tuple) if a type match occurs, otherwise none if no matches are found.


taskflow.utils.misc.countdown_iter(start_at, decr=1)
Generator that decrements after each generation until <= zero.

NOTE(harlowja): we can likely remove this when we can use an itertools.count that takes a step (on py2.6 which we still support that step parameter does not exist and therefore can't be used).


taskflow.utils.misc.extract_driver_and_conf(conf, conf_key)
Common function to get a driver name and its configuration.

taskflow.utils.misc.reverse_enumerate(items)
Like reversed(enumerate(items)) but with less copying/cloning...

taskflow.utils.misc.merge_uri(uri, conf)
Merges a parsed uri into the given configuration dictionary.

Merges the username, password, hostname, port, and query parameters of a URI into the given configuration dictionary (it does not overwrite existing configuration keys if they already exist) and returns the merged configuration.

NOTE(harlowja): does not merge the path, scheme or fragment.


taskflow.utils.misc.find_subclasses(locations, base_cls, exclude_hidden=True)
Finds subclass types in the given locations.

This will examines the given locations for types which are subclasses of the base class type provided and returns the found subclasses (or fails with exceptions if this introspection can not be accomplished).

If a string is provided as one of the locations it will be imported and examined if it is a subclass of the base class. If a module is given, all of its members will be examined for attributes which are subclasses of the base class. If a type itself is given it will be examined for being a subclass of the base class.


taskflow.utils.misc.pick_first_not_none(*values)
Returns first of values that is not None (or None if all are/were).

taskflow.utils.misc.parse_uri(uri)
Parses a uri into its components.

taskflow.utils.misc.disallow_when_frozen(excp_cls)
Frozen checking/raising method decorator.

taskflow.utils.misc.clamp(value, minimum, maximum, on_clamped=None)
Clamps a value to ensure its >= minimum and <= maximum.

taskflow.utils.misc.fix_newlines(text, replacement='\n')
Fixes text that may end with wrong nl by replacing with right nl.

taskflow.utils.misc.binary_encode(text, encoding='utf-8', errors='strict')
Encodes a text string into a binary string using given encoding.

Does nothing if data is already a binary string (raises on unknown types).


taskflow.utils.misc.binary_decode(data, encoding='utf-8', errors='strict')
Decodes a binary string into a text string using given encoding.

Does nothing if data is already a text string (raises on unknown types).


taskflow.utils.misc.decode_msgpack(raw_data, root_types=(<class 'dict'>, ))
Parse raw data to get decoded object.

Decodes a msgback encoded 'blob' from a given raw data binary string and checks that the root type of that decoded object is in the allowed set of types (by default a dict should be the root type).


taskflow.utils.misc.decode_json(raw_data, root_types=(<class 'dict'>, ))
Parse raw data to get decoded object.

Decodes a JSON encoded 'blob' from a given raw data binary string and checks that the root type of that decoded object is in the allowed set of types (by default a dict should be the root type).


class taskflow.utils.misc.cachedproperty(fget=None, require_lock=True)
Bases: object

A thread-safe descriptor property that is only evaluated once.

This caching descriptor can be placed on instance methods to translate those methods into properties that will be cached in the instance (avoiding repeated attribute checking logic to do the equivalent).

NOTE(harlowja): by default the property that will be saved will be under the decorated methods name prefixed with an underscore. For example if we were to attach this descriptor to an instance method 'get_thing(self)' the cached property would be stored under '_get_thing' in the self object after the first call to 'get_thing' occurs.


taskflow.utils.misc.millis_to_datetime(milliseconds)
Converts number of milliseconds (from epoch) into a datetime object.

taskflow.utils.misc.get_version_string(obj)
Gets a object's version as a string.

Returns string representation of object's version taken from its 'version' attribute, or None if object does not have such attribute or its version is None.


taskflow.utils.misc.sequence_minus(seq1, seq2)
Calculate difference of two sequences.

Result contains the elements from first sequence that are not present in second sequence, in original order. Works even if sequence elements are not hashable.


taskflow.utils.misc.as_int(obj, quiet=False)
Converts an arbitrary value into a integer.

taskflow.utils.misc.capture_failure()
Captures the occurring exception and provides a failure object back.

This will save the current exception information and yield back a failure object for the caller to use (it will raise a runtime error if no active exception is being handled).

This is useful since in some cases the exception context can be cleared, resulting in None being attempted to be saved after an exception handler is run. This can happen when eventlet switches greenthreads or when running an exception handler, code raises and catches an exception. In both cases the exception context will be cleared.

To work around this, we save the exception state, yield a failure and then run other code.

For example:

>>> from taskflow.utils import misc
>>>
>>> def cleanup():
...     pass
...
>>>
>>> def save_failure(f):
...     print("Saving %s" % f)
...
>>>
>>> try:
...     raise IOError("Broken")
... except Exception:
...     with misc.capture_failure() as fail:
...         print("Activating cleanup")
...         cleanup()
...         save_failure(fail)
...
Activating cleanup
Saving Failure: IOError: Broken



taskflow.utils.misc.is_iterable(obj)
Tests an object to to determine whether it is iterable.

This function will test the specified object to determine whether it is iterable. String types (both str and unicode) are ignored and will return False.

Parameters
obj -- object to be tested for iterable
Returns
True if object is iterable and is not a string


taskflow.utils.misc.safe_copy_dict(obj)
Copy an existing dictionary or default to empty dict...

This will return a empty dict if given object is falsey, otherwise it will create a dict of the given object (which if provided a dictionary object will make a shallow copy of that object).


Persistence

taskflow.utils.persistence_utils.temporary_log_book(backend=None)
Creates a temporary logbook for temporary usage in the given backend.

Mainly useful for tests and other use cases where a temporary logbook is needed for a short-period of time.


taskflow.utils.persistence_utils.temporary_flow_detail(backend=None, meta=None)
Creates a temporary flow detail and logbook in the given backend.

Mainly useful for tests and other use cases where a temporary flow detail and a temporary logbook is needed for a short-period of time.


taskflow.utils.persistence_utils.create_flow_detail(flow, book=None, backend=None, meta=None)
Creates a flow detail for a flow & adds & saves it in a logbook.

This will create a flow detail for the given flow using the flow name, and add it to the provided logbook and then uses the given backend to save the logbook and then returns the created flow detail.

If no book is provided a temporary one will be created automatically (no reference to the logbook will be returned, so this should nearly always be provided or only used in situations where no logbook is needed, for example in tests). If no backend is provided then no saving will occur and the created flow detail will not be persisted even if the flow detail was added to a given (or temporarily generated) logbook.


Redis

class taskflow.utils.redis_utils.RedisClient(*args, **kwargs)
Bases: Redis

A redis client that can be closed (and raises on-usage after closed).

TODO(harlowja): if https://github.com/andymccurdy/redis-py/issues/613 ever gets resolved or merged or other then we can likely remove this.

execute_command(*args, **options)
Execute a command and return a parsed response

transaction(func, *watches, **kwargs)
Convenience method for executing the callable func as a transaction while watching all keys specified in watches. The 'func' callable should expect a single argument which is a Pipeline object.

pubsub(**kwargs)
Return a Publish/Subscribe object. With this object, you can subscribe to channels and listen for messages that get published to them.


class taskflow.utils.redis_utils.UnknownExpire(value)
Bases: IntEnum

Non-expiry (not ttls) results return from get_expiry().

See: http://redis.io/commands/ttl or http://redis.io/commands/pttl

DOES_NOT_EXPIRE = -1
The command returns -1 if the key exists but has no associated expire.

KEY_NOT_FOUND = -2
The command returns -2 if the key does not exist.


taskflow.utils.redis_utils.get_expiry(client, key, prior_version=None)
Gets an expiry for a key (using best determined ttl method).

taskflow.utils.redis_utils.apply_expiry(client, key, expiry, prior_version=None)
Applies an expiry to a key (using best determined expiry method).

taskflow.utils.redis_utils.is_server_new_enough(client, min_version, default=False, prior_version=None)
Checks if a client is attached to a new enough redis server.

Schema

taskflow.utils.schema_utils.schema_validate(data, schema)
Validates given data using provided json schema.

Threading

taskflow.utils.threading_utils.is_alive(thread)
Helper to determine if a thread is alive (handles none safely).

taskflow.utils.threading_utils.get_ident()
Return the 'thread identifier' of the current thread.

taskflow.utils.threading_utils.get_optimal_thread_count(default=2)
Try to guess optimal thread count for current system.

taskflow.utils.threading_utils.daemon_thread(target, *args, **kwargs)
Makes a daemon thread that calls the given target when started.

taskflow.utils.threading_utils.no_op(*args, **kwargs)
Function that does nothing.

class taskflow.utils.threading_utils.ThreadBundle
Bases: object

A group/bundle of threads that start/stop together.

bind(thread_factory, before_start=None, after_start=None, before_join=None, after_join=None)
Adds a thread (to-be) into this bundle (with given callbacks).
NOTE(harlowja): callbacks provided should not attempt to call
mutating methods (stop(), start(), bind() ...) on this object as that will result in dead-lock since the lock on this object is not meant to be (and is not) reentrant...


start()
Creates & starts all associated threads (that are not running).

stop()
Stops & joins all associated threads (that have been started).


Bookshelf

A useful collection of links, documents, papers, similar projects, frameworks and libraries.

NOTE:

Please feel free to submit your own additions and/or changes.


Libraries & frameworks

  • APScheduler (Python)
  • Async (Python)
  • Celery (Python)
  • Graffiti (Python)
  • JobLib (Python)
  • Luigi (Python)
  • Mesos (C/C++)
  • Papy (Python)
  • Parallel Python (Python)
  • RQ (Python)
  • Spiff (Python)
  • TBB Flow (C/C++)

Languages

  • Ani
  • Make
  • Plaid

Services

  • Cloud Dataflow
  • Mistral

Papers

Advances in Dataflow Programming Languages

  • Dataflow programming
  • Programming paradigm(s)

Release notes

CHANGES

5.2.0

  • Revert "Moves supported python runtimes from version 3.8 to 3.10"
  • Moves supported python runtimes from version 3.8 to 3.10
  • Fix doc building with Sphinx 6.0
  • Prepare taskflow for sqlalchemy2
  • Update master for stable/2023.1
  • Fix parsing of zookeeper jobboard backend options

5.1.0

  • Fix test_while_is_not with python 3.11
  • Change StrictRedis usage to Redis
  • remove unicode prefix from code
  • Adapt to new jsonschema versions
  • Replace abc.abstractproperty with property and abc.abstractmethod

5.0.0

  • Quote string representations
  • Fix formattiing of release list
  • Remove six
  • Drop python3.6/3.7 support in testing runtime

4.7.0

  • Update CI to use unversioned jobs template
  • Fix atomdetails failure column size
  • Fix unit tests

4.6.4

  • Handle invalid redis entries in RedisJobBoard
  • Fix minor typo in ActionEngine exception message
  • Use LOG.warning instead of deprecated LOG.warn

4.6.3

4.6.2

  • Replace deprecated import of ABCs from collections
  • Use custom JSONType columns

4.6.1

  • Updating for OFTC IRC network
  • Fix flowdetails meta size
  • Use unittest.mock instead of mock
  • setup.cfg: Replace dashes with underscores
  • Move flake8 as a pre-commit local target
  • Remove lower-constraints remnants

4.6.0

  • Fix deprecated Alembic function args
  • Dropping lower constraints testing
  • Use TOX_CONSTRAINTS_FILE
  • Use py3 as the default runtime for tox
  • Add Python3 wallaby unit tests
  • Update master for stable/victoria
  • ignore reno generated artifacts
  • Adding pre-commit

4.5.0

[goal] Migrate testing to ubuntu focal

4.4.0

  • Avoid endless loop on StorageFailure
  • Add sentinel redis support
  • Switch from unittest2 compat methods to Python 3.x methods

4.3.1

Make test-setup.sh compatible with mysql8

4.3.0

Stop to use the __future__ module

4.2.0

  • Switch to newer openstackdocstheme and reno versions
  • Cap jsonschema 3.2.0 as the minimal version
  • Import modules, not classes
  • Bump default tox env from py37 to py38
  • Add py38 package metadata
  • Add release notes links to doc index
  • Drop use of deprecated collections classes
  • Add Python3 victoria unit tests
  • Update master for stable/ussuri

4.1.0

Zookeeper backend SSL support

4.0.0

[ussuri][goal] Drop python 2.7 support and testing

3.8.0

  • Switch to Ussuri jobs
  • Update TaskFlow for networkx 2.x
  • Update master for stable/train
  • Fix python3.8 hmac compatibility

3.7.1

  • Use mysql LONGTEXT for atomdetails results
  • Add Python 3 Train unit tests
  • Add local bindep.txt
  • Remove unused tools/tox_install.sh

3.7.0

  • update git.openstack.org to opendev
  • Dropping the py35 testing
  • Remove debtcollector requirement
  • Update Sphinx requirement

3.6.0

  • Remove unsused tools/tox_install.sh
  • Handle collections.abc deprecations
  • Uncap jsonschema
  • OpenDev Migration Patch
  • Update master for stable/stein
  • add python 3.7 unit test job

3.4.0

  • Move test requirements out of runtime requirements
  • Change openstack-dev to openstack-discuss

3.3.1

  • Update doc/conf.py to avoid warnings with sphinx 1.8
  • Use templates for cover and lower-constraints
  • Remove the duplicated word
  • Fix a symbol error
  • Create KazooClient with taskflow logger
  • add lib-forward-testing-python3 test job
  • add python 3.6 unit test job
  • add proper pydot3 dependency
  • import zuul job settings from project-config
  • Switch to use stestr for unit test
  • Add pydot test dependency
  • Remove PyPI downloads
  • Update reno for stable/rocky
  • Update various links in docs

3.2.0

  • Remove unused link target
  • Fix code to support networkx > 1.0
  • add release notes to README.rst
  • replace http with https
  • Update links in README
  • fix tox python3 overrides
  • Drop py34 target in tox.ini
  • Uncap networkx
  • give pep8 and docs environments all of the dependencies they need
  • Trivial: update pypi url to new url
  • Fix doc build
  • Trivial: Update pypi url to new url
  • stop using tox_install.sh
  • only run doc8 as part of pep8 test job
  • standardize indentation in tox.ini
  • set default python to python3
  • don't let tox_install.sh error if there is nothing to do
  • Updated from global requirements
  • add lower-constraints job
  • Updated from global requirements
  • Fix invalid json unit test
  • Update reno for stable/queens
  • Updated from global requirements
  • Updated from global requirements
  • Updated from global requirements
  • Updated from global requirements

3.1.0

  • Updated from global requirements
  • Add doc8 to pep8 environment
  • Use doc/requirements.txt

3.0.1

3.0.0

  • Remove setting of version/release from releasenotes
  • Updated from global requirements
  • Updated from global requirements
  • Updated from global requirements
  • Remove class StopWatch from timing

2.17.0

2.16.0

  • Updated from global requirements
  • Updated from global requirements
  • Update "indentify" to "identify" in comments

2.15.0

  • Updated from global requirements
  • Remove method blather in log adapter
  • Remove kwarg timeout in executor conductor
  • Updated from global requirements
  • Avoid log warning when closing is underway (on purpose)
  • Update reno for stable/pike
  • Updated from global requirements

2.14.0

  • Updated from global requirements
  • Update URLs in documents according to document migration
  • Updated from global requirements
  • Fix process based executor task proxying-back events
  • turn on warning-is-error in doc build
  • switch from oslosphinx to openstackdocstheme
  • rearrange existing documentation into the new standard layout
  • Updated from global requirements

2.13.0

  • Updated from global requirements
  • Fix html_last_updated_fmt for Python3
  • Replace assertRaisesRegexp with assertRaisesRegex

2.12.0

  • Updated from global requirements
  • Updated from global requirements
  • do not allow redis job reclaim by same owner

2.11.0

  • Fix py35 test failure
  • Stop using oslotest.mockpatch
  • Updated from global requirements
  • python3.0 has deprecated LOG.warn

2.10.0

  • Updated from global requirements
  • Updated from global requirements
  • Updated from global requirements
  • Prepare for using standard python tests
  • Use https instead of http for git.openstack.org
  • Updated from global requirements
  • Update reno for stable/ocata
  • Protect storage better against external concurrent access

2.9.0

  • Remove dep on monotonic
  • Rename engine analyzer to be named selector
  • Update author and author-email
  • Updated from global requirements
  • Updated from global requirements
  • Add Constraints support
  • Show team and repo badges on README

2.8.0

  • Replaces uuid.uuid4 with uuidutils.generate_uuid()
  • Updated from global requirements
  • Remove vim header from source files
  • Fix release notes gate job failure
  • Updated from global requirements
  • Use assertIs(Not)None to check for None
  • Fix typo in tox.ini
  • Fix broken link
  • Replace retrying with tenacity
  • Updated from global requirements
  • Add reno for release notes management
  • Updated from global requirements

2.7.0

  • Changed the home-page link
  • Using assertIsNone() instead of assertIs(None, ..)
  • Updated from global requirements
  • Fix a typo in documentation
  • Fix typo: remove redundant 'that'
  • Updated from global requirements
  • Fix a typo in logging.py
  • Use method ensure_tree from oslo.utils
  • Make failure formatter configurable for DynamicLoggingListener
  • Updated from global requirements
  • Some classes not define __ne__() built-in function

2.6.0

2.5.0

  • Updated from global requirements
  • Add logging around metadata, ignore tallying + history

2.4.0

  • Updated from global requirements
  • Start to add a location for contributed useful tasks/flows/more
  • Change dependency to use flavors
  • Updated from global requirements
  • Remove white space between print and ()
  • Updated from global requirements
  • Add Python 3.5 classifier and venv
  • Replace assertEqual(None, *) with assertIsNone in tests

2.3.0

  • Updated from global requirements
  • remove unused LOG
  • Fixes: typo error in comments
  • Updated from global requirements
  • Fix some misspellings in the function name and descriptions
  • Updated from global requirements

2.2.0

  • Don't use deprecated method timeutils.isotime
  • Add tests to verify kwargs behavior on revert validation
  • Make tests less dependent on transient state

2.1.0

  • Updated from global requirements
  • Ensure the fetching jobs does not fetch anything when in bad state
  • Updated from global requirements
  • Use the full 'get_execute_failures' vs the shortname
  • Split revert/execute missing args messages
  • Updated from global requirements
  • Instead of a multiprocessing queue use sockets via asyncore
  • Add a simple sanity test for pydot outputting

2.0.0

  • Updated from global requirements
  • Fix documentation related to missing BaseTask class
  • Remove deprecated things for 2.0 release
  • Always used the library packaged mock

1.32.0

  • Attempt to cancel active futures when suspending is underway
  • Allow for specifying green threaded to parallel engine
  • Make conductor.stop stop the running engine gracefully

1.31.0

  • Updated from global requirements
  • Don't set html_last_updated_fmt without git
  • Updated from global requirements
  • Add the ability to skip resolving from activating
  • Fix export_to_dot for networkx package changes
  • Ensure upgrade for sqlalchemy is protected by a lock
  • Add periodic jobboard refreshing (incase of sync issues)
  • Fallback if git is absent
  • Allow for revert to have a different argument list from execute

1.30.0

  • Updated from global requirements
  • Use a automaton machine for WBE request state machine
  • Sqlalchemy-utils double entry (already in test-requirements.txt)

1.29.0

  • Updated from global requirements
  • Refactor Atom/BaseTask/Task/Retry class hierarchy
  • Add missing direct dependency for sqlalchemy-utils

1.28.0

  • Add WBE worker expiry
  • Some WBE protocol/executor cleanups
  • Remove need for separate notify thread
  • Updated from global requirements
  • Don't bother scanning for workers if no new messages arrived
  • Updated from global requirements
  • Updated from global requirements
  • Updated from global requirements
  • Allow cachedproperty to avoid locking
  • Spice up WBE banner and add simple worker __main__ entrypoint

1.27.0

  • Updated from global requirements
  • Fix for WBE sporadic timeout of tasks
  • Add some basic/initial engine statistics
  • Handle cases where exc_args can't be serialized as JSON in the WBE
  • Enable OS_LOG_CAPTURE so that logs can be seen (on error)
  • Retrieve the store from flowdetails as well, if it exists
  • Disable oslotest LOG capturing
  • Updated from global requirements
  • Updated from global requirements
  • Use helper function for post-atom-completion work
  • Ensure that the engine finishes up even under sent-in failures
  • 99 bottles example trace logging was not being output
  • Add useful/helpful comment to retry scheduler
  • Updated from global requirements
  • Updated from global requirements
  • Replace clear zookeeper python with clear zookeeper bash
  • Remove stray LOG.blather

1.26.0

  • Some additional engine logging
  • Replace deprecated library function os.popen() with subprocess
  • Add comment as to why we continue when tallying edge decider nay voters
  • Add rundimentary and/or non-optimized job priorities
  • Allow for alterations in decider 'area of influence'
  • Fix wrong usage of iter_utils.unique_seen
  • Updated from global requirements
  • Updated from global requirements
  • Updated from global requirements
  • Use the retrying lib. to do basic sqlalchemy engine validation
  • For taskflow patterns don't show taskflow.patterns prefix
  • Rename '_emit' -> '_try_emit' since it is best-effort (not ensured)
  • Cache atom name -> actions and provide accessor function
  • Quote/standardize atom name output
  • Use shared util helper for driver name + config extraction
  • Fix currently broken and inactive mysql tests
  • Trap and expose exception any 'args'
  • Revert "Remove failure version number"
  • Move all internal blather usage/calls to trace usage/calls
  • Start rename of BLATHER -> TRACE
  • Add ability of job poster/job iterator to wait for jobs to complete
  • Updated from global requirements
  • Use 'match_type' utility function instead of staticmethod
  • Remove failure version number
  • Translate kazoo exceptions into job equivalents if register_entity fails
  • Change name of misc.ensure_dict to misc.safe_copy_dict
  • Avoid recreating notify details for each dispatch iteration
  • fix doc change caused by the change of tooz
  • Deprecated tox -downloadcache option removed
  • Updated from global requirements
  • Add some useful commentary on rebinding processes
  • Use small helper routine to fetch atom metadata entries
  • Remove 'MANIFEST.in'
  • Pass through run timeout in engine run()
  • Change engine 'self._check' into a decorator

1.25.0

  • Move validation of compiled unit out of compiler
  • Allow provided flow to be empty
  • Move engine options extraction to __init__ methods
  • Updated from global requirements
  • Updated from global requirements
  • Convert executor proxied engine options into their correct type
  • Enable conversion of the tree nodes into a digraph
  • Use the misc.ensure_dict helper in conductor engine options saving
  • Add optional 'defer_reverts' behavior
  • Add public property from storage to flowdetail.meta
  • Adding notification points for job completion
  • Remove python 2.6 and cleanup tox.ini
  • Correctly apply deciders across flow boundaries
  • Move 'convert_to_timeout' to timing type as a helper function
  • Use conductor entity class constant instead of raw string
  • Add a executor backed conductor and have existing impl. use it
  • Add flow durations to DurationListener
  • Update docstrings on entity type
  • Move 'fill_iter' to 'iter_utils.fill'

1.24.0

  • Updated from global requirements
  • Updated from global requirements
  • Register conductor information on jobboard
  • Add atom priority ability
  • Add validation of base exception type(s) in failure type
  • Fix order of assertEqual for unit.test_*
  • Fix order of assertEqual for unit.worker_based
  • Fix order of assertEqual for unit.persistence
  • Fix order of assertEqual for unit.patterns
  • Fix order of assertEqual for unit.jobs
  • Fix order of assertEqual for unit.action_engine

1.23.0

  • Updated from global requirements
  • feat: add max_dispatches arg to conductor's run
  • Ensure node 'remove' and 'disassociate' can not be called when frozen
  • Add in-memory backend delete() in recursive/non-recursive modes
  • Use batch 'get_atoms_states' where we can
  • Use automaton's converters/pydot
  • Make more of the WBE logging and '__repr__' message more useful
  • Fix bad sphinx module reference
  • Relabel internal engine 'event' -> 'outcome'
  • No need for Oslo Incubator Sync
  • Use the node built-in 'dfs_iter' instead of recursion

1.22.0

  • Simplify flow action engine compilation
  • Fix 'dependened upon' spelling error
  • docs - Set pbr warnerrors option for doc build
  • Rename 'history' -> 'Release notes'
  • Remove dummy/placeholder 'ChangeLog' as its not needed
  • Remove ./taskflow/openstack/common as it no longer exists
  • Remove quotes from subshell call in bash script
  • Refactor common parts of 'get_maybe_ready_for' methods
  • Fix the sphinx build path in .gitignore file
  • Change ignore-errors to ignore_errors
  • Use graphs as the underlying structure of patterns
  • Updated from global requirements
  • Fix '_cache_get' multiple keyword argument name overlap
  • Use the sqlalchemy-utils json type instead of our own

1.21.0

  • Updated from global requirements
  • Fix how the dir persistence backend was not listing logbooks
  • Explain that jobs arch. diagram is only for zookeeper

1.20.0

  • Updated from global requirements
  • iter_nodes method added to flows
  • Updated from global requirements
  • Use 'iter_utils.count' to determine how many unfinished nodes left
  • Fix flow states link
  • Avoid running this example if zookeeper is not found
  • Updated from global requirements
  • Have the storage class provide a 'change_flow_state' method

1.19.0

  • Updated from global requirements
  • Updated from global requirements
  • Add nicely made task structural diagram
  • Updated from global requirements
  • Remove some temporary variables not needed
  • Only remove all 'next_nodes' that were done
  • Fix busted stevedore doc(s) link
  • Extend and improve failure logging
  • Improve docstrings in graph flow to denote exceptions raised
  • Enable testr OS_DEBUG to be TRACE(blather) by default
  • Updated from global requirements
  • Show intermediary compilation(s) when BLATHER is enabled

1.18.0

  • Give the GC more of a break with regard to cycles
  • Base class for deciders
  • Remove extra runner layer and just use use machine in engine
  • Updated from global requirements
  • .gitignore update
  • Avoid adding 1 to a failure (if it gets triggered)
  • Replace the tree 'pformat()' recursion with non-recursive variant
  • Fix seven typos and one readability on taskflow documentation

1.17.0

  • Bump futurist and remove waiting code in taskflow
  • Use the action engine '_check' helper method
  • Modify listeners to handle the results now possible from revert()
  • Remove no longer used '_was_failure' static method
  • Remove legacy py2.6 backwards logging compat. code
  • Updated from global requirements
  • Fix lack of space between functions
  • Create and use a serial retry executor
  • Just link to the worker engine docs instead of including a TOC inline
  • Link to run() method in engines doc
  • Add ability to reset an engine via a `reset` method

1.16.0

  • Updated from global requirements
  • Use 'addCleanup' instead of 'tearDown' in engine(s) test
  • Update 'make_client' kazoo docs and link to them
  • Remove **most** usage of taskflow.utils in examples
  • Move doc8 to being a normal test requirement in test-requirements.txt
  • Updated from global requirements
  • Found another removal_version=? that should be removal_version=2.0
  • Add deprecated module(s) for prior FSM/table code-base
  • Replace internal fsm + table with automaton library
  • Remove direct usage of timeutils overrides and use fixture

1.15.0

  • Provide a deprecated alias for the now removed stop watch class
  • Update all removal_version from being ? to being 2.0
  • Add deprecated and only alias modules for the moved types
  • Unify the zookeeper/redis jobboard iterators
  • Updated from global requirements
  • Run the '99_bottles.py' demo at a fast rate when activated
  • Use io.open vs raw open
  • Retain atom 'revert' result (or failure)
  • Update the version on the old/deprecated logbook module
  • Add docs for u, v, decider on graph flow link method
  • Fix mock calls
  • Remove setup.cfg 'requires-python' incorrect entry
  • Compile lists of retry/task atoms at runtime compile time
  • Integrate futurist (and **remove** taskflow originating code)
  • Allow the 99_bottles.py demo to run in BLATHER mode
  • Make currently implemented jobs use @functools.total_ordering
  • Add more useful `__str__` to redis job
  • Show job posted and goodbye in 99_bottles.py example
  • Rename logbook module -> models module
  • Notify on the individual engine steps

1.14.0

  • Expose strategies so doc generation can easily pick them up
  • Denote mail subject should be '[Oslo][TaskFlow]'
  • Add support for conditional execution
  • Use encodeutils for exception -> string function
  • Updated from global requirements
  • Build-out + test a redis backed jobboard

0.13.0

  • Just make the compiler object at __init__ time
  • Remove kazoo hack/fix for issue no longer needed
  • Add history.rst that uses generated 'ChangeLog' file
  • Add docstrings on runtime objects methods and link to them in docs

0.12.0

  • Updated from global requirements
  • Update states comment to refer to task section
  • Updated from global requirements
  • Remove 2.6 classifier + 2.6 compatibility code
  • Remove reference to 'requirements-pyN.txt' files
  • Add smarter/better/faster impl. of `ensure_atoms`
  • Add bulk `ensure_atoms` method to storage
  • Make it possible to see the queries executed (in BLATHER mode)
  • Add doc warning to engine components
  • Perform a few optimizations to decrease persistence interactions
  • Handle conductor ctrl-c more appropriately
  • Cache the individual atom schedulers at compile time
  • Split-off the additional retry states from the task states
  • Use the `excutils.raise_with_cause` after doing our type check
  • Updated from global requirements
  • Use monotonic lib. to avoid finding monotonic time function
  • Document more of the retry subclasses special keyword arguments

0.11.0

  • Address concurrent mutation of sqlalchemy backend
  • Add indestructible 99 bottles of beer example
  • Use alembic upgrade function/command directly
  • Updated from global requirements
  • Remove usage of deprecated 'task_notifier' property in build_car example
  • Add `simple_linear_listening` example to generated docs
  • Handy access to INFO level
  • Switch badges from 'pypip.in' to 'shields.io'
  • Adding a revert_all option to retry controllers
  • A few jobboard documentation tweaks
  • Use sphinx deprecated docstring markup
  • Use a class constant for the default path based backend path
  • Updated from global requirements
  • Remove example not tested
  • Make the default file encoding a class constant with a docstring
  • Use a lru cache to limit the size of the internal file cache
  • Updated from global requirements
  • Use hash path lookup vs path finding
  • Remove all 'lock_utils' now that fasteners provides equivalents
  • Add a new `ls_r` method
  • Updated from global requirements
  • Refactor machine builder + runner into single unit
  • Replace lock_utils lock(s) with fasteners package
  • Updated from global requirements
  • Use shared '_check' function to check engine stages
  • Remove a couple more useless 'pass' keywords found
  • Add a test that checks for task result visibility
  • Remove testing using persistence sqlalchemy backend with 'mysqldb'
  • Remove customized pyX.Y tox requirements
  • Updated from global requirements
  • Allow same deps for requires and provides in task
  • Remove 'pass' usage not needed
  • Only show state transitions to logging when in BLATHER mode
  • Fix updated_at column of sqlalchemy tables
  • Remove script already nuked from oslo-incubator
  • Ensure path_based abstract base class is included in docs
  • Beef up docs on the logbook/flow detail/atom details models
  • Remove custom py26/py27 tox venvs no longer used
  • Executors come in via options config, not keyword arguments
  • Use newer versions of futures that adds exception tracebacks
  • Ensure empty paths raise a value error
  • Remove listener stack and replace with exit stack
  • Expose action engine no reraising states constants
  • Chain a few more exception raises that were previously missed
  • Expose in memory backend split staticmethod
  • Updated from global requirements
  • Remove tox py33 environment no longer used
  • Avoid creating temporary removal lists

0.10.1

  • Avoid trying to copy tasks results when cloning/copying
  • Avoid re-normalizing paths when following links
  • Add a profiling context manager that can be easily enabled
  • Updated from global requirements

0.10.0

  • Remove validation of state on state read property access
  • Make the default path a constant and tweak class docstring
  • Avoid duplicating exception message
  • Add speed-test tools script
  • Speed up memory backend via a path -> node reverse mapping
  • Updated from global requirements
  • Fix a typo in taskflow docs
  • Small refactoring of 'merge_uri' utility function
  • Fix post coverage job option not recognized
  • Refactor/reduce shared 'ensure(task/retry)' code
  • Move implementations into there own sub-sections
  • Remove run_cross_tests.sh
  • Move zookeeper jobboard constants to class level
  • Retain chain of missing dependencies
  • Expose fake filesystem 'join' and 'normpath'
  • Add + use diagram explaining retry controller area of influence
  • Add openclipart.org conductor image to conductor docs
  • Use oslo_utils eventletutils to warn about eventlet patching
  • Test more engine types in argument passing unit test
  • Add a conductor running example
  • Replace more instance(s) of exception chaining with helper
  • Avoid attribute error by checking executor for being non-none

0.9.0

  • Validate correct exception subclass in 'raise_with_cause'
  • Remove link to kazoo eventlet handler
  • Add states generating venv and use pydot2
  • Add strict job state transition checking
  • Uncap library requirements for liberty
  • Have reset state handlers go through a shared list
  • Add job states in docs + states in python
  • Expose r/o listener callback + details filter callback
  • Expose listener notification type + docs
  • Ensure listener args are always a tuple/immutable
  • Include the 'dump_memory_backend' example in the docs
  • Make resolution/retry strategies more clear and better
  • Rename notifier 'listeners' to 'topics'
  • Mention link to states doc in notify state transitions
  • Ensure we don't get stuck in formatting loops
  • Add note about thread safety of fake filesystem
  • Have the notification/listener docs match other sections
  • Put semantics preservation section into note block
  • Note that the traditional mode also avoids this truncation issue
  • Avoid going into causes of non-taskflow exceptions
  • Use the ability to chain exceptions correctly
  • Add a example showing how to share an executor
  • Shrink the bookshelf description
  • Remove link about implementing job garbage binning
  • Make the storage layer more resilent to failures
  • Put the examples/misc/considerations under a new section
  • Add a suspension engine section

0.8.1

  • Switch back to maxdepth 2
  • Allow ls() to list recursively (using breadth-first)
  • Make an attempt at having taskflow exceptions print causes better
  • fix renamed class to call super correctly
  • Turn 'check_who' into a decorator
  • Use 'node' terminology instead of 'item' terminology
  • Remove 11635 bug reference
  • Allow providing a node stringify function to tree pformat
  • Add in memory filesystem clearing
  • Just unify having a single requirements.txt file
  • Fix a couple of spelling and grammar errors
  • Add memory backend get() support
  • Make the graph '_unsatisfied_requires' be a staticmethod
  • Add more comments to fake in-memory filesystem
  • Add a set of tests to the in memory fake filesystem

0.8.0

  • Adding test to improve CaptureListener coverage
  • Prefer posixpath to os.path
  • By default use a in memory backend (when none is provided)
  • Allow using shallow copy instead of deep copy
  • Move to the newer debtcollector provided functions
  • Move to using the oslo.utils stop watch
  • Updated from global requirements
  • Ensure thread-safety of persistence dir backend
  • Ensure we are really setup before being connected
  • Ensure docstring on storage properties
  • Expose the storage backend being used
  • Use iteration instead of list(s) when extracting scopes
  • Use binary/encode decode helper routines in dir backend
  • Rename memory backend filesystem -> fake filesystem
  • Just let the future executors handle the max workers
  • Always return scope walker instances from `fetch_scopes_for`
  • Give the GC a break
  • Use the class name instead of the TYPE property in __str__
  • Just use the class name instead of TYPE constant
  • Ensure we have a 'coverage-package-name'
  • Attempt to extract traceback from exception
  • Use compatible map and update map/reduce task docs
  • Update engine docs with new validation stage
  • Ensure we register & deregister conductor listeners
  • Rename attribute '_graph' to '_execution_graph'
  • Add a log statement pre-validation that dumps graph info
  • Have this example exit non-zero if incorrect results
  • Use a collections.namedtuple for the request work unit
  • Some small wbe engine doc tweaks
  • Add newline to avoid sphinx warning
  • Allow passing 'many_handler' to fetch_all function
  • Ensure event time listener is in listeners docs
  • Add a in-memory backend dumping example
  • Added a map and a reduce task
  • Restructure the in-memory node usage
  • Switch to non-namespaced module imports
  • Allow the storage unit to use the right scoping strategy
  • Just use the local conf variable
  • Put underscore in-front of alchemist helper
  • lazy loading for logbooks and flowdetails
  • Allow backend connection config (via fetch) to be a string
  • Add + use failure json schema validation
  • Use ordered[set/dict] to retain ordering
  • Allow injected atom args to be persisted
  • add _listeners_from_job method to Conductor base
  • update uses of TimingListener to DurationListener
  • Added EventTimeListner to record when events occur
  • added update_flow_metadata method to Storage class
  • Retain nested causes where/when we can
  • Denote issue 17911 has been merged/accepted
  • Persistence backend refactor
  • Remove support for 3.3
  • Writers can now claim a read lock in ReaderWriterLock
  • Add another probabilistic rw-lock test
  • Add + use read/write lock decorators
  • Add no double writers thread test
  • Use condition context manager instead of acquire/release
  • Remove condition acquiring for read-only ops
  • Set a no-op functor when none is provided
  • Ensure needed locks is used when reading/setting intention
  • Specialize checking for overlaps
  • Use links instead of raw block quotes
  • Rename the timing listeners to duration listeners
  • Add a bookshelf developer section
  • Ensure the thread bundle stops in last to first order
  • Add warning about transient arguments and worker-based-engines
  • Ensure ordered set is pickleable
  • Add node removal/disassociate functions
  • Add a fully functional orderedset
  • Make the worker banner template part of the worker class
  • Use compilation helper objects
  • Allow node finding to not do a deep search
  • Add a frozen checking decorator
  • Tweak functor used to find flatteners/storage routines
  • Add specific scoping documentation
  • add jobboard trash method
  • Provide more contextual information about invalid periodics
  • Fix lookup scoping multi-match ordering
  • Stick to one space after a period
  • Refactor parts of the periodic worker
  • Use oslo.utils encodeutils for encode/decode functions
  • Bring over pretty_tox.sh from nova/heat/others
  • Tweak some of the types thread safety docstrings
  • Add pypi link badges
  • Switch the note about process pool executor to warning
  • Chain exceptions correctly on py3.x
  • Updated from global requirements
  • Remove WBE experimental documentation note
  • Use the enum library for the retry strategy enumerations
  • Use debtcollector library to replace internal utility
  • add get_flow_details and get_atom_details to all backends
  • Tweaks to atom documentation
  • Update Flow::__str__
  • Add todo note for kombu pull request
  • Move 'provides' and 'name' to instance attributes
  • Allow loading conductors via entrypoints

0.7.1

  • Revert "Add retries to fetching the zookeeper server version"
  • Allow turning off the version check
  • adding check for str/unicode type in requires
  • Make the dispatcher handler be an actual type
  • Add retries to fetching the zookeeper server version
  • Remove duplicate 'the' and link to worker engine section
  • Remove delayed decorator and replace with nicer method
  • Fix log statement
  • Make the atom class an abstract class
  • Improve multilock class and its associated unit test
  • Mark conductor 'stop' method deprecation kwarg with versions
  • Move to hacking 0.10
  • catch NotFound errors when consuming or abandoning
  • Use the new table length constants
  • Improve upon/adjust/move around new optional example
  • Clarify documentation related to inputs
  • Docstrings should document parameters return values
  • Let the multi-lock convert the provided value to a tuple
  • Map optional arguments as well as required arguments
  • Add a BFS tree iterator
  • DFS in right order when not starting at the provided node
  • Rework the sqlalchemy backend
  • Modify stop and add wait on conductor to prevent lockups
  • Default to using a thread-safe storage unit
  • Add warning to sqlalchemy backend size limit docs
  • Updated from global requirements
  • Use a thread-identifier that can't easily be recycled
  • Use a notifier instead of a direct property assignment
  • Tweak the WBE diagram (and present it as an svg)
  • Remove duplicate code
  • Improved diagram for Taskflow
  • Bump up the env_builder.sh to 2.7.9
  • Add a capturing listener (for test or other usage)
  • Add + use a staticmethod to fetch the immediate callables
  • Just directly access the callback attributes
  • Use class constants during pformatting a tree node

0.7.0

  • Abstract out the worker finding from the WBE engine
  • Add and use a nicer kombu message formatter
  • Remove duplicated 'do' in types documentation
  • Use the class defined constant instead of raw strings
  • Use kombu socket.timeout alias instead of socket.timeout
  • Stopwatch usage cleanup/tweak
  • Add note about publicly consumable types
  • Add docstring to wbe proxy to denote not for public use
  • Use monotonic time when/if available
  • Updated from global requirements
  • Link WBE docs together better (especially around arguments)
  • Emit a warning when no routing keys provided on publish()
  • Center SVG state diagrams
  • Use importutils.try_import for optional eventlet imports
  • Shrink the WBE request transition SVG image size
  • Add a thread bundle helper utility + tests
  • Make all/most usage of type errors follow a similar pattern
  • Leave use-cases out of WBE developer documentation
  • Allow just specifying 'workers' for WBE entrypoint
  • Add comments to runner state machine reaction functions
  • Fix coverage environment
  • Use explicit WBE worker object arguments (instead of kwargs)
  • WBE documentation tweaks/adjustments
  • Add a WBE request state diagram + explanation
  • Tidy up the WBE cache (now WBE types) module
  • Fix leftover/remaining 'oslo.utils' usage
  • Show the failure discarded (and the future intention)
  • Use a class provided logger before falling back to module
  • Use explicit WBE object arguments (instead of kwargs)
  • Fix persistence doc inheritance hierarchy
  • The gathered runtime is for failures/not failures
  • add clarification re parallel engine
  • Increase robustness of WBE producer/consumers
  • Move implementation(s) to there own sections
  • Move the jobboard/job bases to a jobboard/base module
  • Have the serial task executor shutdown/restart its executor
  • Mirror the task executor methods in the retry action
  • Add back a 'eventlet_utils' helper utility module
  • Use constants for runner state machine event names
  • Remove 'SaveOrderTask' and test state in class variables
  • Provide the stopwatch elapsed method a maximum
  • Fix unused and conflicting variables
  • Switch to using 'oslo_serialization' vs 'oslo.serialization'
  • Switch to using 'oslo_utils' vs 'oslo.utils'
  • Add executor statistics
  • Use oslo.utils reflection for class name
  • Add split time capturing to the stop watch
  • Use platform neutral line separator(s)
  • Create and use a multiprocessing sync manager subclass
  • Use a single sender
  • Updated from global requirements
  • Include the 'old_state' in all currently provided listeners
  • Update the README.rst with accurate requirements
  • Include docstrings for parallel engine types/strings supported
  • The taskflow logger module does not provide a logging adapter
  • Send in the prior atom state on notification of a state change
  • Pass a string as executor in the example instead of an executor
  • Updated from global requirements
  • Fix for job consumption example using wrong object

0.6.1

  • Remove need to inherit/adjust netutils split
  • Allow specifying the engine 'executor' as a string
  • Disallowing starting the executor when worker running
  • Use a single shared queue for an executors lifecycle
  • Avoid creating a temporary list(s) for tree type
  • Update statement around stopwatch thread safety
  • Register with 'ANY' in the cloned process
  • Add edge labels for engine states
  • Remove less than useful action_engine __str__
  • Ensure manager started/shutdown/joined and reset
  • Return the same namedtuple that the future module returns
  • Add a simplistic hello world example
  • Get event/notification sending working correctly
  • Move the engine scoping test to its engines test folder
  • Get the basics of a process executor working
  • Move the persistence base to the parent directory
  • Correctly trigger 'on_exit' of starting/initial state

0.6.0

  • Add an example which shows how to send events out from tasks
  • Move over to using oslo.utils [reflection, uuidutils]
  • Rework the in-memory backend
  • Updated from global requirements
  • Add a basic map/reduce example to show how this can be done
  • Add a parallel table mutation example
  • Add a 'can_be_registered' method that checks before notifying
  • Base task executor should provide 'wait_for_any'
  • Replace autobind with a notifier module helper function
  • Cleanup some doc warnings/bad/broken links
  • Use the notifier type in the task class/module directly
  • Use a tiny clamp helper to clamp the 'on_progress' value
  • Retain the existence of a 'EngineBase' until 0.7 or later
  • Remove the base postfix from the internal task executor
  • Remove usage of listener base postfix
  • Add a moved_inheritable_class deprecation helper
  • Avoid holding the lock while scanning for existing jobs
  • Remove the base postfix for engine abstract base class
  • Avoid popping while another entity is iterating
  • Updated from global requirements
  • Use explict 'attr_dict' when adding provider->consumer edge
  • Properly handle and skip empty intermediary flows
  • Ensure message gets processed correctly
  • Just assign a empty collection instead of copy/clear
  • Remove rtype from task clone() doc
  • Add and use a new simple helper logging module
  • Have the sphinx copyright date be dynamic
  • Add appropriate links into README.rst
  • Use condition variables using 'with'
  • Use an appropriate ``extract_traceback`` limit
  • Allow all deprecation helpers to take a stacklevel
  • Correctly identify stack level in ``_extract_engine``
  • Stop returning atoms from execute/revert methods
  • Have tasks be able to provide copy() methods
  • Allow stopwatches to be restarted
  • Ensure that failures can be pickled
  • Rework pieces of the task callback capability
  • Just use 4 spaces for classifier indents
  • Move atom action handlers to there own subfolder/submodule
  • Workflow documentation is now in infra-manual
  • Ensure frozen attribute is set in fsm clones/copies
  • Fix split on "+" for connection strings that specify dialects
  • Update listeners to ensure they correctly handle all atoms
  • Allow for the notifier to provide a 'details_filter'
  • Be explicit about publish keyword arguments
  • Some package additions and adjustments to the env_builder.sh
  • Cache immutable visible scopes in the runtime component
  • Raise value errors instead of asserts
  • Add a claims listener that connects job claims to engines
  • Split the scheduler into sub-schedulers
  • Use a module level constant to provide the DEFAULT_LISTEN_FOR
  • Move the _pformat() method to be a classmethod
  • Add link to issue 17911
  • Avoid deepcopying exception values
  • Include documentation of the utility modules
  • Use a metaclass to dynamically add testcases to example runner
  • Remove default setting of 'mysql_traditional_mode'
  • Move scheduler and completer classes to there own modules
  • Ensure that the zookeeper backend creates missing atoms
  • Use the deprecation utility module instead of warnings
  • Tweaks to setup.cfg
  • Add a jobboard high level architecture diagram
  • Mark 'task_notifier' as renamed to 'atom_notifier'
  • Revert wrapt usage until further notice
  • Updated from global requirements
  • Add a history retry object, makes retry histories easier to use
  • Format failures via a static method
  • When creating daemon threads use the bundled threading_utils
  • Ensure failure types contain only immutable items
  • Mark 'task_notifier' as renamed to 'atom_notifier'
  • Use wrapt to provide the deprecated class proxy
  • Updated from global requirements
  • Updated from global requirements
  • Updated from global requirements
  • Reduce the worker-engine joint testing time
  • Link bug in requirements so people understand why pbr is listed
  • Updated from global requirements
  • Use standard threading locks in the cache types
  • Handle the case where '_exc_type_names' is empty
  • Add pbr to installation requirements
  • Updated from global requirements
  • Remove direct usage of the deprecated failure location
  • Fix the example 'default_provides'
  • Use constants for retry automatically provided kwargs
  • Remove direct usage of the deprecated notifier location
  • Remove attrdict and just use existing types
  • Use the mock that finds a working implementation
  • Add a futures type that can unify our future functionality
  • Bump the deprecation version number
  • Use and verify event and latch wait() return using timeouts
  • Deprecate `engine_conf` and prefer `engine` instead
  • Use constants for link metadata keys
  • Bump up the sqlalchemy version for py26
  • Hoist the notifier to its own module
  • Move failure to its own type specific module
  • Use constants for revert automatically provided kwargs
  • Improve some of the task docstrings
  • We can now use PyMySQL in py3.x tests
  • Updated from global requirements
  • Add the database schema to the sqlalchemy docs
  • Change messaging from handler connection timeouts -> operation timeouts
  • Switch to a custom NotImplementedError derivative
  • Allow the worker banner to be written to an arbitrary location
  • Update engine class names to better reflect there usage

0.5.0

  • Avoid usage of six.moves in local functions
  • Refactor parts of the job lock/job condition zookeeper usage
  • Make it so that the import works for older versions of kombu
  • Rework the state documentation
  • Updated from global requirements
  • Add a more dynamic/useful logging listener
  • Use timeutils functions instead of misc.wallclock
  • Expose only `ensure_atom` from storage
  • Adjust docs+venv tox environments requirements/dependencies
  • Increase robustness of WBE message and request processing
  • Adjust the WBE log levels
  • Use the features that the oslotest mock base class provides
  • Use oslotest to provide our base test case class
  • Jobboard example that show jobs + workers + producers
  • Adjust on_job_posting to not hold the lock while investigating
  • Bring in a newer optional eventlet
  • Move some of the custom requirements out of tox.ini
  • Document more function/class/method params
  • Stop using intersphinx
  • Expand toctree to three levels
  • Documentation cleanups and tweaks
  • Fix multilock concurrency when shared by > 1 threads
  • Increase/adjust the logging of the WBE response/send activities
  • Color some of the states depending on there meaning
  • Switch to using oslo.utils and oslo.serialization
  • Typos "searchs"
  • Update the requirements-py2.txt file
  • Remove no longer needed r/w lock interface base class
  • Updated from global requirements
  • Better handle the tree freeze method
  • Ensure state machine can be frozen
  • Link a few of the classes to implemented features/bugs in python
  • Add a timing listener that also prints the results
  • Remove useless __exit__ return
  • Example which shows how to move values from one task to another
  • Mention issue with more than one thread and reduce workers
  • Add a mandelbrot parallel calculation WBE example
  • Add existing types to generated documentation
  • Remove the dependency on prettytable
  • Work toward Python 3.4 support and testing
  • Add a state machine copy() method
  • Update the state graph builder to use state machine type
  • Add a docs virtualenv
  • Reduce unused tox environments

0.4.0

  • Add a couple of scope shadowing test cases
  • Relax the graph flow symbol constraints
  • Relax the unordered flow symbol constraints
  • Relax the linear flow symbol constraints
  • Revamp the symbol lookup mechanism
  • Be smarter about required flow symbols
  • Update oslo-incubator to 32e7f0b56f52742754
  • Translate the engine runner into a well defined state-machine
  • Raise a runtime error when mixed green/non-green futures
  • Ensure the cachedproperty creation/setting is thread-safe
  • warn against sorting requirements
  • Updated from global requirements
  • Update transitioning function name to be more understandable
  • Move parts of action engine tests to a subdirectory
  • Tweak engine iteration 'close-up shop' runtime path
  • Use explicit WBE request state transitions
  • Reject WBE messages if they can't be put in an ack state
  • Make version.py handle pbr not being installed
  • Cleanup WBE example to be simpler to understand
  • Use __qualname__ where appropriate
  • Updated from global requirements
  • Updated from global requirements
  • Make the WBE worker banner information more meaningful
  • Have the dispatch_job function return a future
  • Expand documention on failures and wrapped failures types
  • Allow worker count to be specified when no executor provided
  • Remove sphinx examples emphasize-lines
  • Split requirements into py2 and py3 files
  • Update oslo-incubator to 037dee004c3e2239
  • Remove db locks and use random db names for tests
  • Allow WBE request transition timeout to be dynamic
  • Avoid naming time type module the same as a builtin
  • LOG which requeue filter callback failed
  • Add a pformat() failure method and use it in the conductor
  • add pre/post execute/retry callbacks to tasks
  • Use checked_commit() around consume() and abandon()
  • Use a check + create transaction when claiming a job
  • Improve WBE testing coverage
  • Add basic WBE validation sanity tests
  • WBE request message validation
  • WBE response message validation
  • WBE notification message validation
  • Allow handlers to provide validation callables
  • Use a common message dispatcher
  • Use checked commit when committing kazoo transactions
  • Enable hacking checks H305 and H307 in tox.ini template
  • Fixes unsorted dicts and sets in doctests
  • README.rst: Avoid using non-ascii character
  • Updated from global requirements
  • Add a sample script that can be used to build a test environment
  • Enabled hacking checks H305 and H307
  • Bump hacking to version 0.9.2
  • Allow a jobs posted book to be none by default
  • Cleanup some of the example code & docs
  • Make greenexecutor not keep greenthreads active
  • Add the arch/big picture omnigraffle diagram
  • Remove pbr as a runtime dependency
  • Use the `state_graph.py` for all states diagrams
  • Make the examples documentation more relevant
  • Raise NotImplementedError instead of NotImplemented
  • Move the stopwatch tests to test_types
  • Remove need to do special exception catching in parse_uri
  • Update oslo incubator code to commit 0b02fc0f36814968
  • Fix the section name in CONTRIBUTING.rst
  • Add a conductor considerations section
  • Make the expiring cache a top level cache type
  • Use `flow_uuid` and `flow_name` from storage
  • Fix traces left in zookeeper
  • Clarify locked decorator is for instance methods
  • Extract the state changes from the ensure storage method
  • Create a top level time type
  • Simplify identity transition handling for tasks and retries
  • Remove check_doc.py and use doc8
  • Remove functions created for pre-six 1.7.0
  • Add a tree type
  • Make intentions a tuple (to denote immutability)
  • Updated from global requirements
  • Add example for pseudo-scoping
  • Fix E265 hacking warnings
  • Fix doc which should state fetch() usage
  • Adjust sphinx requirement
  • Upgrade hacking version and fix some of the issues
  • Denote that other projects can use this library
  • Remove misc.as_bool as oslo provides an equivalent
  • Update zake to requirements version

0.3.21

  • Rename additional to general/higher-level
  • Sync our version of the interprocess lock
  • Increase usefulness of the retry component compile errors
  • Switch to a restructuredtext README file
  • Create a considerations section
  • Include the function name on internal errors
  • Add in default transaction isolation levels
  • Allow the mysql mode to be more than just TRADITIONAL
  • Make the runner a runtime provided property
  • Rename inject_task_args to inject_atom_args
  • Rename the graph analyzer to analyzer
  • Provide the compilation object instead of just a part of it
  • Ensure cachedproperty descriptor picks up docstrings

0.3

  • Warn about internal helper/utility usage
  • Rename to atom from task
  • Invert the conductor stop() returned result
  • Move flattening to the action engine compiler
  • Increase the level of usefulness of the dispatching logging
  • Avoid forcing engine_conf to a dict
  • Allow for two ways to find a flow detail in a job for a conductor
  • Add docs related to the new conductor feature
  • Add docstring describing the inject instance variable
  • Finish factoring apart the graph_action module
  • Update sphinx pin from global requirements
  • Fix docstring list format
  • Allow indent text to be passed in
  • Factor out the on_failure to a mixin type
  • Use a name property setter instead of a set_name method
  • Adds a single threaded flow conductor
  • add the ability to inject arguments into tasks at task creation
  • Synced jsonutils from oslo-incubator
  • Remove wording issue (track does not make sense here)
  • Fix case of taskflow in docs
  • Put the job external wiki link in a note section
  • Rework atom documentation
  • Add doc link to examples
  • Rework the overview of the notification mechanism
  • Standardize on the same capitalization pattern
  • Regenerate engine-state sequence diagram
  • Add source of engine-state sequence diagram
  • Add kwarg check_pending argument to fake lock
  • Add a example which uses the run_iter function in a for loop
  • Fix error string interpolation
  • Rename t_storage to atom_storage
  • Create and use a new compilation module
  • Add engine state diagram
  • Add tests for the misc.cachedproperty descriptor
  • Complete the cachedproperty descriptor protocol
  • Don't create fake LogBook when we can not fetch one
  • Use futures wait() when possible
  • Use /taskflow/flush-test in the flush function
  • Add a reset nodes function
  • Default the impl_memory conf to none
  • Fix spelling mistake
  • Add a helper tool which clears zookeeper test dirs
  • Add a zookeeper jobboard integration test
  • Cleanup zookeeper integration testing
  • Use a more stable flush method
  • Remove the _clear method and do not reset the job_watcher
  • Allow command and connection retry configuration
  • Check documentation for simple style requirements
  • Add an example which uses the run iteration functionality
  • Implement run iterations
  • Put provides and requires code to basic Flow
  • Allow the watcher to re-register if the session is lost
  • Add a new wait() method that waits for jobs to arrive
  • Add a cachedproperty descriptor
  • Add an example for the job board feature
  • Engine _cls postfix is not correct
  • Pass executor via kwargs instead of config
  • Allow the WBE to use a preexisting executor
  • Tweaks to object hiearchy diagrams
  • Adjust doc linking
  • Medium-level docs on engines
  • Add docs for the worker based engine (WBE)
  • Updated from global requirements
  • Move from generator to iterator for iterjobs
  • Add a jobboard fetching context manager
  • Wrap the failure to load in the not found exception
  • Update jobboard docs
  • Synced jsonutils from oslo-incubator
  • Remove persistence wiki page link
  • Load engines with defined args and provided kwargs
  • Integrate urlparse for configuration augmentation
  • Fix "occured" -> "occurred"
  • Documentation tune-ups
  • Fix spelling error
  • Add a resumption strategy doc
  • Docs and cleanups for test_examples runner
  • Skip loading (and failing to load) lock files
  • Add a persistence backend fetching context manager
  • Add a example that activates a future when a result is ready
  • Fix documentation spelling errors
  • Add a job consideration doc
  • Add last_modified & created_on attributes to jobs
  • Allow jobboard event notification
  • Use sequencing when posting jobs
  • Add a directed graph type (new types module)
  • Add persistence docs + adjustments
  • Updated from global requirements
  • Stings -> Strings
  • Be better at failure tolerance
  • Ensure example abandons job when it fails
  • Add docs for jobs and jobboards
  • Get persistence backend via kwargs instead of conf
  • Allow fetching jobboard implementations
  • Reuse already defined variable
  • More keywords & classifier topics
  • Allow transient values to be stored in storage
  • Doc adjustments
  • Move the daemon thread helper function
  • Create a periodic worker helper class
  • Fix not found being raised when iterating
  • Allow for only iterating over the most 'fresh' jobs
  • Updated from global requirements
  • Update oslo-incubator to 46f2b697b6aacc67
  • import run_cross_tests.sh from incubator
  • Exception in worker queue thread
  • Avoid holding the state lock while notifying

0.2

  • Allow atoms to save their own state/result
  • Use correct exception in the timing listener
  • Add a engine preparation stage
  • Decrease extraneous logging
  • Handle retry last_results/last_failure better
  • Improve documentation for engines
  • Worker executor adjustments
  • Revert "Move taskflow.utils.misc.Failure to its own module"
  • Move taskflow.utils.misc.Failure to its own module
  • Leave the execution_graph as none until compiled
  • Move state link to developer docs
  • Raise error if atom asked to schedule with unknown intention
  • Removed unused TIMED_OUT state
  • Rework documentation of notifications
  • Test retry fails on revert
  • Exception when scheduling task with invalid state
  • Fix race in worker-based executor result processing
  • Set logbook/flowdetail/atomdetail meta to empty dict
  • Move 'inputs and outputs' to developers docs
  • tests: Discover absence of zookeeper faster
  • Fix spelling mistake
  • Should be greater or equal to zero and not greater than
  • Persistence cleanup part one
  • Run worker-based engine tests faster
  • SQLAlchemy requirements put in order
  • Add timeout to WaitForOneFromTask
  • Use same code to reset flow and parts of it
  • Optimize dependency links in flattening
  • Adjust the exception hierachy
  • docs: Links to methods on arguments and results page
  • Add __repr__ method to Atom
  • Flattening improvements
  • tests: Fix WaitForOneFromTask constructor parameter introspection
  • Rework graph flow unit tests
  • Rewrite assertion for same elements in sequences
  • Unit tests for unordered flow
  • Linear flow: mark links and rework unit tests
  • Drop indexing operator from linear flow
  • Drop obsolete test_unordered_flow
  • Iteration over links in flow interface
  • Add a timeout object that can be interrupted
  • Avoid shutting down of a passed executor
  • Add more tests for resumption with retry
  • Improve logging for proxy publish
  • Small documentation fix
  • Improve proxy publish method
  • Add Retry to developers documentation
  • Move flow states to developers documentation
  • Remove extraneous vim configuration comments
  • Make schedule a proper method of GraphAction
  • Simplify graph analyzer interface
  • Test storage with memory and sqlite backends
  • Fix few minor spelling errors
  • Fix executor requests publishing bug
  • Flow smart revert with retry controller
  • Add atom intentions for tasks and retries
  • [WBE] Collect information from workers
  • Add tox environment for pypy
  • docs: Add inheritance diagram to exceptions documentation
  • Adjust logging levels and usage to follow standards
  • Introduce message types for WBE protocol
  • Add retry action to execute retries
  • Extend logbook and storage to work with retry
  • Add retry to execution graph
  • Add retry to Flow patterns
  • Add base class for Retry
  • Update request `expired` property docsting
  • docs: Add page describing atom arguments and results
  • docs: Improve BaseTask method docstrings
  • Remove extra quote symbol
  • docs: Relative links improvements
  • docs: Ingore 'taskflow.' prefix when sorting module index
  • Update comment + six.text_type instead of str for name
  • Avoid calling callbacks while holding locks
  • Rename remote task to request
  • Rework proxy publish functionality
  • Updated from global requirements
  • Use message.requeue instead of message.reject
  • Lock test tweaks
  • Move endpoint subclass finding to reflection util
  • Correct LOG.warning in persistence utils
  • Introduce remote tasks cache for worker-executor
  • Worker-based engine clean-ups
  • A few worker-engine cleanups
  • Add a delay before releasing the lock
  • Allow connection string to be just backend name
  • Get rid of openstack.common.py3kcompat
  • Clean-up several comments in reflection.py
  • Fix try_clean not getting the job_path
  • Updated from global requirements
  • Rename uuid to topic
  • Fixups for threads_count usage and logging
  • Use the stop watch utility instead of custom timing
  • Unify usage of storage error exception type
  • Add zookeeper job/jobboard impl
  • Updated from global requirements
  • Removed copyright from empty files
  • Remove extraneous vim configuration comments
  • Use six.text_type() instead of str() in sqlalchemy backend
  • Fix dummy lock missing pending_writers method
  • Move some common/to be shared kazoo utils to kazoo_utils
  • Switch to using the type checking decode_json
  • Fix few spelling and grammar errors
  • Fixed spelling error
  • Run action-engine tests with worker-based engine
  • Message-oriented worker-based flow with kombu
  • Check atom doesn't provide and return same values
  • Fix command for pylint tox env
  • Remove locale overrides form tox template
  • Reduce test and optional requirements to global requirements
  • Rework sphinx documentation
  • Remove extraneous vim configuration comments
  • Sync with global requirements
  • Instead of doing set diffing just partition when state checking
  • Add ZooKeeper backend to examples
  • Storage protects lower level backend against thread safety
  • Remove tox locale overrides
  • Update .gitreview after repo rename
  • Small storage tests clean-up
  • Support building wheels (PEP-427)

0.1.3

  • Add validate() base method
  • Fix deadlock on waiting for pending_writers to be empty
  • Rename self._zk to self._client
  • Use listener instead of AutoSuspendTask in test_suspend_flow
  • Use test utils in test_suspend_flow
  • Use reader/writer locks in storage
  • Allow the usage of a passed in sqlalchemy engine
  • Be really careful with non-ascii data in exceptions/failures
  • Run zookeeper tests if localhost has a compat. zookeeper server
  • Add optional-requirements.txt
  • Move kazoo to testenv requirements
  • Unpin testtools version and bump subunit to >=0.0.18
  • Remove use of str() in utils.misc.Failure
  • Be more resilent around import/detection/setup errors
  • Some zookeeper persistence improvements/adjustments
  • Add a validate method to dir and memory backends
  • Update oslo copy to oslo commit 39e1c5c5f39204
  • Update oslo.lock from incubator commit 3c125e66d183
  • Refactor task/flow flattening
  • Engine tests refactoring
  • Tests: don't pass 'values' to task constructor
  • Test fetching backends via entry points
  • Pin testtools to 0.9.34 in test requirements
  • Ensure we register the new zookeeper backend as an entrypoint
  • Implement ZooKeeper as persistence storage backend
  • Use addCleanup instead of tearDown in test_sql_persistence
  • Retain the same api for all helpers
  • Update execute/revert comments
  • Added more unit tests for Task and FunctorTask
  • Doc strings and comments clean-up
  • List examples function doesn't accept arguments
  • Tests: Persistence test mixin fix
  • Test using mysql + postgres if available
  • Clean-up and improve async-utils tests
  • Use already defined PENDING variable
  • Add utilities for working with binary data
  • Cleanup engine base class
  • Engine cleanups
  • Update atom comments
  • Put full set of requirements to py26, py27 and py33 envs
  • Add base class Atom for all flow units
  • Add more requirements to cover tox environment
  • Put SQLAlchemy requirements on single line
  • Proper exception raised from check_task_transition
  • Fix function name typo in persistence utils
  • Use the same way of assert isinstance in all tests
  • Minor cleanup in test_examples
  • Add possibility to create Failure from exception
  • Exceptions cleanup
  • Alter is_locked() helper comment
  • Add a setup.cfg keywords to describe taskflow
  • Use the released toxgen tool instead of our copy

0.1.2

  • Move autobinding to task base class
  • Assert functor task revert/execute are callable
  • Use the six callback checker
  • Add envs for different sqlalchemy versions
  • Refactor task handler binding
  • Move six to the right location
  • Use constants for the execution event strings
  • Added htmlcov folder to .gitignore
  • Reduce visibility of task_action
  • Change internal data store of LogBook from list to dict
  • Misc minor fixes to taskflow/examples
  • Add connection_proxy param
  • Ignore doc build files
  • Fix spelling errors
  • Switch to just using tox
  • Enable H202 warning for flake8
  • Check tasks should not provide same values
  • Allow max_backoff and use count instead of attempts
  • Skip invariant checking and adding when nothing provided
  • Avoid not_done naming conflict
  • Add stronger checking of backend configuration
  • Raise type error instead of silencing it
  • Move the container fetcher function to utils
  • Explicitly list the valid transitions to RESUMING state
  • Name the graph property the same as in engine
  • Bind outside of the try block
  • Graph action refactoring
  • Add make_completed_future to async_utils
  • Update oslo-incubator copy to oslo-incubator commit 8b2b0b743
  • Ensure that mysql traditional mode is enabled
  • Move async utils to own file
  • Update requirements from opentack/requirements
  • Code cleanup for eventlet_utils.wait_fo_any
  • Refactor engine internals
  • Add wait_for_any method to eventlet utils
  • Introduce TaskExecutor
  • Run some engine tests with eventlet if it's available
  • Do not create TaskAction for each task
  • Storage: use names instead of uuids in interface
  • Add tests for metadata updates
  • Fix sqlalchemy 0.8 issues
  • Fix minor python3 incompatibility
  • Speed up FlowDetail.find
  • Fix misspellings
  • Raise exception when trying to run empty flow
  • Use update_task_metadata in set_task_progress
  • Capture task duration
  • Fix another instance of callback comparison
  • Don't forget to return self
  • Fixes how instances methods are not deregistered
  • Targeted graph flow pattern
  • All classes should explicitly inherit object class
  • Initial commit of sphinx related files
  • Improve is_valid_attribute_name utility function
  • Coverage calculation improvements
  • Fix up python 3.3 incompatabilities

0.1.1

  • Pass flow failures to task's revert method
  • Storage: add methods to get all flow failures
  • Pbr requirement went missing
  • Update code to comply with hacking 0.8.0
  • Don't reset tasks to PENDING state while reverting
  • Let pbr determine version automatically
  • Be more careful when passing result to revert()

0.1

  • Support for optional task arguments
  • Do not erase task progress details
  • Storage: restore injected data on resumption
  • Inherit the greenpool default size
  • Add debug logging showing what is flattened
  • Remove incorrect comment
  • Unit tests refactoring
  • Use py3kcompat.urlutils from oslo instead of six.urllib_parse
  • Update oslo and bring py3kcompat in
  • Support several output formats in state_graph tool
  • Remove task_action state checks
  • Wrapped exception doc/intro comment updates
  • Doc/intro updates for simple_linear_listening
  • Add docs/intro to simple_linear example
  • Update intro/comments for reverting_linear example
  • Add docs explaining what/how resume_volume_create works
  • A few resuming from backend comment adjustments
  • Add an introduction to explain resume_many example
  • Increase persistence example comments
  • Boost graph flow example comments
  • Also allow "_" to be valid identifier
  • Remove uuid from taskflow.flow.Flow
  • A few additional example boot_vm comments + tweaks
  • Add a resuming booting vm example
  • Add task state verification
  • Beef up storage comments
  • Removed unused utilities
  • Helpers to save flow factory in metadata
  • Storage: add flow name and uuid properties
  • Create logbook if not provided for create_flow_details
  • Prepare for 0.1 release
  • Comment additions for exponential backoff
  • Beef up the action engine comments
  • Pattern comment additions/adjustments
  • Add more comments to flow/task
  • Save with the same connection
  • Add a persistence util logbook formatting function
  • Rename get_graph() -> execution_graph
  • Continue adding docs to examples
  • Add more comments that explain example & usage
  • Add more comments that explain example & usage
  • Add more comments that explain example & usage
  • Add more comments that explain example & usage
  • Fix several python3 incompatibilities
  • Python3 compatibility for utils.reflection
  • No module name for builtin type and exception names
  • Fix python3 compatibility issues in examples
  • Fix print statements for python 2/3
  • Add a mini-cinder volume create with resumption
  • Update oslo copy and bring over versionutils
  • Move toward python 3/2 compatible metaclass
  • Add a secondary booting vm example
  • Resumption from backend for action engine
  • A few wording/spelling adjustments
  • Create a green executor & green future
  • Add a simple mini-billing stack example
  • Add a example which uses a sqlite persistence layer
  • Add state to dot->svg tool
  • Add a set of useful listeners
  • Remove decorators and move to utils
  • Add reasons as to why the edges were created
  • Fix entrypoints being updated/created by update.py
  • Validate each flow state change
  • Update state sequence for failed flows
  • Flow utils and adding comments
  • Bump requirements to the latest
  • Add a inspect sanity check and note about bound methods
  • Some small exception cleanups
  • Check for duplicate task names on flattening
  • Correctly save task versions
  • Allow access by index
  • Fix importing of module files
  • Wrapping and serializing failures
  • Simpler API to load flows into engines
  • Avoid setting object variables
  • A few adjustments to the progress code
  • Cleanup unused states
  • Remove d2to dependency
  • Warn if multiple providers found
  • Memory persistence backend improvements
  • Create database from models for SQLite
  • Don't allow mutating operations on the underlying graph
  • Add graph density
  • Suspend single and multi threaded engines
  • Remove old tests for unexisted flow types
  • Boot fake vm example fixed
  • Export graph to dot util
  • Remove unused utility classes
  • Remove black list of graph flow
  • Task decorator was removed and examples updated
  • Remove weakref usage
  • Add basic sanity tests for unordered flow
  • Clean up job/jobboard code
  • Add a directory/filesystem based persistence layer
  • Remove the older (not used) resumption mechanism
  • Reintegrate parallel action
  • Add a flow flattening util
  • Allow to specify default provides at task definition
  • Graph flow, sequential graph action
  • Task progress
  • Verify provides and requires
  • Remap the emails of the committers
  • Use executors instead of pools
  • Fix linked exception forming
  • Remove threaded and distributed flows
  • Add check that task provides all results it should
  • Use six string types instead of basestring
  • Remove usage of oslo.db and oslo.config
  • Move toward using a backend+connection model
  • Add provides and requires properties to Flow
  • Fixed crash when running the engine
  • Remove the common config since its not needed
  • Allow the lock decorator to take a list
  • Allow provides to be a set and results to be a dictionary
  • Allow engines to be copied + blacklist broken flows
  • Add link to why we have to make this factory due to late binding
  • Use the lock decorator and close/join the thread pool
  • Engine, task, linear_flow unification
  • Combine multiple exceptions into a linked one
  • Converted some examples to use patterns/engines
  • MultiThreaded engine and parallel action
  • State management for engines
  • Action engine: save task results
  • Initial implementation of action-based engine
  • Further updates to update.py
  • Split utils module
  • Rename Task.__call__ to Task.execute
  • Reader/writer no longer used
  • Rename "revert_with" => "revert" and "execute_with" to "execute"
  • Notify on task reversion
  • Have runner keep the exception
  • Use distutil version classes
  • Add features to task.Task
  • Add get_required_callable_args utility function
  • Add get_callable_name utility function
  • Require uuid + move functor_task to task.py
  • Check examples when running tests
  • Use the same root test class
  • LazyPluggable is no longer used
  • Add a locally running threaded flow
  • Change namings in functor_task and add docs to its __init__
  • Rework the persistence layer
  • Do not have the runner modify the uuid
  • Refactor decorators
  • Nicer way to make task out of any callable
  • Use oslo's sqlalchemy layer
  • File movements
  • Added Backend API Database Implementation
  • Added Memory Persistence API and Generic Datatypes
  • Resync the latest oslo code
  • Remove openstack.common.exception usage
  • Forgot to move this one to the right folder
  • Add a new simple calculator example
  • Quiet the provider linking
  • Deep-copy not always possible
  • Add a example which simulates booting a vm
  • Add a more complicated graph example
  • Move examples under the source tree
  • Adjust a bunch of hacking violations
  • Fix typos in test_linear_flow.py and simple_linear_listening.py
  • Fix minor code style
  • Fix two minor bugs in docs/examples
  • Show file modifications and fix dirpath based on config file
  • Add a way to use taskflow until library stabilized
  • Provide the length of the flows
  • Parents should be frozen after creation
  • Allow graph dependencies to be manually provided
  • Add helper reset internals function
  • Move to using pbr
  • Unify creation/usage of uuids
  • Use the runner interface as the best task lookup
  • Ensure we document and complete correct removal
  • Pass runners instead of task objects/uuids
  • Move how resuming is done to be disconnected from jobs/flows
  • Clear out before connecting
  • Make connection/validation of tasks be after they are added
  • Add helper to do notification
  • Store results by add() uuid instead of in array format
  • Integrate better locking and a runner helper class
  • Cleaning up various components
  • Move some of the ordered flow helper classes to utils
  • Allow instance methods to be wrapped and unwrapped correctly
  • Add a start of a few simple examples
  • Update readme to point to links
  • Fix most of the hacking rules
  • Fix all flake8 E* and F* errors
  • Fix the current flake8 errors
  • Don't keep the state/version in the task name
  • Dinky change to trigger jenkins so I can cleanup
  • Add the task to the accumulator before running
  • Add .settings and .venv into .gitignore
  • Fix tests for python 2.6
  • Add the ability to soft_reset a workflow
  • Add a .gitreview file so that git-review works
  • Ensure we have an exception and capture the exc_info
  • Update how graph results are fetched when they are optional
  • Allow for optional task requirements
  • We were not notifying when errors occured so fix that
  • Bring over the nova get_wrapped_function helper and use it
  • Allow for passing in the metadata when creating a task detail entry
  • Update how the version task functor attribute is found
  • Remove more tabs incidents
  • Removed test noise and formatted for pep8
  • Continue work on decorator usage
  • Ensure we pickup the packages
  • Fixed pep8 formatting... Finally
  • Add flow disassociation and adjust the assocate path
  • Add a setup.cfg and populate it with a default set of nosetests options
  • Fix spacing
  • Add a better task name algorithm
  • Add a major/minor version
  • Add a get many attr/s and join helper functions
  • Reduce test noise
  • Fix a few unit tests due to changes
  • Ensure we handle functor names and resetting correctly
  • Remove safe_attr
  • Modifying db tests
  • Removing .pyc
  • Fixing .py in .gitignore
  • Update db api test
  • DB api test cases and revisions
  • Allow for turning off auto-extract and add a test
  • Use a function to filter args and add comments
  • Use update instead of overwrite
  • Move decorators to new file and update to use better wraps()
  • Continue work with decorator usage
  • Update with adding a provides and requires decorator for standalone function usage
  • Instead of apply use __call__
  • Add comment to why we accumulate before notifying task listeners
  • Use a default sqlite backing using a taskflow file
  • Add a basic rollback accumlator test
  • Use rollback accumulator and remove requires()/provides() from being functions
  • Allow (or disallow) multiple providers of items
  • Clean the lines in a seperate function
  • Resync with oslo-incubator
  • Remove uuid since we are now using uuidutils
  • Remove error code not found in strict version of pylint
  • Include more dev testing packages + matching versions
  • Update dependencies for new db/distributed backends
  • Move some of the functions to use there openstack/common counterparts
  • More import fixups
  • Patch up the imports
  • Fix syntax error
  • Rename cause -> exception and make exception optional
  • Allow any of the previous tasks to satisfy requirements
  • Ensure we change the self and parents states correctly
  • Always have a name provided
  • Cleaning up files/extraneous files/fixing relations
  • More pylint cleanups
  • Make more tests for linear and shuffle test utils to common file
  • Only do differences on set objects
  • Ensure we fetch the appropriate inputs for the running task
  • Have the linear workflow verify the tasks inputs
  • Specify that task provides/requires must be an immutable set
  • Clean Up for DB changes
  • db api defined
  • Fleshing out sqlalchemy api
  • Almost done with sqlalchemy api
  • Fix state check
  • Fix flow exception wording
  • Ensure job is pending before we associate and run
  • More pylint cleanups
  • Ensure we associate with parent flows as well
  • Add a nice run() method to the job class that will run a flow
  • Massive pylint cleanup
  • deleting .swp files
  • deleting .swp files
  • cleaning for initial pull request
  • Add a few more graph ordering test cases
  • Update automatic naming and arg checks
  • Update order calls and connect call
  • Move flow failure to flow file and correctly catch ordering failure
  • Just kidding - really fixing relations this time
  • Fixing table relations
  • Allow job id to be passed in
  • Check who is being connected to and ensure > 0 connectors
  • Move the await function to utils
  • Graph tests and adjustments releated to
  • Add graph flow tests
  • Fix name changes missed
  • Enable extraction of what a functor requires from its args
  • Called flow now, not workflow
  • Second pass at models
  • More tests
  • Simplify existence checks
  • More pythonic functions and workflow -> flow renaming
  • Added more utils, added model for workflow
  • Spelling errors and stuff
  • adding parentheses to read method
  • Implemented basic sqlalchemy session class
  • Setting up Configs and SQLAlchemy/DB backend
  • Fix the import
  • Use a different logger method if tolerant vs not tolerant
  • More function comments
  • Add a bunch of linear workflow tests
  • Allow resuming stage to be interrupted
  • Fix the missing context variable
  • Moving over celery/distributed workflows
  • Update description wording
  • Pep fix
  • Instead of using notify member functions, just use functors
  • More wording fixes
  • Add the ability to alter the task failure reconcilation
  • Correctly run the tasks after partial resumption
  • Another wording fix
  • Spelling fix
  • Allow the functor task to take a name and provide it a default
  • Updated functor task comments
  • Move some of the useful helpers and functions to other files
  • Add the ability to associate a workflow with a job
  • Move the useful functor wrapping task from test to wrappers file
  • Add a thread posting/claiming example and rework tests to use it
  • After adding reposting/unclaiming reflect those changes here
  • Add a nicer string name that shows what the class name is
  • Adjust some of the states jobs and workflows could be in
  • Add a more useful name that shows this is a task
  • Remove impl of erasing which doesn't do much and allow for job reposting
  • Various reworkings
  • Rename logbook contents
  • Get a memory test example working
  • Add a pylintrc file to be used with pylint
  • Rework the logbook to be chapter/page based
  • Move ordered workflow to its own file
  • Increase the number of comments
  • Start adding in a more generic DAG based workflow
  • Remove dict_provider dependency
  • Rework due to code comments
  • Begin adding testing functionality
  • Fill in the majority of the memory job
  • Rework how we should be using lists instead of ordereddicts for optimal usage
  • Add a context manager to the useful read/writer lock
  • Ensure that the task has a name
  • Add a running state which can be used to know when a workflow is running
  • Rename the date created field
  • Add some search functionality and adjust the await() function params
  • Remove and add a few new exceptions
  • Shrink down the exposed methods
  • Remove the promise object for now
  • Add RESUMING
  • Fix spelling
  • Continue on getting ready for the memory impl. to be useful
  • On python <= 2.6 we need to import ordereddict
  • Remove a few other references to nova
  • Add in openstack common and remove patch references
  • Move simplification over
  • Continue moving here
  • Update README.md
  • Update readme
  • Move the code over for now
  • Initial commit

RELEASE NOTES

Read also the taskflow Release Notes.

INDICES AND TABLES

  • Index
  • Module Index
  • Search Page

[1]
It should be noted that even though it is designed with OpenStack integration in mind, and that is where most of its current integration is it aims to be generally usable and useful in any project.

AUTHOR

unknown

COPYRIGHT

2023, OpenStack Foundation

May 15, 2023 5.2.0