Framework for executing actions and rollbacks
Project description
Action Queues
actionqueues
is a lightweight way to queue up commands for execution and
rollback on failures.
The idea is that it provides a framework for safely executing sequences of action with side-effects, like database writes, that might need later rolling back if later actions fail. In addition, it provides a standardised way for actions to be retried.
For example, a user sign up process may write to several different systems.
If one system is down, then the other systems modified so far need cleaning
up before the failure is propagated back to the user. Using actionqueues
with an action for each external system to be modified enables this pattern,
along with simple retry semantics for likely-transient failures such as network
blips.
Installing
pip install actionqueues
Using Action Queues
It's barebones, the main point is to provide a framework to work within for actions that have side effects. It's basically the Command pattern, with a tiny execution framework.
An Action
is the lowest-level building block. It's any object with execute
and rollback
methods. The Action
is what handles executing each step of the
overall workflow, and rolling back any changes made to external state. It's
a single object so it can save state for rollback -- for example, primary
keys for any created database rows so they can be deleted during rollback.
The main task of a user of actionqueues
is to create the Action
classes
which implement the tasks their workflows require.
Once Action
classes are written, they can be executed. An ActionQueue
holds Actions
for execution and rollback. Add Action
objects to an ActionQueue
for execution. Call ActionQueue#execute
to start running each action's
execute
method in the order the Action
objects were added to the
ActionQueue
. Behaviour after this point is controlled by the execute
and
rollback
methods on the Action
objects being executed by the queue.
Normal operation
The default case is that no exception is raised during an execute
and the
next action in the queue is has execute
called. This is shown below for a
sequence of Action
objects within an ActionQueue
.
Exceptions during execute
cause rollback
If an Action#execute
raises an exception, the ActionQueue notes where it's
up to in the Actions queued up and then propagates the exception
back up to the caller.
It is then the caller's responsibility to catch the exception and then to call
ActionQueue#rollback
. This is so the caller can know that the queue of
actions failed and is able to log the exception before calling rollback
.
Calling ActionQueue#rollback
will execute the rollback
method on all
actions where the execute
method was called, including the one raising the
exception, in the reverse order to that which the execute
method was called.
Rollback will not be called on actions where execute
has not been called.
Again, the default case at this point is that rollback
methods succeed and
don't throw exceptions, leading to each being executed in succession.
Exceptions during rollback
In contrast to a raised exception from execute
, if an exception is raised
during the rollback
method, the ActionQueue
will
silently swallow the exception and continue executing the rollback
methods
of earlier Action
objects in the queue.
This is because, in the rollback scenario, it's most likely that all rollback
actions should happen so the library assumes this. Therefore rollback
methods
should do their own logging of exceptions before re-raising them.
Retrying failed operations
There is an exception to the above rules. If the execute
or rollback
method
raises a actionqueue.ActionRetryException
then the execute
or rollback
method will be called again. The ActionRetryException
init method takes an
optional ms_backoff
argument to specify a time to sleep before trying the
method again, in milliseconds.
The ActionQueue
will retry as long as the action keeps raising
ActionRetryException
, so the action must maintain a retry count
to avoid endless retries. See below for some
helper classes which cover common cases.
Example
import random
from actionqueues import actionqueue, action
SUCCEED = 0
RETRY = 1
FAIL = 2
class MyAction(action.Action):
def __init__(self, id):
self._id = id
self._value = 0
def execute(self):
"""Called if actions before it in the queue complete successfully.
Raise any exception to indicate failure.
"""
action = random.choice([SUCCEED, RETRY, FAIL])
if action == RETRY:
print self._id, "Throwing retry exception"
raise actionqueue.ActionRetryException(ms_backoff=0)
elif action == FAIL:
print self._id, "Throwing failure exception"
raise Exception()
else:
print self._id, "Executing success action"
self._value = 1
def rollback(self):
"""Called in reverse order for all actions queued whose execute
method was called when the ActionQueue's rollback method is called.
"""
print self._id, "Rolling back action"
if self._value == 1:
self._value = 0
q = actionqueue.ActionQueue()
q.add(MyAction("a"))
q.add(MyAction("b"))
try:
q.execute()
except:
q.rollback()
Retry exception helpers
It can be tedious to keep track of the backoff and retry count for an action.
Therefore actionqueues
provides helpers for this called exception factories.
These are created when the Action
is initialised, and when an execute
method hits a retriable exception, it calls the factory's raise_exception()
method. In general, this will throw ActionRetryException
exceptions for a
given number of retries, then throw a generic exception, or one provided by
the Action
object.
Using separate ExceptionFactory objects for execute
and rollback
is usually
required.
The available exception factories are:
DoublingBackoffExceptionFactory
which will throw a configurable numberActionRetryException
exceptions, each doubling its backoff time.
In this example, the ZeroDivisionError
will cause 5 retries, at 100, 200,
400, 800 and 1600ms delays, by using a DoublingBackoffExceptionFactory
:
from actionqueues import actionqueue, action
from actionqueues.exceptionfactory import DoublingBackoffExceptionFactory
class MyFailingAction(action.Action):
def __init__(self):
self._run = 1
self._execute_ex_factory = DoublingBackoffExceptionFactory(
retries=5,
ms_backoff_initial=100
)
self._rollback_ex_factory = DoublingBackoffExceptionFactory(
retries=10,
ms_backoff_initial=100
)
def execute(self):
"""Execute an always failing action, but have it retried 5 times."""
print "Executing action", self._run
self._run += 1
try:
1 / 0
except ZeroDivisionError, ex:
self._execute_ex_factory.raise_exception(original_exception=ex)
def rollback(self):
print "Rollback action", self._run
self._run += 1
try:
1 / 0
except ZeroDivisionError, ex:
self._rollback_ex_factory.raise_exception(original_exception=ex)
q = actionqueue.ActionQueue()
q.add(MyFailingAction())
try:
q.execute()
except:
print "boom"
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file actionqueues-0.8.2.tar.gz
.
File metadata
- Download URL: actionqueues-0.8.2.tar.gz
- Upload date:
- Size: 5.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/1.11.0 pkginfo/1.4.2 requests/2.19.1 setuptools/39.2.0 requests-toolbelt/0.8.0 tqdm/4.24.0 CPython/2.7.10
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 0d56506df379538be0fb123b115e60b62674651598f18fc4cbd5cfb708529251 |
|
MD5 | 9f1fbf64904ca27244639a1db20fd037 |
|
BLAKE2b-256 | aaed9a77866c99d0c244d67f8267a3667e8f3a48a914f3f6b6031eb6b4c6f84b |
File details
Details for the file actionqueues-0.8.2-py2-none-any.whl
.
File metadata
- Download URL: actionqueues-0.8.2-py2-none-any.whl
- Upload date:
- Size: 6.6 kB
- Tags: Python 2
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/1.11.0 pkginfo/1.4.2 requests/2.19.1 setuptools/39.2.0 requests-toolbelt/0.8.0 tqdm/4.24.0 CPython/2.7.10
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 489cadf5d9e7278094532a88374e3013f3ea9e4b47cbf35e72542c68befe83c2 |
|
MD5 | 4f9bd0e18ad20b54ef9bf74760feb192 |
|
BLAKE2b-256 | 854b05952245174588fbe5682b0e10579da3703fb586cf9747dd4b681b7108c8 |