From 1f3752cb108d85c850113904d6b1308fbc1a53c1 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 30 Mar 2015 18:22:07 -0700 Subject: [PATCH 01/55] Address concurrent mutation of sqlalchemy backend In order to prevent a thread from closing a backend while another thread is getting a connection (which can result in an engine being created) stop this kind of concurrent mutation by creating a engine (if it was not user provided) in the constructor. In the close the engine dispose is called (which will according to the docs just create a new pool anyway) so there is no need to recreate the full engine object from its same configuration again. Change-Id: Id1fa3001b3ebbe76bbcdb08ed4add6a9e16ea96b --- .../persistence/backends/impl_sqlalchemy.py | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/taskflow/persistence/backends/impl_sqlalchemy.py b/taskflow/persistence/backends/impl_sqlalchemy.py index 483dd445..a06850df 100644 --- a/taskflow/persistence/backends/impl_sqlalchemy.py +++ b/taskflow/persistence/backends/impl_sqlalchemy.py @@ -237,14 +237,15 @@ class SQLAlchemyBackend(base.Backend): self._engine = engine self._owns_engine = False else: - self._engine = None + self._engine = self._create_engine(self._conf) self._owns_engine = True self._validated = False - def _create_engine(self): + @staticmethod + def _create_engine(conf): # NOTE(harlowja): copy the internal one so that we don't modify it via # all the popping that will happen below. - conf = copy.deepcopy(self._conf) + conf = copy.deepcopy(conf) engine_args = { 'echo': _as_bool(conf.pop('echo', False)), 'convert_unicode': _as_bool(conf.pop('convert_unicode', True)), @@ -307,8 +308,6 @@ class SQLAlchemyBackend(base.Backend): @property def engine(self): - if self._engine is None: - self._engine = self._create_engine() return self._engine def get_connection(self): @@ -323,15 +322,11 @@ class SQLAlchemyBackend(base.Backend): return conn def close(self): - if self._engine is not None and self._owns_engine: - # NOTE(harlowja): Only dispose of the engine and clear it from - # our local state if we actually own the engine in the first - # place. If the user passed in their own engine we should not - # be disposing it on their behalf (and we shouldn't be clearing - # our local engine either, since then we would just recreate a - # new engine if the engine property is accessed). + # NOTE(harlowja): Only dispose of the engine if we actually own the + # engine in the first place. If the user passed in their own engine + # we should not be disposing it on their behalf... + if self._owns_engine: self._engine.dispose() - self._engine = None self._validated = False From 9e6ef18ac41e9f02a9342648066e99f6dffe405c Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 15 Jun 2015 17:16:47 -0700 Subject: [PATCH 02/55] Document more of the retry subclasses special keyword arguments Change-Id: Iaa3949da61c95ffe697fd80cef3ee8a6febd2a8c --- taskflow/retry.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/taskflow/retry.py b/taskflow/retry.py index 3015c79d..93991326 100644 --- a/taskflow/retry.py +++ b/taskflow/retry.py @@ -238,7 +238,20 @@ class AlwaysRevertAll(Retry): class Times(Retry): - """Retries subflow given number of times. Returns attempt number.""" + """Retries subflow given number of times. Returns attempt number. + + :param attempts: number of attempts to retry the associated subflow + before giving up + :type attempts: int + :param revert_all: 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) + :type revert_all: bool + + Further arguments are interpreted as defined in the + :py:class:`~taskflow.atom.Atom` constructor. + """ def __init__(self, attempts=1, name=None, provides=None, requires=None, auto_extract=True, rebind=None, revert_all=False): @@ -297,6 +310,21 @@ class ForEach(ForEachBase): Accepts a collection of decision strategies on construction and returns the next element of the collection on each try. + + :param values: values collection to iterate over and provide to + atoms other in the flow as a result of this functions + :py:meth:`~taskflow.atom.Atom.execute` method, which + other dependent atoms can consume (for example, to alter + their own behavior) + :type values: list + :param revert_all: 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) + :type revert_all: bool + + Further arguments are interpreted as defined in the + :py:class:`~taskflow.atom.Atom` constructor. """ def __init__(self, values, name=None, provides=None, requires=None, @@ -320,6 +348,15 @@ class ParameterizedForEach(ForEachBase): 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. + + :param revert_all: 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) + :type revert_all: bool + + Further arguments are interpreted as defined in the + :py:class:`~taskflow.atom.Atom` constructor. """ def __init__(self, name=None, provides=None, requires=None, From 3e16e249a2cd5436560b212d31fa0642ecde794a Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 22 Jun 2015 11:20:04 -0700 Subject: [PATCH 03/55] Update states comment to refer to task section Change-Id: Ie766a590cc0849e00d64fc485278767b74164c9e --- taskflow/states.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taskflow/states.py b/taskflow/states.py index 265d6b2c..3cbf91b0 100644 --- a/taskflow/states.py +++ b/taskflow/states.py @@ -156,7 +156,7 @@ def check_flow_transition(old_state, new_state): # Task state transitions -# See: http://docs.openstack.org/developer/taskflow/states.html +# See: http://docs.openstack.org/developer/taskflow/states.html#task _ALLOWED_TASK_TRANSITIONS = frozenset(( (PENDING, RUNNING), # run it! From db7af3f19bf31e9379edcb6c4a610b8f9a367ca9 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Tue, 12 May 2015 12:00:26 -0700 Subject: [PATCH 04/55] Remove kazoo hack/fix for issue no longer needed Depends-On: I5cf30d2952850de140f4bcc8bb3eac100ee8001e Change-Id: Ifdf6ec863a3596b6b5e2e58ea383112484b47c26 --- taskflow/utils/kazoo_utils.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/taskflow/utils/kazoo_utils.py b/taskflow/utils/kazoo_utils.py index f681dc46..4baa317c 100644 --- a/taskflow/utils/kazoo_utils.py +++ b/taskflow/utils/kazoo_utils.py @@ -109,13 +109,7 @@ def checked_commit(txn): def finalize_client(client): """Stops and closes a client, even if it wasn't started.""" client.stop() - try: - client.close() - except TypeError: - # NOTE(harlowja): https://github.com/python-zk/kazoo/issues/167 - # - # This can be removed after that one is fixed/merged. - pass + client.close() def check_compatible(client, min_version=None, max_version=None): From 109b5984b1c62369e17aa6b007321958cf70dc57 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Sun, 28 Jun 2015 20:54:31 -0700 Subject: [PATCH 05/55] Just make the compiler object at __init__ time There really isn't any benefit in delaying the local attributes creation until its first used so just upfront create the compiler object. Change-Id: I48c4342a6669077dd35f9cb8f25b5313d6814c8e --- taskflow/engines/action_engine/engine.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/taskflow/engines/action_engine/engine.py b/taskflow/engines/action_engine/engine.py index 142f181c..9c03ca90 100644 --- a/taskflow/engines/action_engine/engine.py +++ b/taskflow/engines/action_engine/engine.py @@ -64,7 +64,6 @@ class ActionEngine(base.Engine): valid states in the states module to learn more about what other states the tasks and flow being ran can go through. """ - _compiler_factory = compiler.PatternCompiler NO_RERAISING_STATES = frozenset([states.SUSPENDED, states.SUCCESS]) """ @@ -78,6 +77,7 @@ class ActionEngine(base.Engine): self._runtime = None self._compiled = False self._compilation = None + self._compiler = compiler.PatternCompiler(flow) self._lock = threading.RLock() self._state_lock = threading.RLock() self._storage_ensured = False @@ -282,10 +282,6 @@ class ActionEngine(base.Engine): self._runtime.reset_all() self._change_state(states.PENDING) - @misc.cachedproperty - def _compiler(self): - return self._compiler_factory(self._flow) - @fasteners.locked def compile(self): if self._compiled: From 0d884a2fc5ec947960884767c998abc1b2d05b69 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Tue, 16 Jun 2015 15:57:11 -0700 Subject: [PATCH 06/55] Use encodeutils for exception -> string function The oslo.utils library now provides a better version of this that always returns a unicode exception message, so update our usage to use it (and remove our own local function). This guarantee of unicode also means we have to update a few other places to make sure we get back bytes or unicode as needed. Change-Id: I924380408aaf6d2aec418ceaaf623c75900268f7 --- doc/source/utils.rst | 5 +++++ taskflow/exceptions.py | 33 +++++++++++++-------------- taskflow/tests/unit/test_failure.py | 30 ++++++++++++++----------- taskflow/types/failure.py | 12 ++++++---- taskflow/utils/mixins.py | 35 +++++++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 34 deletions(-) create mode 100644 taskflow/utils/mixins.py diff --git a/doc/source/utils.rst b/doc/source/utils.rst index ac0dd5c4..ab051237 100644 --- a/doc/source/utils.rst +++ b/doc/source/utils.rst @@ -38,6 +38,11 @@ Miscellaneous .. automodule:: taskflow.utils.misc +Mixins +~~~~~~ + +.. automodule:: taskflow.utils.mixins + Persistence ~~~~~~~~~~~ diff --git a/taskflow/exceptions.py b/taskflow/exceptions.py index 2f22b4f7..857d6634 100644 --- a/taskflow/exceptions.py +++ b/taskflow/exceptions.py @@ -20,6 +20,7 @@ import traceback from oslo_utils import excutils from oslo_utils import reflection import six +from taskflow.utils import mixins def raise_with_cause(exc_cls, message, *args, **kwargs): @@ -231,7 +232,7 @@ class NotImplementedError(NotImplementedError): """ -class WrappedFailure(Exception): +class WrappedFailure(mixins.StrMixin, Exception): """Wraps one or several failure objects. When exception/s cannot be re-raised (for example, because the value and @@ -284,20 +285,18 @@ class WrappedFailure(Exception): return result return None - def __str__(self): - causes = [exception_message(cause) for cause in self._causes] - return 'WrappedFailure: %s' % causes + def __bytes__(self): + buf = six.BytesIO() + buf.write(b'WrappedFailure: [') + causes_gen = (six.binary_type(cause) for cause in self._causes) + buf.write(b", ".join(causes_gen)) + buf.write(b']') + return buf.getvalue() - -def exception_message(exc): - """Return the string representation of exception. - - :param exc: exception object to get a string representation of. - """ - # NOTE(imelnikov): Dealing with non-ascii data in python is difficult: - # https://bugs.launchpad.net/taskflow/+bug/1275895 - # https://bugs.launchpad.net/taskflow/+bug/1276053 - try: - return six.text_type(exc) - except UnicodeError: - return str(exc) + def __unicode__(self): + buf = six.StringIO() + buf.write(u'WrappedFailure: [') + causes_gen = (six.text_type(cause) for cause in self._causes) + buf.write(u", ".join(causes_gen)) + buf.write(u']') + return buf.getvalue() diff --git a/taskflow/tests/unit/test_failure.py b/taskflow/tests/unit/test_failure.py index fab9cb9a..9e5b4f79 100644 --- a/taskflow/tests/unit/test_failure.py +++ b/taskflow/tests/unit/test_failure.py @@ -16,6 +16,7 @@ import sys +from oslo_utils import encodeutils import six from six.moves import cPickle as pickle import testtools @@ -301,9 +302,19 @@ class NonAsciiExceptionsTestCase(test.TestCase): def test_exception_with_non_ascii_str(self): bad_string = chr(200) - fail = failure.Failure.from_exception(ValueError(bad_string)) - self.assertEqual(fail.exception_str, bad_string) - self.assertEqual(str(fail), 'Failure: ValueError: %s' % bad_string) + excp = ValueError(bad_string) + fail = failure.Failure.from_exception(excp) + self.assertEqual(fail.exception_str, + encodeutils.exception_to_unicode(excp)) + # This is slightly different on py2 vs py3... due to how + # __str__ or __unicode__ is called and what is expected from + # both... + if six.PY2: + msg = encodeutils.exception_to_unicode(excp) + expected = 'Failure: ValueError: %s' % msg.encode('utf-8') + else: + expected = u'Failure: ValueError: \xc8' + self.assertEqual(str(fail), expected) def test_exception_non_ascii_unicode(self): hi_ru = u'привет' @@ -316,18 +327,11 @@ class NonAsciiExceptionsTestCase(test.TestCase): def test_wrapped_failure_non_ascii_unicode(self): hi_cn = u'嗨' fail = ValueError(hi_cn) - self.assertEqual(hi_cn, exceptions.exception_message(fail)) + self.assertEqual(hi_cn, encodeutils.exception_to_unicode(fail)) fail = failure.Failure.from_exception(fail) wrapped_fail = exceptions.WrappedFailure([fail]) - if six.PY2: - # Python 2.x will unicode escape it, while python 3.3+ will not, - # so we sadly have to differentiate between these two... - expected_result = (u"WrappedFailure: " - "[u'Failure: ValueError: %s']" - % (hi_cn.encode("unicode-escape"))) - else: - expected_result = (u"WrappedFailure: " - "['Failure: ValueError: %s']" % (hi_cn)) + expected_result = (u"WrappedFailure: " + "[Failure: ValueError: %s]" % (hi_cn)) self.assertEqual(expected_result, six.text_type(wrapped_fail)) def test_failure_equality_with_non_ascii_str(self): diff --git a/taskflow/types/failure.py b/taskflow/types/failure.py index d713098d..fa831ace 100644 --- a/taskflow/types/failure.py +++ b/taskflow/types/failure.py @@ -19,12 +19,16 @@ import os import sys import traceback +from oslo_utils import encodeutils from oslo_utils import reflection import six from taskflow import exceptions as exc +from taskflow.utils import mixins from taskflow.utils import schema_utils as su +_exception_message = encodeutils.exception_to_unicode + def _copy_exc_info(exc_info): if exc_info is None: @@ -65,7 +69,7 @@ def _are_equal_exc_info_tuples(ei1, ei2): if ei1[0] is not ei2[0]: return False if not all((type(ei1[1]) == type(ei2[1]), - exc.exception_message(ei1[1]) == exc.exception_message(ei2[1]), + _exception_message(ei1[1]) == _exception_message(ei2[1]), repr(ei1[1]) == repr(ei2[1]))): return False if ei1[2] == ei2[2]: @@ -75,7 +79,7 @@ def _are_equal_exc_info_tuples(ei1, ei2): return tb1 == tb2 -class Failure(object): +class Failure(mixins.StrMixin): """An immutable object that represents failure. Failure objects encapsulate exception information so that they can be @@ -191,7 +195,7 @@ class Failure(object): if not self._exc_type_names: raise TypeError("Invalid exception type '%s' (%s)" % (exc_info[0], type(exc_info[0]))) - self._exception_str = exc.exception_message(self._exc_info[1]) + self._exception_str = _exception_message(self._exc_info[1]) self._traceback_str = ''.join( traceback.format_tb(self._exc_info[2])) self._causes = kwargs.pop('causes', None) @@ -387,7 +391,7 @@ class Failure(object): self._causes = tuple(self._extract_causes_iter(self.exception)) return self._causes - def __str__(self): + def __unicode__(self): return self.pformat() def pformat(self, traceback=False): diff --git a/taskflow/utils/mixins.py b/taskflow/utils/mixins.py new file mode 100644 index 00000000..5bb0fa47 --- /dev/null +++ b/taskflow/utils/mixins.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six + + +class StrMixin(object): + """Mixin that helps deal with the PY2 and PY3 method differences. + + http://lucumr.pocoo.org/2011/1/22/forwards-compatible-python/ explains + why this is quite useful... + """ + + if six.PY2: + def __str__(self): + try: + return self.__bytes__() + except AttributeError: + return self.__unicode__().encode('utf-8') + else: + def __str__(self): + return self.__unicode__() From 14de80d4b1aa04f0cf0c25a2940fb78ff0b0814b Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 8 Jul 2015 18:35:57 -0700 Subject: [PATCH 07/55] Make currently implemented jobs use @functools.total_ordering This allows jobs to be fully be comparable using the total ordering function to provide the complexity around the various functions needed to achieve/perform comparisons. Also fixes up the various job __eq__ and __lt__ methods to correctly return 'NotImplemented' on unknown types and adds a __hash__ to the redis job (so that it can be used in hashable collections, just like the zookeeper job class). Change-Id: I8820d5cc6b2e7f346ac329f011f41b76fa94b777 --- taskflow/jobs/backends/impl_redis.py | 22 +++++++++++++++++++--- taskflow/jobs/backends/impl_zookeeper.py | 9 +++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/taskflow/jobs/backends/impl_redis.py b/taskflow/jobs/backends/impl_redis.py index 4d61dc01..582b2710 100644 --- a/taskflow/jobs/backends/impl_redis.py +++ b/taskflow/jobs/backends/impl_redis.py @@ -16,6 +16,7 @@ import contextlib import datetime +import functools import string import threading import time @@ -59,6 +60,7 @@ def _translate_failures(): " internal error") +@functools.total_ordering class RedisJob(base.Job): """A redis job.""" @@ -127,10 +129,24 @@ class RedisJob(base.Job): prior_version=self._redis_version) def __lt__(self, other): - if self.created_on == other.created_on: - return self.sequence < other.sequence + if not isinstance(other, RedisJob): + return NotImplemented + if self.board.listings_key == other.board.listings_key: + if self.created_on == other.created_on: + return self.sequence < other.sequence + else: + return self.created_on < other.created_on else: - return self.created_on < other.created_on + return self.board.listings_key < other.board.listings_key + + def __eq__(self, other): + if not isinstance(other, RedisJob): + return NotImplemented + return ((self.board.listings_key, self.created_on, self.sequence) == + (other.board.listings_key, other.created_on, other.sequence)) + + def __hash__(self): + return hash((self.board.listings_key, self.created_on, self.sequence)) @property def created_on(self): diff --git a/taskflow/jobs/backends/impl_zookeeper.py b/taskflow/jobs/backends/impl_zookeeper.py index 15b31034..d1d9eceb 100644 --- a/taskflow/jobs/backends/impl_zookeeper.py +++ b/taskflow/jobs/backends/impl_zookeeper.py @@ -41,6 +41,7 @@ from taskflow.utils import misc LOG = logging.getLogger(__name__) +@functools.total_ordering class ZookeeperJob(base.Job): """A zookeeper job.""" @@ -171,11 +172,19 @@ class ZookeeperJob(base.Job): return states.CLAIMED def __lt__(self, other): + if not isinstance(other, ZookeeperJob): + return NotImplemented if self.root == other.root: return self.sequence < other.sequence else: return self.root < other.root + def __eq__(self, other): + if not isinstance(other, ZookeeperJob): + return NotImplemented + return ((self.root, self.sequence) == + (other.root, other.sequence)) + def __hash__(self): return hash(self.path) From f7e15248152066f90b38916fa7690f868a9bab97 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 22 Jun 2015 16:27:51 -0700 Subject: [PATCH 08/55] Compile lists of retry/task atoms at runtime compile time Instead of recompiling and rebuilding this list every iteration of the ``iterate_retries`` function we can just locally cache this information in the runtime compile function and later just use it directly. Change-Id: I70e8409391d655730da61413300db05b25843350 --- taskflow/engines/action_engine/analyzer.py | 15 ++++++++------- taskflow/engines/action_engine/runtime.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/taskflow/engines/action_engine/analyzer.py b/taskflow/engines/action_engine/analyzer.py index 909d6751..3a88a461 100644 --- a/taskflow/engines/action_engine/analyzer.py +++ b/taskflow/engines/action_engine/analyzer.py @@ -14,12 +14,12 @@ # License for the specific language governing permissions and limitations # under the License. +import functools import itertools from networkx.algorithms import traversal import six -from taskflow import retry as retry_atom from taskflow import states as st @@ -92,6 +92,8 @@ class Analyzer(object): self._execution_graph = runtime.compilation.execution_graph self._check_atom_transition = runtime.check_atom_transition self._fetch_edge_deciders = runtime.fetch_edge_deciders + self._fetch_retries = functools.partial( + runtime.fetch_atoms_by_kind, 'retry') def get_next_nodes(self, node=None): """Get next nodes to run (originating from node or all nodes).""" @@ -207,14 +209,13 @@ class Analyzer(object): yield dst def iterate_retries(self, state=None): - """Iterates retry controllers that match the provided state. + """Iterates retry atoms that match the provided state. - If no state is provided it will yield back all retry controllers. + If no state is provided it will yield back all retry atoms. """ - for node in self._execution_graph.nodes_iter(): - if isinstance(node, retry_atom.Retry): - if not state or self.get_state(node) == state: - yield node + for atom in self._fetch_retries(): + if not state or self.get_state(atom) == state: + yield atom def iterate_all_nodes(self): """Yields back all nodes in the execution graph.""" diff --git a/taskflow/engines/action_engine/runtime.py b/taskflow/engines/action_engine/runtime.py index 0cf303c6..2616b868 100644 --- a/taskflow/engines/action_engine/runtime.py +++ b/taskflow/engines/action_engine/runtime.py @@ -43,6 +43,7 @@ class Runtime(object): self._storage = storage self._compilation = compilation self._atom_cache = {} + self._atoms_by_kind = {} def compile(self): """Compiles & caches frequently used execution helper objects. @@ -63,6 +64,8 @@ class Runtime(object): 'task': self.task_scheduler, } execution_graph = self._compilation.execution_graph + all_retry_atoms = [] + all_task_atoms = [] for atom in self.analyzer.iterate_all_nodes(): metadata = {} walker = sc.ScopeWalker(self.compilation, atom, names_only=True) @@ -70,10 +73,12 @@ class Runtime(object): check_transition_handler = st.check_task_transition change_state_handler = change_state_handlers['task'] scheduler = schedulers['task'] + all_task_atoms.append(atom) else: check_transition_handler = st.check_retry_transition change_state_handler = change_state_handlers['retry'] scheduler = schedulers['retry'] + all_retry_atoms.append(atom) edge_deciders = {} for previous_atom in execution_graph.predecessors(atom): # If there is any link function that says if this connection @@ -89,6 +94,8 @@ class Runtime(object): metadata['scheduler'] = scheduler metadata['edge_deciders'] = edge_deciders self._atom_cache[atom.name] = metadata + self._atoms_by_kind['retry'] = all_retry_atoms + self._atoms_by_kind['task'] = all_task_atoms @property def compilation(self): @@ -150,6 +157,15 @@ class Runtime(object): metadata = self._atom_cache[atom.name] return metadata['edge_deciders'] + def fetch_atoms_by_kind(self, kind): + """Fetches all the atoms of a given kind. + + NOTE(harlowja): Currently only ``task`` or ``retry`` are valid + kinds of atoms (requesting other kinds will just + return empty lists). + """ + return self._atoms_by_kind.get(kind, []) + def fetch_scheduler(self, atom): """Fetches the cached specific scheduler for the given atom.""" # This does not check if the name exists (since this is only used From 7c2148d55a0ea881d09023acbc24cdb406969f5d Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 9 Jul 2015 17:03:26 -0700 Subject: [PATCH 09/55] Remove setup.cfg 'requires-python' incorrect entry The classifiers provide this same information and it does not appear that this is frequently used in openstack so let's just remove it. Change-Id: Iced678888e037f060f2b1e33aa950c82392639c8 --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index de69b76a..31fc17ab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,7 +7,6 @@ author = Taskflow Developers author-email = taskflow-dev@lists.launchpad.net home-page = https://launchpad.net/taskflow keywords = reliable,tasks,execution,parallel,dataflow,workflows,distributed -requires-python = >=2.6 classifier = Development Status :: 4 - Beta Environment :: OpenStack From 65cecfedbdded1d36aa3dcbc6734e65ecac72f74 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 9 Jul 2015 16:17:01 -0700 Subject: [PATCH 10/55] Add docs for u, v, decider on graph flow link method The docstring is now quite important to know what the params are (they are no longer as obvious due to the decider addition) so add a useful docstring to that method. This also fixes how the targeted flow add was not passing allow **kwargs as it should have been and removes the duplicated docstrings that are in the child class (the parent class docstrings are just fine). Change-Id: Idacb7ee9f652fab4bdc762c1b49d7523e46e9a7b --- taskflow/patterns/graph_flow.py | 41 ++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/taskflow/patterns/graph_flow.py b/taskflow/patterns/graph_flow.py index 50a4d61d..9e255871 100644 --- a/taskflow/patterns/graph_flow.py +++ b/taskflow/patterns/graph_flow.py @@ -69,7 +69,20 @@ class Flow(flow.Flow): _unsatisfied_requires = staticmethod(_unsatisfied_requires) def link(self, u, v, decider=None): - """Link existing node u as a runtime dependency of existing node v.""" + """Link existing node u as a runtime dependency of existing node v. + + :param u: task or flow to create a link from (must exist already) + :param v: task or flow to create a link to (must exist already) + :param 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`` decideable + links that have ``v`` as a target. It is expected to + return a single boolean (``True`` to allow ``v`` + execution or ``False`` to not). + """ if not self._graph.has_node(u): raise ValueError("Node '%s' not found to link from" % (u)) if not self._graph.has_node(v): @@ -251,6 +264,18 @@ class Flow(flow.Flow): return frozenset(requires) +def _reset_cached_subgraph(func): + """Resets cached subgraph after execution, in case it was affected.""" + + @six.wraps(func) + def wrapper(self, *args, **kwargs): + result = func(self, *args, **kwargs) + self._subgraph = None + return result + + return wrapper + + class TargetedFlow(Flow): """Graph flow with a target. @@ -282,19 +307,9 @@ class TargetedFlow(Flow): self._target = None self._subgraph = None - def add(self, *nodes): - """Adds a given task/tasks/flow/flows to this flow.""" - super(TargetedFlow, self).add(*nodes) - # reset cached subgraph, in case it was affected - self._subgraph = None - return self + add = _reset_cached_subgraph(Flow.add) - def link(self, u, v, decider=None): - """Link existing node u as a runtime dependency of existing node v.""" - super(TargetedFlow, self).link(u, v, decider=decider) - # reset cached subgraph, in case it was affected - self._subgraph = None - return self + link = _reset_cached_subgraph(Flow.link) def _get_subgraph(self): if self._subgraph is not None: From a3fe3eb698e7bfa20b0b7fddd91c37a44c092f2c Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 17 Jun 2015 11:28:57 -0700 Subject: [PATCH 11/55] Retain atom 'revert' result (or failure) When a atom is reverted it can be useful to retain the result of that 'revert' method being called, so that it can be later analyzed (or used for various purposes) so adjust the storage, and actions to enable it to be stored. Change-Id: I38a9a5f3bf7550e924468bb4a86652cb8beb306c --- doc/source/img/retry_states.svg | 4 +- doc/source/img/task_states.svg | 4 +- doc/source/states.rst | 34 +++- .../engines/action_engine/actions/base.py | 16 +- .../engines/action_engine/actions/retry.py | 20 +- .../engines/action_engine/actions/task.py | 22 +- taskflow/engines/action_engine/completer.py | 1 + taskflow/engines/action_engine/engine.py | 7 +- ..._add_revert_results_and_revert_failure_.py | 42 ++++ .../persistence/backends/sqlalchemy/tables.py | 2 + taskflow/persistence/models.py | 189 +++++++++++++----- taskflow/states.py | 15 +- taskflow/storage.py | 179 ++++++++++++----- .../tests/unit/action_engine/test_runner.py | 3 +- taskflow/tests/unit/test_check_transition.py | 12 +- taskflow/tests/unit/test_engines.py | 56 ++++-- taskflow/tests/unit/test_retries.py | 182 ++++++++--------- taskflow/tests/unit/test_storage.py | 36 +++- taskflow/tests/unit/test_suspend.py | 18 +- .../tests/unit/worker_based/test_worker.py | 2 +- taskflow/tests/utils.py | 9 + taskflow/types/failure.py | 12 +- tools/state_graph.py | 2 +- 23 files changed, 582 insertions(+), 285 deletions(-) create mode 100644 taskflow/persistence/backends/sqlalchemy/alembic/versions/3162c0f3f8e4_add_revert_results_and_revert_failure_.py diff --git a/doc/source/img/retry_states.svg b/doc/source/img/retry_states.svg index d6801b19..abf8498e 100644 --- a/doc/source/img/retry_states.svg +++ b/doc/source/img/retry_states.svg @@ -3,6 +3,6 @@ - -Retries statesPENDINGIGNORERUNNINGSUCCESSFAILURERETRYINGREVERTINGREVERTEDstart + +Retries statesPENDINGIGNORERUNNINGSUCCESSFAILURERETRYINGREVERTINGREVERTEDREVERT_FAILUREstart diff --git a/doc/source/img/task_states.svg b/doc/source/img/task_states.svg index 9c27c843..a9368e31 100644 --- a/doc/source/img/task_states.svg +++ b/doc/source/img/task_states.svg @@ -3,6 +3,6 @@ - -Tasks statesPENDINGIGNORERUNNINGFAILURESUCCESSREVERTINGREVERTEDstart + +Tasks statesPENDINGIGNORERUNNINGFAILURESUCCESSREVERTINGREVERTEDREVERT_FAILUREstart diff --git a/doc/source/states.rst b/doc/source/states.rst index 01e9da59..3d42bad1 100644 --- a/doc/source/states.rst +++ b/doc/source/states.rst @@ -136,19 +136,25 @@ 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 -execution). +running its :py:meth:`~taskflow.task.BaseTask.execute` method). **FAILURE** - The engine running the task transitions the task to this state -after it has finished with an error. +after it has finished with an error (ie exception/s were raised during +running its :py:meth:`~taskflow.task.BaseTask.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 :py:meth:`~taskflow.task.BaseTask.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 :py:meth:`~taskflow.task.BaseTask.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 ``FAILURE`` state (if it was already -in the ``FAILURE`` state then this is a no-op). +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** - A task that has been reverted appears in this 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 :py:meth:`~taskflow.task.BaseTask.revert` method). Retry ===== @@ -188,17 +194,23 @@ state until its :py:meth:`~taskflow.retry.Retry.execute` method returns. it was finished successfully (ie no exception/s were raised during execution). -**FAILURE** - The engine running the retry transitions it to this state after -it has finished with an error. +**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 :py:meth:`~taskflow.retry.Retry.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 :py:meth:`~taskflow.retry.Retry.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 :py:meth:`~taskflow.retry.Retry.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 ``FAILURE`` state (if it was -already in the ``FAILURE`` state then this is a no-op). +raises an exception), the retry goes to the ``REVERT_FAILURE`` state. -**REVERTED** - A retry that has been reverted appears in this 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 :py:meth:`~taskflow.retry.Retry.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 diff --git a/taskflow/engines/action_engine/actions/base.py b/taskflow/engines/action_engine/actions/base.py index 369a6c66..48846746 100644 --- a/taskflow/engines/action_engine/actions/base.py +++ b/taskflow/engines/action_engine/actions/base.py @@ -21,17 +21,19 @@ import six from taskflow import states -#: Sentinel use to represent no-result (none can be a valid result...) -NO_RESULT = object() - -#: States that are expected to/may have a result to save... -SAVE_RESULT_STATES = (states.SUCCESS, states.FAILURE) - - @six.add_metaclass(abc.ABCMeta) class Action(object): """An action that handles executing, state changes, ... of atoms.""" + NO_RESULT = object() + """ + Sentinel use to represent lack of any result (none can be a valid result) + """ + + #: States that are expected to have a result to save... + SAVE_RESULT_STATES = (states.SUCCESS, states.FAILURE, + states.REVERTED, states.REVERT_FAILURE) + def __init__(self, storage, notifier): self._storage = storage self._notifier = notifier diff --git a/taskflow/engines/action_engine/actions/retry.py b/taskflow/engines/action_engine/actions/retry.py index c8cad50a..e8b076b4 100644 --- a/taskflow/engines/action_engine/actions/retry.py +++ b/taskflow/engines/action_engine/actions/retry.py @@ -60,19 +60,21 @@ class RetryAction(base.Action): arguments.update(addons) return arguments - def change_state(self, retry, state, result=base.NO_RESULT): + def change_state(self, retry, state, result=base.Action.NO_RESULT): old_state = self._storage.get_atom_state(retry.name) - if state in base.SAVE_RESULT_STATES: + if state in self.SAVE_RESULT_STATES: save_result = None - if result is not base.NO_RESULT: + if result is not self.NO_RESULT: save_result = result self._storage.save(retry.name, save_result, state) - elif state == states.REVERTED: - self._storage.cleanup_retry_history(retry.name, state) + # TODO(harlowja): combine this with the save to avoid a call + # back into the persistence layer... + if state == states.REVERTED: + self._storage.cleanup_retry_history(retry.name, state) else: if state == old_state: # NOTE(imelnikov): nothing really changed, so we should not - # write anything to storage and run notifications + # write anything to storage and run notifications. return self._storage.set_atom_state(retry.name, state) retry_uuid = self._storage.get_atom_uuid(retry.name) @@ -81,7 +83,7 @@ class RetryAction(base.Action): 'retry_uuid': retry_uuid, 'old_state': old_state, } - if result is not base.NO_RESULT: + if result is not self.NO_RESULT: details['result'] = result self._notifier.notify(state, details) @@ -106,9 +108,9 @@ class RetryAction(base.Action): def _on_done_callback(fut): result = fut.result()[-1] if isinstance(result, failure.Failure): - self.change_state(retry, states.FAILURE) + self.change_state(retry, states.REVERT_FAILURE, result=result) else: - self.change_state(retry, states.REVERTED) + self.change_state(retry, states.REVERTED, result=result) self.change_state(retry, states.REVERTING) arg_addons = { diff --git a/taskflow/engines/action_engine/actions/task.py b/taskflow/engines/action_engine/actions/task.py index ab4b50d9..7ae6b55f 100644 --- a/taskflow/engines/action_engine/actions/task.py +++ b/taskflow/engines/action_engine/actions/task.py @@ -32,8 +32,8 @@ class TaskAction(base.Action): super(TaskAction, self).__init__(storage, notifier) self._task_executor = task_executor - def _is_identity_transition(self, old_state, state, task, progress): - if state in base.SAVE_RESULT_STATES: + def _is_identity_transition(self, old_state, state, task, progress=None): + if state in self.SAVE_RESULT_STATES: # saving result is never identity transition return False if state != old_state: @@ -50,16 +50,17 @@ class TaskAction(base.Action): return True def change_state(self, task, state, - result=base.NO_RESULT, progress=None): + progress=None, result=base.Action.NO_RESULT): old_state = self._storage.get_atom_state(task.name) - if self._is_identity_transition(old_state, state, task, progress): + if self._is_identity_transition(old_state, state, task, + progress=progress): # NOTE(imelnikov): ignore identity transitions in order # to avoid extra write to storage backend and, what's - # more important, extra notifications + # more important, extra notifications. return - if state in base.SAVE_RESULT_STATES: + if state in self.SAVE_RESULT_STATES: save_result = None - if result is not base.NO_RESULT: + if result is not self.NO_RESULT: save_result = result self._storage.save(task.name, save_result, state) else: @@ -72,7 +73,7 @@ class TaskAction(base.Action): 'task_uuid': task_uuid, 'old_state': old_state, } - if result is not base.NO_RESULT: + if result is not self.NO_RESULT: details['result'] = result self._notifier.notify(state, details) if progress is not None: @@ -140,9 +141,10 @@ class TaskAction(base.Action): def complete_reversion(self, task, result): if isinstance(result, failure.Failure): - self.change_state(task, states.FAILURE) + self.change_state(task, states.REVERT_FAILURE, result=result) else: - self.change_state(task, states.REVERTED, progress=1.0) + self.change_state(task, states.REVERTED, progress=1.0, + result=result) def wait_for_any(self, fs, timeout): return self._task_executor.wait_for_any(fs, timeout) diff --git a/taskflow/engines/action_engine/completer.py b/taskflow/engines/action_engine/completer.py index 318e3bc0..47300a46 100644 --- a/taskflow/engines/action_engine/completer.py +++ b/taskflow/engines/action_engine/completer.py @@ -152,6 +152,7 @@ class Completer(object): if event == ex.EXECUTED: self._process_atom_failure(node, result) else: + # Reverting failed, always retain the failure... return True return False diff --git a/taskflow/engines/action_engine/engine.py b/taskflow/engines/action_engine/engine.py index 9da9ae9d..fed8d4ce 100644 --- a/taskflow/engines/action_engine/engine.py +++ b/taskflow/engines/action_engine/engine.py @@ -16,6 +16,7 @@ import collections import contextlib +import itertools import threading from concurrent import futures @@ -194,8 +195,10 @@ class ActionEngine(base.Engine): if last_state and last_state not in ignorable_states: self._change_state(last_state) if last_state not in self.NO_RERAISING_STATES: - failures = self.storage.get_failures() - failure.Failure.reraise_if_any(failures.values()) + it = itertools.chain( + six.itervalues(self.storage.get_failures()), + six.itervalues(self.storage.get_revert_failures())) + failure.Failure.reraise_if_any(it) def _change_state(self, state): with self._state_lock: diff --git a/taskflow/persistence/backends/sqlalchemy/alembic/versions/3162c0f3f8e4_add_revert_results_and_revert_failure_.py b/taskflow/persistence/backends/sqlalchemy/alembic/versions/3162c0f3f8e4_add_revert_results_and_revert_failure_.py new file mode 100644 index 00000000..dd54dff3 --- /dev/null +++ b/taskflow/persistence/backends/sqlalchemy/alembic/versions/3162c0f3f8e4_add_revert_results_and_revert_failure_.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Add 'revert_results' and 'revert_failure' atom detail column. + +Revision ID: 3162c0f3f8e4 +Revises: 589dccdf2b6e +Create Date: 2015-06-17 15:52:56.575245 + +""" + +# revision identifiers, used by Alembic. +revision = '3162c0f3f8e4' +down_revision = '589dccdf2b6e' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('atomdetails', + sa.Column('revert_results', sa.Text(), nullable=True)) + op.add_column('atomdetails', + sa.Column('revert_failure', sa.Text(), nullable=True)) + + +def downgrade(): + op.drop_column('atomdetails', 'revert_results') + op.drop_column('atomdetails', 'revert_failure') diff --git a/taskflow/persistence/backends/sqlalchemy/tables.py b/taskflow/persistence/backends/sqlalchemy/tables.py index 28acca1a..65969fb2 100644 --- a/taskflow/persistence/backends/sqlalchemy/tables.py +++ b/taskflow/persistence/backends/sqlalchemy/tables.py @@ -92,6 +92,8 @@ def fetch(metadata): default=uuidutils.generate_uuid), Column('failure', Json), Column('results', Json), + Column('revert_results', Json), + Column('revert_failure', Json), Column('atom_type', Enum(*models.ATOM_TYPES, name='atom_types')), Column('intention', Enum(*states.INTENTIONS, diff --git a/taskflow/persistence/models.py b/taskflow/persistence/models.py index c7a6eae5..e41d6d79 100644 --- a/taskflow/persistence/models.py +++ b/taskflow/persistence/models.py @@ -32,6 +32,14 @@ LOG = logging.getLogger(__name__) # Internal helpers... +def _is_all_none(arg, *args): + if arg is not None: + return False + for more_arg in args: + if more_arg is not None: + return False + return True + def _copy_function(deep_copy): if deep_copy: @@ -413,11 +421,18 @@ class AtomDetail(object): strategies). :ivar results: Any results the atom produced from either its ``execute`` method or from other sources. - :ivar failure: If the atom failed (possibly due to its ``execute`` - method raising) this will be a + :ivar revert_results: Any results the atom produced from either its + ``revert`` method or from other sources. + :ivar failure: If the atom failed (due to its ``execute`` method + raising) this will be a :py:class:`~taskflow.types.failure.Failure` object that represents that failure (if there was no failure this will be set to none). + :ivar revert_failure: If the atom failed (possibly due to its ``revert`` + method raising) this will be a + :py:class:`~taskflow.types.failure.Failure` object + that represents that failure (if there was no + failure this will be set to none). """ def __init__(self, name, uuid): @@ -427,6 +442,8 @@ class AtomDetail(object): self.intention = states.EXECUTE self.results = None self.failure = None + self.revert_results = None + self.revert_failure = None self.meta = {} self.version = None @@ -465,6 +482,8 @@ class AtomDetail(object): self.meta = ad.meta self.failure = ad.failure self.results = ad.results + self.revert_results = ad.revert_results + self.revert_failure = ad.revert_failure self.version = ad.version return self @@ -503,6 +522,16 @@ class AtomDetail(object): self.failure = other.failure else: self.failure = None + if self.revert_failure != other.revert_failure: + # NOTE(imelnikov): we can't just deep copy Failures, as they + # contain tracebacks, which are not copyable. + if other.revert_failure: + if deep_copy: + self.revert_failure = other.revert_failure.copy() + else: + self.revert_failure = other.revert_failure + else: + self.revert_failure = None if self.meta != other.meta: self.meta = copy_fn(other.meta) if self.version != other.version: @@ -522,11 +551,17 @@ class AtomDetail(object): failure = self.failure.to_dict() else: failure = None + if self.revert_failure: + revert_failure = self.revert_failure.to_dict() + else: + revert_failure = None return { 'failure': failure, + 'revert_failure': revert_failure, 'meta': self.meta, 'name': self.name, 'results': self.results, + 'revert_results': self.revert_results, 'state': self.state, 'version': self.version, 'intention': self.intention, @@ -547,11 +582,15 @@ class AtomDetail(object): obj.state = data.get('state') obj.intention = data.get('intention') obj.results = data.get('results') + obj.revert_results = data.get('revert_results') obj.version = data.get('version') obj.meta = _fix_meta(data) failure = data.get('failure') if failure: obj.failure = ft.Failure.from_dict(failure) + revert_failure = data.get('revert_failure') + if revert_failure: + obj.revert_failure = ft.Failure.from_dict(revert_failure) return obj @property @@ -582,47 +621,65 @@ class TaskDetail(AtomDetail): def reset(self, state): """Resets this task detail and sets ``state`` attribute value. - This sets any previously set ``results`` and ``failure`` attributes - back to ``None`` and sets the state to the provided one, as well as - setting this task details ``intention`` attribute to ``EXECUTE``. + 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``. """ self.results = None self.failure = None + self.revert_results = None + self.revert_failure = None self.state = state self.intention = states.EXECUTE def put(self, state, result): """Puts a result (acquired in the given state) into this detail. - If the result is a :py:class:`~taskflow.types.failure.Failure` object - then the ``failure`` attribute will be set (and the ``results`` - attribute will be set to ``None``); if the result is not a - :py:class:`~taskflow.types.failure.Failure` object then the - ``results`` attribute will be set (and the ``failure`` attribute - will be set to ``None``). In either case the ``state`` - attribute will be set to the provided state. + Returns whether this object was modified (or whether it was not). """ was_altered = False - if self.state != state: + if state != self.state: self.state = state was_altered = True - if self._was_failure(state, result): + if state == states.REVERT_FAILURE: + if self.revert_failure != result: + self.revert_failure = result + was_altered = True + if not _is_all_none(self.results, self.revert_results): + self.results = None + self.revert_results = None + was_altered = True + elif state == states.FAILURE: if self.failure != result: self.failure = result was_altered = True - if self.results is not None: + if not _is_all_none(self.results, self.revert_results, + self.revert_failure): self.results = None + self.revert_results = None + self.revert_failure = None + was_altered = True + elif state == states.SUCCESS: + if not _is_all_none(self.revert_results, self.revert_failure, + self.failure): + self.revert_results = None + self.revert_failure = None + self.failure = None was_altered = True - else: # We don't really have the ability to determine equality of # task (user) results at the current time, without making # potentially bad guesses, so assume the task detail always needs # to be saved if they are not exactly equivalent... - if self.results is not result: + if result is not self.results: self.results = result was_altered = True - if self.failure is not None: - self.failure = None + elif state == states.REVERTED: + if not _is_all_none(self.revert_failure): + self.revert_failure = None + was_altered = True + if result is not self.revert_results: + self.revert_results = result was_altered = True return was_altered @@ -630,10 +687,11 @@ class TaskDetail(AtomDetail): """Merges the current task detail with the given one. NOTE(harlowja): This merge does **not** copy and replace - the ``results`` attribute if it differs. Instead the current - objects ``results`` attribute directly becomes (via assignment) the - other objects ``results`` attribute. Also note that if the provided - object is this object itself then **no** merging is done. + 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 @@ -648,8 +706,8 @@ class TaskDetail(AtomDetail): if other is self: return self super(TaskDetail, self).merge(other, deep_copy=deep_copy) - if self.results != other.results: - self.results = other.results + self.results = other.results + self.revert_results = other.revert_results return self def copy(self): @@ -659,10 +717,10 @@ class TaskDetail(AtomDetail): version information that this object maintains is shallow copied via ``copy.copy``). - NOTE(harlowja): This copy does **not** perform ``copy.copy`` on - the ``results`` attribute of this object (before assigning to the - copy). Instead the current objects ``results`` attribute directly - becomes (via assignment) the copied objects ``results`` attribute. + 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 @@ -673,6 +731,7 @@ class TaskDetail(AtomDetail): """ clone = copy.copy(self) clone.results = self.results + clone.revert_results = self.revert_results if self.meta: clone.meta = self.meta.copy() if self.version: @@ -694,12 +753,15 @@ class RetryDetail(AtomDetail): """Resets this retry detail and sets ``state`` attribute value. This sets any previously added ``results`` back to an empty list - and resets the ``failure`` attribute back to ``None`` and sets the - state to the provided one, as well as setting this atom + 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``. """ self.results = [] + self.revert_results = None self.failure = None + self.revert_failure = None self.state = state self.intention = states.EXECUTE @@ -711,14 +773,15 @@ class RetryDetail(AtomDetail): copied via ``copy.copy``). NOTE(harlowja): This copy does **not** copy - the incoming objects ``results`` attribute. 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 + 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 :py:class:`~taskflow.types.failure.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. + 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 @@ -738,6 +801,7 @@ class RetryDetail(AtomDetail): copied_failures[key] = failure results.append((data, copied_failures)) clone.results = results + clone.revert_results = self.revert_results if self.meta: clone.meta = self.meta.copy() if self.version: @@ -771,21 +835,50 @@ class RetryDetail(AtomDetail): def put(self, state, result): """Puts a result (acquired in the given state) into this detail. - If the result is a :py:class:`~taskflow.types.failure.Failure` object - then the ``failure`` attribute will be set; if the result is not a - :py:class:`~taskflow.types.failure.Failure` object then the - ``results`` attribute will be appended to (and the ``failure`` - attribute will be set to ``None``). In either case the ``state`` - attribute will be set to the provided state. + Returns whether this object was modified (or whether it was not). """ # Do not clean retry history (only on reset does this happen). - self.state = state - if self._was_failure(state, result): - self.failure = result - else: + was_altered = False + if state != self.state: + self.state = state + was_altered = True + if state == states.REVERT_FAILURE: + if result != self.revert_failure: + self.revert_failure = result + was_altered = True + if not _is_all_none(self.revert_results): + self.revert_results = None + was_altered = True + elif state == states.FAILURE: + if result != self.failure: + self.failure = result + was_altered = True + if not _is_all_none(self.revert_results, self.revert_failure): + self.revert_results = None + self.revert_failure = None + was_altered = True + elif state == states.SUCCESS: + if not _is_all_none(self.failure, self.revert_failure, + self.revert_results): + self.failure = None + self.revert_failure = None + self.revert_results = None + # Track what we produced, so that we can examine it (or avoid + # using it again). self.results.append((result, {})) - self.failure = None - return True + was_altered = True + elif state == states.REVERTED: + # We don't really have the ability to determine equality of + # task (user) results at the current time, without making + # potentially bad guesses, so assume the retry detail always needs + # to be saved if they are not exactly equivalent... + if result is not self.revert_results: + self.revert_results = result + was_altered = True + if not _is_all_none(self.revert_failure): + self.revert_failure = None + was_altered = True + return was_altered @classmethod def from_dict(cls, data): diff --git a/taskflow/states.py b/taskflow/states.py index cbef58c7..07e70dd1 100644 --- a/taskflow/states.py +++ b/taskflow/states.py @@ -41,6 +41,7 @@ SUCCESS = SUCCESS RUNNING = RUNNING RETRYING = 'RETRYING' IGNORE = 'IGNORE' +REVERT_FAILURE = 'REVERT_FAILURE' # Atom intentions. EXECUTE = 'EXECUTE' @@ -157,20 +158,20 @@ def check_flow_transition(old_state, new_state): # Task state transitions -# See: http://docs.openstack.org/developer/taskflow/states.html +# See: http://docs.openstack.org/developer/taskflow/states.html#task _ALLOWED_TASK_TRANSITIONS = frozenset(( (PENDING, RUNNING), # run it! (PENDING, IGNORE), # skip it! - (RUNNING, SUCCESS), # the task finished successfully - (RUNNING, FAILURE), # the task failed + (RUNNING, SUCCESS), # the task executed successfully + (RUNNING, FAILURE), # the task execution failed - (FAILURE, REVERTING), # task failed, do cleanup now - (SUCCESS, REVERTING), # some other task failed, do cleanup now + (FAILURE, REVERTING), # task execution failed, try reverting... + (SUCCESS, REVERTING), # some other task failed, try reverting... - (REVERTING, REVERTED), # revert done - (REVERTING, FAILURE), # revert failed + (REVERTING, REVERTED), # the task reverted successfully + (REVERTING, REVERT_FAILURE), # the task failed reverting (terminal!) (REVERTED, PENDING), # try again (IGNORE, PENDING), # try again diff --git a/taskflow/storage.py b/taskflow/storage.py index 05b48999..cab68f6d 100644 --- a/taskflow/storage.py +++ b/taskflow/storage.py @@ -28,15 +28,43 @@ from taskflow.persistence import models from taskflow import retry from taskflow import states from taskflow import task -from taskflow.types import failure from taskflow.utils import misc LOG = logging.getLogger(__name__) -STATES_WITH_RESULTS = (states.SUCCESS, states.REVERTING, states.FAILURE) + + +_EXECUTE_STATES_WITH_RESULTS = ( + # The atom ``execute`` worked out :) + states.SUCCESS, + # The atom ``execute`` didn't work out :( + states.FAILURE, + # In this state we will still have access to prior SUCCESS (or FAILURE) + # results, so make sure extraction is still allowed in this state... + states.REVERTING, +) + +_REVERT_STATES_WITH_RESULTS = ( + # The atom ``revert`` worked out :) + states.REVERTED, + # The atom ``revert`` didn't work out :( + states.REVERT_FAILURE, + # In this state we will still have access to prior SUCCESS (or FAILURE) + # results, so make sure extraction is still allowed in this state... + states.REVERTING, +) + +# Atom states that may have results... +STATES_WITH_RESULTS = set() +STATES_WITH_RESULTS.update(_REVERT_STATES_WITH_RESULTS) +STATES_WITH_RESULTS.update(_EXECUTE_STATES_WITH_RESULTS) +STATES_WITH_RESULTS = tuple(sorted(STATES_WITH_RESULTS)) # TODO(harlowja): do this better (via a singleton or something else...) _TRANSIENT_PROVIDER = object() +# Only for these intentions will we cache any failures that happened... +_SAVE_FAILURE_INTENTIONS = (states.EXECUTE, states.REVERT) + # NOTE(harlowja): Perhaps the container is a dictionary-like object and that # key does not exist (key error), or the container is a tuple/list and a # non-numeric key is being requested (index error), or there was no container @@ -164,8 +192,12 @@ class Storage(object): # so we cache failures here, in atom name -> failure mapping. self._failures = {} for ad in self._flowdetail: + fail_cache = {} if ad.failure is not None: - self._failures[ad.name] = ad.failure + fail_cache[states.EXECUTE] = ad.failure + if ad.revert_failure is not None: + fail_cache[states.REVERT] = ad.revert_failure + self._failures[ad.name] = fail_cache self._atom_name_to_uuid = dict((ad.name, ad.uuid) for ad in self._flowdetail) @@ -247,6 +279,7 @@ class Storage(object): atom_ids[i] = ad.uuid self._atom_name_to_uuid[atom_name] = ad.uuid self._set_result_mapping(atom_name, atom.save_as) + self._failures.setdefault(atom_name, {}) return atom_ids def ensure_atom(self, atom): @@ -448,21 +481,23 @@ class Storage(object): "with index %r (name %s)", atom_name, index, name) @fasteners.write_locked - def save(self, atom_name, data, state=states.SUCCESS): - """Save result for named atom into storage with given state.""" + def save(self, atom_name, result, state=states.SUCCESS): + """Put result for atom with provided name to storage.""" source, clone = self._atomdetail_by_name(atom_name, clone=True) - if clone.put(state, data): - result = self._with_connection(self._save_atom_detail, - source, clone) - else: - result = clone - if state == states.FAILURE and isinstance(data, failure.Failure): + if clone.put(state, result): + self._with_connection(self._save_atom_detail, source, clone) + # We need to somehow place more of this responsibility on the atom + # detail class itself, vs doing it here; since it ties those two + # together (which is bad)... + if state in (states.FAILURE, states.REVERT_FAILURE): # NOTE(imelnikov): failure serialization looses information, # so we cache failures here, in atom name -> failure mapping so # that we can later use the better version on fetch/get. - self._failures[result.name] = data - else: - self._check_all_results_provided(result.name, data) + if clone.intention in _SAVE_FAILURE_INTENTIONS: + fail_cache = self._failures[clone.name] + fail_cache[clone.intention] = result + if state == states.SUCCESS and clone.intention == states.EXECUTE: + self._check_all_results_provided(clone.name, result) @fasteners.write_locked def save_retry_failure(self, retry_name, failed_atom_name, failure): @@ -491,39 +526,69 @@ class Storage(object): self._with_connection(self._save_atom_detail, source, clone) @fasteners.read_locked - def _get(self, atom_name, only_last=False): + def _get(self, atom_name, + results_attr_name, fail_attr_name, + allowed_states, fail_cache_key): source, _clone = self._atomdetail_by_name(atom_name) - if source.failure is not None: - cached = self._failures.get(atom_name) - if source.failure.matches(cached): - # Try to give the version back that should have the backtrace - # instead of one that has it stripped (since backtraces are not - # serializable). - return cached - return source.failure - if source.state not in STATES_WITH_RESULTS: + failure = getattr(source, fail_attr_name) + if failure is not None: + fail_cache = self._failures[atom_name] + try: + fail = fail_cache[fail_cache_key] + if failure.matches(fail): + # Try to give the version back that should have the + # backtrace instead of one that has it + # stripped (since backtraces are not serializable). + failure = fail + except KeyError: + pass + return failure + # TODO(harlowja): this seems like it should be checked before fetching + # the potential failure, instead of after, fix this soon... + if source.state not in allowed_states: raise exceptions.NotFound("Result for atom %s is not currently" " known" % atom_name) - if only_last: - return source.last_results - else: - return source.results + return getattr(source, results_attr_name) - def get(self, atom_name): - """Gets the results for an atom with a given name from storage.""" - return self._get(atom_name) + def get_execute_result(self, atom_name): + """Gets the ``execute`` results for an atom from storage.""" + return self._get(atom_name, 'results', 'failure', + _EXECUTE_STATES_WITH_RESULTS, states.EXECUTE) @fasteners.read_locked - def get_failures(self): - """Get list of failures that happened with this flow. + def _get_failures(self, fail_cache_key): + failures = {} + for atom_name, fail_cache in six.iteritems(self._failures): + try: + failures[atom_name] = fail_cache[fail_cache_key] + except KeyError: + pass + return failures - No order guaranteed. - """ - return self._failures.copy() + def get_execute_failures(self): + """Get all ``execute`` failures that happened with this flow.""" + return self._get_failures(states.EXECUTE) + # TODO(harlowja): remove these in the future? + get = get_execute_result + get_failures = get_execute_failures + + def get_revert_result(self, atom_name): + """Gets the ``revert`` results for an atom from storage.""" + return self._get(atom_name, 'revert_results', 'revert_failure', + _REVERT_STATES_WITH_RESULTS, states.REVERT) + + def get_revert_failures(self): + """Get all ``revert`` failures that happened with this flow.""" + return self._get_failures(states.REVERT) + + @fasteners.read_locked def has_failures(self): - """Returns True if there are failed tasks in the storage.""" - return bool(self._failures) + """Returns true if there are **any** failures in storage.""" + for fail_cache in six.itervalues(self._failures): + if fail_cache: + return True + return False @fasteners.write_locked def reset(self, atom_name, state=states.PENDING): @@ -534,8 +599,8 @@ class Storage(object): if source.state == state: return clone.reset(state) - result = self._with_connection(self._save_atom_detail, source, clone) - self._failures.pop(result.name, None) + self._with_connection(self._save_atom_detail, source, clone) + self._failures[clone.name].clear() def inject_atom_args(self, atom_name, pairs, transient=True): """Add values into storage for a specific atom only. @@ -681,7 +746,7 @@ class Storage(object): @fasteners.read_locked def fetch(self, name, many_handler=None): - """Fetch a named result.""" + """Fetch a named ``execute`` result.""" def _many_handler(values): # By default we just return the first of many (unless provided # a different callback that can translate many results into @@ -702,7 +767,10 @@ class Storage(object): self._transients, name)) else: try: - container = self._get(provider.name, only_last=True) + container = self._get(provider.name, + 'last_results', 'failure', + _EXECUTE_STATES_WITH_RESULTS, + states.EXECUTE) except exceptions.NotFound: pass else: @@ -717,7 +785,7 @@ class Storage(object): @fasteners.read_locked def fetch_unsatisfied_args(self, atom_name, args_mapping, scope_walker=None, optional_args=None): - """Fetch unsatisfied atom arguments using an atoms argument mapping. + """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 @@ -756,7 +824,9 @@ class Storage(object): results = self._transients else: try: - results = self._get(p.name, only_last=True) + results = self._get(p.name, 'last_results', 'failure', + _EXECUTE_STATES_WITH_RESULTS, + states.EXECUTE) except exceptions.NotFound: results = {} try: @@ -802,7 +872,7 @@ class Storage(object): @fasteners.read_locked def fetch_all(self, many_handler=None): - """Fetch all named results known so far.""" + """Fetch all named ``execute`` results known so far.""" def _many_handler(values): if len(values) > 1: return values @@ -821,7 +891,7 @@ class Storage(object): def fetch_mapped_args(self, args_mapping, atom_name=None, scope_walker=None, optional_args=None): - """Fetch arguments for an atom using an atoms argument mapping.""" + """Fetch ``execute`` arguments for an atom using its args mapping.""" def _extract_first_from(name, sources): """Extracts/returns first occurence of key in list of dicts.""" @@ -835,7 +905,9 @@ class Storage(object): def _get_results(looking_for, provider): """Gets the results saved for a given provider.""" try: - return self._get(provider.name, only_last=True) + return self._get(provider.name, 'last_results', 'failure', + _EXECUTE_STATES_WITH_RESULTS, + states.EXECUTE) except exceptions.NotFound: exceptions.raise_with_cause(exceptions.NotFound, "Expected to be able to find" @@ -963,11 +1035,14 @@ class Storage(object): # NOTE(harlowja): Try to use our local cache to get a more # complete failure object that has a traceback (instead of the # one that is saved which will *typically* not have one)... - cached = self._failures.get(ad.name) - if ad.failure.matches(cached): - failure = cached - else: - failure = ad.failure + failure = ad.failure + fail_cache = self._failures[ad.name] + try: + fail = fail_cache[states.EXECUTE] + if failure.matches(fail): + failure = fail + except KeyError: + pass return retry.History(ad.results, failure=failure) @fasteners.read_locked diff --git a/taskflow/tests/unit/action_engine/test_runner.py b/taskflow/tests/unit/action_engine/test_runner.py index 401cf50d..9d43f312 100644 --- a/taskflow/tests/unit/action_engine/test_runner.py +++ b/taskflow/tests/unit/action_engine/test_runner.py @@ -126,7 +126,8 @@ class RunnerTest(test.TestCase, _RunnerTestMixin): failure = failures[0] self.assertTrue(failure.check(RuntimeError)) - self.assertEqual(st.FAILURE, rt.storage.get_atom_state(tasks[0].name)) + self.assertEqual(st.REVERT_FAILURE, + rt.storage.get_atom_state(tasks[0].name)) def test_run_iterations_suspended(self): flow = lf.Flow("root") diff --git a/taskflow/tests/unit/test_check_transition.py b/taskflow/tests/unit/test_check_transition.py index 7c820fd9..a8b5a7c3 100644 --- a/taskflow/tests/unit/test_check_transition.py +++ b/taskflow/tests/unit/test_check_transition.py @@ -21,11 +21,16 @@ from taskflow import test class TransitionTest(test.TestCase): + _DISALLOWED_TPL = "Transition from '%s' to '%s' was found to be disallowed" + _NOT_IGNORED_TPL = "Transition from '%s' to '%s' was not ignored" + def assertTransitionAllowed(self, from_state, to_state): - self.assertTrue(self.check_transition(from_state, to_state)) + msg = self._DISALLOWED_TPL % (from_state, to_state) + self.assertTrue(self.check_transition(from_state, to_state), msg=msg) def assertTransitionIgnored(self, from_state, to_state): - self.assertFalse(self.check_transition(from_state, to_state)) + msg = self._NOT_IGNORED_TPL % (from_state, to_state) + self.assertFalse(self.check_transition(from_state, to_state), msg=msg) def assertTransitionForbidden(self, from_state, to_state): self.assertRaisesRegexp(exc.InvalidState, @@ -101,7 +106,8 @@ class CheckTaskTransitionTest(TransitionTest): def test_from_reverting_state(self): self.assertTransitions(from_state=states.REVERTING, - allowed=(states.FAILURE, states.REVERTED), + allowed=(states.REVERT_FAILURE, + states.REVERTED), ignored=(states.RUNNING, states.REVERTING, states.PENDING, states.SUCCESS)) diff --git a/taskflow/tests/unit/test_engines.py b/taskflow/tests/unit/test_engines.py index 255ac4fd..5cc242cb 100644 --- a/taskflow/tests/unit/test_engines.py +++ b/taskflow/tests/unit/test_engines.py @@ -66,7 +66,7 @@ class EngineTaskTest(object): engine = self._make_engine(flow) expected = ['fail.f RUNNING', 'fail.t RUNNING', 'fail.t FAILURE(Failure: RuntimeError: Woot!)', - 'fail.t REVERTING', 'fail.t REVERTED', + 'fail.t REVERTING', 'fail.t REVERTED(None)', 'fail.f REVERTED'] with utils.CaptureListener(engine, values=values) as capturer: self.assertFailuresRegexp(RuntimeError, '^Woot', engine.run) @@ -374,6 +374,29 @@ class EngineLinearFlowTest(utils.EngineTestBase): self.assertFailuresRegexp(RuntimeError, '^Woot', engine.run) self.assertEqual(engine.storage.fetch_all(), {}) + def test_revert_provided(self): + flow = lf.Flow('revert').add( + utils.GiveBackRevert('giver'), + utils.FailingTask(name='fail') + ) + engine = self._make_engine(flow, store={'value': 0}) + self.assertFailuresRegexp(RuntimeError, '^Woot', engine.run) + self.assertEqual(engine.storage.get_revert_result('giver'), 2) + + def test_nasty_revert(self): + flow = lf.Flow('revert').add( + utils.NastyTask('nasty'), + utils.FailingTask(name='fail') + ) + engine = self._make_engine(flow) + self.assertFailuresRegexp(RuntimeError, '^Gotcha', engine.run) + fail = engine.storage.get_revert_result('nasty') + self.assertIsNotNone(fail.check(RuntimeError)) + exec_failures = engine.storage.get_execute_failures() + self.assertIn('fail', exec_failures) + rev_failures = engine.storage.get_revert_failures() + self.assertIn('nasty', rev_failures) + def test_sequential_flow_nested_blocks(self): flow = lf.Flow('nested-1').add( utils.ProgressingTask('task1'), @@ -406,7 +429,7 @@ class EngineLinearFlowTest(utils.EngineTestBase): self.assertFailuresRegexp(RuntimeError, '^Woot', engine.run) expected = ['fail.t RUNNING', 'fail.t FAILURE(Failure: RuntimeError: Woot!)', - 'fail.t REVERTING', 'fail.t REVERTED'] + 'fail.t REVERTING', 'fail.t REVERTED(None)'] self.assertEqual(expected, capturer.values) def test_correctly_reverts_children(self): @@ -424,9 +447,9 @@ class EngineLinearFlowTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t SUCCESS(5)', 'fail.t RUNNING', 'fail.t FAILURE(Failure: RuntimeError: Woot!)', - 'fail.t REVERTING', 'fail.t REVERTED', - 'task2.t REVERTING', 'task2.t REVERTED', - 'task1.t REVERTING', 'task1.t REVERTED'] + 'fail.t REVERTING', 'fail.t REVERTED(None)', + 'task2.t REVERTING', 'task2.t REVERTED(None)', + 'task1.t REVERTING', 'task1.t REVERTED(None)'] self.assertEqual(expected, capturer.values) @@ -529,18 +552,19 @@ class EngineLinearAndUnorderedExceptionsTest(utils.EngineTestBase): self.assertFailuresRegexp(RuntimeError, '^Woot', engine.run) # NOTE(imelnikov): we don't know if task 3 was run, but if it was, - # it should have been reverted in correct order. + # it should have been REVERTED(None) in correct order. possible_values_no_task3 = [ 'task1.t RUNNING', 'task2.t RUNNING', 'fail.t FAILURE(Failure: RuntimeError: Woot!)', - 'task2.t REVERTED', 'task1.t REVERTED' + 'task2.t REVERTED(None)', 'task1.t REVERTED(None)' ] self.assertIsSuperAndSubsequence(capturer.values, possible_values_no_task3) if 'task3' in capturer.values: possible_values_task3 = [ 'task1.t RUNNING', 'task2.t RUNNING', 'task3.t RUNNING', - 'task3.t REVERTED', 'task2.t REVERTED', 'task1.t REVERTED' + 'task3.t REVERTED(None)', 'task2.t REVERTED(None)', + 'task1.t REVERTED(None)' ] self.assertIsSuperAndSubsequence(capturer.values, possible_values_task3) @@ -561,12 +585,12 @@ class EngineLinearAndUnorderedExceptionsTest(utils.EngineTestBase): self.assertFailuresRegexp(RuntimeError, '^Gotcha', engine.run) # NOTE(imelnikov): we don't know if task 3 was run, but if it was, - # it should have been reverted in correct order. + # it should have been REVERTED(None) in correct order. possible_values = ['task1.t RUNNING', 'task1.t SUCCESS(5)', 'task2.t RUNNING', 'task2.t SUCCESS(5)', 'task3.t RUNNING', 'task3.t SUCCESS(5)', 'task3.t REVERTING', - 'task3.t REVERTED'] + 'task3.t REVERTED(None)'] self.assertIsSuperAndSubsequence(possible_values, capturer.values) possible_values_no_task3 = ['task1.t RUNNING', 'task2.t RUNNING'] self.assertIsSuperAndSubsequence(capturer.values, @@ -589,12 +613,12 @@ class EngineLinearAndUnorderedExceptionsTest(utils.EngineTestBase): # NOTE(imelnikov): if task1 was run, it should have been reverted. if 'task1' in capturer.values: task1_story = ['task1.t RUNNING', 'task1.t SUCCESS(5)', - 'task1.t REVERTED'] + 'task1.t REVERTED(None)'] self.assertIsSuperAndSubsequence(capturer.values, task1_story) # NOTE(imelnikov): task2 should have been run and reverted task2_story = ['task2.t RUNNING', 'task2.t SUCCESS(5)', - 'task2.t REVERTED'] + 'task2.t REVERTED(None)'] self.assertIsSuperAndSubsequence(capturer.values, task2_story) def test_revert_raises_for_linear_in_unordered(self): @@ -608,7 +632,7 @@ class EngineLinearAndUnorderedExceptionsTest(utils.EngineTestBase): engine = self._make_engine(flow) with utils.CaptureListener(engine, capture_flow=False) as capturer: self.assertFailuresRegexp(RuntimeError, '^Gotcha', engine.run) - self.assertNotIn('task2.t REVERTED', capturer.values) + self.assertNotIn('task2.t REVERTED(None)', capturer.values) class EngineGraphFlowTest(utils.EngineTestBase): @@ -697,11 +721,11 @@ class EngineGraphFlowTest(utils.EngineTestBase): 'task3.t RUNNING', 'task3.t FAILURE(Failure: RuntimeError: Woot!)', 'task3.t REVERTING', - 'task3.t REVERTED', + 'task3.t REVERTED(None)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'task1.t REVERTING', - 'task1.t REVERTED'] + 'task1.t REVERTED(None)'] self.assertEqual(expected, capturer.values) self.assertEqual(engine.storage.get_flow_state(), states.REVERTED) diff --git a/taskflow/tests/unit/test_retries.py b/taskflow/tests/unit/test_retries.py index ddb256b0..54f51bbf 100644 --- a/taskflow/tests/unit/test_retries.py +++ b/taskflow/tests/unit/test_retries.py @@ -82,8 +82,8 @@ class RetryTest(utils.EngineTestBase): 'task1.t RUNNING', 'task1.t SUCCESS(5)', 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot!)', - 'task2.t REVERTING', 'task2.t REVERTED', - 'task1.t REVERTING', 'task1.t REVERTED', + 'task2.t REVERTING', 'task2.t REVERTED(None)', + 'task1.t REVERTING', 'task1.t REVERTED(None)', 'r1.r RETRYING', 'task1.t PENDING', 'task2.t PENDING', @@ -114,9 +114,9 @@ class RetryTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot!)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'task1.t REVERTING', - 'task1.t REVERTED', + 'task1.t REVERTED(None)', 'r1.r RETRYING', 'task1.t PENDING', 'task2.t PENDING', @@ -127,11 +127,11 @@ class RetryTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot!)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'task1.t REVERTING', - 'task1.t REVERTED', + 'task1.t REVERTED(None)', 'r1.r REVERTING', - 'r1.r REVERTED', + 'r1.r REVERTED(None)', 'flow-1.f REVERTED'] self.assertEqual(expected, capturer.values) @@ -153,9 +153,9 @@ class RetryTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot!)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'task1.t REVERTING', - 'task1.t FAILURE', + 'task1.t REVERT_FAILURE(Failure: RuntimeError: Gotcha!)', 'flow-1.f FAILURE'] self.assertEqual(expected, capturer.values) @@ -185,9 +185,9 @@ class RetryTest(utils.EngineTestBase): 'task3.t RUNNING', 'task3.t FAILURE(Failure: RuntimeError: Woot!)', 'task3.t REVERTING', - 'task3.t REVERTED', + 'task3.t REVERTED(None)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'r2.r RETRYING', 'task2.t PENDING', 'task3.t PENDING', @@ -231,15 +231,15 @@ class RetryTest(utils.EngineTestBase): 'task4.t RUNNING', 'task4.t FAILURE(Failure: RuntimeError: Woot!)', 'task4.t REVERTING', - 'task4.t REVERTED', + 'task4.t REVERTED(None)', 'task3.t REVERTING', - 'task3.t REVERTED', + 'task3.t REVERTED(None)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'r2.r REVERTING', - 'r2.r REVERTED', + 'r2.r REVERTED(None)', 'task1.t REVERTING', - 'task1.t REVERTED', + 'task1.t REVERTED(None)', 'r1.r RETRYING', 'task1.t PENDING', 'r2.r PENDING', @@ -280,8 +280,8 @@ class RetryTest(utils.EngineTestBase): 'task2.t FAILURE(Failure: RuntimeError: Woot!)', 'task2.t REVERTING', 'task1.t REVERTING', - 'task2.t REVERTED', - 'task1.t REVERTED', + 'task2.t REVERTED(None)', + 'task1.t REVERTED(None)', 'r.r RETRYING', 'task1.t PENDING', 'task2.t PENDING', @@ -316,11 +316,11 @@ class RetryTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot!)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'r2.r REVERTING', - 'r2.r REVERTED', + 'r2.r REVERTED(None)', 'task1.t REVERTING', - 'task1.t REVERTED', + 'task1.t REVERTED(None)', 'r1.r RETRYING', 'task1.t PENDING', 'r2.r PENDING', @@ -359,9 +359,9 @@ class RetryTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot!)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'r1.r REVERTING', - 'r1.r REVERTED', + 'r1.r REVERTED(None)', 'flow-1.f REVERTED'] self.assertEqual(expected, capturer.values) @@ -388,11 +388,11 @@ class RetryTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot!)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'r1.r REVERTING', - 'r1.r REVERTED', + 'r1.r REVERTED(None)', 'task1.t REVERTING', - 'task1.t REVERTED', + 'task1.t REVERTED(None)', 'flow-1.f REVERTED'] self.assertEqual(expected, capturer.values) @@ -417,13 +417,13 @@ class RetryTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot!)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'r2.r REVERTING', - 'r2.r REVERTED', + 'r2.r REVERTED(None)', 'task1.t REVERTING', - 'task1.t REVERTED', + 'task1.t REVERTED(None)', 'r1.r REVERTING', - 'r1.r REVERTED', + 'r1.r REVERTED(None)', 'flow-1.f REVERTED'] self.assertEqual(expected, capturer.values) @@ -515,7 +515,7 @@ class RetryTest(utils.EngineTestBase): 'c.t RUNNING', 'c.t FAILURE(Failure: RuntimeError: Woot!)', 'c.t REVERTING', - 'c.t REVERTED', + 'c.t REVERTED(None)', 'r1.r RETRYING', 'c.t PENDING', 'r1.r RUNNING', @@ -542,9 +542,9 @@ class RetryTest(utils.EngineTestBase): 't2.t RUNNING', 't2.t FAILURE(Failure: RuntimeError: Woot!)', 't2.t REVERTING', - 't2.t REVERTED', + 't2.t REVERTED(None)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r RETRYING', 't1.t PENDING', 't2.t PENDING', @@ -555,9 +555,9 @@ class RetryTest(utils.EngineTestBase): 't2.t RUNNING', 't2.t FAILURE(Failure: RuntimeError: Woot!)', 't2.t REVERTING', - 't2.t REVERTED', + 't2.t REVERTED(None)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r RETRYING', 't1.t PENDING', 't2.t PENDING', @@ -568,11 +568,11 @@ class RetryTest(utils.EngineTestBase): 't2.t RUNNING', 't2.t FAILURE(Failure: RuntimeError: Woot!)', 't2.t REVERTING', - 't2.t REVERTED', + 't2.t REVERTED(None)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r REVERTING', - 'r1.r REVERTED', + 'r1.r REVERTED(None)', 'flow-1.f REVERTED'] self.assertEqual(expected, capturer.values) @@ -589,7 +589,7 @@ class RetryTest(utils.EngineTestBase): 't1.t RUNNING', 't1.t FAILURE(Failure: RuntimeError: Woot with 3)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r RETRYING', 't1.t PENDING', 'r1.r RUNNING', @@ -597,7 +597,7 @@ class RetryTest(utils.EngineTestBase): 't1.t RUNNING', 't1.t FAILURE(Failure: RuntimeError: Woot with 2)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r RETRYING', 't1.t PENDING', 'r1.r RUNNING', @@ -605,7 +605,7 @@ class RetryTest(utils.EngineTestBase): 't1.t RUNNING', 't1.t FAILURE(Failure: RuntimeError: Woot with 3)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r RETRYING', 't1.t PENDING', 'r1.r RUNNING', @@ -613,9 +613,9 @@ class RetryTest(utils.EngineTestBase): 't1.t RUNNING', 't1.t FAILURE(Failure: RuntimeError: Woot with 5)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r REVERTING', - 'r1.r REVERTED', + 'r1.r REVERTED(None)', 'flow-1.f REVERTED'] self.assertEqual(expected, capturer.values) @@ -632,7 +632,7 @@ class RetryTest(utils.EngineTestBase): 't1.t RUNNING', 't1.t FAILURE(Failure: RuntimeError: Woot with 2)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r RETRYING', 't1.t PENDING', 'r1.r RUNNING', @@ -640,7 +640,7 @@ class RetryTest(utils.EngineTestBase): 't1.t RUNNING', 't1.t FAILURE(Failure: RuntimeError: Woot with 3)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r RETRYING', 't1.t PENDING', 'r1.r RUNNING', @@ -648,9 +648,9 @@ class RetryTest(utils.EngineTestBase): 't1.t RUNNING', 't1.t FAILURE(Failure: RuntimeError: Woot with 5)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r REVERTING', - 'r1.r REVERTED', + 'r1.r REVERTED(None)', 'flow-1.f REVERTED'] self.assertItemsEqual(capturer.values, expected) @@ -674,7 +674,7 @@ class RetryTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot with 3)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'r1.r RETRYING', 'task2.t PENDING', 'r1.r RUNNING', @@ -682,7 +682,7 @@ class RetryTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot with 2)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'r1.r RETRYING', 'task2.t PENDING', 'r1.r RUNNING', @@ -690,7 +690,7 @@ class RetryTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot with 3)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'r1.r RETRYING', 'task2.t PENDING', 'r1.r RUNNING', @@ -698,9 +698,9 @@ class RetryTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot with 5)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'r1.r REVERTING', - 'r1.r REVERTED', + 'r1.r REVERTED(None)', 'flow-1.f REVERTED'] self.assertEqual(expected, capturer.values) @@ -724,7 +724,7 @@ class RetryTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot with 3)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'r1.r RETRYING', 'task2.t PENDING', 'r1.r RUNNING', @@ -732,7 +732,7 @@ class RetryTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot with 2)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'r1.r RETRYING', 'task2.t PENDING', 'r1.r RUNNING', @@ -740,7 +740,7 @@ class RetryTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot with 3)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'r1.r RETRYING', 'task2.t PENDING', 'r1.r RUNNING', @@ -748,11 +748,11 @@ class RetryTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot with 5)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'r1.r REVERTING', - 'r1.r REVERTED', + 'r1.r REVERTED(None)', 'task1.t REVERTING', - 'task1.t REVERTED', + 'task1.t REVERTED(None)', 'flow-1.f REVERTED'] self.assertEqual(expected, capturer.values) @@ -778,7 +778,7 @@ class RetryTest(utils.EngineTestBase): 't1.t RUNNING', 't1.t FAILURE(Failure: RuntimeError: Woot with 3)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r RETRYING', 't1.t PENDING', 'r1.r RUNNING', @@ -786,7 +786,7 @@ class RetryTest(utils.EngineTestBase): 't1.t RUNNING', 't1.t FAILURE(Failure: RuntimeError: Woot with 2)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r RETRYING', 't1.t PENDING', 'r1.r RUNNING', @@ -794,9 +794,9 @@ class RetryTest(utils.EngineTestBase): 't1.t RUNNING', 't1.t FAILURE(Failure: RuntimeError: Woot with 5)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r REVERTING', - 'r1.r REVERTED', + 'r1.r REVERTED(None)', 'flow-1.f REVERTED'] self.assertEqual(expected, capturer.values) @@ -814,7 +814,7 @@ class RetryTest(utils.EngineTestBase): 't1.t RUNNING', 't1.t FAILURE(Failure: RuntimeError: Woot with 3)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r RETRYING', 't1.t PENDING', 'r1.r RUNNING', @@ -822,7 +822,7 @@ class RetryTest(utils.EngineTestBase): 't1.t RUNNING', 't1.t FAILURE(Failure: RuntimeError: Woot with 2)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r RETRYING', 't1.t PENDING', 'r1.r RUNNING', @@ -830,9 +830,9 @@ class RetryTest(utils.EngineTestBase): 't1.t RUNNING', 't1.t FAILURE(Failure: RuntimeError: Woot with 5)', 't1.t REVERTING', - 't1.t REVERTED', + 't1.t REVERTED(None)', 'r1.r REVERTING', - 'r1.r REVERTED', + 'r1.r REVERTED(None)', 'flow-1.f REVERTED'] self.assertItemsEqual(capturer.values, expected) @@ -857,7 +857,7 @@ class RetryTest(utils.EngineTestBase): 'task-2.t RUNNING', 'task-2.t FAILURE(Failure: RuntimeError: Woot with 3)', 'task-2.t REVERTING', - 'task-2.t REVERTED', + 'task-2.t REVERTED(None)', 'r1.r RETRYING', 'task-2.t PENDING', 'r1.r RUNNING', @@ -865,7 +865,7 @@ class RetryTest(utils.EngineTestBase): 'task-2.t RUNNING', 'task-2.t FAILURE(Failure: RuntimeError: Woot with 2)', 'task-2.t REVERTING', - 'task-2.t REVERTED', + 'task-2.t REVERTED(None)', 'r1.r RETRYING', 'task-2.t PENDING', 'r1.r RUNNING', @@ -873,9 +873,9 @@ class RetryTest(utils.EngineTestBase): 'task-2.t RUNNING', 'task-2.t FAILURE(Failure: RuntimeError: Woot with 5)', 'task-2.t REVERTING', - 'task-2.t REVERTED', + 'task-2.t REVERTED(None)', 'r1.r REVERTING', - 'r1.r REVERTED', + 'r1.r REVERTED(None)', 'flow-1.f REVERTED'] self.assertEqual(expected, capturer.values) @@ -901,7 +901,7 @@ class RetryTest(utils.EngineTestBase): 'task-2.t RUNNING', 'task-2.t FAILURE(Failure: RuntimeError: Woot with 3)', 'task-2.t REVERTING', - 'task-2.t REVERTED', + 'task-2.t REVERTED(None)', 'r1.r RETRYING', 'task-2.t PENDING', 'r1.r RUNNING', @@ -909,7 +909,7 @@ class RetryTest(utils.EngineTestBase): 'task-2.t RUNNING', 'task-2.t FAILURE(Failure: RuntimeError: Woot with 2)', 'task-2.t REVERTING', - 'task-2.t REVERTED', + 'task-2.t REVERTED(None)', 'r1.r RETRYING', 'task-2.t PENDING', 'r1.r RUNNING', @@ -917,11 +917,11 @@ class RetryTest(utils.EngineTestBase): 'task-2.t RUNNING', 'task-2.t FAILURE(Failure: RuntimeError: Woot with 5)', 'task-2.t REVERTING', - 'task-2.t REVERTED', + 'task-2.t REVERTED(None)', 'r1.r REVERTING', - 'r1.r REVERTED', + 'r1.r REVERTED(None)', 'task-1.t REVERTING', - 'task-1.t REVERTED', + 'task-1.t REVERTED(None)', 'flow-1.f REVERTED'] self.assertEqual(expected, capturer.values) @@ -973,7 +973,7 @@ class RetryTest(utils.EngineTestBase): with utils.CaptureListener(engine) as capturer: engine.run() expected = ['task1.t REVERTING', - 'task1.t REVERTED', + 'task1.t REVERTED(None)', 'flow-1_retry.r RETRYING', 'task1.t PENDING', 'flow-1_retry.r RUNNING', @@ -988,7 +988,7 @@ class RetryTest(utils.EngineTestBase): with utils.CaptureListener(engine) as capturer: engine.run() expected = ['task1.t REVERTING', - 'task1.t REVERTED', + 'task1.t REVERTED(None)', 'flow-1_retry.r RETRYING', 'task1.t PENDING', 'flow-1_retry.r RUNNING', @@ -1003,7 +1003,7 @@ class RetryTest(utils.EngineTestBase): with utils.CaptureListener(engine) as capturer: engine.run() expected = ['task1.t REVERTING', - 'task1.t REVERTED', + 'task1.t REVERTED(None)', 'flow-1_retry.r RETRYING', 'task1.t PENDING', 'flow-1_retry.r RUNNING', @@ -1018,7 +1018,7 @@ class RetryTest(utils.EngineTestBase): with utils.CaptureListener(engine) as capturer: engine.run() expected = ['task1.t REVERTING', - 'task1.t REVERTED', + 'task1.t REVERTED(None)', 'flow-1_retry.r RETRYING', 'task1.t PENDING', 'flow-1_retry.r RUNNING', @@ -1032,7 +1032,7 @@ class RetryTest(utils.EngineTestBase): engine = self._pretend_to_run_a_flow_and_crash('revert scheduled') with utils.CaptureListener(engine) as capturer: engine.run() - expected = ['task1.t REVERTED', + expected = ['task1.t REVERTED(None)', 'flow-1_retry.r RETRYING', 'task1.t PENDING', 'flow-1_retry.r RUNNING', @@ -1077,16 +1077,16 @@ class RetryTest(utils.EngineTestBase): 'c.t FAILURE(Failure: RuntimeError: Woot!)', 'a.t REVERTING', 'c.t REVERTING', - 'a.t REVERTED', - 'c.t REVERTED', + 'a.t REVERTED(None)', + 'c.t REVERTED(None)', 'b.t REVERTING', - 'b.t REVERTED'] + 'b.t REVERTED(None)'] self.assertItemsEqual(capturer.values[:8], expected) # Task 'a' was or was not executed again, both cases are ok. self.assertIsSuperAndSubsequence(capturer.values[8:], [ 'b.t RUNNING', 'c.t FAILURE(Failure: RuntimeError: Woot!)', - 'b.t REVERTED', + 'b.t REVERTED(None)', ]) self.assertEqual(engine.storage.get_flow_state(), st.REVERTED) @@ -1107,9 +1107,9 @@ class RetryTest(utils.EngineTestBase): with utils.CaptureListener(engine, capture_flow=False) as capturer: engine.run() expected = ['c.t REVERTING', - 'c.t REVERTED', + 'c.t REVERTED(None)', 'b.t REVERTING', - 'b.t REVERTED'] + 'b.t REVERTED(None)'] self.assertItemsEqual(capturer.values[:4], expected) expected = ['test2_retry.r RETRYING', 'b.t PENDING', @@ -1149,10 +1149,10 @@ class RetryParallelExecutionTest(utils.EngineTestBase): 'task2.t RUNNING', 'task2.t FAILURE(Failure: RuntimeError: Woot!)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'task1.t SUCCESS(5)', 'task1.t REVERTING', - 'task1.t REVERTED', + 'task1.t REVERTED(None)', 'r.r RETRYING', 'task1.t PENDING', 'task2.t PENDING', @@ -1189,10 +1189,10 @@ class RetryParallelExecutionTest(utils.EngineTestBase): 'task3.t FAILURE(Failure: RuntimeError: Woot!)', 'task3.t REVERTING', 'task1.t REVERTING', - 'task3.t REVERTED', - 'task1.t REVERTED', + 'task3.t REVERTED(None)', + 'task1.t REVERTED(None)', 'task2.t REVERTING', - 'task2.t REVERTED', + 'task2.t REVERTED(None)', 'r.r RETRYING', 'task1.t PENDING', 'task2.t PENDING', diff --git a/taskflow/tests/unit/test_storage.py b/taskflow/tests/unit/test_storage.py index 958d5a53..0e1c47fc 100644 --- a/taskflow/tests/unit/test_storage.py +++ b/taskflow/tests/unit/test_storage.py @@ -118,13 +118,6 @@ class StorageTestMixin(object): self.assertEqual(s.fetch_all(), {}) self.assertEqual(s.get_atom_state('my task'), states.SUCCESS) - def test_save_and_get_other_state(self): - s = self._get_storage() - s.ensure_atom(test_utils.NoopTask('my task')) - s.save('my task', 5, states.FAILURE) - self.assertEqual(s.get('my task'), 5) - self.assertEqual(s.get_atom_state('my task'), states.FAILURE) - def test_save_and_get_cached_failure(self): a_failure = failure.Failure.from_exception(RuntimeError('Woot!')) s = self._get_storage() @@ -141,7 +134,7 @@ class StorageTestMixin(object): s.ensure_atom(test_utils.NoopTask('my task')) s.save('my task', a_failure, states.FAILURE) self.assertEqual(s.get('my task'), a_failure) - s._failures['my task'] = None + s._failures['my task'] = {} self.assertTrue(a_failure.matches(s.get('my task'))) def test_get_failure_from_reverted_task(self): @@ -564,6 +557,33 @@ class StorageTestMixin(object): args = s.fetch_mapped_args(t.rebind, atom_name=t.name) self.assertEqual(3, args['x']) + def test_save_fetch(self): + t = test_utils.GiveBackRevert('my task') + s = self._get_storage() + s.ensure_atom(t) + s.save('my task', 2) + self.assertEqual(2, s.get('my task')) + self.assertRaises(exceptions.NotFound, + s.get_revert_result, 'my task') + + def test_save_fetch_revert(self): + t = test_utils.GiveBackRevert('my task') + s = self._get_storage() + s.ensure_atom(t) + s.set_atom_intention('my task', states.REVERT) + s.save('my task', 2, state=states.REVERTED) + self.assertRaises(exceptions.NotFound, s.get, 'my task') + self.assertEqual(2, s.get_revert_result('my task')) + + def test_save_fail_fetch_revert(self): + t = test_utils.GiveBackRevert('my task') + s = self._get_storage() + s.ensure_atom(t) + s.set_atom_intention('my task', states.REVERT) + a_failure = failure.Failure.from_exception(RuntimeError('Woot!')) + s.save('my task', a_failure, state=states.REVERT_FAILURE) + self.assertEqual(a_failure, s.get_revert_result('my task')) + class StorageMemoryTest(StorageTestMixin, test.TestCase): def setUp(self): diff --git a/taskflow/tests/unit/test_suspend.py b/taskflow/tests/unit/test_suspend.py index e5d0288f..1b358acc 100644 --- a/taskflow/tests/unit/test_suspend.py +++ b/taskflow/tests/unit/test_suspend.py @@ -97,14 +97,14 @@ class SuspendTest(utils.EngineTestBase): 'c.t RUNNING', 'c.t FAILURE(Failure: RuntimeError: Woot!)', 'c.t REVERTING', - 'c.t REVERTED', + 'c.t REVERTED(None)', 'b.t REVERTING', - 'b.t REVERTED'] + 'b.t REVERTED(None)'] self.assertEqual(expected, capturer.values) with utils.CaptureListener(engine, capture_flow=False) as capturer: self.assertRaisesRegexp(RuntimeError, '^Woot', engine.run) self.assertEqual(engine.storage.get_flow_state(), states.REVERTED) - expected = ['a.t REVERTING', 'a.t REVERTED'] + expected = ['a.t REVERTING', 'a.t REVERTED(None)'] self.assertEqual(expected, capturer.values) def test_suspend_and_resume_linear_flow_on_revert(self): @@ -124,9 +124,9 @@ class SuspendTest(utils.EngineTestBase): 'c.t RUNNING', 'c.t FAILURE(Failure: RuntimeError: Woot!)', 'c.t REVERTING', - 'c.t REVERTED', + 'c.t REVERTED(None)', 'b.t REVERTING', - 'b.t REVERTED'] + 'b.t REVERTED(None)'] self.assertEqual(expected, capturer.values) # pretend we are resuming @@ -135,7 +135,7 @@ class SuspendTest(utils.EngineTestBase): self.assertRaisesRegexp(RuntimeError, '^Woot', engine2.run) self.assertEqual(engine2.storage.get_flow_state(), states.REVERTED) expected = ['a.t REVERTING', - 'a.t REVERTED'] + 'a.t REVERTED(None)'] self.assertEqual(expected, capturer2.values) def test_suspend_and_revert_even_if_task_is_gone(self): @@ -157,9 +157,9 @@ class SuspendTest(utils.EngineTestBase): 'c.t RUNNING', 'c.t FAILURE(Failure: RuntimeError: Woot!)', 'c.t REVERTING', - 'c.t REVERTED', + 'c.t REVERTED(None)', 'b.t REVERTING', - 'b.t REVERTED'] + 'b.t REVERTED(None)'] self.assertEqual(expected, capturer.values) # pretend we are resuming, but task 'c' gone when flow got updated @@ -171,7 +171,7 @@ class SuspendTest(utils.EngineTestBase): with utils.CaptureListener(engine2, capture_flow=False) as capturer2: self.assertRaisesRegexp(RuntimeError, '^Woot', engine2.run) self.assertEqual(engine2.storage.get_flow_state(), states.REVERTED) - expected = ['a.t REVERTING', 'a.t REVERTED'] + expected = ['a.t REVERTING', 'a.t REVERTED(None)'] self.assertEqual(capturer2.values, expected) def test_storage_is_rechecked(self): diff --git a/taskflow/tests/unit/worker_based/test_worker.py b/taskflow/tests/unit/worker_based/test_worker.py index 3acf245b..c5986739 100644 --- a/taskflow/tests/unit/worker_based/test_worker.py +++ b/taskflow/tests/unit/worker_based/test_worker.py @@ -33,7 +33,7 @@ class TestWorker(test.MockTestCase): self.broker_url = 'test-url' self.exchange = 'test-exchange' self.topic = 'test-topic' - self.endpoint_count = 25 + self.endpoint_count = 26 # patch classes self.executor_mock, self.executor_inst_mock = self.patchClass( diff --git a/taskflow/tests/utils.py b/taskflow/tests/utils.py index b295fc2a..266a0e8c 100644 --- a/taskflow/tests/utils.py +++ b/taskflow/tests/utils.py @@ -117,6 +117,15 @@ class AddOne(task.Task): return source + 1 +class GiveBackRevert(task.Task): + + def execute(self, value): + return value + 1 + + def revert(self, *args, **kwargs): + return kwargs.get('result') + 1 + + class FakeTask(object): def execute(self, **kwargs): diff --git a/taskflow/types/failure.py b/taskflow/types/failure.py index d713098d..dafe73e6 100644 --- a/taskflow/types/failure.py +++ b/taskflow/types/failure.py @@ -291,13 +291,15 @@ class Failure(object): def reraise_if_any(failures): """Re-raise exceptions if argument is not empty. - If argument is empty list, this method returns None. If - argument is a list with a single ``Failure`` object in it, - that failure is reraised. Else, a + If argument is empty list/tuple/iterator, this method returns + None. If argument is coverted into a list with a + single ``Failure`` object in it, that failure is reraised. Else, a :class:`~taskflow.exceptions.WrappedFailure` exception - is raised with a failure list as causes. + is raised with the failure list as causes. """ - failures = list(failures) + if not isinstance(failures, (list, tuple)): + # Convert generators/other into a list... + failures = list(failures) if len(failures) == 1: failures[0].reraise() elif len(failures) > 1: diff --git a/tools/state_graph.py b/tools/state_graph.py index c37cd703..635ec687 100755 --- a/tools/state_graph.py +++ b/tools/state_graph.py @@ -68,7 +68,7 @@ def make_machine(start_state, transitions): def map_color(internal_states, state): if state in internal_states: return 'blue' - if state == states.FAILURE: + if state in (states.FAILURE, states.REVERT_FAILURE): return 'red' if state == states.REVERTED: return 'darkorange' From e0041973b271c6eda0a2d37cc20d049cb3cf1fbd Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 10 Jul 2015 19:43:40 -0700 Subject: [PATCH 12/55] Use io.open vs raw open The io.open call can take in a encoding so we don't need to read in binary mode, then convert it since it can just do that on our behalf. Change-Id: I0cce2841b40f1566ba07ff95a553cb18ea9059ee --- taskflow/persistence/backends/impl_dir.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/taskflow/persistence/backends/impl_dir.py b/taskflow/persistence/backends/impl_dir.py index 1047d671..f91c4e72 100644 --- a/taskflow/persistence/backends/impl_dir.py +++ b/taskflow/persistence/backends/impl_dir.py @@ -17,6 +17,7 @@ import contextlib import errno +import io import os import shutil @@ -98,16 +99,15 @@ class Connection(path_based.PathBasedConnection): mtime = os.path.getmtime(filename) cache_info = self.backend.file_cache.setdefault(filename, {}) if not cache_info or mtime > cache_info.get('mtime', 0): - with open(filename, 'rb') as fp: - cache_info['data'] = misc.binary_decode( - fp.read(), encoding=self.backend.encoding) + with io.open(filename, 'r', encoding=self.backend.encoding) as fp: + cache_info['data'] = fp.read() cache_info['mtime'] = mtime return cache_info['data'] def _write_to(self, filename, contents): contents = misc.binary_encode(contents, encoding=self.backend.encoding) - with open(filename, 'wb') as fp: + with io.open(filename, 'wb') as fp: fp.write(contents) self.backend.file_cache.pop(filename, None) From 7b9861ea35088a6609f105f22dbb8b90d0561aaa Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 10 Jul 2015 15:22:09 -0700 Subject: [PATCH 13/55] Run the '99_bottles.py' demo at a fast rate when activated When this is ran without any arguments just run it locally by starting a producer, then a conductor, then stopping after it finishes the first song request. This allows this example to be ran during unit testing to make sure it functions as expected (with zero return code). Change-Id: I26c210e2c993e770955985c9c779d303eb8c0616 --- taskflow/examples/99_bottles.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/taskflow/examples/99_bottles.py b/taskflow/examples/99_bottles.py index 0211df5b..983bc201 100644 --- a/taskflow/examples/99_bottles.py +++ b/taskflow/examples/99_bottles.py @@ -15,6 +15,7 @@ # under the License. import contextlib +import functools import logging import os import sys @@ -104,17 +105,16 @@ def make_bottles(count): return s -def run_conductor(): +def run_conductor(only_run_once=False): # This continuously 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(event, details): + def on_conductor_event(cond, event, details): print("Event '%s' has been received..." % event) print("Details = %s" % details) if event.endswith("_start"): @@ -131,6 +131,8 @@ def run_conductor(): % (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 @@ -144,6 +146,7 @@ def run_conductor(): 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: @@ -184,9 +187,23 @@ def run_poster(): 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 main(): if len(sys.argv) == 1: - sys.stderr.write("%s p|c\n" % os.path.basename(sys.argv[0])) + main_local() elif sys.argv[1] in ('p', 'c'): if sys.argv[-1] == "v": logging.basicConfig(level=5) From 7bc1be0febf902d404793659bfff7a0109635977 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 10 Jul 2015 17:45:33 -0700 Subject: [PATCH 14/55] Unify the zookeeper/redis jobboard iterators To make the zookeeper jobboard and redis jobboard iterjobs and wait functions that much similar have both return iterator objects from the same class iterator. This makes the iterator code and the jobboard code that much easier to follow and understand. Change-Id: Ia772cde881c2631002140e06684521fd42441534 --- taskflow/jobs/backends/impl_redis.py | 42 ++++++------ taskflow/jobs/backends/impl_zookeeper.py | 86 +++--------------------- taskflow/jobs/base.py | 73 ++++++++++++++++++++ 3 files changed, 105 insertions(+), 96 deletions(-) diff --git a/taskflow/jobs/backends/impl_redis.py b/taskflow/jobs/backends/impl_redis.py index 4d61dc01..c2350c02 100644 --- a/taskflow/jobs/backends/impl_redis.py +++ b/taskflow/jobs/backends/impl_redis.py @@ -747,22 +747,24 @@ return cmsgpack.pack(result) while True: jc = self.job_count if jc > 0: - it = self.iterjobs() - return it + curr_jobs = self._fetch_jobs() + if curr_jobs: + return base.JobBoardIterator( + self, LOG, + board_fetch_func=lambda ensure_fresh: curr_jobs) + if w.expired(): + raise exc.NotFound("Expired waiting for jobs to" + " arrive; waited %s seconds" + % w.elapsed()) else: - if w.expired(): - raise exc.NotFound("Expired waiting for jobs to" - " arrive; waited %s seconds" - % w.elapsed()) + remaining = w.leftover(return_none=True) + if remaining is not None: + delay = min(delay * 2, remaining, max_delay) else: - remaining = w.leftover(return_none=True) - if remaining is not None: - delay = min(delay * 2, remaining, max_delay) - else: - delay = min(delay * 2, max_delay) - sleep_func(delay) + delay = min(delay * 2, max_delay) + sleep_func(delay) - def iterjobs(self, only_unclaimed=False, ensure_fresh=False): + def _fetch_jobs(self): with _translate_failures(): raw_postings = self._client.hgetall(self.listings_key) postings = [] @@ -776,13 +778,13 @@ return cmsgpack.pack(result) book_data=posting.get('book'), backend=self._persistence) postings.append(job) - postings = sorted(postings) - for job in postings: - if only_unclaimed: - if job.state == states.UNCLAIMED: - yield job - else: - yield job + return sorted(postings) + + def iterjobs(self, only_unclaimed=False, ensure_fresh=False): + return base.JobBoardIterator( + self, LOG, only_unclaimed=only_unclaimed, + ensure_fresh=ensure_fresh, + board_fetch_func=lambda ensure_fresh: self._fetch_jobs()) @base.check_who def consume(self, job, who): diff --git a/taskflow/jobs/backends/impl_zookeeper.py b/taskflow/jobs/backends/impl_zookeeper.py index 15b31034..fe25cc53 100644 --- a/taskflow/jobs/backends/impl_zookeeper.py +++ b/taskflow/jobs/backends/impl_zookeeper.py @@ -14,7 +14,6 @@ # License for the specific language governing permissions and limitations # under the License. -import collections import contextlib import functools import sys @@ -180,74 +179,6 @@ class ZookeeperJob(base.Job): return hash(self.path) -class ZookeeperJobBoardIterator(six.Iterator): - """Iterator over a zookeeper jobboard that iterates over potential jobs. - - It supports the following attributes/constructor arguments: - - * ``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). - * ``only_unclaimed``: boolean that indicates whether to only iterate - over unclaimed jobs. - """ - - _UNCLAIMED_JOB_STATES = ( - states.UNCLAIMED, - ) - - _JOB_STATES = ( - states.UNCLAIMED, - states.COMPLETE, - states.CLAIMED, - ) - - def __init__(self, board, only_unclaimed=False, ensure_fresh=False): - self._board = board - self._jobs = collections.deque() - self._fetched = False - self.ensure_fresh = ensure_fresh - self.only_unclaimed = only_unclaimed - - @property - def board(self): - """The board this iterator was created from.""" - return self._board - - def __iter__(self): - return self - - def _next_job(self): - if self.only_unclaimed: - allowed_states = self._UNCLAIMED_JOB_STATES - else: - allowed_states = self._JOB_STATES - job = None - while self._jobs and job is None: - maybe_job = self._jobs.popleft() - try: - if maybe_job.state in allowed_states: - job = maybe_job - except excp.JobFailure: - LOG.warn("Failed determining the state of job: %s (%s)", - maybe_job.uuid, maybe_job.path, exc_info=True) - except excp.NotFound: - self._board._remove_job(maybe_job.path) - return job - - def __next__(self): - if not self._jobs: - if not self._fetched: - jobs = self._board._fetch_jobs(ensure_fresh=self.ensure_fresh) - self._jobs.extend(jobs) - self._fetched = True - job = self._next_job() - if job is None: - raise StopIteration - else: - return job - - class ZookeeperJobBoard(base.NotifyingJobBoard): """A jobboard backed by `zookeeper`_. @@ -378,9 +309,10 @@ class ZookeeperJobBoard(base.NotifyingJobBoard): self._on_job_posting(children, delayed=False) def iterjobs(self, only_unclaimed=False, ensure_fresh=False): - return ZookeeperJobBoardIterator(self, - only_unclaimed=only_unclaimed, - ensure_fresh=ensure_fresh) + return base.JobBoardIterator( + self, LOG, only_unclaimed=only_unclaimed, + ensure_fresh=ensure_fresh, board_fetch_func=self._fetch_jobs, + board_removal_func=lambda a_job: self._remove_job(a_job.path)) def _remove_job(self, path): if path not in self._known_jobs: @@ -688,10 +620,12 @@ class ZookeeperJobBoard(base.NotifyingJobBoard): # must recalculate the amount of time we really have left. self._job_cond.wait(watch.leftover(return_none=True)) else: - it = ZookeeperJobBoardIterator(self) - it._jobs.extend(self._fetch_jobs()) - it._fetched = True - return it + curr_jobs = self._fetch_jobs() + fetch_func = lambda ensure_fresh: curr_jobs + removal_func = lambda a_job: self._remove_job(a_job.path) + return base.JobBoardIterator( + self, LOG, board_fetch_func=fetch_func, + board_removal_func=removal_func) @property def connected(self): diff --git a/taskflow/jobs/base.py b/taskflow/jobs/base.py index a9ff0274..81e4a574 100644 --- a/taskflow/jobs/base.py +++ b/taskflow/jobs/base.py @@ -16,11 +16,14 @@ # under the License. import abc +import collections import contextlib from oslo_utils import uuidutils import six +from taskflow import exceptions as excp +from taskflow import states from taskflow.types import notifier @@ -147,6 +150,76 @@ class Job(object): self.details) +class JobBoardIterator(six.Iterator): + """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 + """ + + _UNCLAIMED_JOB_STATES = (states.UNCLAIMED,) + _JOB_STATES = (states.UNCLAIMED, states.COMPLETE, states.CLAIMED) + + def __init__(self, board, logger, + board_fetch_func=None, board_removal_func=None, + only_unclaimed=False, ensure_fresh=False): + self._board = board + self._logger = logger + self._board_removal_func = board_removal_func + self._board_fetch_func = board_fetch_func + self._fetched = False + self._jobs = collections.deque() + self.only_unclaimed = only_unclaimed + self.ensure_fresh = ensure_fresh + + @property + def board(self): + """The board this iterator was created from.""" + return self._board + + def __iter__(self): + return self + + def _next_job(self): + if self.only_unclaimed: + allowed_states = self._UNCLAIMED_JOB_STATES + else: + allowed_states = self._JOB_STATES + job = None + while self._jobs and job is None: + maybe_job = self._jobs.popleft() + try: + if maybe_job.state in allowed_states: + job = maybe_job + except excp.JobFailure: + self._logger.warn("Failed determining the state of" + " job '%s'", maybe_job, exc_info=True) + except excp.NotFound: + if self._board_removal_func is not None: + self._board_removal_func(maybe_job) + return job + + def __next__(self): + if not self._jobs: + if not self._fetched: + if self._board_fetch_func is not None: + self._jobs.extend( + self._board_fetch_func( + ensure_fresh=self.ensure_fresh)) + self._fetched = True + job = self._next_job() + if job is None: + raise StopIteration + else: + return job + + @six.add_metaclass(abc.ABCMeta) class JobBoard(object): """A place where jobs can be posted, reposted, claimed and transferred. From 33818cd483e36a8348ef935107967fffb1a4a017 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 13 Jul 2015 14:14:07 -0700 Subject: [PATCH 15/55] Remove direct usage of timeutils overrides and use fixture Change-Id: Ifa99497672dbc8fa60672ce4bfbfed1832b128af --- taskflow/tests/unit/worker_based/test_executor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/taskflow/tests/unit/worker_based/test_executor.py b/taskflow/tests/unit/worker_based/test_executor.py index e6c21eb4..504433de 100644 --- a/taskflow/tests/unit/worker_based/test_executor.py +++ b/taskflow/tests/unit/worker_based/test_executor.py @@ -18,6 +18,7 @@ import threading import time import futurist +from oslo_utils import fixture from oslo_utils import timeutils from taskflow.engines.worker_based import executor @@ -184,12 +185,12 @@ class TestWorkerTaskExecutor(test.MockTestCase): def test_on_wait_task_expired(self): now = timeutils.utcnow() + f = self.useFixture(fixture.TimeFixture(override_time=now)) + self.request_inst_mock.expired = True self.request_inst_mock.created_on = now - timeutils.set_time_override(now) - self.addCleanup(timeutils.clear_time_override) - timeutils.advance_time_seconds(120) + f.advance_time_seconds(120) ex = self.executor() ex._requests_cache[self.task_uuid] = self.request_inst_mock From 50f710eaeec8556e7ea43e52f00528822afbcb22 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 5 Dec 2014 21:23:56 -0800 Subject: [PATCH 16/55] Replace internal fsm + table with automaton library Instead of having our own inbuilt fsm and table library used to print the fsm states just use the newly created and released automaton that contains the same/similar code but as a released library that others can use and benefit from. Library @ http://pypi.python.org/pypi/automaton Change-Id: I1ca40a0805e704fbb37b0106c1831a7e45c6ad68 --- doc/source/types.rst | 20 +- requirements.txt | 3 + taskflow/engines/action_engine/runner.py | 16 +- .../tests/unit/action_engine/test_runner.py | 23 +- taskflow/tests/unit/test_types.py | 216 ---------- taskflow/types/fsm.py | 381 ------------------ taskflow/types/table.py | 139 ------- tools/state_graph.py | 8 +- 8 files changed, 33 insertions(+), 773 deletions(-) delete mode 100644 taskflow/types/fsm.py delete mode 100644 taskflow/types/table.py diff --git a/doc/source/types.rst b/doc/source/types.rst index 84d446ac..254ed28a 100644 --- a/doc/source/types.rst +++ b/doc/source/types.rst @@ -6,11 +6,9 @@ Types 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 (for example - the ``FSM`` code *may* move to its own oslo supported ``automaton`` library - at some point in the future [#f1]_). 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 + 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). @@ -24,11 +22,6 @@ Failure .. automodule:: taskflow.types.failure -FSM -=== - -.. automodule:: taskflow.types.fsm - Graph ===== @@ -45,11 +38,6 @@ Sets .. automodule:: taskflow.types.sets -Table -===== - -.. automodule:: taskflow.types.table - Timing ====== @@ -60,5 +48,3 @@ Tree .. automodule:: taskflow.types.tree -.. [#f1] See: https://review.openstack.org/#/c/141961 for a proposal to - do this. diff --git a/requirements.txt b/requirements.txt index 24414c6d..25f2dccb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,6 +37,9 @@ monotonic>=0.1 # Apache-2.0 # Used for structured input validation jsonschema!=2.5.0,<3.0.0,>=2.0.0 +# For the state machine we run with +automaton>=0.2.0 # Apache-2.0 + # For common utilities oslo.utils>=1.6.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 diff --git a/taskflow/engines/action_engine/runner.py b/taskflow/engines/action_engine/runner.py index 9b6043a3..e8cd1734 100644 --- a/taskflow/engines/action_engine/runner.py +++ b/taskflow/engines/action_engine/runner.py @@ -14,10 +14,13 @@ # License for the specific language governing permissions and limitations # under the License. + +from automaton import machines +from automaton import runners + from taskflow import logging from taskflow import states as st from taskflow.types import failure -from taskflow.types import fsm # Waiting state timeout (in seconds). _WAITING_TIMEOUT = 60 @@ -236,7 +239,7 @@ class Runner(object): watchers['on_exit'] = on_exit watchers['on_enter'] = on_enter - m = fsm.FSM(_UNDEFINED) + m = machines.FiniteMachine() m.add_state(_GAME_OVER, **watchers) m.add_state(_UNDEFINED, **watchers) m.add_state(st.ANALYZING, **watchers) @@ -247,6 +250,7 @@ class Runner(object): m.add_state(st.SUSPENDED, terminal=True, **watchers) m.add_state(st.WAITING, **watchers) m.add_state(st.FAILURE, terminal=True, **watchers) + m.default_start_state = _UNDEFINED m.add_transition(_GAME_OVER, st.REVERTED, _REVERTED) m.add_transition(_GAME_OVER, st.SUCCESS, _SUCCESS) @@ -267,12 +271,14 @@ class Runner(object): m.add_reaction(st.WAITING, _WAIT, wait) m.freeze() - return (m, memory) + + r = runners.FiniteRunner(m) + return (m, r, memory) def run_iter(self, timeout=None): """Runs iteratively using a locally built state machine.""" - machine, memory = self.build(timeout=timeout) - for (_prior_state, new_state) in machine.run_iter(_START): + machine, runner, memory = self.build(timeout=timeout) + for (_prior_state, new_state) in runner.run_iter(_START): # NOTE(harlowja): skip over meta-states. if new_state not in _META_STATES: if new_state == st.FAILURE: diff --git a/taskflow/tests/unit/action_engine/test_runner.py b/taskflow/tests/unit/action_engine/test_runner.py index 401cf50d..4e917df8 100644 --- a/taskflow/tests/unit/action_engine/test_runner.py +++ b/taskflow/tests/unit/action_engine/test_runner.py @@ -14,19 +14,18 @@ # License for the specific language governing permissions and limitations # under the License. +from automaton import exceptions as excp import six from taskflow.engines.action_engine import compiler from taskflow.engines.action_engine import executor from taskflow.engines.action_engine import runner from taskflow.engines.action_engine import runtime -from taskflow import exceptions as excp from taskflow.patterns import linear_flow as lf from taskflow import states as st from taskflow import storage from taskflow import test from taskflow.tests import utils as test_utils -from taskflow.types import fsm from taskflow.types import notifier from taskflow.utils import persistence_utils as pu @@ -184,9 +183,9 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin): flow.add(*tasks) rt = self._make_runtime(flow, initial_state=st.RUNNING) - machine, memory = rt.runner.build() + machine, machine_runner, memory = rt.runner.build() self.assertTrue(rt.runner.runnable()) - self.assertRaises(fsm.NotInitialized, machine.process_event, 'poke') + self.assertRaises(excp.NotInitialized, machine.process_event, 'poke') # Should now be pending... self.assertEqual(st.PENDING, rt.storage.get_atom_state(tasks[0].name)) @@ -253,10 +252,10 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin): flow.add(*tasks) rt = self._make_runtime(flow, initial_state=st.RUNNING) - machine, memory = rt.runner.build() + machine, machine_runner, memory = rt.runner.build() self.assertTrue(rt.runner.runnable()) - transitions = list(machine.run_iter('start')) + transitions = list(machine_runner.run_iter('start')) self.assertEqual((runner._UNDEFINED, st.RESUMING), transitions[0]) self.assertEqual((runner._GAME_OVER, st.SUCCESS), transitions[-1]) self.assertEqual(st.SUCCESS, rt.storage.get_atom_state(tasks[0].name)) @@ -267,10 +266,10 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin): flow.add(*tasks) rt = self._make_runtime(flow, initial_state=st.RUNNING) - machine, memory = rt.runner.build() + machine, machine_runner, memory = rt.runner.build() self.assertTrue(rt.runner.runnable()) - transitions = list(machine.run_iter('start')) + transitions = list(machine_runner.run_iter('start')) self.assertEqual((runner._GAME_OVER, st.FAILURE), transitions[-1]) self.assertEqual(1, len(memory.failures)) @@ -280,10 +279,10 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin): flow.add(*tasks) rt = self._make_runtime(flow, initial_state=st.RUNNING) - machine, memory = rt.runner.build() + machine, machine_runner, memory = rt.runner.build() self.assertTrue(rt.runner.runnable()) - transitions = list(machine.run_iter('start')) + transitions = list(machine_runner.run_iter('start')) self.assertEqual((runner._GAME_OVER, st.REVERTED), transitions[-1]) self.assertEqual(st.REVERTED, rt.storage.get_atom_state(tasks[0].name)) @@ -294,8 +293,8 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin): flow.add(*tasks) rt = self._make_runtime(flow, initial_state=st.RUNNING) - machine, memory = rt.runner.build() - transitions = list(machine.run_iter('start')) + machine, machine_runner, memory = rt.runner.build() + transitions = list(machine_runner.run_iter('start')) occurrences = dict((t, transitions.count(t)) for t in transitions) self.assertEqual(10, occurrences.get((st.SCHEDULING, st.WAITING))) diff --git a/taskflow/tests/unit/test_types.py b/taskflow/tests/unit/test_types.py index 8980aa5c..1d3f5410 100644 --- a/taskflow/tests/unit/test_types.py +++ b/taskflow/tests/unit/test_types.py @@ -15,15 +15,11 @@ # under the License. import networkx as nx -import six from six.moves import cPickle as pickle -from taskflow import exceptions as excp from taskflow import test -from taskflow.types import fsm from taskflow.types import graph from taskflow.types import sets -from taskflow.types import table from taskflow.types import tree @@ -251,218 +247,6 @@ class TreeTest(test.TestCase): 'horse', 'human', 'monkey'], things) -class TableTest(test.TestCase): - def test_create_valid_no_rows(self): - tbl = table.PleasantTable(['Name', 'City', 'State', 'Country']) - self.assertGreater(0, len(tbl.pformat())) - - def test_create_valid_rows(self): - tbl = table.PleasantTable(['Name', 'City', 'State', 'Country']) - before_rows = tbl.pformat() - tbl.add_row(["Josh", "San Jose", "CA", "USA"]) - after_rows = tbl.pformat() - self.assertGreater(len(before_rows), len(after_rows)) - - def test_create_invalid_columns(self): - self.assertRaises(ValueError, table.PleasantTable, []) - - def test_create_invalid_rows(self): - tbl = table.PleasantTable(['Name', 'City', 'State', 'Country']) - self.assertRaises(ValueError, tbl.add_row, ['a', 'b']) - - -class FSMTest(test.TestCase): - def setUp(self): - super(FSMTest, self).setUp() - # NOTE(harlowja): this state machine will never stop if run() is used. - self.jumper = fsm.FSM("down") - self.jumper.add_state('up') - self.jumper.add_state('down') - self.jumper.add_transition('down', 'up', 'jump') - self.jumper.add_transition('up', 'down', 'fall') - self.jumper.add_reaction('up', 'jump', lambda *args: 'fall') - self.jumper.add_reaction('down', 'fall', lambda *args: 'jump') - - def test_bad_start_state(self): - m = fsm.FSM('unknown') - self.assertRaises(excp.NotFound, m.run, 'unknown') - - def test_contains(self): - m = fsm.FSM('unknown') - self.assertNotIn('unknown', m) - m.add_state('unknown') - self.assertIn('unknown', m) - - def test_duplicate_state(self): - m = fsm.FSM('unknown') - m.add_state('unknown') - self.assertRaises(excp.Duplicate, m.add_state, 'unknown') - - def test_duplicate_reaction(self): - self.assertRaises( - # Currently duplicate reactions are not allowed... - excp.Duplicate, - self.jumper.add_reaction, 'down', 'fall', lambda *args: 'skate') - - def test_bad_transition(self): - m = fsm.FSM('unknown') - m.add_state('unknown') - m.add_state('fire') - self.assertRaises(excp.NotFound, m.add_transition, - 'unknown', 'something', 'boom') - self.assertRaises(excp.NotFound, m.add_transition, - 'something', 'unknown', 'boom') - - def test_bad_reaction(self): - m = fsm.FSM('unknown') - m.add_state('unknown') - self.assertRaises(excp.NotFound, m.add_reaction, 'something', 'boom', - lambda *args: 'cough') - - def test_run(self): - m = fsm.FSM('down') - m.add_state('down') - m.add_state('up') - m.add_state('broken', terminal=True) - m.add_transition('down', 'up', 'jump') - m.add_transition('up', 'broken', 'hit-wall') - m.add_reaction('up', 'jump', lambda *args: 'hit-wall') - self.assertEqual(['broken', 'down', 'up'], sorted(m.states)) - self.assertEqual(2, m.events) - m.initialize() - self.assertEqual('down', m.current_state) - self.assertFalse(m.terminated) - m.run('jump') - self.assertTrue(m.terminated) - self.assertEqual('broken', m.current_state) - self.assertRaises(excp.InvalidState, m.run, 'jump', initialize=False) - - def test_on_enter_on_exit(self): - enter_transitions = [] - exit_transitions = [] - - def on_exit(state, event): - exit_transitions.append((state, event)) - - def on_enter(state, event): - enter_transitions.append((state, event)) - - m = fsm.FSM('start') - m.add_state('start', on_exit=on_exit) - m.add_state('down', on_enter=on_enter, on_exit=on_exit) - m.add_state('up', on_enter=on_enter, on_exit=on_exit) - m.add_transition('start', 'down', 'beat') - m.add_transition('down', 'up', 'jump') - m.add_transition('up', 'down', 'fall') - - m.initialize() - m.process_event('beat') - m.process_event('jump') - m.process_event('fall') - self.assertEqual([('down', 'beat'), - ('up', 'jump'), ('down', 'fall')], enter_transitions) - self.assertEqual( - [('start', 'beat'), ('down', 'jump'), ('up', 'fall')], - exit_transitions) - - def test_run_iter(self): - up_downs = [] - for (old_state, new_state) in self.jumper.run_iter('jump'): - up_downs.append((old_state, new_state)) - if len(up_downs) >= 3: - break - self.assertEqual([('down', 'up'), ('up', 'down'), ('down', 'up')], - up_downs) - self.assertFalse(self.jumper.terminated) - self.assertEqual('up', self.jumper.current_state) - self.jumper.process_event('fall') - self.assertEqual('down', self.jumper.current_state) - - def test_run_send(self): - up_downs = [] - it = self.jumper.run_iter('jump') - while True: - up_downs.append(it.send(None)) - if len(up_downs) >= 3: - it.close() - break - self.assertEqual('up', self.jumper.current_state) - self.assertFalse(self.jumper.terminated) - self.assertEqual([('down', 'up'), ('up', 'down'), ('down', 'up')], - up_downs) - self.assertRaises(StopIteration, six.next, it) - - def test_run_send_fail(self): - up_downs = [] - it = self.jumper.run_iter('jump') - up_downs.append(six.next(it)) - self.assertRaises(excp.NotFound, it.send, 'fail') - it.close() - self.assertEqual([('down', 'up')], up_downs) - - def test_not_initialized(self): - self.assertRaises(fsm.NotInitialized, - self.jumper.process_event, 'jump') - - def test_copy_states(self): - c = fsm.FSM('down') - self.assertEqual(0, len(c.states)) - d = c.copy() - c.add_state('up') - c.add_state('down') - self.assertEqual(2, len(c.states)) - self.assertEqual(0, len(d.states)) - - def test_copy_reactions(self): - c = fsm.FSM('down') - d = c.copy() - - c.add_state('down') - c.add_state('up') - c.add_reaction('down', 'jump', lambda *args: 'up') - c.add_transition('down', 'up', 'jump') - - self.assertEqual(1, c.events) - self.assertEqual(0, d.events) - self.assertNotIn('down', d) - self.assertNotIn('up', d) - self.assertEqual([], list(d)) - self.assertEqual([('down', 'jump', 'up')], list(c)) - - def test_copy_initialized(self): - j = self.jumper.copy() - self.assertIsNone(j.current_state) - - for i, transition in enumerate(self.jumper.run_iter('jump')): - if i == 4: - break - - self.assertIsNone(j.current_state) - self.assertIsNotNone(self.jumper.current_state) - - def test_iter(self): - transitions = list(self.jumper) - self.assertEqual(2, len(transitions)) - self.assertIn(('up', 'fall', 'down'), transitions) - self.assertIn(('down', 'jump', 'up'), transitions) - - def test_freeze(self): - self.jumper.freeze() - self.assertRaises(fsm.FrozenMachine, self.jumper.add_state, 'test') - self.assertRaises(fsm.FrozenMachine, - self.jumper.add_transition, 'test', 'test', 'test') - self.assertRaises(fsm.FrozenMachine, - self.jumper.add_reaction, - 'test', 'test', lambda *args: 'test') - - def test_invalid_callbacks(self): - m = fsm.FSM('working') - m.add_state('working') - m.add_state('broken') - self.assertRaises(ValueError, m.add_state, 'b', on_enter=2) - self.assertRaises(ValueError, m.add_state, 'b', on_exit=2) - - class OrderedSetTest(test.TestCase): def test_pickleable(self): diff --git a/taskflow/types/fsm.py b/taskflow/types/fsm.py deleted file mode 100644 index 1ed3193f..00000000 --- a/taskflow/types/fsm.py +++ /dev/null @@ -1,381 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import collections - -import six - -from taskflow import exceptions as excp -from taskflow.types import table -from taskflow.utils import misc - - -class _Jump(object): - """A FSM transition tracks this data while jumping.""" - def __init__(self, name, on_enter, on_exit): - self.name = name - self.on_enter = on_enter - self.on_exit = on_exit - - -class FrozenMachine(Exception): - """Exception raised when a frozen machine is modified.""" - def __init__(self): - super(FrozenMachine, self).__init__("Frozen machine can't be modified") - - -class NotInitialized(excp.TaskFlowException): - """Error raised when an action is attempted on a not inited machine.""" - - -class FSM(object): - """A finite state machine. - - This state machine can be used to automatically run a given set of - transitions and states in response to events (either from callbacks or from - generator/iterator send() values, see PEP 342). On each triggered event, a - on_enter and on_exit callback can also be provided which will be called to - perform some type of action on leaving a prior state and before entering a - new state. - - NOTE(harlowja): reactions will *only* be called when the generator/iterator - from run_iter() does *not* send back a new event (they will always be - called if the run() method is used). This allows for two unique ways (these - ways can also be intermixed) to use this state machine when using - run_iter(); one where *external* events trigger the next state transition - and one where *internal* reaction callbacks trigger the next state - transition. The other way to use this state machine is to skip using run() - or run_iter() completely and use the process_event() method explicitly and - trigger the events via some *external* functionality. - """ - def __init__(self, start_state): - self._transitions = {} - self._states = collections.OrderedDict() - self._start_state = start_state - self._current = None - self.frozen = False - - @property - def start_state(self): - return self._start_state - - @property - def current_state(self): - """Return the current state name. - - :returns: current state name - :rtype: string - """ - if self._current is not None: - return self._current.name - return None - - @property - def terminated(self): - """Returns whether the state machine is in a terminal state. - - :returns: whether the state machine is in - terminal state or not - :rtype: boolean - """ - if self._current is None: - return False - return self._states[self._current.name]['terminal'] - - @misc.disallow_when_frozen(FrozenMachine) - def add_state(self, state, terminal=False, on_enter=None, on_exit=None): - """Adds a given state to the state machine. - - :param on_enter: callback, if provided will be expected to take - two positional parameters, these being state being - entered and the second parameter is the event that is - being processed that caused the state transition - :param on_exit: callback, if provided will be expected to take - two positional parameters, these being state being - entered and the second parameter is the event that is - being processed that caused the state transition - :param state: state being entered or exited - :type state: string - """ - if state in self._states: - raise excp.Duplicate("State '%s' already defined" % state) - if on_enter is not None: - if not six.callable(on_enter): - raise ValueError("On enter callback must be callable") - if on_exit is not None: - if not six.callable(on_exit): - raise ValueError("On exit callback must be callable") - self._states[state] = { - 'terminal': bool(terminal), - 'reactions': {}, - 'on_enter': on_enter, - 'on_exit': on_exit, - } - self._transitions[state] = collections.OrderedDict() - - @misc.disallow_when_frozen(FrozenMachine) - def add_reaction(self, state, event, reaction, *args, **kwargs): - """Adds a reaction that may get triggered by the given event & state. - - :param state: the last stable state expressed - :type state: string - :param event: event that caused the transition - :param args: non-keyworded arguments - :type args: list - :param kwargs: key-value pair arguments - :type kwargs: dictionary - - Reaction callbacks may (depending on how the state machine is ran) be - used after an event is processed (and a transition occurs) to cause - the machine to react to the newly arrived at stable state. The - expected result of a callback is expected to be a - new event that the callback wants the state machine to react to. - This new event may (depending on how the state machine is ran) get - processed (and this process typically repeats) until the state - machine reaches a terminal state. - """ - if state not in self._states: - raise excp.NotFound("Can not add a reaction to event '%s' for an" - " undefined state '%s'" % (event, state)) - if not six.callable(reaction): - raise ValueError("Reaction callback must be callable") - if event not in self._states[state]['reactions']: - self._states[state]['reactions'][event] = (reaction, args, kwargs) - else: - raise excp.Duplicate("State '%s' reaction to event '%s'" - " already defined" % (state, event)) - - @misc.disallow_when_frozen(FrozenMachine) - def add_transition(self, start, end, event): - """Adds an allowed transition from start -> end for the given event. - - :param start: start of the transition - :param end: end of the transition - :param event: event that caused the transition - """ - if start not in self._states: - raise excp.NotFound("Can not add a transition on event '%s' that" - " starts in a undefined state '%s'" % (event, - start)) - if end not in self._states: - raise excp.NotFound("Can not add a transition on event '%s' that" - " ends in a undefined state '%s'" % (event, - end)) - self._transitions[start][event] = _Jump(end, - self._states[end]['on_enter'], - self._states[start]['on_exit']) - - def process_event(self, event): - """Trigger a state change in response to the provided event. - - :param event: event to be processed to cause a potential transition - """ - current = self._current - if current is None: - raise NotInitialized("Can only process events after" - " being initialized (not before)") - if self._states[current.name]['terminal']: - raise excp.InvalidState("Can not transition from terminal" - " state '%s' on event '%s'" - % (current.name, event)) - if event not in self._transitions[current.name]: - raise excp.NotFound("Can not transition from state '%s' on" - " event '%s' (no defined transition)" - % (current.name, event)) - replacement = self._transitions[current.name][event] - if current.on_exit is not None: - current.on_exit(current.name, event) - if replacement.on_enter is not None: - replacement.on_enter(replacement.name, event) - self._current = replacement - return ( - self._states[replacement.name]['reactions'].get(event), - self._states[replacement.name]['terminal'], - ) - - def initialize(self): - """Sets up the state machine (sets current state to start state...).""" - if self._start_state not in self._states: - raise excp.NotFound("Can not start from a undefined" - " state '%s'" % (self._start_state)) - if self._states[self._start_state]['terminal']: - raise excp.InvalidState("Can not start from a terminal" - " state '%s'" % (self._start_state)) - # No on enter will be called, since we are priming the state machine - # and have not really transitioned from anything to get here, we will - # though allow 'on_exit' to be called on the event that causes this - # to be moved from... - self._current = _Jump(self._start_state, None, - self._states[self._start_state]['on_exit']) - - def run(self, event, initialize=True): - """Runs the state machine, using reactions only.""" - for _transition in self.run_iter(event, initialize=initialize): - pass - - def copy(self): - """Copies the current state machine. - - NOTE(harlowja): the copy will be left in an *uninitialized* state. - """ - c = FSM(self.start_state) - c.frozen = self.frozen - for state, data in six.iteritems(self._states): - copied_data = data.copy() - copied_data['reactions'] = copied_data['reactions'].copy() - c._states[state] = copied_data - for state, data in six.iteritems(self._transitions): - c._transitions[state] = data.copy() - return c - - def run_iter(self, event, initialize=True): - """Returns a iterator/generator that will run the state machine. - - NOTE(harlowja): only one runner iterator/generator should be active for - a machine, if this is not observed then it is possible for - initialization and other local state to be corrupted and cause issues - when running... - """ - if initialize: - self.initialize() - while True: - old_state = self.current_state - reaction, terminal = self.process_event(event) - new_state = self.current_state - try: - sent_event = yield (old_state, new_state) - except GeneratorExit: - break - if terminal: - break - if reaction is None and sent_event is None: - raise excp.NotFound("Unable to progress since no reaction (or" - " sent event) has been made available in" - " new state '%s' (moved to from state '%s'" - " in response to event '%s')" - % (new_state, old_state, event)) - elif sent_event is not None: - event = sent_event - else: - cb, args, kwargs = reaction - event = cb(old_state, new_state, event, *args, **kwargs) - - def __contains__(self, state): - """Returns if this state exists in the machines known states. - - :param state: input state - :type state: string - :returns: checks whether the state exists in the machine - known states - :rtype: boolean - """ - return state in self._states - - def freeze(self): - """Freezes & stops addition of states, transitions, reactions...""" - self.frozen = True - - @property - def states(self): - """Returns the state names.""" - return list(six.iterkeys(self._states)) - - @property - def events(self): - """Returns how many events exist. - - :returns: how many events exist - :rtype: number - """ - c = 0 - for state in six.iterkeys(self._states): - c += len(self._transitions[state]) - return c - - def __iter__(self): - """Iterates over (start, event, end) transition tuples.""" - for state in six.iterkeys(self._states): - for event, target in six.iteritems(self._transitions[state]): - yield (state, event, target.name) - - def pformat(self, sort=True): - """Pretty formats the state + transition table into a string. - - NOTE(harlowja): the sort parameter can be provided to sort the states - and transitions by sort order; with it being provided as false the rows - will be iterated in addition order instead. - - **Example**:: - - >>> from taskflow.types import fsm - >>> f = fsm.FSM("sits") - >>> f.add_state("sits") - >>> f.add_state("barks") - >>> f.add_state("wags tail") - >>> f.add_transition("sits", "barks", "squirrel!") - >>> f.add_transition("barks", "wags tail", "gets petted") - >>> f.add_transition("wags tail", "sits", "gets petted") - >>> f.add_transition("wags tail", "barks", "squirrel!") - >>> print(f.pformat()) - +-----------+-------------+-----------+----------+---------+ - Start | Event | End | On Enter | On Exit - +-----------+-------------+-----------+----------+---------+ - barks | gets petted | wags tail | | - sits[^] | squirrel! | barks | | - wags tail | gets petted | sits | | - wags tail | squirrel! | barks | | - +-----------+-------------+-----------+----------+---------+ - """ - def orderedkeys(data): - if sort: - return sorted(six.iterkeys(data)) - return list(six.iterkeys(data)) - tbl = table.PleasantTable(["Start", "Event", "End", - "On Enter", "On Exit"]) - for state in orderedkeys(self._states): - prefix_markings = [] - if self.current_state == state: - prefix_markings.append("@") - postfix_markings = [] - if self.start_state == state: - postfix_markings.append("^") - if self._states[state]['terminal']: - postfix_markings.append("$") - pretty_state = "%s%s" % ("".join(prefix_markings), state) - if postfix_markings: - pretty_state += "[%s]" % "".join(postfix_markings) - if self._transitions[state]: - for event in orderedkeys(self._transitions[state]): - target = self._transitions[state][event] - row = [pretty_state, event, target.name] - if target.on_enter is not None: - try: - row.append(target.on_enter.__name__) - except AttributeError: - row.append(target.on_enter) - else: - row.append('') - if target.on_exit is not None: - try: - row.append(target.on_exit.__name__) - except AttributeError: - row.append(target.on_exit) - else: - row.append('') - tbl.add_row(row) - else: - tbl.add_row([pretty_state, "", "", "", ""]) - return tbl.pformat() diff --git a/taskflow/types/table.py b/taskflow/types/table.py deleted file mode 100644 index 5966051d..00000000 --- a/taskflow/types/table.py +++ /dev/null @@ -1,139 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import itertools -import os - -import six - - -class PleasantTable(object): - """A tiny pretty printing table (like prettytable/tabulate but smaller). - - Creates simply formatted tables (with no special sauce):: - - >>> from taskflow.types import table - >>> tbl = table.PleasantTable(['Name', 'City', 'State', 'Country']) - >>> tbl.add_row(["Josh", "San Jose", "CA", "USA"]) - >>> print(tbl.pformat()) - +------+----------+-------+---------+ - Name | City | State | Country - +------+----------+-------+---------+ - Josh | San Jose | CA | USA - +------+----------+-------+---------+ - """ - - # Constants used when pretty formatting the table. - COLUMN_STARTING_CHAR = ' ' - COLUMN_ENDING_CHAR = '' - COLUMN_SEPARATOR_CHAR = '|' - HEADER_FOOTER_JOINING_CHAR = '+' - HEADER_FOOTER_CHAR = '-' - LINE_SEP = os.linesep - - @staticmethod - def _center_text(text, max_len, fill=' '): - return '{0:{fill}{align}{size}}'.format(text, fill=fill, - align="^", size=max_len) - - @classmethod - def _size_selector(cls, possible_sizes): - """Select the maximum size, utility function for adding borders. - - The number two is used so that the edges of a column have spaces - around them (instead of being right next to a column separator). - - :param possible_sizes: possible sizes available - :returns: maximum size - :rtype: number - """ - try: - return max(x + 2 for x in possible_sizes) - except ValueError: - return 0 - - def __init__(self, columns): - if len(columns) == 0: - raise ValueError("Column count must be greater than zero") - self._columns = [column.strip() for column in columns] - self._rows = [] - - def add_row(self, row): - if len(row) != len(self._columns): - raise ValueError("Row must have %s columns instead of" - " %s columns" % (len(self._columns), len(row))) - self._rows.append([six.text_type(column) for column in row]) - - def pformat(self): - # Figure out the maximum column sizes... - column_count = len(self._columns) - column_sizes = [0] * column_count - headers = [] - for i, column in enumerate(self._columns): - possible_sizes_iter = itertools.chain( - [len(column)], (len(row[i]) for row in self._rows)) - column_sizes[i] = self._size_selector(possible_sizes_iter) - headers.append(self._center_text(column, column_sizes[i])) - # Build the header and footer prefix/postfix. - header_footer_buf = six.StringIO() - header_footer_buf.write(self.HEADER_FOOTER_JOINING_CHAR) - for i, header in enumerate(headers): - header_footer_buf.write(self.HEADER_FOOTER_CHAR * len(header)) - if i + 1 != column_count: - header_footer_buf.write(self.HEADER_FOOTER_JOINING_CHAR) - header_footer_buf.write(self.HEADER_FOOTER_JOINING_CHAR) - # Build the main header. - content_buf = six.StringIO() - content_buf.write(header_footer_buf.getvalue()) - content_buf.write(self.LINE_SEP) - content_buf.write(self.COLUMN_STARTING_CHAR) - for i, header in enumerate(headers): - if i + 1 == column_count: - if self.COLUMN_ENDING_CHAR: - content_buf.write(headers[i]) - content_buf.write(self.COLUMN_ENDING_CHAR) - else: - content_buf.write(headers[i].rstrip()) - else: - content_buf.write(headers[i]) - content_buf.write(self.COLUMN_SEPARATOR_CHAR) - content_buf.write(self.LINE_SEP) - content_buf.write(header_footer_buf.getvalue()) - # Build the main content. - row_count = len(self._rows) - if row_count: - content_buf.write(self.LINE_SEP) - for i, row in enumerate(self._rows): - pieces = [] - for j, column in enumerate(row): - pieces.append(self._center_text(column, column_sizes[j])) - if j + 1 != column_count: - pieces.append(self.COLUMN_SEPARATOR_CHAR) - blob = ''.join(pieces) - if self.COLUMN_ENDING_CHAR: - content_buf.write(self.COLUMN_STARTING_CHAR) - content_buf.write(blob) - content_buf.write(self.COLUMN_ENDING_CHAR) - else: - blob = blob.rstrip() - if blob: - content_buf.write(self.COLUMN_STARTING_CHAR) - content_buf.write(blob) - if i + 1 != row_count: - content_buf.write(self.LINE_SEP) - content_buf.write(self.LINE_SEP) - content_buf.write(header_footer_buf.getvalue()) - return content_buf.getvalue() diff --git a/tools/state_graph.py b/tools/state_graph.py index c37cd703..e7f2d139 100755 --- a/tools/state_graph.py +++ b/tools/state_graph.py @@ -29,10 +29,11 @@ sys.path.insert(0, top_dir) # $ pip install pydot2 import pydot +from automaton import machines + from taskflow.engines.action_engine import runner from taskflow.engines.worker_based import protocol from taskflow import states -from taskflow.types import fsm # This is just needed to get at the runner builder object (we will not @@ -52,7 +53,7 @@ def clean_event(name): def make_machine(start_state, transitions): - machine = fsm.FSM(start_state) + machine = machines.FiniteMachine() machine.add_state(start_state) for (start_state, end_state) in transitions: if start_state not in machine: @@ -62,6 +63,7 @@ def make_machine(start_state, transitions): # Make a fake event (not used anyway)... event = "on_%s" % (end_state) machine.add_transition(start_state, end_state, event.lower()) + machine.default_start_state = start_state return machine @@ -192,7 +194,7 @@ def main(): start = pydot.Node("__start__", shape="point", width="0.1", xlabel='start', fontcolor='green', **node_attrs) g.add_node(start) - g.add_edge(pydot.Edge(start, nodes[source.start_state], style='dotted')) + g.add_edge(pydot.Edge(start, nodes[source.default_start_state], style='dotted')) print("*" * len(graph_name)) print(graph_name) From 4d06fe289a08316e65ffa7a565529987c789b669 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 13 Jul 2015 16:01:25 -0700 Subject: [PATCH 17/55] Add deprecated module(s) for prior FSM/table code-base This allows those who were using it to still continue using it until 2.0 where it will be removed; this makes it possible for those users to get off that code in a way that will be easily do-able (without totally breaking there code-bases, until we do that in the 2.0 release). Change-Id: Ib61f35170351ddba2d48174299e8c2ca20fde885 --- taskflow/types/fsm.py | 37 ++++++++++ taskflow/types/table.py | 147 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 taskflow/types/fsm.py create mode 100644 taskflow/types/table.py diff --git a/taskflow/types/fsm.py b/taskflow/types/fsm.py new file mode 100644 index 00000000..a7075365 --- /dev/null +++ b/taskflow/types/fsm.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import automaton +from automaton import exceptions as excp +from automaton import machines +from debtcollector import removals + + +# TODO(harlowja): remove me in a future version, since the futurist +# is the replacement for this whole module... +removals.removed_module(__name__, + replacement="the '%s' library" % automaton.__name__, + version="1.16", removal_version='2.0', + stacklevel=4) + + +# Keep alias classes/functions... around until this module is removed. +FSM = machines.FiniteMachine +FrozenMachine = excp.FrozenMachine +NotInitialized = excp.NotInitialized +InvalidState = excp.InvalidState +NotFound = excp.NotFound +Duplicate = excp.Duplicate diff --git a/taskflow/types/table.py b/taskflow/types/table.py new file mode 100644 index 00000000..42c7cfb4 --- /dev/null +++ b/taskflow/types/table.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import itertools +import os + +from debtcollector import removals +import six + +# TODO(harlowja): remove me in a future version, since the futurist +# is the replacement for this whole module... +removals.removed_module(__name__, + replacement="the 'prettytable' library", + version="1.16", removal_version='2.0', + stacklevel=4) + + +class PleasantTable(object): + """A tiny pretty printing table (like prettytable/tabulate but smaller). + + Creates simply formatted tables (with no special sauce):: + + >>> from taskflow.types import table + >>> tbl = table.PleasantTable(['Name', 'City', 'State', 'Country']) + >>> tbl.add_row(["Josh", "San Jose", "CA", "USA"]) + >>> print(tbl.pformat()) + +------+----------+-------+---------+ + Name | City | State | Country + +------+----------+-------+---------+ + Josh | San Jose | CA | USA + +------+----------+-------+---------+ + """ + + # Constants used when pretty formatting the table. + COLUMN_STARTING_CHAR = ' ' + COLUMN_ENDING_CHAR = '' + COLUMN_SEPARATOR_CHAR = '|' + HEADER_FOOTER_JOINING_CHAR = '+' + HEADER_FOOTER_CHAR = '-' + LINE_SEP = os.linesep + + @staticmethod + def _center_text(text, max_len, fill=' '): + return '{0:{fill}{align}{size}}'.format(text, fill=fill, + align="^", size=max_len) + + @classmethod + def _size_selector(cls, possible_sizes): + """Select the maximum size, utility function for adding borders. + + The number two is used so that the edges of a column have spaces + around them (instead of being right next to a column separator). + + :param possible_sizes: possible sizes available + :returns: maximum size + :rtype: number + """ + try: + return max(x + 2 for x in possible_sizes) + except ValueError: + return 0 + + def __init__(self, columns): + if len(columns) == 0: + raise ValueError("Column count must be greater than zero") + self._columns = [column.strip() for column in columns] + self._rows = [] + + def add_row(self, row): + if len(row) != len(self._columns): + raise ValueError("Row must have %s columns instead of" + " %s columns" % (len(self._columns), len(row))) + self._rows.append([six.text_type(column) for column in row]) + + def pformat(self): + # Figure out the maximum column sizes... + column_count = len(self._columns) + column_sizes = [0] * column_count + headers = [] + for i, column in enumerate(self._columns): + possible_sizes_iter = itertools.chain( + [len(column)], (len(row[i]) for row in self._rows)) + column_sizes[i] = self._size_selector(possible_sizes_iter) + headers.append(self._center_text(column, column_sizes[i])) + # Build the header and footer prefix/postfix. + header_footer_buf = six.StringIO() + header_footer_buf.write(self.HEADER_FOOTER_JOINING_CHAR) + for i, header in enumerate(headers): + header_footer_buf.write(self.HEADER_FOOTER_CHAR * len(header)) + if i + 1 != column_count: + header_footer_buf.write(self.HEADER_FOOTER_JOINING_CHAR) + header_footer_buf.write(self.HEADER_FOOTER_JOINING_CHAR) + # Build the main header. + content_buf = six.StringIO() + content_buf.write(header_footer_buf.getvalue()) + content_buf.write(self.LINE_SEP) + content_buf.write(self.COLUMN_STARTING_CHAR) + for i, header in enumerate(headers): + if i + 1 == column_count: + if self.COLUMN_ENDING_CHAR: + content_buf.write(headers[i]) + content_buf.write(self.COLUMN_ENDING_CHAR) + else: + content_buf.write(headers[i].rstrip()) + else: + content_buf.write(headers[i]) + content_buf.write(self.COLUMN_SEPARATOR_CHAR) + content_buf.write(self.LINE_SEP) + content_buf.write(header_footer_buf.getvalue()) + # Build the main content. + row_count = len(self._rows) + if row_count: + content_buf.write(self.LINE_SEP) + for i, row in enumerate(self._rows): + pieces = [] + for j, column in enumerate(row): + pieces.append(self._center_text(column, column_sizes[j])) + if j + 1 != column_count: + pieces.append(self.COLUMN_SEPARATOR_CHAR) + blob = ''.join(pieces) + if self.COLUMN_ENDING_CHAR: + content_buf.write(self.COLUMN_STARTING_CHAR) + content_buf.write(blob) + content_buf.write(self.COLUMN_ENDING_CHAR) + else: + blob = blob.rstrip() + if blob: + content_buf.write(self.COLUMN_STARTING_CHAR) + content_buf.write(blob) + if i + 1 != row_count: + content_buf.write(self.LINE_SEP) + content_buf.write(self.LINE_SEP) + content_buf.write(header_footer_buf.getvalue()) + return content_buf.getvalue() From 38a4b21f5f473ff85bd3a8b2ff30d07547b0d02f Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Tue, 14 Jul 2015 15:38:32 -0700 Subject: [PATCH 18/55] =?UTF-8?q?Found=20another=20removal=5Fversion=3D=3F?= =?UTF-8?q?=20that=20should=20be=20removal=5Fversion=3D2.0?= Change-Id: I515f36dfea037929231ca8c591c870ebfe895344 --- taskflow/conductors/single_threaded.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taskflow/conductors/single_threaded.py b/taskflow/conductors/single_threaded.py index 6f50fe74..85eebaef 100644 --- a/taskflow/conductors/single_threaded.py +++ b/taskflow/conductors/single_threaded.py @@ -28,4 +28,4 @@ removals.removed_module(__name__, # TODO(harlowja): remove this proxy/legacy class soon... SingleThreadedConductor = moves.moved_class( impl_blocking.BlockingConductor, 'SingleThreadedConductor', - __name__, version="0.8", removal_version="?") + __name__, version="0.8", removal_version="2.0") From 8409f2ced0594b5f79d0d1c346a44d3b4a29b424 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 15 Jul 2015 01:38:24 +0000 Subject: [PATCH 19/55] Updated from global requirements Change-Id: I9e76aa94e681e68b5e4fa0fc772f9266355aa666 --- requirements.txt | 4 ++-- setup.py | 2 +- test-requirements.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 24414c6d..ebb42afe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. # See: https://bugs.launchpad.net/pbr/+bug/1384919 for why this is here... -pbr<2.0,>=0.11 +pbr<2.0,>=1.3 # Packages needed for using this library. @@ -38,7 +38,7 @@ monotonic>=0.1 # Apache-2.0 jsonschema!=2.5.0,<3.0.0,>=2.0.0 # For common utilities -oslo.utils>=1.6.0 # Apache-2.0 +oslo.utils>=1.9.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 # For lru caches and such diff --git a/setup.py b/setup.py index 056c16c2..d8080d05 100644 --- a/setup.py +++ b/setup.py @@ -25,5 +25,5 @@ except ImportError: pass setuptools.setup( - setup_requires=['pbr'], + setup_requires=['pbr>=1.3'], pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt index 028c9536..6a4f6ae2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. hacking<0.11,>=0.10.0 -oslotest>=1.5.1 # Apache-2.0 +oslotest>=1.7.0 # Apache-2.0 mock>=1.1;python_version!='2.6' mock==1.0.1;python_version=='2.6' testtools>=1.4.0 From da28e4c904c27b436b3ea117f21b04d812f90da0 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 15 Jul 2015 22:58:44 -0700 Subject: [PATCH 20/55] Move doc8 to being a normal test requirement in test-requirements.txt Change-Id: I6d81cc86d422be359f1b2339943bdfe6067ee5da --- test-requirements.txt | 3 +++ tox.ini | 12 ++---------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 6a4f6ae2..155fe752 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,6 +12,9 @@ testscenarios>=0.4 # Used for testing the WBE engine. kombu>=3.0.7 +# Used for doc style checking +doc8 # Apache-2.0 + # Used for testing zookeeper & backends. zake>=0.1.6 # Apache-2.0 kazoo>=2.2 diff --git a/tox.ini b/tox.ini index 535e611f..5ee35ac8 100644 --- a/tox.ini +++ b/tox.ini @@ -19,14 +19,11 @@ deps = -r{toxinidir}/requirements.txt commands = python setup.py testr --slowest --testr-args='{posargs}' [testenv:docs] -basepython = python2.7 -deps = {[testenv:py27]deps} commands = python setup.py build_sphinx doc8 doc/source [testenv:update-states] -basepython = python2.7 -deps = {[testenv:py27]deps} +deps = {[testenv]deps} pydot2 commands = {toxinidir}/tools/update_states.sh @@ -43,14 +40,11 @@ deps = {[testenv]deps} commands = pylint --rcfile=pylintrc taskflow [testenv:cover] -basepython = python2.7 -deps = {[testenv:py27]deps} +deps = {[testenv]deps} coverage>=3.6 commands = python setup.py testr --coverage --testr-args='{posargs}' [testenv:venv] -basepython = python2.7 -deps = {[testenv:py27]deps} commands = {posargs} [flake8] @@ -63,8 +57,6 @@ import_exceptions = six.moves unittest.mock [testenv:py27] -deps = {[testenv]deps} - doc8 commands = python setup.py testr --slowest --testr-args='{posargs}' sphinx-build -b doctest doc/source doc/build From 02c83d40612bbe3146a9f2ff212759ecdcdeb8ba Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 13 Jul 2015 11:33:12 -0700 Subject: [PATCH 21/55] Remove **most** usage of taskflow.utils in examples It appears folks are using the taskflow.utils code in there own code-bases (likely taking it from the examples) which we do not want to encourage, so remove the usage of **most** of taskflow.utils code from the examples so that people are less likely to copy/paste/reference it. Change-Id: I0ce3c520de347e3e746e7912aa1366a515458424 --- taskflow/examples/dump_memory_backend.py | 14 +-- taskflow/examples/hello_world.py | 14 ++- taskflow/examples/parallel_table_multiply.py | 6 +- taskflow/examples/persistence_example.py | 3 +- taskflow/examples/resume_from_backend.py | 31 ++++--- taskflow/examples/resume_vm_boot.py | 18 ++-- taskflow/examples/resume_volume_create.py | 13 ++- taskflow/examples/run_by_iter.py | 7 +- taskflow/examples/run_by_iter_enumerate.py | 7 +- taskflow/examples/switch_graph_flow.py | 12 +-- taskflow/persistence/models.py | 93 ++++++++++++++++++++ taskflow/utils/persistence_utils.py | 75 ---------------- 12 files changed, 159 insertions(+), 134 deletions(-) diff --git a/taskflow/examples/dump_memory_backend.py b/taskflow/examples/dump_memory_backend.py index 6c6d5488..d4486671 100644 --- a/taskflow/examples/dump_memory_backend.py +++ b/taskflow/examples/dump_memory_backend.py @@ -29,9 +29,7 @@ sys.path.insert(0, self_dir) from taskflow import engines from taskflow.patterns import linear_flow as lf -from taskflow.persistence import backends from taskflow import task -from taskflow.utils import persistence_utils as pu # 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 @@ -43,22 +41,18 @@ class PrintTask(task.Task): def execute(self): print("Running '%s'" % self.name) - -backend = backends.fetch({ - 'connection': 'memory://', -}) -book, flow_detail = pu.temporary_flow_detail(backend=backend) - # 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, flow_detail=flow_detail, - book=book, backend=backend) +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("----------") diff --git a/taskflow/examples/hello_world.py b/taskflow/examples/hello_world.py index 38a6b387..2ec1c953 100644 --- a/taskflow/examples/hello_world.py +++ b/taskflow/examples/hello_world.py @@ -31,7 +31,6 @@ from taskflow import engines from taskflow.patterns import linear_flow as lf from taskflow.patterns import unordered_flow as uf from taskflow import task -from taskflow.utils import eventlet_utils # INTRO: This is the defacto hello world equivalent for taskflow; it shows how @@ -82,25 +81,34 @@ song.add(PrinterTask("conductor@begin", show_name=False, inject={'output': "*dong*"})) # Run in parallel using eventlet green threads... -if eventlet_utils.EVENTLET_AVAILABLE: - with futurist.GreenThreadPoolExecutor() as executor: +try: + executor = futurist.GreenThreadPoolExecutor() +except RuntimeError: + # No eventlet currently active, skip running with it... + pass +else: + print("-- Running in parallel using eventlet --") + with executor: e = engines.load(song, executor=executor, engine='parallel') e.run() # Run in parallel using real threads... with futurist.ThreadPoolExecutor(max_workers=1) as executor: + print("-- Running in parallel using threads --") e = engines.load(song, executor=executor, engine='parallel') e.run() # Run in parallel using external processes... with futurist.ProcessPoolExecutor(max_workers=1) as executor: + print("-- Running in parallel using processes --") e = engines.load(song, executor=executor, engine='parallel') 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() diff --git a/taskflow/examples/parallel_table_multiply.py b/taskflow/examples/parallel_table_multiply.py index e06e36d8..5cd8e9c8 100644 --- a/taskflow/examples/parallel_table_multiply.py +++ b/taskflow/examples/parallel_table_multiply.py @@ -33,7 +33,6 @@ from six.moves import range as compat_range from taskflow import engines from taskflow.patterns import unordered_flow as uf from taskflow import task -from taskflow.utils import eventlet_utils # 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 @@ -97,9 +96,10 @@ def main(): f = make_flow(tbl) # Now run it (using the specified executor)... - if eventlet_utils.EVENTLET_AVAILABLE: + try: executor = futurist.GreenThreadPoolExecutor(max_workers=5) - else: + 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) diff --git a/taskflow/examples/persistence_example.py b/taskflow/examples/persistence_example.py index de9b4274..c7c0954d 100644 --- a/taskflow/examples/persistence_example.py +++ b/taskflow/examples/persistence_example.py @@ -33,7 +33,6 @@ from taskflow import engines from taskflow.patterns import linear_flow as lf from taskflow.persistence import models from taskflow import task -from taskflow.utils import persistence_utils as p_utils import example_utils as eu # noqa @@ -110,4 +109,4 @@ with eu.get_backend(backend_uri) as backend: traceback.print_exc(file=sys.stdout) eu.print_wrapped("Book contents") - print(p_utils.pformat(book)) + print(book.pformat()) diff --git a/taskflow/examples/resume_from_backend.py b/taskflow/examples/resume_from_backend.py index 677937d4..1b8d1605 100644 --- a/taskflow/examples/resume_from_backend.py +++ b/taskflow/examples/resume_from_backend.py @@ -14,6 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. +import contextlib import logging import os import sys @@ -27,10 +28,12 @@ top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 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 -from taskflow.utils import persistence_utils as p_utils import example_utils as eu # noqa @@ -99,19 +102,25 @@ def flow_factory(): # INITIALIZE PERSISTENCE #################################### with eu.get_backend() as backend: - logbook = p_utils.temporary_log_book(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() - flowdetail = p_utils.create_flow_detail(flow, logbook, backend) - engine = taskflow.engines.load(flow, flow_detail=flowdetail, - backend=backend) + engine = taskflow.engines.load(flow, flow_detail=flow_detail, + book=book, backend=backend) - print_task_states(flowdetail, "At the beginning, there is no state") + print_task_states(flow_detail, "At the beginning, there is no state") eu.print_wrapped("Running") engine.run() - print_task_states(flowdetail, "After running") + print_task_states(flow_detail, "After running") # RE-CREATE, RESUME, RUN #################################### @@ -127,9 +136,9 @@ with eu.get_backend() as backend: # start it again for situations where this is useful to-do (say the process # running the above flow crashes). flow2 = flow_factory() - flowdetail2 = find_flow_detail(backend, logbook.uuid, flowdetail.uuid) + flow_detail_2 = find_flow_detail(backend, book.uuid, flow_detail.uuid) engine2 = taskflow.engines.load(flow2, - flow_detail=flowdetail2, - backend=backend) + flow_detail=flow_detail_2, + backend=backend, book=book) engine2.run() - print_task_states(flowdetail2, "At the end") + print_task_states(flow_detail_2, "At the end") diff --git a/taskflow/examples/resume_vm_boot.py b/taskflow/examples/resume_vm_boot.py index ec2293bf..70c8d283 100644 --- a/taskflow/examples/resume_vm_boot.py +++ b/taskflow/examples/resume_vm_boot.py @@ -38,9 +38,8 @@ 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 -from taskflow.utils import eventlet_utils -from taskflow.utils import persistence_utils as p_utils import example_utils as eu # noqa @@ -226,6 +225,8 @@ 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): @@ -237,14 +238,17 @@ with eu.get_backend() as backend: flow_id = None # Set up how we want our engine to run, serial, parallel... - executor = None - if eventlet_utils.EVENTLET_AVAILABLE: - executor = futurist.GreenThreadPoolExecutor(5) + 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) @@ -252,7 +256,9 @@ with eu.get_backend() as backend: except exc.NotFound: pass if book is None and flow_detail is None: - book = p_utils.temporary_log_book(backend) + 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', diff --git a/taskflow/examples/resume_volume_create.py b/taskflow/examples/resume_volume_create.py index 93025d95..3c118122 100644 --- a/taskflow/examples/resume_volume_create.py +++ b/taskflow/examples/resume_volume_create.py @@ -31,11 +31,13 @@ top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 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 -from taskflow.utils import persistence_utils as p_utils import example_utils # noqa @@ -134,9 +136,12 @@ with example_utils.get_backend() as backend: # 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. - logbook = p_utils.temporary_log_book(backend) - flow_detail = p_utils.create_flow_detail(flow, logbook, backend) - print("!! Your tracking id is: '%s+%s'" % (logbook.uuid, + 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: diff --git a/taskflow/examples/run_by_iter.py b/taskflow/examples/run_by_iter.py index 3a00a102..37087ec9 100644 --- a/taskflow/examples/run_by_iter.py +++ b/taskflow/examples/run_by_iter.py @@ -32,9 +32,7 @@ sys.path.insert(0, self_dir) from taskflow import engines from taskflow.patterns import linear_flow as lf -from taskflow.persistence import backends as persistence_backends from taskflow import task -from taskflow.utils import persistence_utils # INTRO: This example shows how to run a set of engines at the same time, each @@ -73,12 +71,9 @@ flows = [] for i in range(0, flow_count): f = make_alphabet_flow(i + 1) flows.append(make_alphabet_flow(i + 1)) -be = persistence_backends.fetch(conf={'connection': 'memory'}) -book = persistence_utils.temporary_log_book(be) engine_iters = [] for f in flows: - fd = persistence_utils.create_flow_detail(f, book, be) - e = engines.load(f, flow_detail=fd, backend=be, book=book) + e = engines.load(f) e.compile() e.storage.inject({'A': 'A'}) e.prepare() diff --git a/taskflow/examples/run_by_iter_enumerate.py b/taskflow/examples/run_by_iter_enumerate.py index 07334cc7..37901b26 100644 --- a/taskflow/examples/run_by_iter_enumerate.py +++ b/taskflow/examples/run_by_iter_enumerate.py @@ -29,9 +29,7 @@ sys.path.insert(0, self_dir) from taskflow import engines from taskflow.patterns import linear_flow as lf -from taskflow.persistence import backends as persistence_backends from taskflow import task -from taskflow.utils import persistence_utils # INTRO: These examples show how to run an engine using the engine iteration # capability, in between iterations other activities occur (in this case a @@ -48,10 +46,7 @@ f = lf.Flow("counter") for i in range(0, 10): f.add(EchoNameTask("echo_%s" % (i + 1))) -be = persistence_backends.fetch(conf={'connection': 'memory'}) -book = persistence_utils.temporary_log_book(be) -fd = persistence_utils.create_flow_detail(f, book, be) -e = engines.load(f, flow_detail=fd, backend=be, book=book) +e = engines.load(f) e.compile() e.prepare() diff --git a/taskflow/examples/switch_graph_flow.py b/taskflow/examples/switch_graph_flow.py index 273763cd..471e633f 100644 --- a/taskflow/examples/switch_graph_flow.py +++ b/taskflow/examples/switch_graph_flow.py @@ -27,9 +27,7 @@ sys.path.insert(0, top_dir) from taskflow import engines from taskflow.patterns import graph_flow as gf -from taskflow.persistence import backends from taskflow import task -from taskflow.utils import persistence_utils as pu class DummyTask(task.Task): @@ -42,18 +40,15 @@ def allow(history): return False +# Declare our work to be done... r = gf.Flow("root") r_a = DummyTask('r-a') r_b = DummyTask('r-b') r.add(r_a, r_b) r.link(r_a, r_b, decider=allow) -backend = backends.fetch({ - 'connection': 'memory://', -}) -book, flow_detail = pu.temporary_flow_detail(backend=backend) - -e = engines.load(r, flow_detail=flow_detail, book=book, backend=backend) +# Setup and run the engine layer. +e = engines.load(r) e.compile() e.prepare() e.run() @@ -62,6 +57,7 @@ e.run() print("---------") print("After run") print("---------") +backend = e.storage.backend entries = [os.path.join(backend.memory.root_path, child) for child in backend.memory.ls(backend.memory.root_path)] while entries: diff --git a/taskflow/persistence/models.py b/taskflow/persistence/models.py index c7a6eae5..93314f0e 100644 --- a/taskflow/persistence/models.py +++ b/taskflow/persistence/models.py @@ -17,6 +17,7 @@ import abc import copy +import os from oslo_utils import timeutils from oslo_utils import uuidutils @@ -26,6 +27,7 @@ from taskflow import exceptions as exc from taskflow import logging from taskflow import states from taskflow.types import failure as ft +from taskflow.utils import misc LOG = logging.getLogger(__name__) @@ -33,6 +35,35 @@ LOG = logging.getLogger(__name__) # Internal helpers... +def _format_meta(metadata, indent): + """Format the common metadata dictionary in the same manner.""" + if not metadata: + return [] + lines = [ + '%s- metadata:' % (" " * indent), + ] + for (k, v) in metadata.items(): + # Progress for now is a special snowflake and will be formatted + # in percent format. + if k == 'progress' and isinstance(v, misc.NUMERIC_TYPES): + v = "%0.2f%%" % (v * 100.0) + lines.append("%s+ %s = %s" % (" " * (indent + 2), k, v)) + return lines + + +def _format_shared(obj, indent): + """Format the common shared attributes in the same manner.""" + if obj is None: + return [] + lines = [] + for attr_name in ("uuid", "state"): + if not hasattr(obj, attr_name): + continue + lines.append("%s- %s = %s" % (" " * indent, attr_name, + getattr(obj, attr_name))) + return lines + + def _copy_function(deep_copy): if deep_copy: return copy.deepcopy @@ -96,6 +127,33 @@ class LogBook(object): self.updated_at = None self.meta = {} + def pformat(self, indent=0, linesep=os.linesep): + """Pretty formats this logbook into a string. + + >>> from taskflow.persistence import models + >>> tmp = models.LogBook("example") + >>> print(tmp.pformat()) + LogBook: 'example' + - uuid = ... + - created_at = ... + """ + cls_name = self.__class__.__name__ + lines = ["%s%s: '%s'" % (" " * indent, cls_name, self.name)] + lines.extend(_format_shared(self, indent=indent + 1)) + lines.extend(_format_meta(self.meta, indent=indent + 1)) + if self.created_at is not None: + lines.append("%s- created_at = %s" + % (" " * (indent + 1), + timeutils.isotime(self.created_at))) + if self.updated_at is not None: + lines.append("%s- updated_at = %s" + % (" " * (indent + 1), + timeutils.isotime(self.updated_at))) + for flow_detail in self: + lines.append(flow_detail.pformat(indent=indent + 1, + linesep=linesep)) + return linesep.join(lines) + def add(self, fd): """Adds a new flow detail into this logbook. @@ -267,6 +325,27 @@ class FlowDetail(object): self.meta = fd.meta return self + def pformat(self, indent=0, linesep=os.linesep): + """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 = ... + """ + cls_name = self.__class__.__name__ + lines = ["%s%s: '%s'" % (" " * indent, cls_name, self.name)] + lines.extend(_format_shared(self, indent=indent + 1)) + lines.extend(_format_meta(self.meta, indent=indent + 1)) + for atom_detail in self: + lines.append(atom_detail.pformat(indent=indent + 1, + linesep=linesep)) + return linesep.join(lines) + def merge(self, fd, deep_copy=False): """Merges the current object state with the given one's state. @@ -572,6 +651,20 @@ class AtomDetail(object): def copy(self): """Copies this atom detail.""" + def pformat(self, indent=0, linesep=os.linesep): + """Pretty formats this atom detail into a string.""" + cls_name = self.__class__.__name__ + lines = ["%s%s: '%s'" % (" " * (indent), cls_name, self.name)] + lines.extend(_format_shared(self, indent=indent + 1)) + lines.append("%s- version = %s" + % (" " * (indent + 1), misc.get_version_string(self))) + lines.append("%s- results = %s" + % (" " * (indent + 1), self.results)) + lines.append("%s- failure = %s" % (" " * (indent + 1), + bool(self.failure))) + lines.extend(_format_meta(self.meta, indent=indent + 1)) + return linesep.join(lines) + class TaskDetail(AtomDetail): """A task detail (an atom detail typically associated with a |tt| atom). diff --git a/taskflow/utils/persistence_utils.py b/taskflow/utils/persistence_utils.py index 0837afb9..1d4dc26e 100644 --- a/taskflow/utils/persistence_utils.py +++ b/taskflow/utils/persistence_utils.py @@ -15,14 +15,11 @@ # under the License. import contextlib -import os -from oslo_utils import timeutils from oslo_utils import uuidutils from taskflow import logging from taskflow.persistence import models -from taskflow.utils import misc LOG = logging.getLogger(__name__) @@ -97,75 +94,3 @@ def create_flow_detail(flow, book=None, backend=None, meta=None): return book.find(flow_id) else: return flow_detail - - -def _format_meta(metadata, indent): - """Format the common metadata dictionary in the same manner.""" - if not metadata: - return [] - lines = [ - '%s- metadata:' % (" " * indent), - ] - for (k, v) in metadata.items(): - # Progress for now is a special snowflake and will be formatted - # in percent format. - if k == 'progress' and isinstance(v, misc.NUMERIC_TYPES): - v = "%0.2f%%" % (v * 100.0) - lines.append("%s+ %s = %s" % (" " * (indent + 2), k, v)) - return lines - - -def _format_shared(obj, indent): - """Format the common shared attributes in the same manner.""" - if obj is None: - return [] - lines = [] - for attr_name in ("uuid", "state"): - if not hasattr(obj, attr_name): - continue - lines.append("%s- %s = %s" % (" " * indent, attr_name, - getattr(obj, attr_name))) - return lines - - -def pformat_atom_detail(atom_detail, indent=0): - """Pretty formats a atom detail.""" - detail_type = models.atom_detail_type(atom_detail) - lines = ["%s%s: '%s'" % (" " * (indent), detail_type, atom_detail.name)] - lines.extend(_format_shared(atom_detail, indent=indent + 1)) - lines.append("%s- version = %s" - % (" " * (indent + 1), misc.get_version_string(atom_detail))) - lines.append("%s- results = %s" - % (" " * (indent + 1), atom_detail.results)) - lines.append("%s- failure = %s" % (" " * (indent + 1), - bool(atom_detail.failure))) - lines.extend(_format_meta(atom_detail.meta, indent=indent + 1)) - return os.linesep.join(lines) - - -def pformat_flow_detail(flow_detail, indent=0): - """Pretty formats a flow detail.""" - lines = ["%sFlow: '%s'" % (" " * indent, flow_detail.name)] - lines.extend(_format_shared(flow_detail, indent=indent + 1)) - lines.extend(_format_meta(flow_detail.meta, indent=indent + 1)) - for task_detail in flow_detail: - lines.append(pformat_atom_detail(task_detail, indent=indent + 1)) - return os.linesep.join(lines) - - -def pformat(book, indent=0): - """Pretty formats a logbook.""" - lines = ["%sLogbook: '%s'" % (" " * indent, book.name)] - lines.extend(_format_shared(book, indent=indent + 1)) - lines.extend(_format_meta(book.meta, indent=indent + 1)) - if book.created_at is not None: - lines.append("%s- created_at = %s" - % (" " * (indent + 1), - timeutils.isotime(book.created_at))) - if book.updated_at is not None: - lines.append("%s- updated_at = %s" - % (" " * (indent + 1), - timeutils.isotime(book.updated_at))) - for flow_detail in book: - lines.append(pformat_flow_detail(flow_detail, indent=indent + 1)) - return os.linesep.join(lines) From 050a52dfb17a9920c1fcec490e2a78eb5e95708b Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 10 Jul 2015 16:10:34 -0700 Subject: [PATCH 22/55] Update 'make_client' kazoo docs and link to them Since the creation of a client is somewhat important and knowing what the options that are transfereed to kazoo are we should explicitly document what keys are and what the values should be. Change-Id: I1a5037b274828190270ea5c402be8b2100306de4 --- taskflow/jobs/backends/impl_zookeeper.py | 10 +++++ .../persistence/backends/impl_zookeeper.py | 10 +++++ taskflow/utils/kazoo_utils.py | 38 ++++++++++++++++++- 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/taskflow/jobs/backends/impl_zookeeper.py b/taskflow/jobs/backends/impl_zookeeper.py index 15b31034..41818c98 100644 --- a/taskflow/jobs/backends/impl_zookeeper.py +++ b/taskflow/jobs/backends/impl_zookeeper.py @@ -273,6 +273,16 @@ class ZookeeperJobBoard(base.NotifyingJobBoard): zookeeper when the ephemeral node and associated session is deemed to have been lost). + Do note that the creation of a kazoo client is achieved + by :py:func:`~taskflow.utils.kazoo_utils.make_client` and the transfer + of this jobboard configuration to that function to make a + client may happen at ``__init__`` time. This implies that certain + parameters from this jobboard configuration may be provided to + :py:func:`~taskflow.utils.kazoo_utils.make_client` such + that if a client was not provided by the caller one will be created + according to :py:func:`~taskflow.utils.kazoo_utils.make_client`'s + specification + .. _zookeeper: http://zookeeper.apache.org/ .. _json: http://json.org/ """ diff --git a/taskflow/persistence/backends/impl_zookeeper.py b/taskflow/persistence/backends/impl_zookeeper.py index 687f103d..11897233 100644 --- a/taskflow/persistence/backends/impl_zookeeper.py +++ b/taskflow/persistence/backends/impl_zookeeper.py @@ -39,6 +39,16 @@ class ZkBackend(path_based.PathBasedBackend): "hosts": "192.168.0.1:2181,192.168.0.2:2181,192.168.0.3:2181", "path": "/taskflow", } + + Do note that the creation of a kazoo client is achieved + by :py:func:`~taskflow.utils.kazoo_utils.make_client` and the transfer + of this backend configuration to that function to make a + client may happen at ``__init__`` time. This implies that certain + parameters from this backend configuration may be provided to + :py:func:`~taskflow.utils.kazoo_utils.make_client` such + that if a client was not provided by the caller one will be created + according to :py:func:`~taskflow.utils.kazoo_utils.make_client`'s + specification """ #: Default path used when none is provided. diff --git a/taskflow/utils/kazoo_utils.py b/taskflow/utils/kazoo_utils.py index f681dc46..c60a9a82 100644 --- a/taskflow/utils/kazoo_utils.py +++ b/taskflow/utils/kazoo_utils.py @@ -151,7 +151,43 @@ def check_compatible(client, min_version=None, max_version=None): def make_client(conf): - """Creates a kazoo client given a configuration dictionary.""" + """Creates a `kazoo`_ `client`_ given a configuration dictionary. + + :param conf: configuration dictionary that will be used to configure + the created client + :type conf: dict + + The keys that will be extracted are: + + - ``read_only``: boolean that specifies whether to allow connections to + read only servers, defaults to ``False`` + - ``randomize_hosts``: boolean that specifies whether to randomize + host lists provided, defaults to ``False`` + - ``command_retry``: a kazoo `retry`_ object (or dict of options which + will be used for creating one) that will be used for retrying commands + that are executed + - ``connection_retry``: a kazoo `retry`_ object (or dict of options which + will be used for creating one) that will be used for retrying + connection failures that occur + - ``hosts``: a string, list, set (or dict with host keys) that will + specify the hosts the kazoo client should be connected to, if none + is provided then ``localhost:2181`` will be used by default + - ``timeout``: a float value that specifies the default timeout that the + kazoo client will use + - ``handler``: a kazoo handler object that can be used to provide the + client with alternate async strategies (the default is `thread`_ + based, but `gevent`_, or `eventlet`_ ones can be provided as needed) + + .. _client: http://kazoo.readthedocs.org/en/latest/api/client.html + .. _kazoo: kazoo.readthedocs.org/ + .. _retry: http://kazoo.readthedocs.org/en/latest/api/retry.html + .. _gevent: http://kazoo.readthedocs.org/en/latest/api/\ + handlers/gevent.html + .. _eventlet: http://kazoo.readthedocs.org/en/latest/api/\ + handlers/eventlet.html + .. _thread: http://kazoo.readthedocs.org/en/latest/api/\ + handlers/threading.html + """ # See: http://kazoo.readthedocs.org/en/latest/api/client.html client_kwargs = { 'read_only': bool(conf.get('read_only')), From 03702c767660a66ce3721b736e617bce8687aa21 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 18 Jul 2015 01:57:34 +0000 Subject: [PATCH 23/55] Updated from global requirements Change-Id: I96f5548c5f7522c882928207871e618165b02651 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 155fe752..b0ed54cf 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,7 @@ hacking<0.11,>=0.10.0 oslotest>=1.7.0 # Apache-2.0 -mock>=1.1;python_version!='2.6' +mock!=1.1.4,>=1.1;python_version!='2.6' mock==1.0.1;python_version=='2.6' testtools>=1.4.0 testscenarios>=0.4 From 58fbfd0f90a75febba45174c6ad2a9b8c28f3fe6 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 27 May 2015 19:18:36 -0700 Subject: [PATCH 24/55] Add ability to reset an engine via a `reset` method If an engines work was previously partially completed and it is desired to reset it (and re-run) so that partially completed or ignored (or other) work inside of it can run again make that possible by exposing and documenting a new `reset` method (and use it internally as well). Change-Id: I47f82010a2108d5d8fd5e42ca9f7e5f165e65488 --- doc/source/img/flow_states.svg | 6 +- doc/source/states.rst | 9 +- taskflow/engines/action_engine/engine.py | 15 +- taskflow/engines/base.py | 15 +- taskflow/states.py | 1 + taskflow/tests/unit/test_engines.py | 186 +++++++++++++++++++++++ 6 files changed, 222 insertions(+), 10 deletions(-) diff --git a/doc/source/img/flow_states.svg b/doc/source/img/flow_states.svg index 80bf1a0a..cf60000f 100644 --- a/doc/source/img/flow_states.svg +++ b/doc/source/img/flow_states.svg @@ -1,8 +1,8 @@ - - -Flow statesPENDINGRUNNINGFAILURESUSPENDINGREVERTEDSUCCESSRESUMINGSUSPENDEDstart + +Flow statesPENDINGRUNNINGFAILURESUSPENDINGREVERTEDSUCCESSRESUMINGSUSPENDEDstart diff --git a/doc/source/states.rst b/doc/source/states.rst index 3d42bad1..d8e19eae 100644 --- a/doc/source/states.rst +++ b/doc/source/states.rst @@ -50,10 +50,11 @@ Flow :align: center :alt: Flow state transitions -**PENDING** - A flow starts 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). +**PENDING** - A flow starts (or +via :py:meth:`~taskflow.engines.base.Engine.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. diff --git a/taskflow/engines/action_engine/engine.py b/taskflow/engines/action_engine/engine.py index 6d9ee264..9dae8d46 100644 --- a/taskflow/engines/action_engine/engine.py +++ b/taskflow/engines/action_engine/engine.py @@ -283,8 +283,19 @@ class ActionEngine(base.Engine): self._storage_ensured = True # Reset everything back to pending (if we were previously reverted). if self.storage.get_flow_state() == states.REVERTED: - self._runtime.reset_all() - self._change_state(states.PENDING) + self.reset() + + @fasteners.locked + def reset(self): + if not self._storage_ensured: + raise exc.InvalidState("Can not reset an engine" + " which has not has its storage" + " populated") + # This transitions *all* contained atoms back into the PENDING state + # with an intention to EXECUTE (or dies trying to do that) and then + # changes the state of the flow to PENDING so that it can then run... + self._runtime.reset_all() + self._change_state(states.PENDING) @fasteners.locked def compile(self): diff --git a/taskflow/engines/base.py b/taskflow/engines/base.py index 4b6a648d..a500dd47 100644 --- a/taskflow/engines/base.py +++ b/taskflow/engines/base.py @@ -92,13 +92,26 @@ class Engine(object): could not be achieved. """ + @abc.abstractmethod + def reset(self): + """Reset back to the ``PENDING`` state. + + If a flow had previously ended up (from a prior engine + :py:func:`.run`) in the ``FAILURE``, ``SUCCESS`` or ``REVERTED`` + states (or for some reason it ended up in an intermediary state) it + can be desireable 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 :py:meth:`.run` is called + directly without doing a reset). + """ + @abc.abstractmethod def prepare(self): """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. + the engine will enter the ``PENDING`` state. """ @abc.abstractmethod diff --git a/taskflow/states.py b/taskflow/states.py index 07e70dd1..1939012b 100644 --- a/taskflow/states.py +++ b/taskflow/states.py @@ -103,6 +103,7 @@ _ALLOWED_FLOW_TRANSITIONS = frozenset(( (FAILURE, RUNNING), # see note below (REVERTED, PENDING), # try again + (SUCCESS, PENDING), # run it again (SUSPENDING, SUSPENDED), # suspend finished (SUSPENDING, SUCCESS), # all tasks finished while we were waiting diff --git a/taskflow/tests/unit/test_engines.py b/taskflow/tests/unit/test_engines.py index 0c3ac031..c56d7569 100644 --- a/taskflow/tests/unit/test_engines.py +++ b/taskflow/tests/unit/test_engines.py @@ -16,6 +16,7 @@ import contextlib import functools +import threading import futurist import six @@ -798,6 +799,138 @@ class EngineMissingDepsTest(utils.EngineTestBase): self.assertIsNotNone(c_e.cause) +class EngineResetTests(utils.EngineTestBase): + def test_completed_reset_run_again(self): + task1 = utils.ProgressingTask(name='task1') + task2 = utils.ProgressingTask(name='task2') + task3 = utils.ProgressingTask(name='task3') + + flow = lf.Flow('root') + flow.add(task1, task2, task3) + + engine = self._make_engine(flow) + with utils.CaptureListener(engine, capture_flow=False) as capturer: + engine.run() + expected = [ + 'task1.t RUNNING', + 'task1.t SUCCESS(5)', + + 'task2.t RUNNING', + 'task2.t SUCCESS(5)', + + 'task3.t RUNNING', + 'task3.t SUCCESS(5)', + ] + self.assertEqual(expected, capturer.values) + + with utils.CaptureListener(engine, capture_flow=False) as capturer: + engine.run() + self.assertEqual([], capturer.values) + + engine.reset() + with utils.CaptureListener(engine, capture_flow=False) as capturer: + engine.run() + self.assertEqual(expected, capturer.values) + + def test_failed_reset_run_again(self): + task1 = utils.ProgressingTask(name='task1') + task2 = utils.ProgressingTask(name='task2') + task3 = utils.FailingTask(name='task3') + + flow = lf.Flow('root') + flow.add(task1, task2, task3) + engine = self._make_engine(flow) + + with utils.CaptureListener(engine, capture_flow=False) as capturer: + # Also allow a WrappedFailure exception so that when this is used + # with the WBE engine (as it can't re-raise the original + # exception) that we will work correctly.... + self.assertRaises((RuntimeError, exc.WrappedFailure), engine.run) + + expected = [ + 'task1.t RUNNING', + 'task1.t SUCCESS(5)', + 'task2.t RUNNING', + 'task2.t SUCCESS(5)', + 'task3.t RUNNING', + + 'task3.t FAILURE(Failure: RuntimeError: Woot!)', + + 'task3.t REVERTING', + 'task3.t REVERTED(None)', + 'task2.t REVERTING', + 'task2.t REVERTED(None)', + 'task1.t REVERTING', + 'task1.t REVERTED(None)', + ] + self.assertEqual(expected, capturer.values) + + engine.reset() + with utils.CaptureListener(engine, capture_flow=False) as capturer: + self.assertRaises((RuntimeError, exc.WrappedFailure), engine.run) + self.assertEqual(expected, capturer.values) + + def test_suspended_reset_run_again(self): + task1 = utils.ProgressingTask(name='task1') + task2 = utils.ProgressingTask(name='task2') + task3 = utils.ProgressingTask(name='task3') + + flow = lf.Flow('root') + flow.add(task1, task2, task3) + engine = self._make_engine(flow) + suspend_at = object() + expected_states = [ + states.RESUMING, + states.SCHEDULING, + states.WAITING, + states.ANALYZING, + states.SCHEDULING, + states.WAITING, + # Stop/suspend here... + suspend_at, + states.SUSPENDED, + ] + with utils.CaptureListener(engine, capture_flow=False) as capturer: + for i, st in enumerate(engine.run_iter()): + expected = expected_states[i] + if expected is suspend_at: + engine.suspend() + else: + self.assertEqual(expected, st) + + expected = [ + 'task1.t RUNNING', + 'task1.t SUCCESS(5)', + + 'task2.t RUNNING', + 'task2.t SUCCESS(5)', + ] + self.assertEqual(expected, capturer.values) + + with utils.CaptureListener(engine, capture_flow=False) as capturer: + engine.run() + expected = [ + 'task3.t RUNNING', + 'task3.t SUCCESS(5)', + ] + self.assertEqual(expected, capturer.values) + + engine.reset() + with utils.CaptureListener(engine, capture_flow=False) as capturer: + engine.run() + expected = [ + 'task1.t RUNNING', + 'task1.t SUCCESS(5)', + + 'task2.t RUNNING', + 'task2.t SUCCESS(5)', + + 'task3.t RUNNING', + 'task3.t SUCCESS(5)', + ] + self.assertEqual(expected, capturer.values) + + class EngineGraphConditionalFlowTest(utils.EngineTestBase): def test_graph_flow_conditional(self): @@ -829,6 +962,54 @@ class EngineGraphConditionalFlowTest(utils.EngineTestBase): ]) self.assertEqual(expected, set(capturer.values)) + def test_graph_flow_conditional_ignore_reset(self): + allow_execute = threading.Event() + flow = gf.Flow('root') + + task1 = utils.ProgressingTask(name='task1') + task2 = utils.ProgressingTask(name='task2') + task3 = utils.ProgressingTask(name='task3') + + flow.add(task1, task2, task3) + flow.link(task1, task2) + flow.link(task2, task3, decider=lambda history: allow_execute.is_set()) + + engine = self._make_engine(flow) + with utils.CaptureListener(engine, capture_flow=False) as capturer: + engine.run() + + expected = set([ + 'task1.t RUNNING', + 'task1.t SUCCESS(5)', + + 'task2.t RUNNING', + 'task2.t SUCCESS(5)', + + 'task3.t IGNORE', + ]) + self.assertEqual(expected, set(capturer.values)) + self.assertEqual(states.IGNORE, + engine.storage.get_atom_state('task3')) + self.assertEqual(states.IGNORE, + engine.storage.get_atom_intention('task3')) + + engine.reset() + allow_execute.set() + with utils.CaptureListener(engine, capture_flow=False) as capturer: + engine.run() + + expected = set([ + 'task1.t RUNNING', + 'task1.t SUCCESS(5)', + + 'task2.t RUNNING', + 'task2.t SUCCESS(5)', + + 'task3.t RUNNING', + 'task3.t SUCCESS(5)', + ]) + self.assertEqual(expected, set(capturer.values)) + def test_graph_flow_diamond_ignored(self): flow = gf.Flow('root') @@ -951,6 +1132,7 @@ class SerialEngineTest(EngineTaskTest, EngineOptionalRequirementsTest, EngineGraphFlowTest, EngineMissingDepsTest, + EngineResetTests, EngineGraphConditionalFlowTest, EngineCheckingTaskTest, test.TestCase): @@ -978,6 +1160,7 @@ class ParallelEngineWithThreadsTest(EngineTaskTest, EngineLinearAndUnorderedExceptionsTest, EngineOptionalRequirementsTest, EngineGraphFlowTest, + EngineResetTests, EngineMissingDepsTest, EngineGraphConditionalFlowTest, EngineCheckingTaskTest, @@ -1018,6 +1201,7 @@ class ParallelEngineWithEventletTest(EngineTaskTest, EngineLinearAndUnorderedExceptionsTest, EngineOptionalRequirementsTest, EngineGraphFlowTest, + EngineResetTests, EngineMissingDepsTest, EngineGraphConditionalFlowTest, EngineCheckingTaskTest, @@ -1041,6 +1225,7 @@ class ParallelEngineWithProcessTest(EngineTaskTest, EngineLinearAndUnorderedExceptionsTest, EngineOptionalRequirementsTest, EngineGraphFlowTest, + EngineResetTests, EngineMissingDepsTest, EngineGraphConditionalFlowTest, test.TestCase): @@ -1069,6 +1254,7 @@ class WorkerBasedEngineTest(EngineTaskTest, EngineLinearAndUnorderedExceptionsTest, EngineOptionalRequirementsTest, EngineGraphFlowTest, + EngineResetTests, EngineMissingDepsTest, EngineGraphConditionalFlowTest, test.TestCase): From 5bbf8cf199aa0d1b72d97e8f896a9dfa2d8085de Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Sat, 18 Jul 2015 15:39:27 -0700 Subject: [PATCH 25/55] Link to run() method in engines doc Change-Id: I571d8e4d767efddc23e28fd41602f0ac24b29f3c --- doc/source/engines.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/engines.rst b/doc/source/engines.rst index fa37274e..69f6ea6c 100644 --- a/doc/source/engines.rst +++ b/doc/source/engines.rst @@ -142,12 +142,12 @@ Serial **Engine type**: ``'serial'`` -Runs all tasks on a single thread -- the same thread ``engine.run()`` is -called from. +Runs all tasks on a single thread -- the same thread +:py:meth:`~taskflow.engines.base.Engine.run` is called from. .. note:: - This engine is used by default. + This engine is used by **default**. .. tip:: From e5092b62c3e6571eb369fc940ff00dae254147de Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Sat, 18 Jul 2015 18:09:31 -0700 Subject: [PATCH 26/55] Just link to the worker engine docs instead of including a TOC inline Change-Id: I54b77d7f0665431f5faa4c25015cb93e1c5b32cd --- doc/source/engines.rst | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/doc/source/engines.rst b/doc/source/engines.rst index fa37274e..e54d7540 100644 --- a/doc/source/engines.rst +++ b/doc/source/engines.rst @@ -193,14 +193,7 @@ 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. - -For further information, please refer to the the following: - -.. toctree:: - :maxdepth: 2 - - workers + whole documentation :doc:`section ` to it. How they run ============ From 359cc490bd2426c412e166b5c455f5bccd87bd9b Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 25 Jun 2015 16:02:21 -0700 Subject: [PATCH 27/55] Create and use a serial retry executor To make it easily possible to change the retry atom execution from being in the engine thread this creates a retry executor (which is similar to the task executor) and provide that a serial executor (which it will use to execute with). This makes the retry and task actions closer to being the same and makes the surrounding code that much similar (which makes understanding it easier). Change-Id: I993e938280df3bd97f8076293183ef21989e2dba --- .../engines/action_engine/actions/base.py | 16 +++++ .../engines/action_engine/actions/retry.py | 66 +++++-------------- .../engines/action_engine/actions/task.py | 6 +- taskflow/engines/action_engine/completer.py | 11 +++- taskflow/engines/action_engine/engine.py | 22 +++++-- taskflow/engines/action_engine/executor.py | 48 ++++++++++++-- taskflow/engines/action_engine/runner.py | 3 +- taskflow/engines/action_engine/runtime.py | 10 ++- taskflow/engines/action_engine/scheduler.py | 6 +- .../tests/unit/action_engine/test_runner.py | 4 +- .../tests/unit/worker_based/test_executor.py | 24 ------- .../tests/unit/worker_based/test_pipeline.py | 5 +- 12 files changed, 120 insertions(+), 101 deletions(-) diff --git a/taskflow/engines/action_engine/actions/base.py b/taskflow/engines/action_engine/actions/base.py index 48846746..3a014e12 100644 --- a/taskflow/engines/action_engine/actions/base.py +++ b/taskflow/engines/action_engine/actions/base.py @@ -37,3 +37,19 @@ class Action(object): def __init__(self, storage, notifier): self._storage = storage self._notifier = notifier + + @abc.abstractmethod + def schedule_execution(self, atom): + """Schedules atom execution.""" + + @abc.abstractmethod + def schedule_reversion(self, atom): + """Schedules atom reversion.""" + + @abc.abstractmethod + def complete_reversion(self, atom, result): + """Completes atom reversion.""" + + @abc.abstractmethod + def complete_execution(self, atom, result): + """Completes atom execution.""" diff --git a/taskflow/engines/action_engine/actions/retry.py b/taskflow/engines/action_engine/actions/retry.py index 0be19af9..126b9038 100644 --- a/taskflow/engines/action_engine/actions/retry.py +++ b/taskflow/engines/action_engine/actions/retry.py @@ -14,10 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. -import futurist - from taskflow.engines.action_engine.actions import base -from taskflow.engines.action_engine import executor as ex from taskflow import logging from taskflow import retry as retry_atom from taskflow import states @@ -26,28 +23,12 @@ from taskflow.types import failure LOG = logging.getLogger(__name__) -def _execute_retry(retry, arguments): - try: - result = retry.execute(**arguments) - except Exception: - result = failure.Failure() - return (ex.EXECUTED, result) - - -def _revert_retry(retry, arguments): - try: - result = retry.revert(**arguments) - except Exception: - result = failure.Failure() - return (ex.REVERTED, result) - - class RetryAction(base.Action): """An action that handles executing, state changes, ... of retry atoms.""" - def __init__(self, storage, notifier): + def __init__(self, storage, notifier, retry_executor): super(RetryAction, self).__init__(storage, notifier) - self._executor = futurist.SynchronousExecutor() + self._retry_executor = retry_executor def _get_retry_args(self, retry, addons=None): arguments = self._storage.fetch_mapped_args( @@ -88,41 +69,30 @@ class RetryAction(base.Action): details['result'] = result self._notifier.notify(state, details) - def execute(self, retry): - - def _on_done_callback(fut): - result = fut.result()[-1] - if isinstance(result, failure.Failure): - self.change_state(retry, states.FAILURE, result=result) - else: - self.change_state(retry, states.SUCCESS, result=result) - + def schedule_execution(self, retry): self.change_state(retry, states.RUNNING) - fut = self._executor.submit(_execute_retry, retry, - self._get_retry_args(retry)) - fut.add_done_callback(_on_done_callback) - fut.atom = retry - return fut + return self._retry_executor.execute_retry( + retry, self._get_retry_args(retry)) - def revert(self, retry): + def complete_reversion(self, retry, result): + if isinstance(result, failure.Failure): + self.change_state(retry, states.REVERT_FAILURE, result=result) + else: + self.change_state(retry, states.REVERTED, result=result) - def _on_done_callback(fut): - result = fut.result()[-1] - if isinstance(result, failure.Failure): - self.change_state(retry, states.REVERT_FAILURE, result=result) - else: - self.change_state(retry, states.REVERTED, result=result) + def complete_execution(self, retry, result): + if isinstance(result, failure.Failure): + self.change_state(retry, states.FAILURE, result=result) + else: + self.change_state(retry, states.SUCCESS, result=result) + def schedule_reversion(self, retry): self.change_state(retry, states.REVERTING) arg_addons = { retry_atom.REVERT_FLOW_FAILURES: self._storage.get_failures(), } - fut = self._executor.submit(_revert_retry, retry, - self._get_retry_args(retry, - addons=arg_addons)) - fut.add_done_callback(_on_done_callback) - fut.atom = retry - return fut + return self._retry_executor.revert_retry( + retry, self._get_retry_args(retry, addons=arg_addons)) def on_failure(self, retry, atom, last_failure): self._storage.save_retry_failure(retry.name, atom.name, last_failure) diff --git a/taskflow/engines/action_engine/actions/task.py b/taskflow/engines/action_engine/actions/task.py index 7ae6b55f..ac117e1c 100644 --- a/taskflow/engines/action_engine/actions/task.py +++ b/taskflow/engines/action_engine/actions/task.py @@ -134,10 +134,9 @@ class TaskAction(base.Action): task) else: progress_callback = None - future = self._task_executor.revert_task( + return self._task_executor.revert_task( task, task_uuid, arguments, task_result, failures, progress_callback=progress_callback) - return future def complete_reversion(self, task, result): if isinstance(result, failure.Failure): @@ -145,6 +144,3 @@ class TaskAction(base.Action): else: self.change_state(task, states.REVERTED, progress=1.0, result=result) - - def wait_for_any(self, fs, timeout): - return self._task_executor.wait_for_any(fs, timeout) diff --git a/taskflow/engines/action_engine/completer.py b/taskflow/engines/action_engine/completer.py index 47300a46..0ab727a4 100644 --- a/taskflow/engines/action_engine/completer.py +++ b/taskflow/engines/action_engine/completer.py @@ -106,9 +106,9 @@ class Completer(object): def __init__(self, runtime): self._runtime = weakref.proxy(runtime) self._analyzer = runtime.analyzer - self._retry_action = runtime.retry_action self._storage = runtime.storage self._task_action = runtime.task_action + self._retry_action = runtime.retry_action self._undefined_resolver = RevertAll(self._runtime) def _complete_task(self, task, event, result): @@ -118,6 +118,13 @@ class Completer(object): else: self._task_action.complete_reversion(task, result) + def _complete_retry(self, retry, event, result): + """Completes the given retry, processes retry failure.""" + if event == ex.EXECUTED: + self._retry_action.complete_execution(retry, result) + else: + self._retry_action.complete_reversion(retry, result) + def resume(self): """Resumes nodes in the contained graph. @@ -148,6 +155,8 @@ class Completer(object): """ if isinstance(node, task_atom.BaseTask): self._complete_task(node, event, result) + else: + self._complete_retry(node, event, result) if isinstance(result, failure.Failure): if event == ex.EXECUTED: self._process_atom_failure(node, result) diff --git a/taskflow/engines/action_engine/engine.py b/taskflow/engines/action_engine/engine.py index 6d9ee264..fa6247d8 100644 --- a/taskflow/engines/action_engine/engine.py +++ b/taskflow/engines/action_engine/engine.py @@ -41,13 +41,17 @@ LOG = logging.getLogger(__name__) @contextlib.contextmanager -def _start_stop(executor): - # A teenie helper context manager to safely start/stop a executor... - executor.start() +def _start_stop(task_executor, retry_executor): + # A teenie helper context manager to safely start/stop engine executors... + task_executor.start() try: - yield executor + retry_executor.start() + try: + yield (task_executor, retry_executor) + finally: + retry_executor.stop() finally: - executor.stop() + task_executor.stop() class ActionEngine(base.Engine): @@ -82,6 +86,9 @@ class ActionEngine(base.Engine): self._lock = threading.RLock() self._state_lock = threading.RLock() self._storage_ensured = False + # Retries are not *currently* executed out of the engines process + # or thread (this could change in the future if we desire it to). + self._retry_executor = executor.SerialRetryExecutor() def _check(self, name, check_compiled, check_storage_ensured): """Check (and raise) if the engine has not reached a certain stage.""" @@ -167,7 +174,7 @@ class ActionEngine(base.Engine): self.validate() runner = self._runtime.runner last_state = None - with _start_stop(self._task_executor): + with _start_stop(self._task_executor, self._retry_executor): self._change_state(states.RUNNING) try: closed = False @@ -294,7 +301,8 @@ class ActionEngine(base.Engine): self._runtime = runtime.Runtime(self._compilation, self.storage, self.atom_notifier, - self._task_executor) + self._task_executor, + self._retry_executor) self._runtime.compile() self._compiled = True diff --git a/taskflow/engines/action_engine/executor.py b/taskflow/engines/action_engine/executor.py index f03aa8e2..b47322d7 100644 --- a/taskflow/engines/action_engine/executor.py +++ b/taskflow/engines/action_engine/executor.py @@ -33,7 +33,6 @@ from taskflow import logging from taskflow import task as task_atom from taskflow.types import failure from taskflow.types import notifier -from taskflow.utils import async_utils from taskflow.utils import threading_utils # Execution and reversion events. @@ -58,6 +57,22 @@ _KIND_EVENT = 'event' LOG = logging.getLogger(__name__) +def _execute_retry(retry, arguments): + try: + result = retry.execute(**arguments) + except Exception: + result = failure.Failure() + return (EXECUTED, result) + + +def _revert_retry(retry, arguments): + try: + result = retry.revert(**arguments) + except Exception: + result = failure.Failure() + return (REVERTED, result) + + def _execute_task(task, arguments, progress_callback=None): with notifier.register_deregister(task.notifier, _UPDATE_PROGRESS, @@ -322,6 +337,33 @@ class _Dispatcher(object): self._dead.wait(leftover) +class SerialRetryExecutor(object): + """Executes and reverts retries.""" + + def __init__(self): + self._executor = futurist.SynchronousExecutor() + + def start(self): + """Prepare to execute retries.""" + self._executor.restart() + + def stop(self): + """Finalize retry executor.""" + self._executor.shutdown() + + def execute_retry(self, retry, arguments): + """Schedules retry execution.""" + fut = self._executor.submit(_execute_retry, retry, arguments) + fut.atom = retry + return fut + + def revert_retry(self, retry, arguments): + """Schedules retry reversion.""" + fut = self._executor.submit(_revert_retry, retry, arguments) + fut.atom = retry + return fut + + @six.add_metaclass(abc.ABCMeta) class TaskExecutor(object): """Executes and reverts tasks. @@ -341,10 +383,6 @@ class TaskExecutor(object): progress_callback=None): """Schedules task reversion.""" - def wait_for_any(self, fs, timeout=None): - """Wait for futures returned by this executor to complete.""" - return async_utils.wait_for_any(fs, timeout=timeout) - def start(self): """Prepare to execute tasks.""" diff --git a/taskflow/engines/action_engine/runner.py b/taskflow/engines/action_engine/runner.py index e8cd1734..f02f3f09 100644 --- a/taskflow/engines/action_engine/runner.py +++ b/taskflow/engines/action_engine/runner.py @@ -117,7 +117,6 @@ class Runner(object): # Cache some local functions/methods... do_schedule = self._scheduler.schedule - wait_for_any = self._waiter.wait_for_any do_complete = self._completer.complete def iter_next_nodes(target_node=None): @@ -171,7 +170,7 @@ class Runner(object): # call sometime in the future, or equivalent that will work in # py2 and py3. if memory.not_done: - done, not_done = wait_for_any(memory.not_done, timeout) + done, not_done = self._waiter(memory.not_done, timeout=timeout) memory.done.update(done) memory.not_done = not_done return _ANALYZE diff --git a/taskflow/engines/action_engine/runtime.py b/taskflow/engines/action_engine/runtime.py index 2616b868..38998b8c 100644 --- a/taskflow/engines/action_engine/runtime.py +++ b/taskflow/engines/action_engine/runtime.py @@ -26,6 +26,7 @@ from taskflow.engines.action_engine import scopes as sc from taskflow import flow as flow_type from taskflow import states as st from taskflow import task +from taskflow.utils import async_utils from taskflow.utils import misc @@ -37,9 +38,11 @@ class Runtime(object): action engine to run to completion. """ - def __init__(self, compilation, storage, atom_notifier, task_executor): + def __init__(self, compilation, storage, atom_notifier, + task_executor, retry_executor): self._atom_notifier = atom_notifier self._task_executor = task_executor + self._retry_executor = retry_executor self._storage = storage self._compilation = compilation self._atom_cache = {} @@ -111,7 +114,7 @@ class Runtime(object): @misc.cachedproperty def runner(self): - return ru.Runner(self, self._task_executor) + return ru.Runner(self, async_utils.wait_for_any) @misc.cachedproperty def completer(self): @@ -132,7 +135,8 @@ class Runtime(object): @misc.cachedproperty def retry_action(self): return ra.RetryAction(self._storage, - self._atom_notifier) + self._atom_notifier, + self._retry_executor) @misc.cachedproperty def task_action(self): diff --git a/taskflow/engines/action_engine/scheduler.py b/taskflow/engines/action_engine/scheduler.py index 4ab0b0e1..404781e6 100644 --- a/taskflow/engines/action_engine/scheduler.py +++ b/taskflow/engines/action_engine/scheduler.py @@ -37,13 +37,13 @@ class RetryScheduler(object): """ intention = self._storage.get_atom_intention(retry.name) if intention == st.EXECUTE: - return self._retry_action.execute(retry) + return self._retry_action.schedule_execution(retry) elif intention == st.REVERT: - return self._retry_action.revert(retry) + return self._retry_action.schedule_reversion(retry) elif intention == st.RETRY: self._retry_action.change_state(retry, st.RETRYING) self._runtime.retry_subflow(retry) - return self._retry_action.execute(retry) + return self._retry_action.schedule_execution(retry) else: raise excp.ExecutionFailure("Unknown how to schedule retry with" " intention: %s" % intention) diff --git a/taskflow/tests/unit/action_engine/test_runner.py b/taskflow/tests/unit/action_engine/test_runner.py index 7af2bd78..bff74cb9 100644 --- a/taskflow/tests/unit/action_engine/test_runner.py +++ b/taskflow/tests/unit/action_engine/test_runner.py @@ -42,10 +42,12 @@ class _RunnerTestMixin(object): store.set_flow_state(initial_state) task_notifier = notifier.Notifier() task_executor = executor.SerialTaskExecutor() + retry_executor = executor.SerialRetryExecutor() task_executor.start() self.addCleanup(task_executor.stop) r = runtime.Runtime(compilation, store, - task_notifier, task_executor) + task_notifier, task_executor, + retry_executor) r.compile() return r diff --git a/taskflow/tests/unit/worker_based/test_executor.py b/taskflow/tests/unit/worker_based/test_executor.py index 504433de..0fad2bd3 100644 --- a/taskflow/tests/unit/worker_based/test_executor.py +++ b/taskflow/tests/unit/worker_based/test_executor.py @@ -17,7 +17,6 @@ import threading import time -import futurist from oslo_utils import fixture from oslo_utils import timeutils @@ -58,8 +57,6 @@ class TestWorkerTaskExecutor(test.MockTestCase): self.request_inst_mock.uuid = self.task_uuid self.request_inst_mock.expired = False self.request_inst_mock.task_cls = self.task.name - self.wait_for_any_mock = self.patch( - 'taskflow.engines.action_engine.executor.async_utils.wait_for_any') self.message_mock = mock.MagicMock(name='message') self.message_mock.properties = {'correlation_id': self.task_uuid, 'type': pr.RESPONSE} @@ -281,27 +278,6 @@ class TestWorkerTaskExecutor(test.MockTestCase): ] self.assertEqual(expected_calls, self.master_mock.mock_calls) - def test_wait_for_any(self): - fs = [futurist.Future(), futurist.Future()] - ex = self.executor() - ex.wait_for_any(fs) - - expected_calls = [ - mock.call(fs, timeout=None) - ] - self.assertEqual(self.wait_for_any_mock.mock_calls, expected_calls) - - def test_wait_for_any_with_timeout(self): - timeout = 30 - fs = [futurist.Future(), futurist.Future()] - ex = self.executor() - ex.wait_for_any(fs, timeout) - - master_mock_calls = [ - mock.call(fs, timeout=timeout) - ] - self.assertEqual(self.wait_for_any_mock.mock_calls, master_mock_calls) - def test_start_stop(self): ex = self.executor() ex.start() diff --git a/taskflow/tests/unit/worker_based/test_pipeline.py b/taskflow/tests/unit/worker_based/test_pipeline.py index 3030b831..a2075763 100644 --- a/taskflow/tests/unit/worker_based/test_pipeline.py +++ b/taskflow/tests/unit/worker_based/test_pipeline.py @@ -24,6 +24,7 @@ from taskflow.engines.worker_based import server as worker_server from taskflow import test from taskflow.tests import utils as test_utils from taskflow.types import failure +from taskflow.utils import async_utils from taskflow.utils import threading_utils @@ -77,7 +78,7 @@ class TestPipeline(test.TestCase): progress_callback = lambda *args, **kwargs: None f = executor.execute_task(t, uuidutils.generate_uuid(), {}, progress_callback=progress_callback) - executor.wait_for_any([f]) + async_utils.wait_for_any([f]) event, result = f.result() self.assertEqual(1, result) @@ -93,7 +94,7 @@ class TestPipeline(test.TestCase): progress_callback = lambda *args, **kwargs: None f = executor.execute_task(t, uuidutils.generate_uuid(), {}, progress_callback=progress_callback) - executor.wait_for_any([f]) + async_utils.wait_for_any([f]) action, result = f.result() self.assertIsInstance(result, failure.Failure) From 7e1d330595a07faa8881533bb88f7a7c5e09c0fe Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 22 Jul 2015 08:54:49 -0700 Subject: [PATCH 28/55] Fix lack of space between functions Somehow this passed through the gate and now it is causing related failures, so fix it so that those other failures will not happen. Change-Id: Idb046b0e4e23af49c947a80cf6f77fef3a9ec0c8 --- taskflow/persistence/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/taskflow/persistence/models.py b/taskflow/persistence/models.py index ce403c0d..a1d04bbd 100644 --- a/taskflow/persistence/models.py +++ b/taskflow/persistence/models.py @@ -63,6 +63,7 @@ def _format_shared(obj, indent): getattr(obj, attr_name))) return lines + def _is_all_none(arg, *args): if arg is not None: return False From 16d9914d33a198ca20d4c6d90ca05a1b9efe4d1a Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 22 Jul 2015 05:09:12 +0000 Subject: [PATCH 29/55] Updated from global requirements Depends-On: Idb046b0e4e23af49c947a80cf6f77fef3a9ec0c8 Change-Id: Id9969fbda10a86dd79d1000ec5ba5c34152fd162 --- requirements.txt | 2 +- test-requirements.txt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4c9740c3..7ae90991 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ six>=1.9.0 enum34;python_version=='2.7' or python_version=='2.6' # For async and/or periodic work -futurist>=0.1.1 # Apache-2.0 +futurist>=0.1.2 # Apache-2.0 # For reader/writer + interprocess locks. fasteners>=0.7 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index b0ed54cf..5a06c57d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,8 +4,7 @@ hacking<0.11,>=0.10.0 oslotest>=1.7.0 # Apache-2.0 -mock!=1.1.4,>=1.1;python_version!='2.6' -mock==1.0.1;python_version=='2.6' +mock>=1.2 testtools>=1.4.0 testscenarios>=0.4 From b64b2b78b647a08ef8e316540c0cf1ff15af2f9a Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Sun, 19 Jul 2015 09:35:33 -0700 Subject: [PATCH 30/55] Remove legacy py2.6 backwards logging compat. code We no longer provide support for py2.6 so we don't need the logging compatibility code to exist anymore. Change-Id: Iaefab67fd8b4e222475d99f57c2c3a7a5ce07d6e --- taskflow/logging.py | 40 +--------------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/taskflow/logging.py b/taskflow/logging.py index 0ededf7f..823f8b0c 100644 --- a/taskflow/logging.py +++ b/taskflow/logging.py @@ -17,7 +17,6 @@ from __future__ import absolute_import import logging -import sys _BASE = __name__.split(".", 1)[0] @@ -49,45 +48,8 @@ class _BlatherLoggerAdapter(logging.LoggerAdapter): self.warning(msg, *args, **kwargs) -# TODO(harlowja): we should remove when we no longer have to support 2.6... -if sys.version_info[0:2] == (2, 6): - - class _FixedBlatherLoggerAdapter(_BlatherLoggerAdapter): - """Ensures isEnabledFor() exists on adapters that are created.""" - - def isEnabledFor(self, level): - return self.logger.isEnabledFor(level) - - _BlatherLoggerAdapter = _FixedBlatherLoggerAdapter - - # Taken from python2.7 (same in python3.4)... - class _NullHandler(logging.Handler): - """This handler does nothing. - - It's intended to be used to avoid the - "No handlers could be found for logger XXX" one-off warning. This is - important for library code, which may contain code to log events. If a - user of the library does not configure logging, the one-off warning - might be produced; to avoid this, the library developer simply needs - to instantiate a _NullHandler and add it to the top-level logger of the - library module or package. - """ - - def handle(self, record): - """Stub.""" - - def emit(self, record): - """Stub.""" - - def createLock(self): - self.lock = None - -else: - _NullHandler = logging.NullHandler - - def getLogger(name=_BASE, extra=None): logger = logging.getLogger(name) if not logger.handlers: - logger.addHandler(_NullHandler()) + logger.addHandler(logging.NullHandler()) return _BlatherLoggerAdapter(logger, extra=extra) From dfbc6ff0a847c26bab6d56040c6e9f0cd6bbbc1e Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 23 Jul 2015 15:30:57 -0700 Subject: [PATCH 31/55] Remove no longer used '_was_failure' static method Change-Id: I74765d376cdaa2c23a6aaa4a74517da4e2df7ad8 --- taskflow/persistence/models.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/taskflow/persistence/models.py b/taskflow/persistence/models.py index a1d04bbd..fd245d57 100644 --- a/taskflow/persistence/models.py +++ b/taskflow/persistence/models.py @@ -527,11 +527,6 @@ class AtomDetail(object): self.meta = {} self.version = None - @staticmethod - def _was_failure(state, result): - # Internal helper method... - return state == states.FAILURE and isinstance(result, ft.Failure) - @property def last_results(self): """Gets the atoms last result. From 9ad7ec6f823cc6978115463450c8e426a32cd7f9 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 23 Jul 2015 23:38:11 -0700 Subject: [PATCH 32/55] Modify listeners to handle the results now possible from revert() Now that the REVERT and REVERT_FAILURE states can produce results or failure objects we need to take that into account in listeners that were not expecting those states to produce anything; this change adjusts the built-in listeners so that they now handle these states and the results they produce. Also removes some no longer needed py2.6 code used in the logging listener, as that is not needed anymore since we dropped py2.6 support. Change-Id: I0d0a9759648b2a2f27a97c68e19c7cdb6375a4f2 --- taskflow/listeners/base.py | 8 ++++---- taskflow/listeners/logging.py | 26 +++++++------------------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/taskflow/listeners/base.py b/taskflow/listeners/base.py index 8a0badb0..57b564fb 100644 --- a/taskflow/listeners/base.py +++ b/taskflow/listeners/base.py @@ -29,11 +29,11 @@ from taskflow.types import notifier LOG = logging.getLogger(__name__) -# NOTE(harlowja): on these states will results be usable, all other states -# do not produce results. -FINISH_STATES = (states.FAILURE, states.SUCCESS) +#: These states will results be usable, other states do not produce results. +FINISH_STATES = (states.FAILURE, states.SUCCESS, + states.REVERTED, states.REVERT_FAILURE) -# What is listened for by default... +#: What is listened for by default... DEFAULT_LISTEN_FOR = (notifier.Notifier.ANY,) diff --git a/taskflow/listeners/logging.py b/taskflow/listeners/logging.py index b2d4d344..37fd58ac 100644 --- a/taskflow/listeners/logging.py +++ b/taskflow/listeners/logging.py @@ -16,9 +16,7 @@ from __future__ import absolute_import -import logging as logging_base import os -import sys from taskflow.listeners import base from taskflow import logging @@ -28,21 +26,6 @@ from taskflow.utils import misc LOG = logging.getLogger(__name__) -if sys.version_info[0:2] == (2, 6): - _PY26 = True -else: - _PY26 = False - - -# Fixes this for python 2.6 which was missing the is enabled for method -# when a logger adapter is being used/provided, this will no longer be needed -# when we can just support python 2.7+ (which fixed the lack of this method -# on adapters). -def _isEnabledFor(logger, level): - if _PY26 and isinstance(logger, logging_base.LoggerAdapter): - return logger.logger.isEnabledFor(level) - return logger.isEnabledFor(level) - class LoggingListener(base.DumpingListener): """Listener that logs notifications it receives. @@ -96,6 +79,7 @@ class DynamicLoggingListener(base.Listener): * ``states.FAILURE`` * ``states.RETRYING`` * ``states.REVERTING`` + * ``states.REVERT_FAILURE`` When a task produces a :py:class:`~taskflow.types.failure.Failure` object as its result (typically this happens when a task raises an exception) this @@ -107,6 +91,9 @@ class DynamicLoggingListener(base.Listener): #: Default logger to use if one is not provided on construction. _LOGGER = None + #: States which are triggered under some type of failure. + _FAILURE_STATES = (states.FAILURE, states.REVERT_FAILURE) + def __init__(self, engine, task_listen_for=base.DEFAULT_LISTEN_FOR, flow_listen_for=base.DEFAULT_LISTEN_FOR, @@ -122,6 +109,7 @@ class DynamicLoggingListener(base.Listener): states.FAILURE: self._failure_level, states.REVERTED: self._failure_level, states.RETRYING: self._failure_level, + states.REVERT_FAILURE: self._failure_level, } self._flow_log_levels = { states.FAILURE: self._failure_level, @@ -181,8 +169,8 @@ class DynamicLoggingListener(base.Listener): # will show or hide results that the task may have produced # during execution. level = self._task_log_levels.get(state, self._level) - if (_isEnabledFor(self._logger, self._level) - or state == states.FAILURE): + if (self._logger.isEnabledFor(self._level) + or state in self._FAILURE_STATES): self._logger.log(level, "Task '%s' (%s) transitioned into" " state '%s' from state '%s' with" " result '%s'", details['task_name'], From ecab10a6d6a1bdc893c0ad17405f9a7323ce9e1d Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Sat, 25 Jul 2015 16:55:51 -0700 Subject: [PATCH 33/55] Use the action engine '_check' helper method Change-Id: I0822bbf1caf28a8fd2b4e914643aca61ae0c7f45 --- taskflow/engines/action_engine/engine.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/taskflow/engines/action_engine/engine.py b/taskflow/engines/action_engine/engine.py index ef557f93..a4f55f47 100644 --- a/taskflow/engines/action_engine/engine.py +++ b/taskflow/engines/action_engine/engine.py @@ -294,10 +294,7 @@ class ActionEngine(base.Engine): @fasteners.locked def reset(self): - if not self._storage_ensured: - raise exc.InvalidState("Can not reset an engine" - " which has not has its storage" - " populated") + self._check('reset', True, True) # This transitions *all* contained atoms back into the PENDING state # with an intention to EXECUTE (or dies trying to do that) and then # changes the state of the flow to PENDING so that it can then run... From 2d4ce6bf7b93dd1f0d1c472442e0abd0b20ee87c Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Tue, 14 Jul 2015 11:51:04 -0700 Subject: [PATCH 34/55] Bump futurist and remove waiting code in taskflow Change-Id: Ifc9780aa129a4a2804cead301a519895c2bfc0b5 --- taskflow/engines/action_engine/runtime.py | 5 +- taskflow/tests/unit/test_utils_async_utils.py | 54 ------------ .../tests/unit/worker_based/test_pipeline.py | 6 +- taskflow/utils/async_utils.py | 88 ------------------- 4 files changed, 6 insertions(+), 147 deletions(-) diff --git a/taskflow/engines/action_engine/runtime.py b/taskflow/engines/action_engine/runtime.py index 38998b8c..2841968a 100644 --- a/taskflow/engines/action_engine/runtime.py +++ b/taskflow/engines/action_engine/runtime.py @@ -16,6 +16,8 @@ import functools +from futurist import waiters + from taskflow.engines.action_engine.actions import retry as ra from taskflow.engines.action_engine.actions import task as ta from taskflow.engines.action_engine import analyzer as an @@ -26,7 +28,6 @@ from taskflow.engines.action_engine import scopes as sc from taskflow import flow as flow_type from taskflow import states as st from taskflow import task -from taskflow.utils import async_utils from taskflow.utils import misc @@ -114,7 +115,7 @@ class Runtime(object): @misc.cachedproperty def runner(self): - return ru.Runner(self, async_utils.wait_for_any) + return ru.Runner(self, waiters.wait_for_any) @misc.cachedproperty def completer(self): diff --git a/taskflow/tests/unit/test_utils_async_utils.py b/taskflow/tests/unit/test_utils_async_utils.py index 1f8b0119..bd8b9a6b 100644 --- a/taskflow/tests/unit/test_utils_async_utils.py +++ b/taskflow/tests/unit/test_utils_async_utils.py @@ -14,56 +14,8 @@ # License for the specific language governing permissions and limitations # under the License. -import futurist -import testtools - from taskflow import test from taskflow.utils import async_utils as au -from taskflow.utils import eventlet_utils as eu - - -class WaitForAnyTestsMixin(object): - timeout = 0.001 - - def test_waits_and_finishes(self): - def foo(): - pass - - with self._make_executor(2) as e: - fs = [e.submit(foo), e.submit(foo)] - # this test assumes that our foo will end within 10 seconds - done, not_done = au.wait_for_any(fs, 10) - self.assertIn(len(done), (1, 2)) - self.assertTrue(any(f in done for f in fs)) - - def test_not_done_futures(self): - fs = [futurist.Future(), futurist.Future()] - done, not_done = au.wait_for_any(fs, self.timeout) - self.assertEqual(len(done), 0) - self.assertEqual(len(not_done), 2) - - def test_mixed_futures(self): - f1 = futurist.Future() - f2 = futurist.Future() - f2.set_result(1) - done, not_done = au.wait_for_any([f1, f2], self.timeout) - self.assertEqual(len(done), 1) - self.assertEqual(len(not_done), 1) - self.assertIs(not_done.pop(), f1) - self.assertIs(done.pop(), f2) - - -@testtools.skipIf(not eu.EVENTLET_AVAILABLE, 'eventlet is not available') -class AsyncUtilsEventletTest(test.TestCase, - WaitForAnyTestsMixin): - def _make_executor(self, max_workers): - return futurist.GreenThreadPoolExecutor(max_workers=max_workers) - - -class AsyncUtilsThreadedTest(test.TestCase, - WaitForAnyTestsMixin): - def _make_executor(self, max_workers): - return futurist.ThreadPoolExecutor(max_workers=max_workers) class MakeCompletedFutureTest(test.TestCase): @@ -73,9 +25,3 @@ class MakeCompletedFutureTest(test.TestCase): future = au.make_completed_future(result) self.assertTrue(future.done()) self.assertIs(future.result(), result) - - -class AsyncUtilsSynchronousTest(test.TestCase, - WaitForAnyTestsMixin): - def _make_executor(self, max_workers): - return futurist.SynchronousExecutor() diff --git a/taskflow/tests/unit/worker_based/test_pipeline.py b/taskflow/tests/unit/worker_based/test_pipeline.py index a2075763..56740159 100644 --- a/taskflow/tests/unit/worker_based/test_pipeline.py +++ b/taskflow/tests/unit/worker_based/test_pipeline.py @@ -15,6 +15,7 @@ # under the License. import futurist +from futurist import waiters from oslo_utils import uuidutils from taskflow.engines.action_engine import executor as base_executor @@ -24,7 +25,6 @@ from taskflow.engines.worker_based import server as worker_server from taskflow import test from taskflow.tests import utils as test_utils from taskflow.types import failure -from taskflow.utils import async_utils from taskflow.utils import threading_utils @@ -78,7 +78,7 @@ class TestPipeline(test.TestCase): progress_callback = lambda *args, **kwargs: None f = executor.execute_task(t, uuidutils.generate_uuid(), {}, progress_callback=progress_callback) - async_utils.wait_for_any([f]) + waiters.wait_for_any([f]) event, result = f.result() self.assertEqual(1, result) @@ -94,7 +94,7 @@ class TestPipeline(test.TestCase): progress_callback = lambda *args, **kwargs: None f = executor.execute_task(t, uuidutils.generate_uuid(), {}, progress_callback=progress_callback) - async_utils.wait_for_any([f]) + waiters.wait_for_any([f]) action, result = f.result() self.assertIsInstance(result, failure.Failure) diff --git a/taskflow/utils/async_utils.py b/taskflow/utils/async_utils.py index c4b114b8..cc24d215 100644 --- a/taskflow/utils/async_utils.py +++ b/taskflow/utils/async_utils.py @@ -14,20 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. -from concurrent import futures as _futures -from concurrent.futures import _base import futurist -from oslo_utils import importutils - -greenthreading = importutils.try_import('eventlet.green.threading') - -from taskflow.utils import eventlet_utils as eu - - -_DONE_STATES = frozenset([ - _base.CANCELLED_AND_NOTIFIED, - _base.FINISHED, -]) def make_completed_future(result): @@ -35,78 +22,3 @@ def make_completed_future(result): future = futurist.Future() future.set_result(result) return future - - -def wait_for_any(fs, timeout=None): - """Wait for one of the futures to complete. - - Works correctly with both green and non-green futures (but not both - together, since this can't be guaranteed to avoid dead-lock due to how - the waiting implementations are different when green threads are being - used). - - Returns pair (done futures, not done futures). - """ - # TODO(harlowja): remove this when - # https://review.openstack.org/#/c/196269/ is merged and is made - # available. - green_fs = sum(1 for f in fs if isinstance(f, futurist.GreenFuture)) - if not green_fs: - return _futures.wait(fs, - timeout=timeout, - return_when=_futures.FIRST_COMPLETED) - else: - non_green_fs = len(fs) - green_fs - if non_green_fs: - raise RuntimeError("Can not wait on %s green futures and %s" - " non-green futures in the same `wait_for_any`" - " call" % (green_fs, non_green_fs)) - else: - return _wait_for_any_green(fs, timeout=timeout) - - -class _GreenWaiter(object): - """Provides the event that wait_for_any() blocks on.""" - def __init__(self): - self.event = greenthreading.Event() - - def add_result(self, future): - self.event.set() - - def add_exception(self, future): - self.event.set() - - def add_cancelled(self, future): - self.event.set() - - -def _partition_futures(fs): - done = set() - not_done = set() - for f in fs: - if f._state in _DONE_STATES: - done.add(f) - else: - not_done.add(f) - return done, not_done - - -def _wait_for_any_green(fs, timeout=None): - eu.check_for_eventlet(RuntimeError('Eventlet is needed to wait on' - ' green futures')) - - with _base._AcquireFutures(fs): - done, not_done = _partition_futures(fs) - if done: - return _base.DoneAndNotDoneFutures(done, not_done) - waiter = _GreenWaiter() - for f in fs: - f._waiters.append(waiter) - - waiter.event.wait(timeout) - for f in fs: - f._waiters.remove(waiter) - - with _base._AcquireFutures(fs): - done, not_done = _partition_futures(fs) - return _base.DoneAndNotDoneFutures(done, not_done) From 13e14828862f0e8492bd59b47d7f262c0a9c90e6 Mon Sep 17 00:00:00 2001 From: Atsushi SAKAI Date: Mon, 27 Jul 2015 13:11:17 +0900 Subject: [PATCH 35/55] Fix seven typos and one readability on taskflow documentation retrys => retries 3 subseqent => subsequent tranfer => transfer exeception => exception overriden => overridden datastructure => data structure Change-Id: Ibb6e3541606f8405d8408c0204f8ad8edc3f058f Closes-Bug: #1478431 --- doc/source/arguments_and_results.rst | 8 ++++---- doc/source/engines.rst | 2 +- doc/source/jobs.rst | 2 +- doc/source/workers.rst | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/source/arguments_and_results.rst b/doc/source/arguments_and_results.rst index a8cc2e57..619a3c41 100644 --- a/doc/source/arguments_and_results.rst +++ b/doc/source/arguments_and_results.rst @@ -87,7 +87,7 @@ Rebinding 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/retrys ``execute`` method with another name. There are two possible +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 @@ -303,7 +303,7 @@ Default provides ++++++++++++++++ As mentioned above, the default base class provides nothing, which means -results are not accessible to other tasks/retrys in the flow. +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: @@ -386,7 +386,7 @@ A |Retry| controller works with arguments in the same way as a |Task|. But it has an additional parameter ``'history'`` that is itself a :py:class:`~taskflow.retry.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 retrys run and a +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 :py:class:`~taskflow.types.failure.Failure` object. @@ -415,7 +415,7 @@ the following history (printed as a list):: At this point (since the implementation returned ``RETRY``) the |retry.execute| method will be called again and it will receive the same -history and it can then return a value that subseqent tasks can use to alter +history and it can then return a value that subsequent tasks can use to alter their behavior. If instead the |retry.execute| method itself raises an exception, diff --git a/doc/source/engines.rst b/doc/source/engines.rst index 6f163231..e2908174 100644 --- a/doc/source/engines.rst +++ b/doc/source/engines.rst @@ -85,7 +85,7 @@ Of course these kind of features can come with some drawbacks: 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 :doc:`persistence `, to help make - it possible to have state and tranfer that state via a argument input and + 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 diff --git a/doc/source/jobs.rst b/doc/source/jobs.rst index 3cd7f95f..dbbc6c7f 100644 --- a/doc/source/jobs.rst +++ b/doc/source/jobs.rst @@ -215,7 +215,7 @@ Redis **Board type**: ``'redis'`` Uses `redis`_ to provide the jobboard capabilities and semantics by using -a redis hash datastructure and individual job ownership keys (that can +a redis hash data structure and individual job ownership keys (that can optionally expire after a given amount of time). .. note:: diff --git a/doc/source/workers.rst b/doc/source/workers.rst index 95c15598..a2dc941d 100644 --- a/doc/source/workers.rst +++ b/doc/source/workers.rst @@ -286,10 +286,10 @@ 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 exeception) or +**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 -overriden). +overridden). **RUNNING** - Workers executor (using threads, processes...) has started to run requested task (once this state is transitioned to any request timeout no From b1c22dc2e288040c2b31dfa4e9147398220cc5fd Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 24 Jul 2015 23:14:50 -0700 Subject: [PATCH 36/55] Replace the tree 'pformat()' recursion with non-recursive variant This adjusted variant creates the same output but is hopefully easier to understand and follow than the recursive version. This version is also not limited by the python stack limit which is a general good thing to avoid being limited by. It also adds a bunch of tests to make sure the format is as expected under various tree structures. Change-Id: I2ae42c7c1bf72794800929250bcf6ccbe658230b --- doc/source/utils.rst | 5 + taskflow/engines/action_engine/compiler.py | 5 +- taskflow/tests/unit/test_types.py | 239 +++++++++++++++++++ taskflow/tests/unit/test_utils_iter_utils.py | 62 +++++ taskflow/types/tree.py | 165 +++++++++---- taskflow/utils/iter_utils.py | 42 ++++ 6 files changed, 472 insertions(+), 46 deletions(-) create mode 100644 taskflow/tests/unit/test_utils_iter_utils.py create mode 100644 taskflow/utils/iter_utils.py diff --git a/doc/source/utils.rst b/doc/source/utils.rst index d8da6c0c..7878cbcb 100644 --- a/doc/source/utils.rst +++ b/doc/source/utils.rst @@ -23,6 +23,11 @@ Eventlet .. automodule:: taskflow.utils.eventlet_utils +Iterators +~~~~~~~~~ + +.. automodule:: taskflow.utils.iter_utils + Kazoo ~~~~~ diff --git a/taskflow/engines/action_engine/compiler.py b/taskflow/engines/action_engine/compiler.py index dc6c24e1..22b130a8 100644 --- a/taskflow/engines/action_engine/compiler.py +++ b/taskflow/engines/action_engine/compiler.py @@ -25,6 +25,7 @@ from taskflow import logging from taskflow import task from taskflow.types import graph as gr from taskflow.types import tree as tr +from taskflow.utils import iter_utils from taskflow.utils import misc LOG = logging.getLogger(__name__) @@ -232,8 +233,8 @@ class _FlowCompiler(object): @staticmethod def _occurence_detector(to_graph, from_graph): - return sum(1 for node in from_graph.nodes_iter() - if node in to_graph) + return iter_utils.count(node for node in from_graph.nodes_iter() + if node in to_graph) def _decompose_flow(self, flow, parent=None): """Decomposes a flow into a graph, tree node + decomposed subgraphs.""" diff --git a/taskflow/tests/unit/test_types.py b/taskflow/tests/unit/test_types.py index 1d3f5410..79044923 100644 --- a/taskflow/tests/unit/test_types.py +++ b/taskflow/tests/unit/test_types.py @@ -140,6 +140,245 @@ class TreeTest(test.TestCase): p.add(tree.Node("human")) return a + def test_pformat_species(self): + root = self._make_species() + expected = """ +animal +|__mammal +| |__horse +| |__primate +| |__monkey +| |__human +|__reptile +""" + self.assertEqual(expected.strip(), root.pformat()) + + def test_pformat_flat(self): + root = tree.Node("josh") + root.add(tree.Node("josh.1")) + expected = """ +josh +|__josh.1 +""" + self.assertEqual(expected.strip(), root.pformat()) + + root[0].add(tree.Node("josh.1.1")) + expected = """ +josh +|__josh.1 + |__josh.1.1 +""" + self.assertEqual(expected.strip(), root.pformat()) + + root[0][0].add(tree.Node("josh.1.1.1")) + expected = """ +josh +|__josh.1 + |__josh.1.1 + |__josh.1.1.1 +""" + self.assertEqual(expected.strip(), root.pformat()) + + root[0][0][0].add(tree.Node("josh.1.1.1.1")) + expected = """ +josh +|__josh.1 + |__josh.1.1 + |__josh.1.1.1 + |__josh.1.1.1.1 +""" + self.assertEqual(expected.strip(), root.pformat()) + + def test_pformat_partial_species(self): + root = self._make_species() + + expected = """ +reptile +""" + self.assertEqual(expected.strip(), root[1].pformat()) + + expected = """ +mammal +|__horse +|__primate + |__monkey + |__human +""" + self.assertEqual(expected.strip(), root[0].pformat()) + + expected = """ +primate +|__monkey +|__human +""" + self.assertEqual(expected.strip(), root[0][1].pformat()) + + expected = """ +monkey +""" + self.assertEqual(expected.strip(), root[0][1][0].pformat()) + + def test_pformat(self): + + root = tree.Node("CEO") + + expected = """ +CEO +""" + + self.assertEqual(expected.strip(), root.pformat()) + + root.add(tree.Node("Infra")) + + expected = """ +CEO +|__Infra +""" + self.assertEqual(expected.strip(), root.pformat()) + + root[0].add(tree.Node("Infra.1")) + expected = """ +CEO +|__Infra + |__Infra.1 +""" + self.assertEqual(expected.strip(), root.pformat()) + + root.add(tree.Node("Mail")) + expected = """ +CEO +|__Infra +| |__Infra.1 +|__Mail +""" + self.assertEqual(expected.strip(), root.pformat()) + + root.add(tree.Node("Search")) + expected = """ +CEO +|__Infra +| |__Infra.1 +|__Mail +|__Search +""" + self.assertEqual(expected.strip(), root.pformat()) + + root[-1].add(tree.Node("Search.1")) + expected = """ +CEO +|__Infra +| |__Infra.1 +|__Mail +|__Search + |__Search.1 +""" + self.assertEqual(expected.strip(), root.pformat()) + + root[-1].add(tree.Node("Search.2")) + expected = """ +CEO +|__Infra +| |__Infra.1 +|__Mail +|__Search + |__Search.1 + |__Search.2 +""" + self.assertEqual(expected.strip(), root.pformat()) + + root[0].add(tree.Node("Infra.2")) + expected = """ +CEO +|__Infra +| |__Infra.1 +| |__Infra.2 +|__Mail +|__Search + |__Search.1 + |__Search.2 +""" + self.assertEqual(expected.strip(), root.pformat()) + + root[0].add(tree.Node("Infra.3")) + expected = """ +CEO +|__Infra +| |__Infra.1 +| |__Infra.2 +| |__Infra.3 +|__Mail +|__Search + |__Search.1 + |__Search.2 +""" + self.assertEqual(expected.strip(), root.pformat()) + + root[0][-1].add(tree.Node("Infra.3.1")) + expected = """ +CEO +|__Infra +| |__Infra.1 +| |__Infra.2 +| |__Infra.3 +| |__Infra.3.1 +|__Mail +|__Search + |__Search.1 + |__Search.2 +""" + self.assertEqual(expected.strip(), root.pformat()) + + root[-1][0].add(tree.Node("Search.1.1")) + expected = """ +CEO +|__Infra +| |__Infra.1 +| |__Infra.2 +| |__Infra.3 +| |__Infra.3.1 +|__Mail +|__Search + |__Search.1 + | |__Search.1.1 + |__Search.2 +""" + self.assertEqual(expected.strip(), root.pformat()) + + root[1].add(tree.Node("Mail.1")) + expected = """ +CEO +|__Infra +| |__Infra.1 +| |__Infra.2 +| |__Infra.3 +| |__Infra.3.1 +|__Mail +| |__Mail.1 +|__Search + |__Search.1 + | |__Search.1.1 + |__Search.2 +""" + self.assertEqual(expected.strip(), root.pformat()) + + root[1][0].add(tree.Node("Mail.1.1")) + expected = """ +CEO +|__Infra +| |__Infra.1 +| |__Infra.2 +| |__Infra.3 +| |__Infra.3.1 +|__Mail +| |__Mail.1 +| |__Mail.1.1 +|__Search + |__Search.1 + | |__Search.1.1 + |__Search.2 +""" + self.assertEqual(expected.strip(), root.pformat()) + def test_path(self): root = self._make_species() human = root.find("human") diff --git a/taskflow/tests/unit/test_utils_iter_utils.py b/taskflow/tests/unit/test_utils_iter_utils.py new file mode 100644 index 00000000..82d470f3 --- /dev/null +++ b/taskflow/tests/unit/test_utils_iter_utils.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import string + +from six.moves import range as compat_range + +from taskflow import test +from taskflow.utils import iter_utils + + +def forever_it(): + i = 0 + while True: + yield i + i += 1 + + +class IterUtilsTest(test.TestCase): + def test_find_first_match(self): + it = forever_it() + self.assertEqual(100, iter_utils.find_first_match(it, + lambda v: v == 100)) + + def test_find_first_match_not_found(self): + it = iter(string.ascii_lowercase) + self.assertIsNone(iter_utils.find_first_match(it, + lambda v: v == '')) + + def test_count(self): + self.assertEqual(0, iter_utils.count([])) + self.assertEqual(1, iter_utils.count(['a'])) + self.assertEqual(10, iter_utils.count(compat_range(0, 10))) + self.assertEqual(1000, iter_utils.count(compat_range(0, 1000))) + self.assertEqual(0, iter_utils.count(compat_range(0))) + self.assertEqual(0, iter_utils.count(compat_range(-1))) + + def test_while_is_not(self): + it = iter(string.ascii_lowercase) + self.assertEqual(['a'], + list(iter_utils.while_is_not(it, 'a'))) + it = iter(string.ascii_lowercase) + self.assertEqual(['a', 'b'], + list(iter_utils.while_is_not(it, 'b'))) + self.assertEqual(list(string.ascii_lowercase[2:]), + list(iter_utils.while_is_not(it, 'zzz'))) + it = iter(string.ascii_lowercase) + self.assertEqual(list(string.ascii_lowercase), + list(iter_utils.while_is_not(it, ''))) diff --git a/taskflow/types/tree.py b/taskflow/types/tree.py index 4faa8291..94c009e1 100644 --- a/taskflow/types/tree.py +++ b/taskflow/types/tree.py @@ -22,6 +22,7 @@ import os import six +from taskflow.utils import iter_utils from taskflow.utils import misc @@ -77,11 +78,25 @@ class _BFSIter(object): class Node(object): """A n-ary node class that can be used to create tree structures.""" - # Constants used when pretty formatting the node (and its children). + #: Default string prefix used in :py:meth:`.pformat`. STARTING_PREFIX = "" + + #: Default string used to create empty space used in :py:meth:`.pformat`. EMPTY_SPACE_SEP = " " + HORIZONTAL_CONN = "__" + """ + Default string used to horizontally connect a node to its + parent (used in :py:meth:`.pformat`.). + """ + VERTICAL_CONN = "|" + """ + Default string used to vertically connect a node to its + parent (used in :py:meth:`.pformat`). + """ + + #: Default line separator used in :py:meth:`.pformat`. LINE_SEP = os.linesep def __init__(self, item, **kwargs): @@ -124,18 +139,22 @@ class Node(object): yield node node = node.parent - def find(self, item, only_direct=False, include_self=True): - """Returns the node for an item if it exists in this node. + def find_first_match(self, 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 and - finally if nothing is found then None is returned instead of a node - object. + 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. - :param item: item to lookup. - :param only_direct: only look at current node and its direct children. + :param matcher: callback that takes one positional argument (a node) + and returns true if it matches desired node or false + if not. + :param only_direct: only look at current node and its + direct children (implies that this does not + search using depth first). :param include_self: include the current node during searching. - :returns: the node for an item if it exists in this node + :returns: the node that matched (or ``None``) """ if only_direct: if include_self: @@ -144,10 +163,26 @@ class Node(object): it = self.reverse_iter() else: it = self.dfs_iter(include_self=include_self) - for n in it: - if n.item == item: - return n - return None + return iter_utils.find_first_match(it, matcher) + + def find(self, 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. + + :param item: item to look for. + :param only_direct: only look at current node and its + direct children (implies that this does not + search using depth first). + :param include_self: include the current node during searching. + + :returns: the node that matched provided item (or ``None``) + """ + return self.find_first_match(lambda n: n.item == item, + only_direct=only_direct, + include_self=include_self) def disassociate(self): """Removes this node from its parent (if any). @@ -176,7 +211,9 @@ class Node(object): the normally returned *removed* node object. :param item: item to lookup. - :param only_direct: only look at current node and its direct children. + :param only_direct: only look at current node and its + direct children (implies that this does not + search using depth first). :param include_self: include the current node during searching. """ node = self.find(item, only_direct=only_direct, @@ -200,8 +237,11 @@ class Node(object): # NOTE(harlowja): 0 is the right most index, len - 1 is the left most return self._children[index] - def pformat(self, stringify_node=None): - """Recursively formats a node into a nice string representation. + def pformat(self, stringify_node=None, + linesep=LINE_SEP, vertical_conn=VERTICAL_CONN, + horizontal_conn=HORIZONTAL_CONN, empty_space=EMPTY_SPACE_SEP, + starting_prefix=STARTING_PREFIX): + """Formats this node + children into a nice string representation. **Example**:: @@ -220,33 +260,73 @@ class Node(object): |__Mobile |__Mail """ - def _inner_pformat(node, level, stringify_node): - if level == 0: - yield stringify_node(node) - prefix = self.STARTING_PREFIX - else: - yield self.HORIZONTAL_CONN + stringify_node(node) - prefix = self.EMPTY_SPACE_SEP * len(self.HORIZONTAL_CONN) - child_count = node.child_count() - for (i, child) in enumerate(node): - for (j, text) in enumerate(_inner_pformat(child, - level + 1, - stringify_node)): - if j == 0 or i + 1 < child_count: - text = prefix + self.VERTICAL_CONN + text - else: - text = prefix + self.EMPTY_SPACE_SEP + text - yield text if stringify_node is None: # Default to making a unicode string out of the nodes item... stringify_node = lambda node: six.text_type(node.item) - expected_lines = self.child_count(only_direct=False) - accumulator = six.StringIO() - for i, line in enumerate(_inner_pformat(self, 0, stringify_node)): - accumulator.write(line) - if i < expected_lines: - accumulator.write(self.LINE_SEP) - return accumulator.getvalue() + expected_lines = self.child_count(only_direct=False) + 1 + buff = six.StringIO() + conn = vertical_conn + horizontal_conn + stop_at_parent = self + for i, node in enumerate(self.dfs_iter(include_self=True), 1): + prefix = [] + connected_to_parent = False + last_node = node + # Walk through *most* of this nodes parents, and form the expected + # prefix that each parent should require, repeat this until we + # hit the root node (self) and use that as our nodes prefix + # string... + parent_node_it = iter_utils.while_is_not( + node.path_iter(include_self=True), stop_at_parent) + for j, parent_node in enumerate(parent_node_it): + if parent_node is stop_at_parent: + if j > 0: + if not connected_to_parent: + prefix.append(conn) + connected_to_parent = True + else: + # If the node was connected already then it must + # have had more than one parent, so we want to put + # the right final starting prefix on (which may be + # a empty space or another vertical connector)... + last_node = self._children[-1] + m = last_node.find_first_match(lambda n: n is node, + include_self=False, + only_direct=False) + if m is not None: + prefix.append(empty_space) + else: + prefix.append(vertical_conn) + elif parent_node is node: + # Skip ourself... (we only include ourself so that + # we can use the 'j' variable to determine if the only + # node requested is ourself in the first place); used + # in the first conditional here... + pass + else: + if not connected_to_parent: + prefix.append(conn) + spaces = len(horizontal_conn) + connected_to_parent = True + else: + # If we have already been connected to our parent + # then determine if this current node is the last + # node of its parent (and in that case just put + # on more spaces), otherwise put a vertical connector + # on and less spaces... + if parent_node[-1] is not last_node: + prefix.append(vertical_conn) + spaces = len(horizontal_conn) + else: + spaces = len(conn) + prefix.append(empty_space * spaces) + last_node = parent_node + prefix.append(starting_prefix) + for prefix_piece in reversed(prefix): + buff.write(prefix_piece) + buff.write(stringify_node(node)) + if i != expected_lines: + buff.write(linesep) + return buff.getvalue() def child_count(self, only_direct=True): """Returns how many children this node has. @@ -257,10 +337,7 @@ class Node(object): NOTE(harlowja): it does not account for the current node in this count. """ if not only_direct: - count = 0 - for _node in self.dfs_iter(): - count += 1 - return count + return iter_utils.count(self.dfs_iter()) return len(self._children) def __iter__(self): diff --git a/taskflow/utils/iter_utils.py b/taskflow/utils/iter_utils.py new file mode 100644 index 00000000..68810e8c --- /dev/null +++ b/taskflow/utils/iter_utils.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +def count(it): + """Returns how many values in the iterator (depletes the iterator).""" + return sum(1 for _value in it) + + +def find_first_match(it, matcher, not_found_value=None): + """Searches iterator for first value that matcher callback returns true.""" + for value in it: + if matcher(value): + return value + return not_found_value + + +def 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). + """ + for value in it: + yield value + if value is stop_value: + break From 6dda5a55c06d71143915635260ff3a31783039ae Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 27 Jul 2015 18:04:12 -0700 Subject: [PATCH 37/55] Avoid adding 1 to a failure (if it gets triggered) Sometimes the CI gate times out and instead of returning normal results a failure object is returned, so be more careful on adding integers to those objects. Closes-Bug: 1478744 Change-Id: Ibdb9d30266d2a7f3bfeacc39e74cf61b44025a56 --- taskflow/tests/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/taskflow/tests/utils.py b/taskflow/tests/utils.py index 266a0e8c..f4654676 100644 --- a/taskflow/tests/utils.py +++ b/taskflow/tests/utils.py @@ -123,7 +123,11 @@ class GiveBackRevert(task.Task): return value + 1 def revert(self, *args, **kwargs): - return kwargs.get('result') + 1 + result = kwargs.get('result') + # If this somehow fails, timeout, or other don't send back a + # valid result... + if isinstance(result, six.integer_types): + return result + 1 class FakeTask(object): From a0a523759220d3ef80e46529e3c600b5e8f72bfb Mon Sep 17 00:00:00 2001 From: Timofey Durakov Date: Wed, 29 Jul 2015 00:04:36 +0300 Subject: [PATCH 38/55] .gitignore update After running tox some extra files were created. TrivialFix Change-Id: I3e5a38abf49cf3b246de436f78137b23154d8101 --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3cf05e0c..b645cf69 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,8 @@ lib64 pip-log.txt # Unit test / coverage reports -.coverage +.coverage* +.diagram-tools/* .tox nosetests.xml .venv From 1f917258035391e32dea9506a4d7d2fc44c8ee58 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 29 Jul 2015 03:51:18 +0000 Subject: [PATCH 39/55] Updated from global requirements Change-Id: I523faa336e00092301c9d066a274040f265deb5a --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 5a06c57d..1684db9c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. hacking<0.11,>=0.10.0 -oslotest>=1.7.0 # Apache-2.0 +oslotest>=1.9.0 # Apache-2.0 mock>=1.2 testtools>=1.4.0 testscenarios>=0.4 From 054ca2a6e2b4d7fdbff48116a651e5c4310f978b Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 16 Jul 2015 17:10:09 -0700 Subject: [PATCH 40/55] Remove extra runner layer and just use use machine in engine Just directly use the built machine in the action engine and avoid having another layer of abstraction that does not provide that much value. This makes the code cleaner, and more easy to understand (and so-on). Change-Id: Iae1279098112254338258c1941c15889f1ad1a79 --- doc/source/engines.rst | 20 +- .../action_engine/{runner.py => builder.py} | 163 +++++++-------- taskflow/engines/action_engine/engine.py | 53 +++-- taskflow/engines/action_engine/runtime.py | 10 +- .../{test_runner.py => test_builder.py} | 197 +++++++++--------- tools/state_graph.py | 10 +- 6 files changed, 225 insertions(+), 228 deletions(-) rename taskflow/engines/action_engine/{runner.py => builder.py} (66%) rename taskflow/tests/unit/action_engine/{test_runner.py => test_builder.py} (57%) diff --git a/doc/source/engines.rst b/doc/source/engines.rst index e2908174..04a4bb86 100644 --- a/doc/source/engines.rst +++ b/doc/source/engines.rst @@ -258,9 +258,10 @@ Execution The graph (and helper objects) previously created are now used for guiding further execution (see :py:func:`~taskflow.engines.base.Engine.run`). The flow is put into the ``RUNNING`` :doc:`state ` and a -:py:class:`~taskflow.engines.action_engine.runner.Runner` implementation -object starts to take over and begins going through the stages listed -below (for a more visual diagram/representation see +:py:class:`~taskflow.engines.action_engine.builder.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 :ref:`engine state diagram `). .. note:: @@ -338,8 +339,8 @@ above stages will be restarted and resuming will occur). Finishing --------- -At this point the -:py:class:`~taskflow.engines.action_engine.runner.Runner` has +At this point the machine (and runner) that was built using the +:py:class:`~taskflow.engines.action_engine.builder.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``). @@ -365,9 +366,9 @@ 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 -:py:class:`~taskflow.engines.action_engine.runner.Runner` 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 +:py:class:`~taskflow.engines.action_engine.builder.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 :py:func:`~taskflow.engines.base.Engine.run` method. @@ -444,10 +445,10 @@ Components cycle). .. automodule:: taskflow.engines.action_engine.analyzer +.. automodule:: taskflow.engines.action_engine.builder .. automodule:: taskflow.engines.action_engine.compiler .. automodule:: taskflow.engines.action_engine.completer .. automodule:: taskflow.engines.action_engine.executor -.. automodule:: taskflow.engines.action_engine.runner .. automodule:: taskflow.engines.action_engine.runtime .. automodule:: taskflow.engines.action_engine.scheduler .. autoclass:: taskflow.engines.action_engine.scopes.ScopeWalker @@ -462,6 +463,7 @@ Hierarchy taskflow.engines.worker_based.engine.WorkerBasedActionEngine :parts: 1 +.. _automaton: http://docs.openstack.org/developer/automaton/ .. _multiprocessing: https://docs.python.org/2/library/multiprocessing.html .. _future: https://docs.python.org/dev/library/concurrent.futures.html#future-objects .. _executor: https://docs.python.org/dev/library/concurrent.futures.html#concurrent.futures.Executor diff --git a/taskflow/engines/action_engine/runner.py b/taskflow/engines/action_engine/builder.py similarity index 66% rename from taskflow/engines/action_engine/runner.py rename to taskflow/engines/action_engine/builder.py index f02f3f09..f46d4a1f 100644 --- a/taskflow/engines/action_engine/runner.py +++ b/taskflow/engines/action_engine/builder.py @@ -16,35 +16,34 @@ from automaton import machines -from automaton import runners from taskflow import logging from taskflow import states as st from taskflow.types import failure -# Waiting state timeout (in seconds). -_WAITING_TIMEOUT = 60 +# Default waiting state timeout (in seconds). +WAITING_TIMEOUT = 60 # Meta states the state machine uses. -_UNDEFINED = 'UNDEFINED' -_GAME_OVER = 'GAME_OVER' -_META_STATES = (_GAME_OVER, _UNDEFINED) +UNDEFINED = 'UNDEFINED' +GAME_OVER = 'GAME_OVER' +META_STATES = (GAME_OVER, UNDEFINED) # Event name constants the state machine uses. -_SCHEDULE = 'schedule_next' -_WAIT = 'wait_finished' -_ANALYZE = 'examine_finished' -_FINISH = 'completed' -_FAILED = 'failed' -_SUSPENDED = 'suspended' -_SUCCESS = 'success' -_REVERTED = 'reverted' -_START = 'start' +SCHEDULE = 'schedule_next' +WAIT = 'wait_finished' +ANALYZE = 'examine_finished' +FINISH = 'completed' +FAILED = 'failed' +SUSPENDED = 'suspended' +SUCCESS = 'success' +REVERTED = 'reverted' +START = 'start' LOG = logging.getLogger(__name__) -class _MachineMemory(object): +class MachineMemory(object): """State machine memory.""" def __init__(self): @@ -54,31 +53,31 @@ class _MachineMemory(object): self.done = set() -class Runner(object): - """State machine *builder* + *runner* that powers the engine components. +class MachineBuilder(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 + | 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 | | + | 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``) @@ -91,11 +90,6 @@ class Runner(object): tasks in parallel, this enables parallel running and/or reversion. """ - # Informational states this action yields while running, not useful to - # have the engine record but useful to provide to end-users when doing - # execution iterations. - ignorable_states = (st.SCHEDULING, st.WAITING, st.RESUMING, st.ANALYZING) - def __init__(self, runtime, waiter): self._runtime = runtime self._analyzer = runtime.analyzer @@ -104,21 +98,21 @@ class Runner(object): self._storage = runtime.storage self._waiter = waiter - def runnable(self): - """Checks if the storage says the flow is still runnable/running.""" - return self._storage.get_flow_state() == st.RUNNING - def build(self, timeout=None): - """Builds a state-machine (that can be/is used during running).""" + """Builds a state-machine (that is used during running).""" - memory = _MachineMemory() + memory = MachineMemory() if timeout is None: - timeout = _WAITING_TIMEOUT + timeout = WAITING_TIMEOUT # Cache some local functions/methods... do_schedule = self._scheduler.schedule do_complete = self._completer.complete + def is_runnable(): + # Checks if the storage says the flow is still runnable... + return self._storage.get_flow_state() == st.RUNNING + def iter_next_nodes(target_node=None): # Yields and filters and tweaks the next nodes to execute... maybe_nodes = self._analyzer.get_next_nodes(node=target_node) @@ -134,7 +128,7 @@ class Runner(object): # that are now ready to be ran. memory.next_nodes.update(self._completer.resume()) memory.next_nodes.update(iter_next_nodes()) - return _SCHEDULE + return SCHEDULE def game_over(old_state, new_state, event): # This reaction function is mainly a intermediary delegation @@ -142,13 +136,13 @@ class Runner(object): # the appropriate handler that will deal with the memory values, # it is *always* called before the final state is entered. if memory.failures: - return _FAILED + return FAILED if any(1 for node in iter_next_nodes()): - return _SUSPENDED + return SUSPENDED elif self._analyzer.is_success(): - return _SUCCESS + return SUCCESS else: - return _REVERTED + return REVERTED def schedule(old_state, new_state, event): # This reaction function starts to schedule the memory's next @@ -156,14 +150,14 @@ class Runner(object): # if the user of this engine has requested the engine/storage # that holds this information to stop or suspend); handles failures # that occur during this process safely... - if self.runnable() and memory.next_nodes: + if is_runnable() and memory.next_nodes: not_done, failures = do_schedule(memory.next_nodes) if not_done: memory.not_done.update(not_done) if failures: memory.failures.extend(failures) memory.next_nodes.clear() - return _WAIT + return WAIT def wait(old_state, new_state, event): # TODO(harlowja): maybe we should start doing 'yield from' this @@ -173,7 +167,7 @@ class Runner(object): done, not_done = self._waiter(memory.not_done, timeout=timeout) memory.done.update(done) memory.not_done = not_done - return _ANALYZE + return ANALYZE def analyze(old_state, new_state, event): # This reaction function is responsible for analyzing all nodes @@ -215,13 +209,13 @@ class Runner(object): memory.failures.append(failure.Failure()) else: next_nodes.update(more_nodes) - if self.runnable() and next_nodes and not memory.failures: + if is_runnable() and next_nodes and not memory.failures: memory.next_nodes.update(next_nodes) - return _SCHEDULE + return SCHEDULE elif memory.not_done: - return _WAIT + return WAIT else: - return _FINISH + return FINISH def on_exit(old_state, event): LOG.debug("Exiting old state '%s' in response to event '%s'", @@ -239,8 +233,8 @@ class Runner(object): watchers['on_enter'] = on_enter m = machines.FiniteMachine() - m.add_state(_GAME_OVER, **watchers) - m.add_state(_UNDEFINED, **watchers) + m.add_state(GAME_OVER, **watchers) + m.add_state(UNDEFINED, **watchers) m.add_state(st.ANALYZING, **watchers) m.add_state(st.RESUMING, **watchers) m.add_state(st.REVERTED, terminal=True, **watchers) @@ -249,38 +243,25 @@ class Runner(object): m.add_state(st.SUSPENDED, terminal=True, **watchers) m.add_state(st.WAITING, **watchers) m.add_state(st.FAILURE, terminal=True, **watchers) - m.default_start_state = _UNDEFINED + m.default_start_state = UNDEFINED - m.add_transition(_GAME_OVER, st.REVERTED, _REVERTED) - m.add_transition(_GAME_OVER, st.SUCCESS, _SUCCESS) - m.add_transition(_GAME_OVER, st.SUSPENDED, _SUSPENDED) - m.add_transition(_GAME_OVER, st.FAILURE, _FAILED) - m.add_transition(_UNDEFINED, st.RESUMING, _START) - m.add_transition(st.ANALYZING, _GAME_OVER, _FINISH) - m.add_transition(st.ANALYZING, st.SCHEDULING, _SCHEDULE) - m.add_transition(st.ANALYZING, st.WAITING, _WAIT) - m.add_transition(st.RESUMING, st.SCHEDULING, _SCHEDULE) - m.add_transition(st.SCHEDULING, st.WAITING, _WAIT) - m.add_transition(st.WAITING, st.ANALYZING, _ANALYZE) + m.add_transition(GAME_OVER, st.REVERTED, REVERTED) + m.add_transition(GAME_OVER, st.SUCCESS, SUCCESS) + m.add_transition(GAME_OVER, st.SUSPENDED, SUSPENDED) + m.add_transition(GAME_OVER, st.FAILURE, FAILED) + m.add_transition(UNDEFINED, st.RESUMING, START) + m.add_transition(st.ANALYZING, GAME_OVER, FINISH) + m.add_transition(st.ANALYZING, st.SCHEDULING, SCHEDULE) + m.add_transition(st.ANALYZING, st.WAITING, WAIT) + m.add_transition(st.RESUMING, st.SCHEDULING, SCHEDULE) + m.add_transition(st.SCHEDULING, st.WAITING, WAIT) + m.add_transition(st.WAITING, st.ANALYZING, ANALYZE) - m.add_reaction(_GAME_OVER, _FINISH, game_over) - m.add_reaction(st.ANALYZING, _ANALYZE, analyze) - m.add_reaction(st.RESUMING, _START, resume) - m.add_reaction(st.SCHEDULING, _SCHEDULE, schedule) - m.add_reaction(st.WAITING, _WAIT, wait) + m.add_reaction(GAME_OVER, FINISH, game_over) + m.add_reaction(st.ANALYZING, ANALYZE, analyze) + m.add_reaction(st.RESUMING, START, resume) + m.add_reaction(st.SCHEDULING, SCHEDULE, schedule) + m.add_reaction(st.WAITING, WAIT, wait) m.freeze() - - r = runners.FiniteRunner(m) - return (m, r, memory) - - def run_iter(self, timeout=None): - """Runs iteratively using a locally built state machine.""" - machine, runner, memory = self.build(timeout=timeout) - for (_prior_state, new_state) in runner.run_iter(_START): - # NOTE(harlowja): skip over meta-states. - if new_state not in _META_STATES: - if new_state == st.FAILURE: - yield (new_state, memory.failures) - else: - yield (new_state, []) + return (m, memory) diff --git a/taskflow/engines/action_engine/engine.py b/taskflow/engines/action_engine/engine.py index ef557f93..309e1a32 100644 --- a/taskflow/engines/action_engine/engine.py +++ b/taskflow/engines/action_engine/engine.py @@ -19,6 +19,7 @@ import contextlib import itertools import threading +from automaton import runners from concurrent import futures import fasteners import networkx as nx @@ -26,6 +27,7 @@ from oslo_utils import excutils from oslo_utils import strutils import six +from taskflow.engines.action_engine import builder from taskflow.engines.action_engine import compiler from taskflow.engines.action_engine import executor from taskflow.engines.action_engine import runtime @@ -59,9 +61,9 @@ class ActionEngine(base.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, runner - and storage classes to attempt to run your flow (and any subflows & - contained atoms) to completion. + 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), @@ -77,6 +79,15 @@ class ActionEngine(base.Engine): failure/s that were captured (if any) to get reraised. """ + IGNORABLE_STATES = frozenset( + itertools.chain([states.SCHEDULING, states.WAITING, states.RESUMING, + states.ANALYZING], builder.META_STATES)) + """ + 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 :py:meth:`.run_iter`. + """ + def __init__(self, flow, flow_detail, backend, options): super(ActionEngine, self).__init__(flow, flow_detail, backend, options) self._runtime = None @@ -151,20 +162,20 @@ class ActionEngine(base.Engine): def run_iter(self, timeout=None): """Runs the engine using iteration (or die trying). - :param timeout: timeout to wait for any tasks to complete (this timeout + :param 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 tasks are being waited - for). + 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 tasks have finished). + 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 + 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. @@ -172,19 +183,24 @@ class ActionEngine(base.Engine): self.compile() self.prepare() self.validate() - runner = self._runtime.runner last_state = None with _start_stop(self._task_executor, self._retry_executor): self._change_state(states.RUNNING) try: closed = False - for (last_state, failures) in runner.run_iter(timeout=timeout): - if failures: - failure.Failure.reraise_if_any(failures) + machine, memory = self._runtime.builder.build(timeout=timeout) + r = runners.FiniteRunner(machine) + for (_prior_state, new_state) in r.run_iter(builder.START): + last_state = new_state + # NOTE(harlowja): skip over meta-states. + if new_state in builder.META_STATES: + continue + if new_state == states.FAILURE: + failure.Failure.reraise_if_any(memory.failures) if closed: continue try: - try_suspend = yield last_state + try_suspend = yield new_state except GeneratorExit: # The generator was closed, attempt to suspend and # continue looping until we have cleanly closed up @@ -198,9 +214,8 @@ class ActionEngine(base.Engine): with excutils.save_and_reraise_exception(): self._change_state(states.FAILURE) else: - ignorable_states = getattr(runner, 'ignorable_states', []) - if last_state and last_state not in ignorable_states: - self._change_state(last_state) + if last_state and last_state not in self.IGNORABLE_STATES: + self._change_state(new_state) if last_state not in self.NO_RERAISING_STATES: it = itertools.chain( six.itervalues(self.storage.get_failures()), diff --git a/taskflow/engines/action_engine/runtime.py b/taskflow/engines/action_engine/runtime.py index 2841968a..d97ba967 100644 --- a/taskflow/engines/action_engine/runtime.py +++ b/taskflow/engines/action_engine/runtime.py @@ -21,11 +21,11 @@ from futurist import waiters from taskflow.engines.action_engine.actions import retry as ra from taskflow.engines.action_engine.actions import task as ta from taskflow.engines.action_engine import analyzer as an +from taskflow.engines.action_engine import builder as bu from taskflow.engines.action_engine import completer as co -from taskflow.engines.action_engine import runner as ru from taskflow.engines.action_engine import scheduler as sched from taskflow.engines.action_engine import scopes as sc -from taskflow import flow as flow_type +from taskflow import flow from taskflow import states as st from taskflow import task from taskflow.utils import misc @@ -89,7 +89,7 @@ class Runtime(object): # is able to run (or should not) ensure we retain it and use # it later as needed. u_v_data = execution_graph.adj[previous_atom][atom] - u_v_decider = u_v_data.get(flow_type.LINK_DECIDER) + u_v_decider = u_v_data.get(flow.LINK_DECIDER) if u_v_decider is not None: edge_deciders[previous_atom.name] = u_v_decider metadata['scope_walker'] = walker @@ -114,8 +114,8 @@ class Runtime(object): return an.Analyzer(self) @misc.cachedproperty - def runner(self): - return ru.Runner(self, waiters.wait_for_any) + def builder(self): + return bu.MachineBuilder(self, waiters.wait_for_any) @misc.cachedproperty def completer(self): diff --git a/taskflow/tests/unit/action_engine/test_runner.py b/taskflow/tests/unit/action_engine/test_builder.py similarity index 57% rename from taskflow/tests/unit/action_engine/test_runner.py rename to taskflow/tests/unit/action_engine/test_builder.py index bff74cb9..b4067449 100644 --- a/taskflow/tests/unit/action_engine/test_runner.py +++ b/taskflow/tests/unit/action_engine/test_builder.py @@ -15,11 +15,12 @@ # under the License. from automaton import exceptions as excp +from automaton import runners import six +from taskflow.engines.action_engine import builder from taskflow.engines.action_engine import compiler from taskflow.engines.action_engine import executor -from taskflow.engines.action_engine import runner from taskflow.engines.action_engine import runtime from taskflow.patterns import linear_flow as lf from taskflow import states as st @@ -30,7 +31,8 @@ from taskflow.types import notifier from taskflow.utils import persistence_utils as pu -class _RunnerTestMixin(object): +class BuildersTest(test.TestCase): + def _make_runtime(self, flow, initial_state=None): compilation = compiler.PatternCompiler(flow).compile() flow_detail = pu.create_flow_detail(flow) @@ -51,17 +53,11 @@ class _RunnerTestMixin(object): r.compile() return r - -class RunnerTest(test.TestCase, _RunnerTestMixin): - def test_running(self): - flow = lf.Flow("root") - flow.add(*test_utils.make_many(1)) - - rt = self._make_runtime(flow, initial_state=st.RUNNING) - self.assertTrue(rt.runner.runnable()) - - rt = self._make_runtime(flow, initial_state=st.SUSPENDED) - self.assertFalse(rt.runner.runnable()) + def _make_machine(self, flow, initial_state=None): + runtime = self._make_runtime(flow, initial_state=initial_state) + machine, memory = runtime.builder.build() + machine_runner = runners.FiniteRunner(machine) + return (runtime, machine, memory, machine_runner) def test_run_iterations(self): flow = lf.Flow("root") @@ -69,29 +65,32 @@ class RunnerTest(test.TestCase, _RunnerTestMixin): 1, task_cls=test_utils.TaskNoRequiresNoReturns) flow.add(*tasks) - rt = self._make_runtime(flow, initial_state=st.RUNNING) - self.assertTrue(rt.runner.runnable()) + runtime, machine, memory, machine_runner = self._make_machine( + flow, initial_state=st.RUNNING) - it = rt.runner.run_iter() - state, failures = six.next(it) - self.assertEqual(st.RESUMING, state) - self.assertEqual(0, len(failures)) + it = machine_runner.run_iter(builder.START) + prior_state, new_state = six.next(it) + self.assertEqual(st.RESUMING, new_state) + self.assertEqual(0, len(memory.failures)) - state, failures = six.next(it) - self.assertEqual(st.SCHEDULING, state) - self.assertEqual(0, len(failures)) + prior_state, new_state = six.next(it) + self.assertEqual(st.SCHEDULING, new_state) + self.assertEqual(0, len(memory.failures)) - state, failures = six.next(it) - self.assertEqual(st.WAITING, state) - self.assertEqual(0, len(failures)) + prior_state, new_state = six.next(it) + self.assertEqual(st.WAITING, new_state) + self.assertEqual(0, len(memory.failures)) - state, failures = six.next(it) - self.assertEqual(st.ANALYZING, state) - self.assertEqual(0, len(failures)) + prior_state, new_state = six.next(it) + self.assertEqual(st.ANALYZING, new_state) + self.assertEqual(0, len(memory.failures)) - state, failures = six.next(it) - self.assertEqual(st.SUCCESS, state) - self.assertEqual(0, len(failures)) + prior_state, new_state = six.next(it) + self.assertEqual(builder.GAME_OVER, new_state) + self.assertEqual(0, len(memory.failures)) + prior_state, new_state = six.next(it) + self.assertEqual(st.SUCCESS, new_state) + self.assertEqual(0, len(memory.failures)) self.assertRaises(StopIteration, six.next, it) @@ -101,15 +100,15 @@ class RunnerTest(test.TestCase, _RunnerTestMixin): 1, task_cls=test_utils.TaskWithFailure) flow.add(*tasks) - rt = self._make_runtime(flow, initial_state=st.RUNNING) - self.assertTrue(rt.runner.runnable()) + runtime, machine, memory, machine_runner = self._make_machine( + flow, initial_state=st.RUNNING) - transitions = list(rt.runner.run_iter()) - state, failures = transitions[-1] - self.assertEqual(st.REVERTED, state) - self.assertEqual([], failures) - - self.assertEqual(st.REVERTED, rt.storage.get_atom_state(tasks[0].name)) + transitions = list(machine_runner.run_iter(builder.START)) + prior_state, new_state = transitions[-1] + self.assertEqual(st.REVERTED, new_state) + self.assertEqual([], memory.failures) + self.assertEqual(st.REVERTED, + runtime.storage.get_atom_state(tasks[0].name)) def test_run_iterations_failure(self): flow = lf.Flow("root") @@ -117,18 +116,17 @@ class RunnerTest(test.TestCase, _RunnerTestMixin): 1, task_cls=test_utils.NastyFailingTask) flow.add(*tasks) - rt = self._make_runtime(flow, initial_state=st.RUNNING) - self.assertTrue(rt.runner.runnable()) + runtime, machine, memory, machine_runner = self._make_machine( + flow, initial_state=st.RUNNING) - transitions = list(rt.runner.run_iter()) - state, failures = transitions[-1] - self.assertEqual(st.FAILURE, state) - self.assertEqual(1, len(failures)) - failure = failures[0] + transitions = list(machine_runner.run_iter(builder.START)) + prior_state, new_state = transitions[-1] + self.assertEqual(st.FAILURE, new_state) + self.assertEqual(1, len(memory.failures)) + failure = memory.failures[0] self.assertTrue(failure.check(RuntimeError)) - self.assertEqual(st.REVERT_FAILURE, - rt.storage.get_atom_state(tasks[0].name)) + runtime.storage.get_atom_state(tasks[0].name)) def test_run_iterations_suspended(self): flow = lf.Flow("root") @@ -136,20 +134,22 @@ class RunnerTest(test.TestCase, _RunnerTestMixin): 2, task_cls=test_utils.TaskNoRequiresNoReturns) flow.add(*tasks) - rt = self._make_runtime(flow, initial_state=st.RUNNING) - self.assertTrue(rt.runner.runnable()) + runtime, machine, memory, machine_runner = self._make_machine( + flow, initial_state=st.RUNNING) transitions = [] - for state, failures in rt.runner.run_iter(): - transitions.append((state, failures)) - if state == st.ANALYZING: - rt.storage.set_flow_state(st.SUSPENDED) + for prior_state, new_state in machine_runner.run_iter(builder.START): + transitions.append((new_state, memory.failures)) + if new_state == st.ANALYZING: + runtime.storage.set_flow_state(st.SUSPENDED) state, failures = transitions[-1] self.assertEqual(st.SUSPENDED, state) self.assertEqual([], failures) - self.assertEqual(st.SUCCESS, rt.storage.get_atom_state(tasks[0].name)) - self.assertEqual(st.PENDING, rt.storage.get_atom_state(tasks[1].name)) + self.assertEqual(st.SUCCESS, + runtime.storage.get_atom_state(tasks[0].name)) + self.assertEqual(st.PENDING, + runtime.storage.get_atom_state(tasks[1].name)) def test_run_iterations_suspended_failure(self): flow = lf.Flow("root") @@ -160,46 +160,44 @@ class RunnerTest(test.TestCase, _RunnerTestMixin): 1, task_cls=test_utils.TaskNoRequiresNoReturns, offset=1) flow.add(*happy_tasks) - rt = self._make_runtime(flow, initial_state=st.RUNNING) - self.assertTrue(rt.runner.runnable()) + runtime, machine, memory, machine_runner = self._make_machine( + flow, initial_state=st.RUNNING) transitions = [] - for state, failures in rt.runner.run_iter(): - transitions.append((state, failures)) - if state == st.ANALYZING: - rt.storage.set_flow_state(st.SUSPENDED) + for prior_state, new_state in machine_runner.run_iter(builder.START): + transitions.append((new_state, memory.failures)) + if new_state == st.ANALYZING: + runtime.storage.set_flow_state(st.SUSPENDED) state, failures = transitions[-1] self.assertEqual(st.SUSPENDED, state) self.assertEqual([], failures) self.assertEqual(st.PENDING, - rt.storage.get_atom_state(happy_tasks[0].name)) + runtime.storage.get_atom_state(happy_tasks[0].name)) self.assertEqual(st.FAILURE, - rt.storage.get_atom_state(sad_tasks[0].name)) + runtime.storage.get_atom_state(sad_tasks[0].name)) - -class RunnerBuildTest(test.TestCase, _RunnerTestMixin): def test_builder_manual_process(self): flow = lf.Flow("root") tasks = test_utils.make_many( 1, task_cls=test_utils.TaskNoRequiresNoReturns) flow.add(*tasks) - rt = self._make_runtime(flow, initial_state=st.RUNNING) - machine, machine_runner, memory = rt.runner.build() - self.assertTrue(rt.runner.runnable()) + runtime, machine, memory, machine_runner = self._make_machine( + flow, initial_state=st.RUNNING) self.assertRaises(excp.NotInitialized, machine.process_event, 'poke') # Should now be pending... - self.assertEqual(st.PENDING, rt.storage.get_atom_state(tasks[0].name)) + self.assertEqual(st.PENDING, + runtime.storage.get_atom_state(tasks[0].name)) machine.initialize() - self.assertEqual(runner._UNDEFINED, machine.current_state) + self.assertEqual(builder.UNDEFINED, machine.current_state) self.assertFalse(machine.terminated) self.assertRaises(excp.NotFound, machine.process_event, 'poke') last_state = machine.current_state - reaction, terminal = machine.process_event('start') + reaction, terminal = machine.process_event(builder.START) self.assertFalse(terminal) self.assertIsNotNone(reaction) self.assertEqual(st.RESUMING, machine.current_state) @@ -208,7 +206,7 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin): last_state = machine.current_state cb, args, kwargs = reaction next_event = cb(last_state, machine.current_state, - 'start', *args, **kwargs) + builder.START, *args, **kwargs) reaction, terminal = machine.process_event(next_event) self.assertFalse(terminal) self.assertIsNotNone(reaction) @@ -225,7 +223,8 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin): self.assertRaises(excp.NotFound, machine.process_event, 'poke') # Should now be running... - self.assertEqual(st.RUNNING, rt.storage.get_atom_state(tasks[0].name)) + self.assertEqual(st.RUNNING, + runtime.storage.get_atom_state(tasks[0].name)) last_state = machine.current_state cb, args, kwargs = reaction @@ -243,10 +242,11 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin): next_event, *args, **kwargs) reaction, terminal = machine.process_event(next_event) self.assertFalse(terminal) - self.assertEqual(runner._GAME_OVER, machine.current_state) + self.assertEqual(builder.GAME_OVER, machine.current_state) # Should now be done... - self.assertEqual(st.SUCCESS, rt.storage.get_atom_state(tasks[0].name)) + self.assertEqual(st.SUCCESS, + runtime.storage.get_atom_state(tasks[0].name)) def test_builder_automatic_process(self): flow = lf.Flow("root") @@ -254,26 +254,25 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin): 1, task_cls=test_utils.TaskNoRequiresNoReturns) flow.add(*tasks) - rt = self._make_runtime(flow, initial_state=st.RUNNING) - machine, machine_runner, memory = rt.runner.build() - self.assertTrue(rt.runner.runnable()) + runtime, machine, memory, machine_runner = self._make_machine( + flow, initial_state=st.RUNNING) - transitions = list(machine_runner.run_iter('start')) - self.assertEqual((runner._UNDEFINED, st.RESUMING), transitions[0]) - self.assertEqual((runner._GAME_OVER, st.SUCCESS), transitions[-1]) - self.assertEqual(st.SUCCESS, rt.storage.get_atom_state(tasks[0].name)) + transitions = list(machine_runner.run_iter(builder.START)) + self.assertEqual((builder.UNDEFINED, st.RESUMING), transitions[0]) + self.assertEqual((builder.GAME_OVER, st.SUCCESS), transitions[-1]) + self.assertEqual(st.SUCCESS, + runtime.storage.get_atom_state(tasks[0].name)) def test_builder_automatic_process_failure(self): flow = lf.Flow("root") tasks = test_utils.make_many(1, task_cls=test_utils.NastyFailingTask) flow.add(*tasks) - rt = self._make_runtime(flow, initial_state=st.RUNNING) - machine, machine_runner, memory = rt.runner.build() - self.assertTrue(rt.runner.runnable()) + runtime, machine, memory, machine_runner = self._make_machine( + flow, initial_state=st.RUNNING) - transitions = list(machine_runner.run_iter('start')) - self.assertEqual((runner._GAME_OVER, st.FAILURE), transitions[-1]) + transitions = list(machine_runner.run_iter(builder.START)) + self.assertEqual((builder.GAME_OVER, st.FAILURE), transitions[-1]) self.assertEqual(1, len(memory.failures)) def test_builder_automatic_process_reverted(self): @@ -281,13 +280,13 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin): tasks = test_utils.make_many(1, task_cls=test_utils.TaskWithFailure) flow.add(*tasks) - rt = self._make_runtime(flow, initial_state=st.RUNNING) - machine, machine_runner, memory = rt.runner.build() - self.assertTrue(rt.runner.runnable()) + runtime, machine, memory, machine_runner = self._make_machine( + flow, initial_state=st.RUNNING) - transitions = list(machine_runner.run_iter('start')) - self.assertEqual((runner._GAME_OVER, st.REVERTED), transitions[-1]) - self.assertEqual(st.REVERTED, rt.storage.get_atom_state(tasks[0].name)) + transitions = list(machine_runner.run_iter(builder.START)) + self.assertEqual((builder.GAME_OVER, st.REVERTED), transitions[-1]) + self.assertEqual(st.REVERTED, + runtime.storage.get_atom_state(tasks[0].name)) def test_builder_expected_transition_occurrences(self): flow = lf.Flow("root") @@ -295,16 +294,16 @@ class RunnerBuildTest(test.TestCase, _RunnerTestMixin): 10, task_cls=test_utils.TaskNoRequiresNoReturns) flow.add(*tasks) - rt = self._make_runtime(flow, initial_state=st.RUNNING) - machine, machine_runner, memory = rt.runner.build() - transitions = list(machine_runner.run_iter('start')) + runtime, machine, memory, machine_runner = self._make_machine( + flow, initial_state=st.RUNNING) + transitions = list(machine_runner.run_iter(builder.START)) occurrences = dict((t, transitions.count(t)) for t in transitions) self.assertEqual(10, occurrences.get((st.SCHEDULING, st.WAITING))) self.assertEqual(10, occurrences.get((st.WAITING, st.ANALYZING))) self.assertEqual(9, occurrences.get((st.ANALYZING, st.SCHEDULING))) - self.assertEqual(1, occurrences.get((runner._GAME_OVER, st.SUCCESS))) - self.assertEqual(1, occurrences.get((runner._UNDEFINED, st.RESUMING))) + self.assertEqual(1, occurrences.get((builder.GAME_OVER, st.SUCCESS))) + self.assertEqual(1, occurrences.get((builder.UNDEFINED, st.RESUMING))) self.assertEqual(0, len(memory.next_nodes)) self.assertEqual(0, len(memory.not_done)) diff --git a/tools/state_graph.py b/tools/state_graph.py index c9bdd0b1..5530a469 100755 --- a/tools/state_graph.py +++ b/tools/state_graph.py @@ -31,12 +31,12 @@ import pydot from automaton import machines -from taskflow.engines.action_engine import runner +from taskflow.engines.action_engine import builder from taskflow.engines.worker_based import protocol from taskflow import states -# This is just needed to get at the runner builder object (we will not +# This is just needed to get at the machine object (we will not # actually be running it...). class DummyRuntime(object): def __init__(self): @@ -134,9 +134,9 @@ def main(): list(states._ALLOWED_RETRY_TRANSITIONS)) elif options.engines: source_type = "Engines" - r = runner.Runner(DummyRuntime(), mock.MagicMock()) - source, memory = r.build() - internal_states.extend(runner._META_STATES) + b = builder.MachineBuilder(DummyRuntime(), mock.MagicMock()) + source, memory = b.build() + internal_states.extend(builder.META_STATES) ordering = 'out' elif options.wbe_requests: source_type = "WBE requests" From 5852ad6b03501ea85beb5d9f7c7bbf22c0bd8ce6 Mon Sep 17 00:00:00 2001 From: Timofey Durakov Date: Fri, 31 Jul 2015 09:25:55 +0300 Subject: [PATCH 41/55] Base class for deciders To align decider classes interface new base class introduced in this patch Change-Id: I42c69d3daa89153f1f3f9da32bccaf8d840ab1be --- taskflow/engines/action_engine/analyzer.py | 45 ++++++++++++++-------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/taskflow/engines/action_engine/analyzer.py b/taskflow/engines/action_engine/analyzer.py index 3a88a461..f5c68722 100644 --- a/taskflow/engines/action_engine/analyzer.py +++ b/taskflow/engines/action_engine/analyzer.py @@ -14,6 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. +import abc import functools import itertools @@ -23,7 +24,33 @@ import six from taskflow import states as st -class IgnoreDecider(object): +@six.add_metaclass(abc.ABCMeta) +class Decider(object): + """Base class for deciders. + + Provides interface to be implemented by sub-classes + Decider checks whether next atom in flow should be executed or not + """ + + @abc.abstractmethod + def check(self, runtime): + """Returns bool of whether this decider should allow running.""" + + @abc.abstractmethod + def affect(self, runtime): + """If the :py:func:`~.check` returns false, affects associated atoms. + + """ + + def check_and_affect(self, runtime): + """Handles :py:func:`~.check` + :py:func:`~.affect` in right order.""" + proceed = self.check(runtime) + if not proceed: + self.affect(runtime) + return proceed + + +class IgnoreDecider(Decider): """Checks any provided edge-deciders and determines if ok to run.""" def __init__(self, atom, edge_deciders): @@ -51,15 +78,8 @@ class IgnoreDecider(object): runtime.reset_nodes(itertools.chain([self._atom], successors_iter), state=st.IGNORE, intention=st.IGNORE) - def check_and_affect(self, runtime): - """Handles :py:func:`~.check` + :py:func:`~.affect` in right order.""" - proceed = self.check(runtime) - if not proceed: - self.affect(runtime) - return proceed - -class NoOpDecider(object): +class NoOpDecider(Decider): """No-op decider that says it is always ok to run & has no effect(s).""" def check(self, runtime): @@ -69,13 +89,6 @@ class NoOpDecider(object): def affect(self, runtime): """Does nothing.""" - def check_and_affect(self, runtime): - """Handles :py:func:`~.check` + :py:func:`~.affect` in right order. - - Does nothing. - """ - return self.check(runtime) - class Analyzer(object): """Analyzes a compilation and aids in execution processes. From aa8a45b3d3004e84e7e1e5791581bee6e5622dc5 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 9 Jul 2015 21:23:15 -0700 Subject: [PATCH 42/55] Give the GC more of a break with regard to cycles We can avoid creating reference cycles relatively easily which will make the GC have to do less to garbage collect these objects so let's just give it a break to start. This is *safe* to do since the runtime components have the same lifetime as the runtime itself and they will never outlive the runtime objects existence (a runtime objects lifetime is directly the same as the engine objects lifetime). Change-Id: I7f1ee91e04f29dd27da1e57a462573e068aee45c --- taskflow/engines/action_engine/analyzer.py | 17 ++++++++--------- taskflow/engines/action_engine/builder.py | 3 ++- taskflow/engines/action_engine/scheduler.py | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/taskflow/engines/action_engine/analyzer.py b/taskflow/engines/action_engine/analyzer.py index f5c68722..78d4c29f 100644 --- a/taskflow/engines/action_engine/analyzer.py +++ b/taskflow/engines/action_engine/analyzer.py @@ -15,8 +15,8 @@ # under the License. import abc -import functools import itertools +import weakref from networkx.algorithms import traversal import six @@ -101,12 +101,9 @@ class Analyzer(object): """ def __init__(self, runtime): + self._runtime = weakref.proxy(runtime) self._storage = runtime.storage self._execution_graph = runtime.compilation.execution_graph - self._check_atom_transition = runtime.check_atom_transition - self._fetch_edge_deciders = runtime.fetch_edge_deciders - self._fetch_retries = functools.partial( - runtime.fetch_atoms_by_kind, 'retry') def get_next_nodes(self, node=None): """Get next nodes to run (originating from node or all nodes).""" @@ -174,7 +171,8 @@ class Analyzer(object): state = self.get_state(atom) intention = self._storage.get_atom_intention(atom.name) - transition = self._check_atom_transition(atom, state, st.RUNNING) + transition = self._runtime.check_atom_transition(atom, state, + st.RUNNING) if not transition or intention != st.EXECUTE: return (False, None) @@ -190,7 +188,7 @@ class Analyzer(object): if not ok_to_run: return (False, None) else: - edge_deciders = self._fetch_edge_deciders(atom) + edge_deciders = self._runtime.fetch_edge_deciders(atom) return (True, IgnoreDecider(atom, edge_deciders)) def _get_maybe_ready_for_revert(self, atom): @@ -198,7 +196,8 @@ class Analyzer(object): state = self.get_state(atom) intention = self._storage.get_atom_intention(atom.name) - transition = self._check_atom_transition(atom, state, st.REVERTING) + transition = self._runtime.check_atom_transition(atom, state, + st.REVERTING) if not transition or intention not in (st.REVERT, st.RETRY): return (False, None) @@ -226,7 +225,7 @@ class Analyzer(object): If no state is provided it will yield back all retry atoms. """ - for atom in self._fetch_retries(): + for atom in self._runtime.fetch_atoms_by_kind('retry'): if not state or self.get_state(atom) == state: yield atom diff --git a/taskflow/engines/action_engine/builder.py b/taskflow/engines/action_engine/builder.py index f46d4a1f..9ab26d4a 100644 --- a/taskflow/engines/action_engine/builder.py +++ b/taskflow/engines/action_engine/builder.py @@ -14,6 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. +import weakref from automaton import machines @@ -91,7 +92,7 @@ class MachineBuilder(object): """ def __init__(self, runtime, waiter): - self._runtime = runtime + self._runtime = weakref.proxy(runtime) self._analyzer = runtime.analyzer self._completer = runtime.completer self._scheduler = runtime.scheduler diff --git a/taskflow/engines/action_engine/scheduler.py b/taskflow/engines/action_engine/scheduler.py index 404781e6..5fdc1995 100644 --- a/taskflow/engines/action_engine/scheduler.py +++ b/taskflow/engines/action_engine/scheduler.py @@ -76,7 +76,7 @@ class Scheduler(object): """Safely schedules atoms using a runtime ``fetch_scheduler`` routine.""" def __init__(self, runtime): - self._fetch_scheduler = runtime.fetch_scheduler + self._runtime = weakref.proxy(runtime) def schedule(self, atoms): """Schedules the provided atoms for *future* completion. @@ -89,7 +89,7 @@ class Scheduler(object): """ futures = set() for atom in atoms: - scheduler = self._fetch_scheduler(atom) + scheduler = self._runtime.fetch_scheduler(atom) try: futures.add(scheduler.schedule(atom)) except Exception: From 12a39199926da68be6e4103bdf0e3813f65c9dac Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Sun, 2 Aug 2015 20:34:28 -0700 Subject: [PATCH 43/55] Show intermediary compilation(s) when BLATHER is enabled This information can be useful for analyzing why/what is generated during each intermediary flow/subflow and task compilation call so include showing it when and only when the BLATHER level logging is on. Change-Id: I8e9508b8250533a4830fe78705d867139b1eab36 --- taskflow/engines/action_engine/compiler.py | 29 ++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/taskflow/engines/action_engine/compiler.py b/taskflow/engines/action_engine/compiler.py index 22b130a8..50ce4eb1 100644 --- a/taskflow/engines/action_engine/compiler.py +++ b/taskflow/engines/action_engine/compiler.py @@ -372,6 +372,7 @@ class PatternCompiler(object): _FlowCompiler(self._compile, self._linker), _TaskCompiler(), ] + self._level = 0 def _compile(self, item, parent=None): """Compiles a item (pattern, task) into a graph + tree node.""" @@ -392,13 +393,28 @@ class PatternCompiler(object): " and/or recursive compiling is not" " supported" % (item, type(item))) self._history.add(item) + if LOG.isEnabledFor(logging.BLATHER): + LOG.blather("%sCompiling '%s'", " " * self._level, item) + self._level += 1 def _post_item_compile(self, item, graph, node): """Called after a item is compiled; doing post-compilation actions.""" + self._level -= 1 + if LOG.isEnabledFor(logging.BLATHER): + prefix = ' ' * self._level + LOG.blather("%sDecomposed '%s' into:", prefix, item) + prefix = ' ' * (self._level + 1) + LOG.blather("%sGraph:", prefix) + for line in graph.pformat().splitlines(): + LOG.blather("%s %s", prefix, line) + LOG.blather("%sHierarchy:", prefix) + for line in node.pformat().splitlines(): + LOG.blather("%s %s", prefix, line) def _pre_compile(self): """Called before the compilation of the root starts.""" self._history.clear() + self._level = 0 def _post_compile(self, graph, node): """Called after the compilation of the root finishes successfully.""" @@ -411,19 +427,6 @@ class PatternCompiler(object): raise exc.Empty("Root container '%s' (%s) is empty" % (self._root, type(self._root))) self._history.clear() - # NOTE(harlowja): this one can be expensive to calculate (especially - # the cycle detection), so only do it if we know BLATHER is enabled - # and not under all cases. - if LOG.isEnabledFor(logging.BLATHER): - LOG.blather("Translated '%s'", self._root) - LOG.blather("Graph:") - for line in graph.pformat().splitlines(): - # Indent it so that it's slightly offset from the above line. - LOG.blather(" %s", line) - LOG.blather("Hierarchy:") - for line in node.pformat().splitlines(): - # Indent it so that it's slightly offset from the above line. - LOG.blather(" %s", line) @fasteners.locked def compile(self): From 8693f8f816010ab1d8a8d747ba022df103c87da7 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 4 Aug 2015 00:49:41 +0000 Subject: [PATCH 44/55] Updated from global requirements Change-Id: I3574313f90c1dcf69c2cdc4fa78caf0e91993b1d --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 1684db9c..37ee6bcc 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. hacking<0.11,>=0.10.0 -oslotest>=1.9.0 # Apache-2.0 +oslotest>=1.10.0 # Apache-2.0 mock>=1.2 testtools>=1.4.0 testscenarios>=0.4 From 9576f59ea86f9c79cf352c90d4e1d04452250f54 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 3 Aug 2015 21:34:34 -0700 Subject: [PATCH 45/55] Enable testr OS_DEBUG to be TRACE(blather) by default This enables the oslo.test recent change to be able to capture/output the verbose trace/blather log level which taskflow has been using. This should help make it easier to diagnosis issues that happen when needed. Change-Id: Ia3c63e66e4b4ad5523a5e0c6ba44b35b918c2acd --- .testr.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/.testr.conf b/.testr.conf index 8aa2cd00..339bf3e7 100644 --- a/.testr.conf +++ b/.testr.conf @@ -2,6 +2,7 @@ test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-160} \ + OS_DEBUG=${OS_DEBUG:-TRACE} \ ${PYTHON:-python} -m subunit.run discover -t ./ ./taskflow/tests $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE From 5ff439299b3fe1474621f0ae9a18351ee66a6e8c Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 5 Aug 2015 17:28:58 -0700 Subject: [PATCH 46/55] Improve docstrings in graph flow to denote exceptions raised Link the existing exceptions mentioned to there sphinx doc about them and also adds a section about when the 'DependencyFailure' exception is raised. Also tweaks the class docstring a little, to make it easier to understand. Change-Id: Ie4b989444c5ad73660cc61c0c3b2b1702b669087 --- taskflow/patterns/graph_flow.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/taskflow/patterns/graph_flow.py b/taskflow/patterns/graph_flow.py index 9e255871..37da34a6 100644 --- a/taskflow/patterns/graph_flow.py +++ b/taskflow/patterns/graph_flow.py @@ -54,10 +54,15 @@ class Flow(flow.Flow): which will be resolved by using the *flows/tasks* provides and requires mappings or by following manually created dependency links. - From dependencies directed graph is build. If it has edge A -> B, this - means B depends on A. + 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. + Note: `cyclic`_ dependencies are not allowed. + + .. _directed graph: https://en.wikipedia.org/wiki/Directed_graph + .. _cyclic: https://en.wikipedia.org/wiki/Cycle_graph """ def __init__(self, name, retry=None): @@ -71,6 +76,12 @@ class Flow(flow.Flow): def link(self, u, v, decider=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 :class:`~taskflow.exceptions.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 + :class:`ValueError` will be raised. + :param u: task or flow to create a link from (must exist already) :param v: task or flow to create a link to (must exist already) :param decider: A callback function that will be expected to decide @@ -82,6 +93,8 @@ class Flow(flow.Flow): links that have ``v`` as a target. It is expected to return a single boolean (``True`` to allow ``v`` execution or ``False`` to not). + + .. _cyclic: https://en.wikipedia.org/wiki/Cycle_graph """ if not self._graph.has_node(u): raise ValueError("Node '%s' not found to link from" % (u)) @@ -135,6 +148,11 @@ class Flow(flow.Flow): def add(self, *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 :class:`~taskflow.exceptions.DependencyFailure` will be + raised and the applied changes will be discarded. + :param nodes: node(s) to add to the flow :param kwargs: keyword arguments, the two keyword arguments currently processed are: @@ -144,13 +162,18 @@ class Flow(flow.Flow): 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. + then a + :class:`~taskflow.exceptions.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. + + .. _cyclic: https://en.wikipedia.org/wiki/Cycle_graph """ # Let's try to avoid doing any work if we can; since the below code From d9b879d5737c6a16bb7b875762223ea39b3546ff Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 7 Aug 2015 09:03:16 -0700 Subject: [PATCH 47/55] Fix busted stevedore doc(s) link Change-Id: If8cb052f695ac6c28574d8facf63d66986d37457 --- doc/source/jobs.rst | 2 +- doc/source/persistence.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/jobs.rst b/doc/source/jobs.rst index dbbc6c7f..f1f457a1 100644 --- a/doc/source/jobs.rst +++ b/doc/source/jobs.rst @@ -303,5 +303,5 @@ Hierarchy .. _paradigm shift: https://wiki.openstack.org/wiki/TaskFlow/Paradigm_shifts#Workflow_ownership_transfer .. _zookeeper: http://zookeeper.apache.org/ .. _kazoo: http://kazoo.readthedocs.org/ -.. _stevedore: http://stevedore.readthedocs.org/ +.. _stevedore: http://docs.openstack.org/developer/stevedore/ .. _redis: http://redis.io/ diff --git a/doc/source/persistence.rst b/doc/source/persistence.rst index 8b451d42..905ff746 100644 --- a/doc/source/persistence.rst +++ b/doc/source/persistence.rst @@ -31,7 +31,7 @@ This abstraction serves the following *major* purposes: vs. stop. * *Something you create...* -.. _stevedore: http://stevedore.readthedocs.org/ +.. _stevedore: http://docs.openstack.org/developer/stevedore/ How it is used ============== From cd122d84d92e9e07842013fe148d0c399d28ec4b Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 10 Aug 2015 02:14:44 +0000 Subject: [PATCH 48/55] Updated from global requirements Change-Id: Iea04e3a4823f294d8c7890bba155651c9e7a1dab --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7ae90991..a8bf5a76 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. # See: https://bugs.launchpad.net/pbr/+bug/1384919 for why this is here... -pbr<2.0,>=1.3 +pbr<2.0,>=1.4 # Packages needed for using this library. @@ -20,7 +20,7 @@ futurist>=0.1.2 # Apache-2.0 fasteners>=0.7 # Apache-2.0 # Very nice graph library -networkx>=1.8 +networkx>=1.10 # For contextlib new additions/compatibility for <= python 3.3 contextlib2>=0.4.0 # PSF License @@ -32,7 +32,7 @@ stevedore>=1.5.0 # Apache-2.0 futures>=3.0;python_version=='2.7' or python_version=='2.6' # Backport for time.monotonic which is in 3.3+ -monotonic>=0.1 # Apache-2.0 +monotonic>=0.3 # Apache-2.0 # Used for structured input validation jsonschema!=2.5.0,<3.0.0,>=2.0.0 From 61e6e7a7ecbbafd2e862390126aa3fc01a93417a Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 7 Aug 2015 18:28:07 -0700 Subject: [PATCH 49/55] Add nicely made task structural diagram Change-Id: Ib1c4c0f2378f11c10c3ca0ecf40b3a64daa1b697 --- doc/diagrams/tasks.graffle.tgz | Bin 0 -> 99579 bytes doc/source/atoms.rst | 7 +++++++ doc/source/img/tasks.png | Bin 0 -> 241180 bytes 3 files changed, 7 insertions(+) create mode 100644 doc/diagrams/tasks.graffle.tgz create mode 100644 doc/source/img/tasks.png diff --git a/doc/diagrams/tasks.graffle.tgz b/doc/diagrams/tasks.graffle.tgz new file mode 100644 index 0000000000000000000000000000000000000000..c014c0c140d2e65020525f87ceaa9cccebb69631 GIT binary patch literal 99579 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%0AsXeQc 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 nH~wz~``00;sA>3U+f literal 0 HcmV?d00001 diff --git a/doc/source/atoms.rst b/doc/source/atoms.rst index 2b7c9d95..f71fa601 100644 --- a/doc/source/atoms.rst +++ b/doc/source/atoms.rst @@ -29,6 +29,13 @@ it (they are *nearly* analogous to functions). These task objects all derive from :py:class:`~taskflow.task.BaseTask` which defines what a task must provide in terms of properties and methods. +**For example:** + +.. image:: img/tasks.png + :width: 525px + :align: left + :alt: Task outline. + Currently the following *provided* types of task subclasses are: * :py:class:`~taskflow.task.Task`: useful for inheriting from and creating your diff --git a/doc/source/img/tasks.png b/doc/source/img/tasks.png new file mode 100644 index 0000000000000000000000000000000000000000..cca91f995cd8d4cb723210183b0833613addab94 GIT binary patch literal 241180 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

BYH|M9PAq%%&-l400>h{L3$H#}_v7z!wOo() zdd*=h`EWfB9yx%F7prmhnQ@ppbr=F%N(|Qn{a4&Z!jivW%0%b1_2xe_urQ&ciFZxM zh`)Y~$`3}-&c10W>Va3u6fUmztA2G*NcM9fHt-;dM_(VMOr}22~-u?(_A~H|z-EM)q z`)&0Jy_E=RD|`|dwHAcnCg0D0*4v)+7vDXMHQ0nlm&h1#lggZ9a^f4A5Iv#j#Eapf)R#NaSqlFAfpuRy>I=-B5 z0*aa}!zA?zdw~&_KY~E3EUXTs^*u=F9$j}XHJWy=q|c;`pT9DJHv@OY+H&&sLEZ4@ zMnU1_^Pkzb(~{}-UTrMbb{95+1@rT!&H;1zI_DgXoxhtp?{A_9H|CbXRYKsJHES?t z%ovoGUUrqZN-esoX($FtP8DPAi3;pHtHs%~)i`lRgZvVECc2?@T{>Sa_WyJi@60-c zheu~%WpW9uviM7vG{4oA>_gI%b8zZRF7&!M%$SvcW)wa+vf6TLDHt@h&|N5p?qVfs zODKLP(^^`s;J630qwa0;zhJs z1Z_?kbfxvthp=5rLU== z2FQ72XP0ruzE&8`y3$R60ex*X%1X}R{Q2{!EU&p?A(pG!Vch6}Xx&nclHzUnbnSn! z?wf%SYV4l$4C+iN7$TTwzt+fo*PFySJLM^q)W=J+McIDgry+Yp-(5AciXKM&HN=xVg!sIv9;Muy)&QUhTUf{KHXbv1$xjts4pVPF}Pnu+35TTJEJL!+j^qJc<^pM?13wh& z&^mSi-u?V5to&#Rh6GihxbP@WWmiC7t3k$=5Af!C(kCl=e(7uI9IV1eD_2O>KK}UQ z*VmTbaP!enefa;wJii%+Oy-WxgD2yqC+|etFa?UvZNbLxzr?P?d4HH;x}`hn6Ap`_ zxG(Jd9n{$JqE-d+zwWht^V_ejwrX4pw; zk}>qv@9Og1_JBLRJAY)m&GY(xGmCKe`xHw)!5#-k$64-GZqCb9ojzcRaAL{q>_X{g zHGSbGk#km)5ohx9kjJB}%6Se;)ij5&Ku20@e7eFh4!;90n&nGtzD7FX{cY{XCNSbL z{vI1mBpX?S@TF18F|I>Pq^(jDEmc;2fzFN!A(!Z2LurRbsZFQroV{3uNo&2cB$FZJlmzRj0)Q%`Q+gs`W-s`RZ3jhL75xyzB1?qZAtXSY8pdZ zjcK)3r$se)m#Ww|AGa>(9NQ8h!7ZRH7JZ|c68l$GnvVAue2J238Ct~8#AA0wqO_w} zOR1cEJMR*f=S{!xKyZ7rT1Pq^VR}Fv5P$%O!y|VG-g@IL9*dEhBN*()!>!BNj(P0o z?AZ&kUDUyExbUbRNEkN_Z?8X$(#-XUH+STCj-7)?^W)2%+w`z%SbVkVu=DI?N3k)* z{Hm>Fei9ho%KQM^cb>Fd+g{`zJ8pYh|FvfubT>2jDPQ3?XEMs1O$ZDPMJSIxi}%!D zmw2NLMSF97q_xIVKvT!q43-*qPfar2B;$)O*JABECUcOne?m_iC!k7yeyf;L`so6Q z?{r663J6eQ$8yrrTW?_TmwW0F=$LCUo*PR$rDU<2M%O^rmM=}lLSr$ai%sY3L857` z6XOo2bKJ3D!|vPm9%9M()rLcsYuk&WJ)2CPWvow|ItQ#Z*k3s{(Xr2R)7&+Nce`mi z+2y5wu$QB-O^sA^R^PGRa0_D_^ zb@%l^M355Yg=I)TQi=RhJzY4sqs#4q2<8U9Ew5IZSAhb)7pDIn%~a?=)DL=@0)frk zoUR|DC)!F4vUBL(kBnhtD-YDtJRv>33>OM@sHu^oO{V~KXh{c`mWK4Wc#gXX`wNFXRg%DJfA8kMyv{LpMI=-wRkZYbnl^%AwcxrVTSS z4A>!SJC=;ng^Y|n#32%e6oRpodn%zKV+;CHmMOj9;b0J{y?7KQAD@LjPe7wW!1Dv) z-8&e{K#Hvugt3}V6-V3#U6Bl`NCzoFEqw_TmqN=O|C&Pn43cSd!1 zh?8V-P)JxQGq9k|Nmjc@QaQFE`q)5OgEp@a`U*NC6%4nZ4?Oz_Sub%N;!O<;yV^}R zG;^q=RtbeS{I!H9RkCZ#OHh@WM-2uFbaZa;ifaXTVdv=-a60wNDTF4wh&w=RmyIa> zz3A-J+L(;}PzD7+>C?bY*n%ktSMcw04Ph~j4^ET8pjyK$z2l1>Ty;wUaa4z9pz+$2;AqX?m7KyD6ZHcUn zNy1^KIAk)?oV*0pbRes&EJRU_z?0~AF}8lP0Gl(3+nOGDZPrAz@N>hv@2;kAI}N&b z?~X2Ax-_Oq)=68{suJXohUF?XTD1shcBI*)>h=<47F|Y3TR@ccSC4+8$@@Agjc;JrNx<}N03FcGQlpmc?6(s zbO*xc14VrciW9gm^+>NR!kM!)TT=#doeZY%vj7>#jv<%paDk6Z+jjcc$ zYO7F8Cw(QPL4}78nni?g6|yD+Rf%%5&(OJ?oQzNeLQGB^%JQ;tp-6+ER*`V8$U@rT zQz)aLt6Q(G6zD2&G3zK!p2~(^707-@qeU}&J7O8Ng@mJDJsmF>9?i zhECmLFX1afi|njSoXyEYIoC#oyBfhE;b`4D48B}%9h07GWWo8fDAOAFJ4GK!L7u28 zIfFxoPx5!Fioa8#Y_BUqIY+It&033ZXcvs0Itpcvpt;C4wbBwnMahNvFJ|c@bpY6HvwA0i+SK+7sE;lxv zI2y5z+8<4t1y+Amg$rb! zHDrF}Wb8cn5jbjmC?-t|LNgx|Gfw8EvZxv#&&)z{p#pbJZjOOIrITqg?bxhj0IW8XxYife2J#)N&V}i84I&WOF1YlEHKM=G52JB5Ut? zJo$VH+$jW^mE?nsZx$hy6;x>igC&EvZ!X5Y&;nB*=EpcGy0*F;|9tigcAu|B6|29H zQ7whw;@&s@`_V=GZ&Lwf0W51ma7c4cEL|53%0lc?-hqwy?3fU*%PFBw#GTP0f>*3J z$>@z+8Aj$fl#xNC4|0B#D-=+8iHb!*_c{?ZN;jrKf6*v9QBSx7zIV4FgKeFb3LmZ} z6hNrx3qwZdj^@%^4N~lAL7$WTOM_2%UrghXAl2o zi1ZIZdl{Mk%#D~i;WYfTXSqx031xLhdfwiE`73`xjfV#wf8}Kii3^gd7STaOnlx#W zbZ*QaA)Gk9KN-{Kt%uq_7UL)N#1BhW;V6B8R7*C@3V6~nRsYeiV#e4W)cmkA+oBzV z-W_K%Qt^*3-o?ISnJ8rxl^V_!mAe!FKZ6D6Ze^WCM*!vMg2WfHsXexFe&ZeM-5r9Zz z?(WUds%1Q0d;1x*wG~j&L1}r;9=tVuK7M6XlM0TvXiYAs;LTT!u7jV#w8_J`wz@lI zm`wmQ=v0#RJ)V93Bd&=ZFm>htY+Lym4i)61iqU0iYvu4(`=Q^+r!n!7!Dtm^6y#JF zX5x$0tMTOz>1Y?%7qi|-LWJc5#(*w{T+Y*^fZR^ zzBNG@@h#vYhhPAzj%|C9%u~S?-DKPAH_h>vhek?k*uvE-}qf^?vgZTKJ zrTBvWU&<<~pyS+Dy3yZ@x-Z^X_9{9zw-ucL6&Lnl+3Sn2?OYCpQdHHZrjx*+NZh!veB38b#Y6Lgv3lm+ zaHFZ*x6A&5N#5<6^K>hGvG7U6NL)Y_PVe1-X#>nmNi3pUHHuTU zYpK4Q*7rPvM6#F?n?@${o>1V!*U88SxS=d34ew9w2}`8P_$7EiVnE#*4YwLr2JD>? zccIOuFOWEd&W;IKRnci|cy}6J5t0U`*uEwqkny&`f0f}w@y{8t7$M<083O{J#>OLm zH-*S#VQ_`cGGvh@R4p9qXJA_08^fpw{WL9Z}3oImmtl15B*B5bN3 zto~D9L+qRsX{SErl>r#%xe@Oq4n=@27n@f86(cMNO+Z5b*7a8;z41h+fp?gMO3B!N zvWyXA8b=3B8)F-=)Lrlt@tSN$!-*K#H^e5BxNMDQR*{+>mk>!OuJ=Hj`XR>jj3Rqm zhP3bAN6(RFdIlD~evh5N!=WO#=+ zd`f>tO46Sc42>9v^=WV69y;Hx$vc4MGrBkpTGP}yU<+LCp#zrN{tga5(^fu;p3_sL z_X|=BG0RS}XDN0=URXJ!8xlMhP}dd%1{7VW!hUKXTtq%Hlp5^Z$B#fpZt$blMIm>k zH2h~!xS@S}FWfiW+ww73tE)vGwHS8om!m?Ji^Iw)?AWEHBw0bP%C$IhQb4Z3$Eym_ zWll2$xEnwIEBV2mnN0_eWaxEd)C;QlQOv!nUPBo+Q+&xgCJGiROEvg)PbGa^Xs~RV z2K)b4iF0HUZi|uO;88N_Y)8KGB8K+&MQn41=ag~xlo}a1=X5AzvxO9F80GzX6bh9* zJ)H@f+TjKqa&nbMjefhf^~l_tLab0oP}>%RhusDro1IC!tmYdTnaxhE5HMavrojX5 z(TpNOwKMm~HV8~q6R&=Z`eDv2&XuT2A+swbA9xR>`n|97DJir9Y*TFswGd96hlUy> zpf3n{02v;3s`g?P8E1-yl*N#(KLW3)-i*XyU)HvsYkMuidQzZru8iYN9)%%5L1C8A zB&qF10R%72ETIJsKGc<$qvmJSgFKj;F8O4>=x9-|N{3IMFsLj{ zjRs}kXP`XgG-^&(5!arOhmhgX(5D3LhRGip6H%cE@_`V9H6FqkCI_xJevrEz0tb;b_JIYBaea!>fjxXeW?g&FC)_-e_}kQD(@-S*~l} zZ5xJX?{6no4JJLR3Xfsc`~SiBslP~TRz!!cXs*`b*uEnu=i-xBNx@31a6EZW7pNH; z_DWKXrVwcd4w57<;xz&$fo9= zyI&{`mxf_jj|da~S&se7JG&2QCw)=j{t3>gj$-$o47mAHo!cW9*}0;@vskm{7u@#h zF!;MG>#Rv`-pvr*F%D5t5jdQB9)%Z<-~^rNwumxAC+hh9k`PviR8v#)`vGyowVaGs zrYBzps@S$}F-?KBO%#FtyGO8k_%y|S6bEHfAKa@|1WW>4>LuM{U?EIk;bDO?= zDmwCrjg6%isEK6kvWBc^i?IW{((eKofs&hc10(6mmI6bR_dke8#d7+h%60N<)Gre5Mi!|J;Mb<>qp0@JQb^PFk${ zs4fi56EXzJ>yps!5u;Ydn&~63W_sPSGuHjfUX!D47U@ar9s_x1KbW@f&Bm0@G19fO zKbCQ|ec7ZjwyOYx9)E_a6V^_|FBtD6bS@Ax?(bN_P`O6APzomXZHGy=Rhh2i=WfEY zJ*;XhPVs0i7v2QgX+ob*Z~xLHm`cE~-gfE?Mo5}8$IYEEo}G@5eHPOlY4UZ_q=lB( zU}P8PG-+B=EajRu2h2d|aAe1irkezGjkXt#xv8vpVQ{&dDqYzZ(oNM>7qHGS;4U`c z$t&fNsXJ9@z@YmB(YKEmT2Okh^XELI9@1jpt`hw4lMmv0dZ1&lkpmM}0V+BWs;SoF zx9>}k#vSaT4+mk`5FfPg)M4Gy9DKFQfd77&i)qh=(&U7@#}rKT9T$#QZr71NlcC~h z2^Ov5Zf*{DjHQ20AQT*}v@8B+Aonc?VHrl+7nkF9I+vFo6Q1|BL5z1I%|@AS0nq||a(uUe}@u-(Mvj<>!L zDWC6<08+OZhC9e8S$ABWz$rcAPHVde>9Eb=9!a(7$&t2?cujKxZ85Y*jLV-ghK@YR z@LImK9dbL~Dtx@5Y8M9i{qArJSJMoHk`N)VI7@#Uzh*+auM*WiXQ1k#F7SyHdmz6@ zqRQEWT~XVEj0?3xv^hDb+?0dbe6j2p>Bt#WMokQk0fl1P-T6?qkAQkW3&^RtQn~Xm zs$c7g1Zl9o*)D zP>=2iuWn7;iIh`w<-RzYn5gLlm616p_mGikg%5Z3FXW**r5wsu&7mIE749wUq*JD@ zR-5HuW+wj4sKjYNiWc+8{edw8VH+;!d znv+a^2EYDt2I`jGaQ{8=h;17TZShh3@WUpgW#nMb_Knyx@=@I1H3+q&g`>NZvFLqj za8!AsWtZW&fAH<-)G-{DIY+R2=g;_h%OPyv@G(5Y`s44<)2XVTaSSOB;%+>Uo3#(Q z6yrqS))P8Xt zU3a2mD{mZTzd!qS4^ACO!L~g|(WQNBc=}VICl+#X7+BUoI&`d^~8;O<_CLPJh!ilqmILD&~wgL;9_5He) z8hkz&{J@jw**O~CMaS^thUM5n^N@8PF2)nn??m@7cWOQ8k-p^}eE7+?$fU5M?>&zp zen3A&xA38+%K_}&@iVslvL8Qv^1VmjmJT|c3)I0wJ{b_#by z#a?0+b3~-;{@P=}*?Q}w)z}2geQ_G5jvs;$>rx_R?Em_gSXVs(k1$%7eTv1RO&=qA z&phMqdRKSr?k$LV#bn9m^Y=rwMq-9KCt?Tv}$&TOy!ga zHk%gEEu?L9hJZd(=tCkBFCKx(7Ze%|xdc5-{Cl>LMf}R$)avmTdbErZ{Rgh;lW# zxbOWpF><=S#3nv*5$25#!aoK)E}4@I>D|@@MEy9A8ucc!x3$N(0h3Hjuo-S~PQZ$7 zi!sp=2HPw)*Pbzh82T1c=-V|P|A_ya7-M5@#BXVLv$_`P!KR$;Ws}C(rUG@3kw}W2 z4r4UaSo~>JPn);4mm!$4x)kkie*r@+<81pLNLsZA^TzeC&H+N*%#_7$ENzN4g?1X- z=QI2F8(T~qk1qCnx&@LZ&2h_owV!w9N|oN$#_Z;9fS~0xw0OLW#U#sI1|S`8oQX3F zM@klBU2W%lQ|ExFyeti$m^Epf;K533V(ZXh0ki^lLtfon(B6xEGC>+H>sI6Yd|7E@qB z73jQb9L?6D6a7`lDR_t**o=n}XC;{ito^x?0t<(2y?Rh8Cu16Wrw?9yB?6BQ^M@zD zuKRVW!OjgAaEM!j8Amm!89=6=D#CYBFk(zP_?{?59%B)gQaw3-cp#pAJkX+H;k+p_ z%i4?Bzh8J)Q=;Rb+Yw5;Z1K^4^^#!7wU+AKdW5m5hPU8G(*z&2FJ$qaWZJ38Vd7N` zT56(nFd%o|K{C@cn>k$aI|?jl`f&$;rSC9yv+ATAntk~cSQsq*zT^dk9B%Ey;Ww!p zd(kBo1?YjojcR@CO zT>dGJl)FQv?187Br_<2Eand>hjKH0JI-@f8Wqf}yA6vg(i<9GeA=H;SGGw_byXytVtFD>4Li&R(fBdH0Nc787VL(lv@cH)`_Hp?MB=%>IG?H1=TguJd$s+zN7PuYA7Z8)TPN zA^f&`F=gsB^p1;w52ZVX0evX!38#;aQhc+6elrej!;$mj(7c^dpkmvm(oKba_f5nz zPfbGqZY@Z|I<(Rq#m+Z2qoQIDf2S%C&FEE1`vI4#@bGJasFq=9;j71q(n=gXbPl>l zTmlPQ{Qln8>dq6XT{ZCv!3e;pH`k*wDG#Slo<~Iu6&R@**`{4$8!(TJfaD?VqHC%VRX(ANfsx6|ztvlg1MbKEdxw2vSv=2}-{#ESx zEe+?2*hlu2f6*qoGrGitH=G4xe?MSU--YRvR<(^Q}R#EA0COg*eK(gGQ6HKY2q9>y=9ff zq40|5@2+FA(@A?+iS>Lq3VE}Xia(yu1K_-8EVsqTtm1Z9%s_4+m5A8N-NbnjI zlNr2lk-y#!&8QkXVM;Uf?@VEVl;r4eyDuUmswKPRvWi+s{onM`a;p0YYkB4L zLdjj|yGMs$>~N}Tn{qUZ_C+W41stJlK`Z&Rs9RJx<&(2da`KBdZXoJrv^){o4`f13 z`;9UDICitsL$_qSq6=H5RPNb0Mn&OG!4MB}0(3$u%s|NKio`8{w=khLBd&!LNd9Wy zlu7t$P{4yxS^VN7EIsgO7tXH`3M?p?FwkU)e|b0=c>nPi0DxL&)J3L@M{fEsY`Ba|kZ>-Qwm=e!>i+s$q6y9)N|{Q{nekle#S9D~0b z)>~NuN_M{fYBv(aGN3u0LdRq>5=R5NE&vgp)9Qoyj>oewZv>qOe-#nDHAz)BB68TK zOj&{aOUeeh33%@ZL4f#GCrW(27#xtP03~stFydv&Fc>{E*XUbV7Y#MN(RMQ}bkJ|4 zIV(glKwptQrDR+>JDflW2%ZPI!KK8#tc>{_WbyCL@-msdkvhjRLt!L+oN%5iK2Lq%7f<+{ z$pG!zK*>_wj9jS7M#ENjY(_sCIKAFUK!HpPp0 zc-{J|MG=8FRwN^Y*1Y$7s_kHFOW5~jXyuQRD2MqBCGha@??(^ymM$DAfLkXz?|k2F zW4Iqn@}FeGx_g}jfXN_o$6g6)2n{2nipgsWr^WJPATu ztuM@C5Bv3L?+8WpTfRl_^)JJ>c!&CNuO@>Z7QAeWCPfGz{>7gR>3rbCvrf;TA}W{t z7=Pqg-pY}$=sff*&PJh7IJ#@?193KXj`4!{`JGZlH4KB51Jo|S;7lB8Pk%2vV4oG< z^t`{%hH{6SL7G6ji8a_4HiLnoA58&FqULtk7t|rj^8DGWQEo&NUvo~H85aqC%8z%N zwte6OboG2SBLg}tL>6~c&rQy6NKr9Ha($ZS0!}wKasgn8dGsJ;z=NC=_H&@Bs)E={ zel8L4KZ|LV=ru1aTkit=xnpCro0d|KWMsLQhYa^x16%=!8nqai<&QiPqeRerOSWYy z7o163E^x(PDi+}BB9d5Oo(tw>HI$r4eBypcI*Cor6{?`FuUk}gj?x5wRE zYu|@k^eefUOo6kRQ^#l1ofb|S21@b{l>|$xSo=|q^ko8J@gx;JcVG9vE;%g|fS|g1 zDQOTXz21snANg3iQ{^`(5?0gnZ#CU9Ir~Q3V3ZEdV7fK)&D*r>ORUO%8NQ7oXZ z>2}nPzIvLf^Tx?D5P6z_ofSHyV(>M}&Ja(Gq=g3PP0%6})hW>DPkw!$Q$1a^c{ss0 z_G0O%$`p~2{;@um5hf-a&)2qA^4wDP-$8m7LMt2k6)fP{p(Xef3XjUBKH&$)#WwgNm3_t#-wHkD;)@(CfXuFgJxV1&>6}3_i!Z`M$Nq;^(x0B{as2gI`Bb`IK`wiX?UL`WU{}54`)a#Y5r$5i)R@ePjl<4YClsV}o%K|dQeJ(X zm&pC@Pq_BvojLJJS&)urHBeE&`_+N^gy|MzfYwD8no!CrNRkvQ%X&RlQ-zi2U{NTF zvKN@cE~4R7xa-RH+4qhcQ^sy0UPq00^Ti`?t0U(bxsv9u>rk+}*hlhfzf~ZKJA_iZ z9fB1h$AE+25~LdL=72xt;;wia4$He4CTBI0_zZ0mW_VlgjmWi}YWD}y0?ohcKPy|y z5Xuie37HG`6;k#ze)nLLySNW7h&hOO2mvMH7(wub9ll5l*3Zf_;M%vBfUek#k&JS5 zKRZ_g(hrrpf4D16H49Y7=$(RY%W3<%1MAW&V9nEef_@h=3O1h=e~%>)mInouFCixv zpOgMc`|`}W5=l2{SM$#4{VY=@krpHwd}=3+N&n!2Vdr@9>qFc!AJRH8N>0@{k=;lq z32}E@YxI(3f9VXHzv!w{o$E)FuWs!J=84bmjOO9KvYu#o1~%t9O$w--s`xb^m$Ozm zgdzm%`LL_vjHFqjU^|h<1L8|emXs6~B_~1ebN0OJV(Mlu@k2{q{*h52s0(N~EVcJW z3(7198`ne1Sjq{}7cnucynaPb;E;m0j4piO(~dwsI#kaE^uv6kNsln4mdEO>HB4wf zPG=FQZ0S7bhaWVx)UKbrlpNtup>9HdD+g@s^>BIRDDoOj`OW=@vsv--?j|Y@Kv1J* z6bYNi4v0|2ic<2;#JQ;DorBsFZy-c=u_EePE`?8XNXnjXI2yf9eSX_!2gL&*h zR!F~+fxS3s>rH&-T+h6Aa}LxCax){0Bv&~z^C={qLD$BOG{iw?FN+`X5U%J>3Yw0y z11GC(A*yPZu;kmp{9tC=1M~V&o{t_SIj5{z&a$^%;@jQB*Ls{I;XeO+km=NU@%}qv zI3mT>K$`yQl?og*L(Y*yP6Xiv;RLMizx%sCaA60cw1ZV_8DHfx?0~7W`=AroLhc6+ zs^-b-2Oq-pqJK)W?-7iWV1^tKtZ+B*d+NOVbpF|S7vFA#}4$Dcrr zFCQ-O^eG-o0CD-+@xSle@!ChInZ6|cJ$ z9?7uTik3-Dg;O!(*b=5AxYZVB_Pg1ryK}EJe@FY~W6CGB#UD}82u!Kn=x-itrtq4Y zpA|2m;y?uHv+ql};plK#&QRuJm?tK~2Zl~(n_&!!#)W_)ZYxSPHXoCJzbQNufmoG% ztmxAYithjvCFaOxIaTr^t#f8Q9{-{Ztv30I`a{n!9FGSiXrH={cpe1*3_b1BPnyx8 z&Ov=`d>p2`IC}uZ!9o4}SoFMPnTW!NjrkETAx~97_(;m#vr{fctDqr84p60eI?z>y zR)!v5W@Tn~FnF-@7L$LM6xr67FK_vX( zG*+=t)$QZ&nS9YPttu7MoDjnvl`GC>M_%a@H#w%@TZ_#^H6InhvDoH*QyAAk*<$XF z>sU00S31=4#q#sJb5Vqc+l9Bf?BKF zq`2tjxv=f^^)&VQQvj-rqJ%Or>t7(o!3+%!!jCw7TV1Q@%15Ky>EThE}6;*9Qt^WmQi9Z2zQmUWfHu8X-5Lw1i=Bx5cbeRN<(S1kvu+bz7^J9rHmX6E^e4W_Jpu z?#Z>w9e8Dh<tK5vlk}u#~q}WH!Cz9KYj@M_@#IQhF5B8`_3>iWNND(Y>L5M8nGQt{VP z1FNc(lPX7d1T%g43EH($HxAIquq5SvVt0l*B{~x=0dsXt@;$R{>HFwZw7GnC%yPxa z$z;yKHF16Gevz8`OMfZJ?f#9vmavo>BL{7vHTcVqi~V253KN4oQ`iU=_>_19Z7|b z0i=@e0m>(r6;~E#J6vP}`Hr6u+&MLcrFXejN%^e91{l3FB^5{P!>2Ds=}JmgEK-Pe6EfKtfJvMhf*O)V|(YPAHC%peaKFek?b&@>De!QvnL?(2u7>PVN9i5Mk- zhPRJq9WQPg!hHo@AQr+odRoVX+)rQ~0yjRLx6^WE|GZ%mJgBhPZTS}2w#Q}O4aBQ( z?vv7Sjv7A0)IWIK6+C^AqwoY5Bq#>gu8;ij9u7Il%rNl{tqD7a#` zLP0+ijLm3(5Hr9DR(uQ+PDoT#UOX|BM^C9v)v5WLDuv37w4#*C^(Cs6ZCf+2s%Eju z9eLq+*|&@;;*&~Hkm0!4whhCPR*9it7iP6IZAOxAP`!YhrrwdEvq;CdCx-U(!cS;0 zJccn$AJHNEP)CH&^hn51^S@@WBvNp&f2&<+CQ>^KEEm!X%Qn(rc>5dLpDaOQxrsF? zgwL2Ks)7lkvp%RKR7}1NQbJ7vg=j(227fEXY@_i7V6*7g7I=_kU|5dwN@l)9V=auV zl|4)$Xy8Nw2~u~?n+ZT@FA#MUY*^A=oB?&so7X+LiT2ucIC8Jz#KGL#WbG1Ff3JC= z3hd<(7DiMR{z3%|qRGL|c_x$N;dHHqYH``ZUb7H>uBc6T+Mr`;h|%Q_%_}R1F(aER zFB5|O_91cjM0wheY|}5OKSRvvykdL1ODu2gG^QLYm#jOlHNM%TBC|TYZ2c^TnB|fP zw)kOX6rAa=5rumiy3My(c>gTLbF0Dj@zOGSGB!d2?ZFn(cR0eE8`3~#*lMjn;oGC< z)7x{HhcQK|`r{H2T5{DK(1h@?5{d85*Zt@{;z=(v1}+^gH!;wNAvZ0Ow5GhKV02dO zyj_-xQ>NMwG&T351JbSvwU3G{!_5~7#MsAu@POp{BmktcGTzG(uY{+vH@B200?Eyl zbgC8HvO-ZME+~tK_q#I5Ot;-S4d!=q-n_dL7|jtLKV*-pw&+}hAAeAHY4@*g&#POu zUUk=8*v`H@J2hB~Q^-x)GQ{|X|EinR(1T~ZKDfE$+b=$L81F3GNi$}I*(%K@^+m!i z8W##P9Rc}yUA-U3mq8fFz#XVU&rR=Nb~rga{x;CMm~ThJAcT(>TJtnI!?9&_tIG8u z-&(9Z7Qj5)PcOKF6zbM(T4`^Cq+qVNzA?nNJ4`!EVUla3@RdUlf~Xt_yh=K8x+=8S z{y<G_$Ks2l5=QFsWUMEUCo-Pab@Oav__W7B$lN$j)8X(! zxdkR~N%6I}-bc6)WS+!`P`q72;CU8`0%2n~F!6@fnm<_~D#pgpe@+nsx5d+I4gEm9 zb;nG^{gA&G>p8eY-B18I)PSew`NXrb#l^(FV^TQ?)g4(ipcWgvSjt51r8H2Ymfwzj zQ;(VXRTQE$fi-$fkN|%pfRFv{ArS z7%p2;FRgJ&gTieM?d;pLGfLw`T=?Q(+1oiL^~3O4GnGYJ3}Mo*U)QI&GKu+>($+3G zjebT3jUms-wf=J?(7$wZdRP!#2{jkzVnrcf3zkOK#`NRscMDYN`Xb?xdBF{y zFAQDt`7f+(Pw*yzB4T&4)hT*VlccH36g+#zH}Mdr-$0E$LpY=N3{V3WZN=k$#*0LO z?jN0oT|y-?x!kpAq)6qbA6dV7oG-J3js8|PE_t|M*h~Joxnl`nK;P1OWAj1F?!69w0jS8d6rO2VXFI`T z3mSD)zK^s3@3w^=eTT=sUuo>I@_wQ1o)$JjVooV<$5pprNx6dBXi0CPT&`03Y#iqR z9&ReS5k9d;nuB@V)r0q7b|M~lR~R*=47bS9^}-us5fx5|ZGV@5x&JrJZsRwUeo%VNXbaHZn-A-v{UI-CZ%`HlUhAH`(8lvm_Xh zZVK=p9s!~*E=@(vZ(hVNW)|y{y5RC_V3Aic#!7p;P#^C0DvW!7pED*iCK=kGXoKt+ zjo0UA4fA4~lE{J*VF1+|USDVwv`!C-CkykwfeF1&Y>?9RqDei!P_-qFBWPuMA4wzh z?aKS|F)Caxc>T`w`-bnO7*Yoh{kyXWvjZL`*~!@Q2I=xxx@2Tj2J-5G5&!WOf{)~P zX6l6a*~u0r?&M4&+@Bh(-{U!|h8Ku0!jRBiE77-~S;aGbr0IxcgJG_96u(1*g^|oy zM-@XF1Gh<7Ss*zB>*c4kf)SW^{aLsG2g*fcc8nmOUk-d^8{OD@do&UoxzPjti12|N zfqBa8j)3_)Ulqj~N@!GjAbAHhEG}qf(fys-0kRur1w%Ar2tyO>f|**hN82A7ibB$- z0;sS=QUrxwf&6e{e^z29MQ|1TvBJ2avTOhb7F3^!%?zCftLkj#5gXXvXha73P^0468f1fwqJVN*Iyr*dREE&0_&WoShydhZ?i6c$A+dbO|3PC=fip2=9Am-eakMsMs*ROMy0J8L0@G zzP=wcYyXaEj$@yPUF3&G+h6f2@4s~3_mlwY_5&daT3oCK#u1@Z47X{9i!em zfBuYnAOgEl^ns}&E<5cm51$2>65b;ohzjnX#zojWsPExha;n;e`4&v03 z87z0_^d)?%u80OH&B01cHoy5Kmu$m+XMMWa)v6$hIdKXMIw_7JoNC2q8YoXqn??nO zXt{_GH|qwUzG^WNm%?7jpklHqt4G22JY(WvF(u95mXR&icl-S?sdZoCIxSx5E8;53 z)(GDkb-sYx`KnN6T9aUoRj5-gEJ2}?{LoOv#EwPpRX)7hxxEu+`}Y@xc*qG*C!_s@ z41k;rR|YpT=64U44_r;e z=rL^8OJDVXEVjGeU929=tV+PIqSwPPKD8rv85)P?iWEYb&^l(F>qZeyxV8_!D=(h~ zsETL0$g&~)5QOGOq^{Wa>RA5Vcl7)#bdS@ZQBnP{33GknWsK-9tJ-1--lvcnn2rVv z$>{?+vPmUd9+t^8gRieH^9g670E$wbXTKYpa-V^l<#R7C23cNo70$@|xh&xBV%i9S zGo-Gr;om3Ul6StZ1C8X3c)!%IFsxYM0GAM8Xlh-4D(Hnl`5sIqv1yVw>0_H7N=__B z6t}S%nSqJRjg$5#<@G)H{=$jd^}QbF?&uw^;q&$k%^Gw;<$#d)0cGQEfH|1G8;8Kw z7XvRTszykpj0fu|iAcCsm3byi$iV^WeI>kY=(-@_jOKE>*sxj*NH%{UeB1*3kiIl8 zsZXgGWw)>RPaC_Pi;lEZfGsk{L+wCwwUvUB z%2YVh0O_f)WI!^-vLxE_sAr1gdS7%qyF&eQtT_4Jx-LYVW%vM!pC`VuIbVd@ve~Hx zs!h$rSEV(3V*^4G5)eFY*CbH1Qu$ml5fP9kQ9IacT?N2Dl?`^*eBideZDMSB($LU2 zygfUl_Utk5?@FEqCp6#j@u~k!ykdsaKVFuamB)vB-?G{C<2oXv?a9RHw0{j#zy4U> zWV{H7xh}Q~woVM@4-2itf%$~}LF9LT!IvLzl+-cSA4pamGtTruoOIqK#as+6Wfh=9 zJ(`4SHZwdlnKU4tj;KaCO3L?se*n&$3U`&iv(8&4dnl(*xSXMit}r^11_8SAtYgVh zCOdH&kf#8#7(l+{Ocz1bmI4}@!4_I03KQcpR+hd;gcoB+?{fv;?f*xRs7XST6JtAv zFGj{{{JzT$^Ad|~f~>0bey4S-o$)%XzbAoAx`;0pmvfG+>bXC?NHICx_k&(Oa}7dFMKRQXLQAOOSAp48Sdg*RnW+%Jel`pL?HD> zP#y{@Tiw~Q^$(+m{iO|N-Lp28$XLnwGgS;)SjxML zxjaI$70bMKHXrUB{=PRTs_BKiNRDD|R0FaIPAGPv|G1)cJHn1^Y71nqz>-By&+$V{ z`mmczif?AL6|Q%2zUkw_s?Gq~<&>|VRbPvg&TI0Gk;`T{KeUSsVu~p>k(y5t(=h;& z2?3d?kF>v^jIkXtO-p)5cVa%Lw>)~pRc-vyLUQOJ=)zjJv==S?^Fzo_8BvPHVpYo+ zJm*!j7t4!=zRD}=)?SH>%&Jm=0Z2hnJdu7Ynx8P&~I7gA3x}U zfq}L32~+%`8cBfF%rpDi`_3fV@Jw?X#z5(D0@yE{odD(eRvOF^24FflxmQ!Go#};z z&>BfZL$*XdfY1)!KbT2RdL+|exsp<_cFqA6Q7lqX;a$agBs#hkbVHvTF9U@JNnMBs zzQwuH$5DBJ-x^8T_60*}v^kRHcpPEI*^hfb(*(v5cl6}O?OoC|RINBwQv3bV`w`3a6_{|& zkMur0_~yqzC8z|sjg=HwO-Gi;gx39TjR;9;X+_fbd^}lTWTZ$#py_LD9tsFcQ|a%A zd-)(o1}{l!9*JP2dqLCu-;wIL)_*@NX=`d4`VICYH!%TUxG+~nqmh`0K`{c6&oF}q z3;%*%`+Ij*S)0=Q!#Z7sLya6j*Eax9#R%pV6@daWB@%U^scXE>H1tg@2kNB8?9jNUi};CTaJ82}GJ0O$I? zmM6(J*H|DFPkI1;w_OVxoGX@&kBjpU0D)Kou}%s2qGm?{v}y?rP%kR5w6s)sbgGZ} ze+2yi?o0pd^7)&mR_QR!`^4&Yk4pd;-38HN9vWu%S8C_ulH!?n_`fNA3#hMW4(B}* zO=B_^15v-T{NH-m=K8zP%#S8g^iNOMUYMsjjewr}3uNjc8$oz$f_^>aFO00w<;@8V z12g-}SR59Md4TAyM~Ee$Sx$gzDS!n{uq*Ekrpv*Iw)2lT1k46AnVu25AG;U}JceuMd%Y)=d3wUlp zT7Z@fgFE>CWENyu$4Rzx`34F=Nk*#wgh;+e;lWB;PY9F>YO2Af9-QlCr9kc|?==vPR8`e#5#depf82lu%&w(#s@{>frjKIJBcf>h1Cz8S(oE7$+I`~*- z^9Jdm@F8iK6j@l;hQJ@62g*fYXk>+vvr|1@F}!SsWstzIh$yTvX_`DDd^As#;`}^} zWGODa^eIvA_)${;#u!eP+g`8{xFkt8+?AVfz2XI$!1`Rng(F~~8eL@Tgq`vvxV2QP9C}JctiT48 zMs@N|2%bw_ONVF?izroyDkw7ynjWK!U=ehOz*XL0zC>0+QZk^nmSHHC!0zd`-XE~M z|0IQ0f`FbLzVGWP1r?NQ^J+p7=MQp)?*Z)9b(HkPhK3xsYA2$`79mu2(?WWU%iS~>K|%SC`WX>xc` z&KBn))Py@Eu;O?e!~F6Trh=ly{C%lJ-D}@CW(RU{ARoe7XEg zG*%4E{V^tPB*huQD%LtzAGPJb2WX|J92i6q z34jdY#Uj)$D#VR%A zp-WkDmldGpPPv`y;h|j4yfN@aj(~1qzAHj8FA$~7XsDx`u}^xf0cK8IsWyJrovxxv@R4X459vv*~8%YYbI3X|(wQtNLS`3&?V za6n1VHn!h4d^@>$76kfoE#3D%okvKDLu2-uLS?rP_hJc5StVL<^OZmFvTk<#vdBV0 z>LxgxC;`_Y#>2u=QEFO4P|+`!ItELTaJJ?8X}DoqPfgK0hbw3xb~B=Ye}8C!b3U=| zHh1vMk_f~eZlmwI(@Tm=!QpA@mu)&RUgjITT{S^IS5i%D;i>(0M7P5S7UNe5^*!`( z_xTk-%VgoKX9kQFrZ6%%H>qpCj)*<;{SKw5MWg&AI0WVwW2JwQNadUPK)LP9^KIwd zbG1(NPZpn9(cD-H{lx?@NX^m_S(A7nG3-lCjc09y4Yq~B0*kr*rxs-z&4-^&RBj>% z8C61WPcFUV;j0Y6g~kYD1gB9Vlr*&cy>M9WzEb4P%9w(2iO6U+hQk~{PA z{K*MkC;4IY=Ji?V0I7`*Jjt_RR1+g6@T|yK3scsUWqCY$=AS}6qZEh~lpW_N71qe& zS%f=duviNQ3F$U?0953Ymp8h&H~w9Ir#OV1V|MN*{I1o;XC4joe&o%e6yB8isl(@q z@%08?n0Bo*Iax3cm&oXB|8VeuNW{}`q{E_(>sw`_iXh86{Ktu7duFD!zWw}T91Z{Ug4D{MzRr&c*bD9uj~l?^LB3XWbKbC1hf=YG(1T_opP2q)jGTxZeRwpDLFJsAWFK9?8kcY470Bb`Qx34loZY zkN^ssACZ87!4{A9P2C<~XYJgglv%&Iy%g`DUIh9ps5!R{!PHJg_mo+07>if+AH5!RPyKQ@52Mw8!)DDD$ zNjoxDsAINoEns!nG3QQm`l9b&P4-T8a`C*0iEqfI;B*RC_wT0&5|swHDo|kX+>20N z=A1`Ds)M-J$PH$7G}uRYyT3IwK(d7N6==l~F)m=~*Zv9p<29EMA7M0sM-Yq9a^_&8 ziZpeEgLqKtFFhWiCtJ|&3rT>!o1JyD$Bkz{jY%*vB4x5V9>#c~%Ui$C7A_A_16TQH zY6zEw=#LGR8vS@5lhTLCK;xy}?Mb^40b}AedOuAE}`A4GqQK)~wiS8ja)FJ?TFzzdRNrb40f%EAlS7Y}Vmv zfL$2!4S}YWS_K*M>hLF-{-D7GqFd{n(zg}^f`JDI@mM)#lVY`NKXml;KZ`0 zPEI-3F=52IACaK#Yi=@`-7Owy)y&646PKcq*5q#UGMd5(K;K3ayS2-&z87_jm%O#D z7Ws02R=WN87qKWHKhbdiIT&A7lR2;u(K8@WECq@0z7y~m6?iuVs+IgrTceTM*Goz# z<_Ud(kX{FDt*Q^%;uMr~J&P+W>j!$A(;)p+qf8H|Y(BjBNI;2#2Ik5B+b?faoC!JB z!D#9Xz0{Y;3Q?9fim`ZY6QQE8#x>MAvt@naL*+99hsTBdrIG*wIK$P#d5+IZ zT%%4EkHP{_1lV_DG|6aYqkZi!yZEmSV87ZZsmO{z8g&Q@L0h)$ky*uR(6UB;7LQ73 zM{_MZ)Gsa4c?bZh&-JyYZQ7}jAM9QQ=C`=zcaI69mu4Joqx&T`C*DsEuR|Gm{Okl} zI~?cF3oSBn*_B&jn(jPR?oNxI1lFZ!crK!(n%Uo{P`ai{wyqW!&Kg0o{&w+5T*b%1 zfetiF^!F&!jx-;D+Ffe&H8lQDzh|WT+tYupn&(RXx>YNggiS3p29_o5fsP~x-XE_- zv3E!{9?<>K_-_8Nf21#Va8z-oVz%Jo(m!lvp^V*?zgnB8O6ZCv(-;}4h}pr)L@6<> zs9c*Dr(>%}NsPtAvzW$I*uMDlA*No5*IV@W63E*rBdA)nmguwBvjg}#UGx@^t2?>Gtyq>Jc5c*4) zzafm=0<;A3gYQ|B)omxpV&Gl-@qDQ}+ zTK|@NpL6RaGtj-~^GX$3NUzaQ9fQ4idS8n%-{R!}o99sn0H7|z{!qBNe9QR~Ro=nv z|8aFAH}L7{DWKO&Q<5FPZoiDkWw8kZU2E*?gZF5Gisu1Q3CG!GI3#0G4jUH3>s{A3 zCr)gBS@;7-IWbRkVFTo=A0LWx4A&%*^CJB zbWF_DUoj9TcxaL{yKEKhzF#bY%Tp@+xN5`W^bW-Hm#&d47ueQk6iXvwqhNKwrP*F`R zhPOe=!PySncU>p^>@4P$Y?tLxc&DyKFZGsj|H08>JnH!= zX!JSutZJ13Sav15FE}vFjrY7pZAlLlSndI6)NyLmOu`3m z>#7go>v}E91r*gtJai9B8;{js1%m@v{j#ocLCp}TJAH}Xf;P*>`A8_!BJVQnid*P# z`+hpNV1uVyXPf*#k2O=nvN$jkhoax(^{E|etOE7j{BXA}z}65$gFuj;Z^s$PELT@c zK+v!w$3SU8+g&dr`uc?2Jv^9DzVM0w|5=M*1Qogf5u;V4|NFXKhZpN^J3Ij?`) zt{T|UGIMIkYoSonI%+P-B5j+OkpS^`7hX<8`&8kfU1Y9#H z># z4m}vQ)+30@R3#k@`@5DV2X07)_=la*KLOym8CX!0Mq7DnS>*3Q&$xrd$c*viZDlJuU>m$>viSZu7S|PY zr2%$b@MMQanw}JnL*?Y-kmlxEfjjwSxX(*Ti+QD9}g*RvjKa!BIaV;osndqtRSN!P^H1{uw zLPaWqbtwJ4SlWoFhQyVP6uh0)Qc-ml(K^P|+nJVfVS=Wj@FsODSr8b;?NItrQqQ*yOIrM#Vs})QBVDo1>@oL8Pw8NoBFFL2YiXbluwgir%csJf@FBN?1i595U$h zw5i_aqaF192JC-|Y>hI2$#}?MGK0;8l)ugc5%Dom3zicb!LG z*zNKIwTP!G-@WY>tg0#5Z&21p_0Hi_l1*jlB!o?uBH9j$7zB302zje6GXf^X8zTSrh%~M2;6Ckg29Ik;_j|piFOu!1>RuPxFud7q%jp zFO_e>JNSaf{{4N82I(gtqwQ3uMnXDElM`H)U7>*JFG_248F_G}tZSiy@$ATFH#|Rnh_Jt&rv6xQ< zODzUpEaQ>|(NdrQydf!wlx$J+FC3OJfE<}|rkV3A(0rKz0EAJlj>Z9Mt&Re?!E7>+ z(KMIc7W;Q+=Y=F|N(dAOYybd$v{`HX`Q|zL1qE_PZO~GpSTKOD^c8d_u^uOWt%CsQ zQnfZkdM7wI_#VJ&G9w3khl3F)zX~zGAndksEI?a)Z5Gg9QiIJg z=ZQ2XNEny~c_K|}03kCaXy|<)d|tiBTL19dZ-5v93WZ#7YAOba;eW4@&-{S0Xj&Vi z>snnk>!=3La)2QEdW~&dz~-~b>**5a$0ymN!R2x-!N%sM^%ng*dW=U50YAO1pUN?q z^UUNV-230B+AG!=JZ^H%=kh0DE}~}D3Oj4Esjz6=)UarO+f3dF49MGgUc#)h8+R~wh{D$9{Q1|)NCPSY1N>VTZ;0f{>cVPaHXOi37+oG<= za3m6JKN5X_#oBX7VPGY`8-k(7h_nm-NiLJ7z>vJMvI2gEVd?$%qQzy%VW%fRY*XHB zy`-G%XX;)7J>XM1I8~^(JRq z9ro*fY_@w&N1_-F)w^A7x#*EJ%VM+JVAj{ypYho7u2gG(r^5epRgs&8fCER5zyedF z-ptiI9Tpw(@WSB*XbdMMg@}kq_`X`R^H@O*^TTbDb-;$V8)B07{N3kciu+2Xy6^3a z<6r_NCS;sX=fGMvU2z5y5R?>D>Ny<`AYCsvppvD)T!nOhhIBImb&wIFc%pP9#{f?I z7k>mu6B8z(&Cbpumh78PWeJAC5|@InF{nKd*?$;qnpa1;cL&*u%(d9xO0?MbnKLt#Jxs$pnI z0oH5uPef*9_5ixE6Q%?MoSgj>?H`txC!Bo$8_88NuOviNS16(1k>|Px1QH6W7XaZo zoXq66)9)L9Nf_oY3F8Q4fBSF2Qi$u^Z;YtUxUYVr0D@O^$*{xY|7Y0t)Ictn8!D#8 z_4OI`27;Z;mm&o3UltS;{BI=WuaS`54m z0NG>&Sm&kOH^2$;&qg!|3CTtUjReF&0iiSM0Pf3H5WCI?^tt(Y>rHA`2&8~&8ms(W zkgs_H=o^A!RW-a>P?q0qJI=A*SK;uI>0tus_dcfa=wh=k_95is!=L8=}{#;&Cwv!6Awv=C#jK= z{Pn#aP#e%FvBG;{?;6bpu~!&KNC6R%U}x|1m0#CSPc550+y8d{YeztUqnqk5zyHAH z`+Rd6F{yVI2x%Ar#Z3f&BphaKzZU*YAWf!33l)Gx{qpbqtt4 z11Pq!A>hh8;#c~B0wwAN7z{ugagYM`h{NR9ubF>?^jH}x5<~*HPugo70s=x>TA1B# zOQ$rUu!w>+s()xs%4*;h9ceD(l0L+Jkj2sAu!%j#^IbwJ>6vz_r zC4^~bBRHG!Yt*7=pAVbu6Faiz3S*BE}-2STbiI|7y_Y^StR*-xahvG5kpWdDxfr$(@ z7)KA=_gJyQ_}}-bNl6+EwGULyO4vN^j5XaKuUf|gKvQ9GFsWhb0NtUTZ81G9ZcIlT z3~>*ILN2j2)7HtnjQ7LMb{er8!uJ0w%pH=BNdTM*un&_1Bi(iY92+GV1g?s|&j28v z0}&`A-@3u@`FhuAx*Plx-{*do3+v$Mh~2%G?E}zAZy9U2F9Ze$4Z`E5_RiNK?#@uG z|Io0KSf|#8cJ1yC5Z4|SYhqY(9eW+UNPg+;xv05#AGs350cbaXCTUJy0wk;AUcC^K zi5~vF0jRlV7wl)8FA*l5C#$V7kob(tEjB0 zzyrcdXSGB|MnQ2Pf`M@`_E|#P!#x&O_}0oZfjjW|KPoU`V3-O~F)-AjWG3PqyiA9B zFKW_*(gcs5^3uWh36f>Oo}_6h0R$~ViM#V{`Tn)#`Fq~LhzXmpb9Xe^4+tFFrvHoD za^M1`EBxZfTA=;Pq5&)rAvHC$`E*Wo>Hvy7PXOQsbNAMrci?{~HcddAI4(OIE?)y_ z_scf1uI*e(sUP+*1inc4VjI(6P9*@zZvg|!<$U75u&}_@KwM=XJe}I^{b#`gguvwg z*k@-4w1S$}U2drtCUMXl5Fcsk6k*2xPej{80zc3}A<21nzZ~JHsd0!<)yMtEbpNLd z9+19r7JelP33GcdK!nErlcML@0=f<2eRk7h$u#+Dl^43+DY5@Y^Z#X0==?LsHe8l< z0T2W?JT8a5Afz`Sp$L#Q5e{mlr4bw){M7;V`sT)A;Jj|)|MN+iJnXL+tAxCKDzo)_ zRf-cpc>#_GAkME!M+-2Bewj^Ti-?JFxz}iT|4-3ihzpP>>TX1P|33iJKrFu(6JP>% zOQ20}YihUDSuiHR1eicG5U^TU(0=py`RAX}w{PEOh?S#Y0!)AjFaajum<0U%{4jp} zc(ycj%;ICInLwQcSYc5omZl#Ro5QxWQdgv5-Go-8)zaxTodVxcCjpZY+OiTD70z%~ ztLnVOQ4hA2I(a#s-*&p*eA5hGe62|JEc?8wbCX?9#dEZQX!5EfKsD=L}v zzHjd{$2J}UY|Al`m{SLfTbGNdz|`N->2#>j1J16_P_zO>@tmw!vEm|x=UbQn6R=kT ztgvW?lF@1^P*wq`U6pWgGQUKdA=+9|6y%noAm0E3y+##kCEPs}@bXZ?N!bc-gj&+^ zfw~D)peXk!=07(Drz1bW|Nb=^u618{nT{G!Tx_V7mP{#!i&_Ci6BdbyL4Iyl3G#9c zkUJ@$a#6s~&k63%O^BqHB$xkyuB;d(TENxA1N2xqR1h8OS6_V<)22;xD7i0PSUrSO zWs^}2S2r~jO{ut-o;b&F*$Ig9)>;Pdw!~#hHJnu99q3qXjx7iEf74f#BPaVjii?bp zt5np#aD}&zH?^`MAGFB9^CAV8jyi z_|Nlc_?3CpGbj^9#bS2a7DhfCfqBmdqicA>b=Oc~K+2JGc=7Rb_-?x`4)ZrP{z;3& z6=4e0)6J`SLo5C^p}gcg-WuN%i<1Iy-J{=O$>UeFqC8DcdgjcTc=_d*n|`B%-;o>t z18%zWKRBNrjCa1Chw<0-hFg0bAvkzhHb(rMV@omW?ssuMv#{FbS5Em4X52Os;|GRR zJMPFoN^%l#c-Jbt`PK*6d#1`{gh>pbuf^(pOA+g?L(Z|q=yTI+wX!POu@4^_2tUuZ z(<#V4y#Wh9cmqFfJcYcHDp@v$Bd-5VSoqy5=-^i6_(gxsz=xBkVaeZzkYftQZTCNc zC!d;%4xTmNW+oJ6#be%G*I?0!8dDI)p7=fCcXaoJ1H1z-8dRM(n)GQnaeNEj`FJ@B zOg@+~sszjbd=H&kugg=XoP3Z8FagISP_q?wyd2np#4aR*=vEkYv}A)c|IHZ+5>jx* z&@_Dbb4_K1NW}!B^w*pza$IHtTKvA~1&kdKhQ7BhK~^ahZWBZPxp@4%=q=1AWree+ zKSnGFe%t{y9LlXLX=`i-jJTkWGF{o;zm4r*LcKq>4qN<729prv^j_I;rsu^ zq*?Q-D=UP=GU{B&H4V0}j@XV}s6dJmS77wyC$Vrv4P}MIL966?F8+66`rG_3Zoc1N4}AybBHm_W^%|GVFFB`wFtD=cD1#5H!)oURxQXtX_5JL{OH>en003mx`ZlFUQ8ykB}OP{ z8H#t?nMi3OpO(D50tbItfc@uO;pM4=zPdp|g9%q?&SU4sZ}8@KCusSOH#!e_7Oy@u zsKL&4gArfi1p&Pxk#R1ZI=yl@JL~YtJ;(9NipDErN{h;|ZgG|*h1x}qe|;K_$EF0r zO=(0)o(|bG76_yI*%;-=C1!bD_CdhE$36J-&)aAzfC|no`B?UU)A8}D zLygM3$)H32x&8Qg>t49Hc|a!?*V|>#H&=eFP zUn4^p&D_i2gn(Z6;Z*#cFqoX+;+l*1ClA8Hy>+Zlu*3PN;<3UW%%#ls2C!px4ow^;Hu zq-kG2Rh6?RPQ+T>B*=wGIsOm6fAwoZ<_cf$K6vfr+hCc`G<11+LgP* z)s_4>HDp?0%@sLwaB{KUOW&1^z0n%M<@zi^ZbljXkuur0Bl?ed26v3;0T-!#b%KXa z0Nkm4wKcCw^1DOY3n`E<2RG8H+fH6Cp<6he93&l=NqwN8{tw{Z+sD#`jhFcyLyCm} z`V(^r&6RD1YYd+IWIk>@5D%Tw7hQYyLZp9v-DG-qDxKls;Y^mbI(T?l%c6ymI6(A1 z;W4;r;vDQ>{uur!%*V=kAK>QeKSdvZ2N0e`WCBdUE(z4v65FM)O%t9lFH8LuEj6{4tz3 zm5733BRu^g&^xXNIs|*!GF0W8$*4zB_8A;Hd<+@+Vy4~+AyM5B*RM;pycvqqar#_7 zd5$*1FSHZWDJf)-sU` zsi4lWDh~(unoU=-{yK&uClU}uS__-$sXL;(M8H*5+Z;WIv`qVUtizw>CIm!w!h~7> zhrVGAo52*O7vbm$4bJCK-J)KYnil2z_&Fmi+#M0YYSgvCh_!CTpDaOUfw^vlM!BPB zoL4peRf_E5zmk(NEtPS?aZ*?ii{5>_;6_enD+7JCszNG{U8XO_@?4xblLalU^Nx-R zgR`Ox=_$!Leln3}B@Hy#382nrU&MCsu3I7+)F_aYuN^F4UHEYPRy$Y0Hp7 z>`F||2ndaakBg4lgnc-1Iu%8w2Ka__!8L>X)k-fj`4n2;26aM*sE+lQcMG>AHAK8@KQodN zkW*Sgio*y*)4;AY?-ULlJcZP(0w|n=(5v5-=os#Wx_lH`>sG?aBx*}^Qr!xPjHR|D zwpLkJ&5%i7j58KD>F42#d;aM0>csJ zS5=K`lAebXXY(Lac_Ow;2YQ>-6{rT(tOPYKGwRDxT9}Tr8RYQ5P=TZa2c;_vMgxu< zK8h<`sBTr*tPwl{JEB8?YpXh{G?telE2o^E5mNp}xKaM2={;0Mxk7DJ{n>7qNex8g zFtP;ala3?)bQ+bLXiMA>(J=;Hx^@A*o-0Ga`6Q&}mC~|CYS$u!vY>7$G&$#yo?D1= z16h9r1tP%D!-PA7#0)t644qv7x4S)~tTmZQ2fCg`;#^xi*>l(aPDW|Q+w znVcM!1|X(ecXaOxgfsOkjFdo?QmA@cweO9! zcFQ}Tg6u*KRGtCw^YMb0Yn56~)<8vh=|~}G)=th|i0Tw(c?Kw7rP=h1Ws!BH3LSfP zp{WopPVC={6K7ITTBbyJ=f1eQPe-WhdbsJX+>BJDoH>ItsX5Yn%FQzf-FuTlCaCH& zSS?G^kHYk1>bstxeu;*dn5bXkM*WgtgoZ^^ztEeU-qwEW%Ve&Ij=L7KK6n6Ip8OKo zIeYQ?$6N8k++hu}%t<_mMEhZSm?nBAij*a?35!wx&y#EUEJv zKF9=^fMXG;G8%L&!5bs((xpo=Z{9ov1qC(c@&>LkVB;6@xa}z#lo)8>K?6!_C@rne zafJ~He`n&??=$h=#X9L_NbRT;WYn4W@^;3O^<6QdN0k?rHB$OixYLW)61AM(nAKm* zRW7`QuX(3d;EsFdLsQfR?|gP2cK>fKwj|KtLQ8oROJ?PAgmfQ)S&z)Ytv7Uq?bTsI z84b36oc99W{B{@g27_(ORT5Sy>w)){FQ&YCK}mVdPyQ7T&R&f4tYUN;_Xggca|3!t zR#$@LCH{r2>%PbTeX<>aQN1x?&U<)jYzN96y{t+3@bkQ9Fn96xI@0Qj*S}hb@dJXT zw5&LaQnupRXFkG#Bkq_m<$A<;rD5d{D{!(yzXASLocT&$k+6n2pXzmeGB`FpYH_%n#|w5`%N8C`K~*9NS4a~<}bI)hRwYw`TZ zsNM1j?IM-;6I9;9M~gXJhPLFn5fFZjE`KP-k{LJP%XC)^CLaj5hy9J`-uRi?6!Sn# zJOc_vARd3?T}&B0fD}>IhKmL>6i8y@-u@6U{@yhS`8dE~EP;_V+&fp^ER z`+h}N()raM4prV#@0a4NQ`qwLvv_`SB3(m@5TW&_P@yfwH?K^?k2QAIx^l*+`0Vj( zY1?Tl6Y28H`k$P3_E3_7LLurem6e>>w?r`M^Ra8qSNP?-AMxA4oa$|fLJc>!XgoiE z0dBuGf|92?_3%(EJnjmoz{m0Xx|s-duUoFf9UtT6`Rfp$9EN)yeguy^J`!FRQ+QP} zt)R42^z6&13avt{c~ zY4H71cj1d8dC-tz#1=d-<$*^qXU@HqZMf~+#eSm+uD2h@r<;$!=cbo&@5CE$%fQge z$STTm@b|_=c;uDU2n@a&i+}qRvGf`c&qnIOZ!v!QLV6YS$C8z^@#CZSV&i$BoZbb} zyF;Nw_nV%>{8y(S+}ow{xzXk)V#UWF;h7jqvUfvRsC zvq0JS>a{2F!B6{8LJBQut5SP*eM?r-96Rnx&^57qRl)4D|IVh7XzW3mZsNg%Pen9=M{v4?2h0y5luvXz(|^ z6iP}9sb|W+dBf6WF4BFyctX=ph z-u`AkiZptf5$=NVlX{`>tcTI7);g}P-JH^~`6RL;4XI;`8Gr_M_4yU*;+!f!vG z!&{^X6LrgjOm(JCb*@#ne%RUr{ev%@e(l(;M@%Tr)nd=qOq|LwkwuINS*a!XlTPbM zq4n**)6mN)kLsfNdSkf}LH$B7>PlB!cB^Y^Juc0{SD&Vkd}XMhwd%h9=GmhhnjO+6 z9l`HE%tNQx_c5`5^_e!kPK!U5yoMb{4T@=-;^=<;U=o%O1}$~)*5SRy!_e)eF$nfH zcbv{2T#kReu@HL>CDF4}4zG|7G~*Nwb$K=^$WGCWmloT8`~Y)uRrq551oLtrQF*8i zX*4wJiv#%2OJvQHUx*4*0Q$#yb&H-y?=GJqVE6!dktK@=XZEhd7YpZO z)n6$nE>$8hpa+Ir7m3W{e`5R5Y)bEM{A<>O2w46N`UYKEvo2TCHnlDqWSok}re*Jw za-xiscQQnD>jr~h0&+rU+JKke`vbqed1K{_nc7Q3Z3(pnB(L>3k%SljQTga-Mq5K{ zD|9k*XPPcjkRw55eg+cu{)BUfbacPDGL@H?~`AVW(p0=bvZtY99`>hbv-Wq658S%%b z&tZ>3OIAOnQ2R%tqjxG!Cu>nwmVyNztU}M&NDSy5N*gx<$$M8}G&J%HidqMNhA$W#S0o5onn;-_7^w82at0mC8y1~`Qhx~5X&Nlv=9tSr3 zg1yP5q`VxT%ZHqOUOpY)P;QL$}>s)BDUO^l@I;iq&(d3ri>OG~L; z_96wBy8bLTK2z1MRJoU-yg~y7DO75;)Wla?L$$jKNFMRTTB(8{Y zf|5EB`RTcMZd4l9ri)&MEmTmW`~8Z1e7%;;`)S|w33CGRpJ&L7&Qk_$p$@k!Pr|)V zYmj!f3=6)=!)H(V)6IHpekT)eAF;$kFW>@FSWy3)9B!51^?#R2nr&+;orfCnJk7#h zn!>^on@oq?oE)hAhU2;Cr(^VB8k}VBz;{c3#?qg*;q=Kg95{4NQdk&uc{q1!E&l!a zUKHtE(BbMwu;~4}XzMR07%R%Kf5ZE@_t|AM;68)}OLybS7lzZC>q^Kp`*~9){dGCWl~ztE%+M+rDUpk^%Ge5 z?wyDve+fq26H;0a{d+m`vX4q>b$@awEjO`f!{h|}P zg(yflAS{__>ojt>s`OAHm`vfVL8@E7=Hi`&I+W9lyr^5RJQa)%-sZY>_u4amYNu6d5JYz=V_FG2bo^gGlEINT5Ti4;mcYeg##AJN;{hzqG z|HLZRLLxh4F;tXKOS^*c!h%mR@#+pRm7XF?gQ-}4qzp%vuSW9h>kvQ+3%TJOe)@13 z4(~gO;tCgBKVceqLZ6B*!Jag@D8jKFEAjd3Z(&<@Do*_UF@8UE9VXHe9Vt}G>4*Hh z40IXsB;F<~j7T3fj{os79)0zDE{k%;WvA+=&6C1#+qc5QT;O-^w82k&)csX-u&P_ zT-PfArI`mY_nv#OIm?8D-~EcTS8hb4^>GuA4c%)~>s1j$(u7q8$=6T2kOut#Gn(D(`am^&E#M zADxD)x`k4C$79i=Px0NVol<$P`fU$}_8JNGWot{4TTNj>V_GR_h2ojMO`P*S?z`=3 z1i2fq=EDc^*&13NlKc;n&gUT}CKUeqlUTKJ6Bc}b7+QI-RJUG!Y81k$%@K9$pAG+m zXkSI$`f%}XeEq^8a{}~#=CK{vpP3^mE2cib5ciM0lKl0MZ3!*A&{veAq!4iT_NqSC zpduG_ON4ssi8yJh(XfhRO$fsA->|#i!p^az?xLCZSf4CBp`c|>- zqk*rBS71G+^i4>#$(NR5-%p?6bDC|>)XAw{>rt*Pr(OUBLoQAw{DimPq{#?N`|9N8 zhihi^x+H}~M*Kh6ux%Umq?JPD6Nzz?AH$Q=hQXJtZAuHyW77{G(tv(Fvd*XCCAyS0x!1;oEsNHd*b|3*KX)x*JAB!%L{!l4(IG3DAOT0?p6YfV#$^2>$ z#v8?_1l#`iI+mQ$lfrkgMSt z)D;iE_71{(UxDZ_A6n9(qBgl4IjKAG(zu7QOIwKT%l9B>=9LJir4#0s$O&Vg{scoG zrsYhOPD%3bc;v!Jv!Y+%JRq2IY?N`Qq|OxVwo(e6mI?q zq(07tf{K>*#`GPI7_7h_>&1R zf%ZwjHVSH=`EM}S6}k$nS(Gj*D`>SeKK(KlLpsqKT?;uMPiOd%143I9YmPGGR4EdV z(2Qf5483}};rVC7&^5xcdxDpWw&{w*3x6ELf7ctaYb|;ICWVC$#ec3ntS`#TXo&@-l}G+GTwQxfY4su>4QAe6Qd+Y&|A>z^8>F-vzHNT7XggBH>Kh zIlbpV+&ZZbHvjJ^4BBjv%L;O~QuDs1|5lR))1cd)#E3r8*#5Uo`se3PLu_OMHF@S| zq?)qAl8j%^+wt1mSs3206I{jmZ7T0cHywv8=eW7N_wA+f9*J;VzP3a|ZArE2)pR76 ze8PEzwTX%l-$!uChNKEf>`J?j*?~DVe(Dlgf6|!b*p>0 zt!`a^%M*BD{nt|6+Ou{CwIzcp>sDErunsbpi%RWDK7s<^NxN2=gC9(RVkuUQ#_N)E z)wl>`a`Hdm?hBfXr*~yB%Fmho$q0FT0)r7k%I4M&&ggRe1dQ>GqooMMN>7$~d-h`g zkrc?CJP^@kAZ{8OZL_eTB@vNbFGcQb9ND~s+K402mUlzv&@p)Cv2lpW>$eTOxTvkUr8oQ{H18c-3E()c8n{j!6WHI>83w+lv$?1Nxmw@M2OkMLgT zO#J~V$jGX~I~o)22q%9;w3Ni6*bwLZ{KnwEN588LJIgu1L z{+ZacK8yT|X%X6M48~2IfeC}Vz-(DU?{tsr@X&K}@cV-cpe;U0OX~7SVPUm?C@0Gb zp*mBcqEtl=UxSIEb7h(O`_psZm}i^ntY=^+4Ctvx;t3-%PG-@Bka)Ay9Xun)V$pj} z_;YsxoPv7cy6Xla!r!(Kx$e98C=*};?SX)`chMg5dQo{a(g-E}tl1{N^Nn8UN6HFX zYFZgYn_at0Mr(CbX)Sn)xR2HiQ>V^YT1GP_HrswZIj;-pKy!;UyOSPYhJ5nc=|eV@ z18(Vnu4Ha6;x!qHuy1iPwvws+fKh>%cCQb7$*+afN2K?Kf&}rZ>Xfols*CyJi-(`P zA6IvefH%$DN_kRvBC>mb3>$q8fdw7WwVN177|HZA6DJPrlV$+qWJ;*k(~^ONbCroI zX+1z-cxU>Ziqf^6%^6(4O-Dr-MrTegKxMVx7~U@ z!e|E2ynIt`S=l#AIGQbO6Vq7H!RrMI?r7!AMs_0D6y4*VI7VUFqt6RrN zu_4v1>ZvyF7j50v|$g05FucJDtCjSDe zD$hsb(rL!4sPJ#UeKfA>6-DeV7BLF)p4Vbj=l8JT zEOpe5B+;^l!PWg57`oz-IkzA>h@4Ye3RvwPNS#a>{>c!27|gyHK=b4oY2*YkEtNzF zB5MS)Vk$XTy)7Z7UO;FJl|v@20jB4TR-E`W!!~~&ff2as*4el&u7jkk5V?~p)d-6f z+tVGTKN}Nq?&1TP4!gu+!i0fP6Aa`PYQ_jE@0)Q| z&nS9UNLe8Qt-}ut3bvKEPHam|Z3+3ok=iX0*X3?Y%#vNU|6{Iu6f?#TL?9_E%;O3f z0%*y`uxnD_>yw2JLB2F_sgUYchE%s)>#SQamfPx9%JFJ-%iQjopNjQgy@oSKsEr*v z9C6)a5Kc-sm6(pT%Rmj4T?7@n4jPHjKINjxraGAgKbanBN6td$7lL88%)s<<(c+bB z3DmZ@`&oZ9`O}~+LM|=w%q7KZSX>PHjkyXD7G;GL8EK)t0zxr-{0*eA_<@#Jq>#hQ zQgkIJx?&N^ImI!u%qhbap*h&TCI@M0NRUeZ+0STK9Vkapqx?VF1lkXfw3SLX&M$fdu$yKtNv8E#<^98jt zEuAQ!KFL{9Di>Bzdq4wZ8X+G|JS|-&EG{hiPwQJDHz3 zBlV3gbf1hIzxL>VJwi!YCzN(RUT)R=lGTcy4=@2H&~6B{=Fq}1Ak%29G~X<0kE=Z4 zOT;eJ;F_MDZ5qh@J>}z%HS;j-{{d72pi3v8s`# z-2Esq@lNvgaYAUQ3;Zg#q%^10Z2ucW(Kjv{p0tOW-)6OA12jiZwo+#DRe&l4X zr=~Z(kxT-$MFk|f8w&FG;FZVcNXuiavWQu%;;cOBr(Q!A24sOEg3Kupqi-IK?|1z~ zYxj@gA6f@Fh-Oq3q}bRWe+oxwcK~nF@DCh09Kn)9I2yDWE6iyLpV;Mx>r6|NeI7cR zq0*O7T4GkEW&s%k5grnSs0eS1vO*Gv%B%KzKO&Q8`t=N7%e86x7i=PZ!a%pme9Lh^wVWKTNa>LR5fMT`Nz? zeTb(4iulSS^o0~)=4K>5+){E+-7$Q0?=`cD#ihIpYwOS@>%F;7wyd$?X~BP5jMA+V}D#7CC9ObS+g^%=h3e2jc?l+f-XA|LdR{tDB{BvkyU{9pR&c>ftXVBl5J zmC6cp1{An*>_dneO3PPM?wy;ghj(S@=x;VyC?A-DH_Zzso)@deiVax za#EHFN#1cuZ4;fjj zc%xI-ZnUcCymZ!F0<|uzJRq#?f~^llaV>?DJ&wvy5jvz3T*rN96H06+jqL_t*fG8z=i zX@KGG4}W!a;r#sk5Y@dLMft&1wl`FeZ;A=%{C}J^E7lA(NGS4@kY{ma+@LRK- zg*2iZ^fVx#^&vDCrgEu6300%&OPP>6(L-C~gc!6-rtY-uot%^tHBQrg)WKFzUGO1? zytI_2tbqJ3NU>G_ul9XpKSA5Xiq3V-ARVH$%TSUp7EiK5s$o+bKxXdIJ%mF`f2nM0 z-hZ*bYl%V!IC#6GW5bJrYfmqd3xBO{cny6bv`LsT* zN<~&U`(wi0&yjNCI{dTe53Kur0}_)HkU|fe*yi`>{_Pla(!L%Ds3oVaMp}kP16toII|N-)9s~nxoulv8*H7&@ zjmS$ofc1<24gTp3CWNaf@04!;`DArOF|&?vI-q3x<&tE$!I z2F(m*8v|@vGxC(WYc>iB!MI(gcJxJ zLJv(T8Wa@~5D~?$V0nt7@`(+eJ_Q^4P!K5=6of|?1f)ag5CSA5f%M+X*6+;Sy<1W@ zAr)rwyV+Z&ocYh(xie=@*|qCly#3B&=oi|d4yFaopcQ45?k1%p#}I^DiKeN1rj_o z3hZOfzO&ZEap9-9kvXRn9p_w{ z5oZ+!G}4YvMVUQ3ZOEI3_O`WCS48&hj99XEot}IFNh#NmMMp|}Caj5B;nlSlx>V6w z6w0EnyMy;gDr;JuW%)QYR(IBbZtW>6mj#kAH!+ZcjJ*RLc2LU5=v;Ot$kr9vvZyCH z?P7K9=S&hGX7KG|hv0BxSllSW;or&aW3r5FeiY#BHFtCmB%@+P3+r3#u&_3#zU5eF z-zqONZ!>+Xva|?=B^BhyoupZ)PY7zl^+3~9j}?_N@_3S~L5_^DG7)T3LQTlCV*ztzGDA3rOc{vNEt^MODt^tNQXTVvuBNiBlCS& zB{1fyLQWoent3d#Ua~PE%+ra&fZ58PmK7DEvRo@YVH~~ItD2ZfwMMS;pq$2+;yfl9 zK#Lq2hs5@7t+7Ny9_jpN7C;6lSfXhev2m-6JOQc9ZQ($M4BM*>K;~_Vy$1~QQc|(V zBU?FD18V~r8U7Rk4O#+JX14I~qEv}Mer6(0C!Qy@M_UXT-2*p{oJ4N?31U!%3Emu3 z3LjFLb)z}ct&Dh8sWRc3jC1Cu-@rk#&ykvI5BGo(nDW3V^d;377duiYqw%>@=`mdR zgDTNRLRncQ*(W1+p+t{}mV!Jfv9Yo#g3#w#<`la0=!Hp>CZR`Qb%{v!EmKkF@F4YE zqd-!EL4Ah8(ZdTJe0>qxJCRgTuj0tggXDJfI`Z;&;?nie2EAF z(bf=CfoA=W2aW(ofFsaQ1dOKH4NctEdJ480Qi($N}J2xoT|B4?o&JZS;S zJ}}Bsa!>kYF3ypwQ{xR=GFIqpP7)auD-*CGn-JZ5xgy;Ac7U69W~|8|rB5f|Y%$D9 zr#&!y5C-^bL8=dAGgpj$coa5Y%|ObPSn>tGf_9@s4YuNYg&lYp^_J&fOJSxa%Jey zu|AFk%S{4La`QTxj;PTJveNF1zua!%mH(C^H(!R#EYde+X47e6!1|Vhvu1*DYkiCL zVoi{+?QQ*~c)Q7dW#sAN*9|*y`D!YpHUa2AbPz@ii=c^46Bo*M+(~!daED(l&qsFZ zH5@yU1aoV$3*zGsCsUfFmfG7mI8eVNCY<>VB&Q`HF{KcmfubT8jW=YBFzxDboXMfS z;7EGvy*gJDy{;we8g4VG9XApicY&I;tgOg3N?5O0T472QVp@D($V3<>P8xJdVetz{18BPMv(Hy=g3=MON#KC9GK$IJb2P ze*LEeN@7^_A2SvA4)01fJ8GZkvsBF+=-WnpOOOa7Pt=X=Tb2%PWUzA>9YauBkb~=& zPvDycAL8#T$;cvglFY1pqeTkqCkpBxte>!6BD{*UV9HK9iEq~aZ9Y)JVzvR3PWBvPkDoyHJ)!W>tzlKfRLsDkoku7M<)H|L ze@D&3fDnJ+Xa*U;-h|lHB*@AKV*2bSa4_Q%HpiXBE;8&y(vSKR*;de#BNpGh zzhwEjyrc+8*V!(<9eR(MiFtGHf~J{BjkPid2~MUhm054n^(do@x~pK+BkHKXI8J6H z=O8bqNO(6`bS0^0T)lW4fBtp4?t)BwYDGq8-RV0;PGHGHnghB-^umat{tX;~$(3%W z5R9T@#Hdo5m&Bu|s~R8wd=LetSCEmDr=6G7MifaXm*yckoyKW;Pu)D-H1B(D(aBF& z?BN4APuQrfJ7GX%Z}@d2wG&-~)SA%ffh99qLcxlRz4O2k;0W9v2;2@U=w@vRGn!Ba z^`^mZ1MPaBF2)ZV^YG*Za_>a06L)-@iO;?+LJ@heFb*V67i3Mw>8wz3yaE@G72>O} zGcac+P3-)v$*MER0GyKiW-G`Yrwq=R zt&UL4nvRIBKCq?lgPe8^DrKd}&d7z6cOco_Fy3MA+;=Sc1nod#QW}oz*(+2^RJn*tq6bPnuF z8D*d}^#C?(8G@h$zVzbb< zqpv}XDyL+l{bZxl@;sUVBlo z9QpaA#z1Z-t;m*%4HN*j$SE%OQ&!QpETBhDQ0TM`gmR;{m8?}Cd5nP zaq09PZ28j%4iAufPx5X9D)0R9Jy^f?4P`LkJ5}dZ2sdHtlD@8=~+x$ni!o;8O+FiFSU0zwYMjYGYWEB7)RbL~Or<|V6K+qZu*6qAhBiS?c^z0?p2m84%S638_BVkr7MAu*j9muxs6F z4D^|a&;YW`Kn55~@<|fMjy58kt&PTVmP2mhQT@0DJ0#?htt4k3+~1$nV_G>dOiC&V z1gfotIWfqKND_+%nTj%!WTRbriGtjM69d=Ik?dly@08RHf!(?wFvydPT*u+;zRmdQ zr$9VBxhFh`gIkhw9S629$FIBNsEzE%Am~G6a8tAJ^AGMx^hTd4MlEbaoXyd>nF>G+!$wF(9XLpn%+=O08tX7(0*s|J#O5 ze=&w>OwHd^DpDpiG$ zPR+POFSG5KD}ZU)TSFlEP6K8`+)Oo2-$M8(n4_ zsi?SsBtabz=%_Zs!?ET8Q zc@AgON#&1hA@v^;Rwu)vEc+Bz{P$}dzLY93Ebf2uRm{FW5?&45VQ?bvS!>p;A$uFO z**mF^a{innz!7L`2yli)JuPBJANsJNj$}zWhd$SGtX`P`A5woXC(iP>|H;Ee+N?2_ zuFz!YNbW4Vh1rq2sY+yI%JAo!Ofp6V434m)j~B>axsr@RRU$P-hT#v}(IUfeilrrr zCLdK+Kk&Ja+Bjb=7DHvPFKy>!$hR*KB9C33?YoV-eowwu4jRK)8 zJ3B`d4ebFkAgw8@yZ|>6QqVVg4*GU+tWnj#)Vdi0;OgT?hByzRTzU~3*8c$)X%RY+ zbVGS*0n#!hnD|IEyq(FoqCp~ini%_fkqD)=3b_Tvh&_7+Wl>~+vxeqn$<$=$V&}RH z$O7zXH_ICKB#mQ7{iCd)0!Md~6?QVNX-%GBn1MV|<(T!Yu3_Zg+_wVp)PGptvanEL zOn;fSZ;_!`Y6I4{hD@n;=$CuA;xp?3PEIOAWl&KPD_c!{@s64n@P}<76Pc1T&@Vq>|RLEp-{x*r?=E<`|306g66$Q7_HjUBW= zNx6WFwCF3R<&Pr`W{$Qt+J%sWq|ZoP-@60U=g5`!&HeaubAJ@~cEP28e#cLnG>c)8 zUVUAZNRHW$%eivMRK&zJFD82mq^3bi`gu3v$RMB;(jsC$D5Y@m=zxJk2Y`l4eIP>t z<~C%ok))C?rd8tP&h_{sqyvWa@g`TDB*8}Yb_$Kgg#NzOTrxA&BHg$}ZgJB{&F^Y5 zi8?h0!-6Wz>a4G~z6w>(RP^0QJoXnZFs&zISXi19qb^;;e9pXf8ix*+5W|TU(qwS9 z?@)3LNnTs%%YsfJJrLfrD~`t>K~{Pkep~r7oSX{~K*qjvlh2aG;|5$Sp_pWl`mTo~ z;i&Dh_GAdtF!Y<)1I}cGJ9N+>*!_HgcH0Rjis0en3Kv(=?YLozMxGNGn(=`#BySux)y9IZ5cXxMpNpN?U0Kr{@yGtNA0fKWU+56mkF27hT7`~qF zsp)#F`h6_YYXw-+#zIxklL=N^6X9dmHHa?{4Yl5;B};Ib>EKy1QddfgBbf?1^eCC> zVJkl{(OKMaI8;(hot^U8{1v5TgG)9*0oOvQPcjR0CcWHTATNk|loI?Zm87>Wb^3G2 z)Z>GV{()ra$5iqV18iQ@kP$wxcqmi9(b*2|WZ*w(kMQVmU&Ijb$wgb&V^I%9iv*}3OI^4eE zUjfAvVIXY~vQJ%+XVh+G&-Hbqwpz$kIkeVg zcaQ9&4al~bDd#izDXo~z{Lc!oH6Bt7&l>dhYGhkRTtU!D$=c zcE+jyJr=7d<;Q%^f-C0`Z!Gdk zTOs3}8*gs0A5>ONz=y(87^94@&@j|cSzkDs{E>vNfa+2E5r;U9#&!Dxjqq3!?g77v zT@k}9eX)!T4o&Ap$cJCNp8i-T_F>8wG&`01MU|ukE6xjw?M&^Bv%JA=Y52%^y>FE2! zXF~M{XTK?^Sy?eDM2x2(^~y~#$b>Iq^1vhy&H{!S3WkdWwffdc(3|t}gAT6}iD_8C zgc$XMi0)C4??t&s?jFh-H@(zu5pjhNmJv&m-IO?4lGjvmVR9&#B^zZ+jdx+L^5(b} zI;6eL3EKkhNCjijM$z7Wulny8y$fEn@_x#Hy~E)3LLLl>^1QhUcSai_BO}9;GWi{~ z_e_P^b6d{Ini@bdxH1&0`#tP+ z05uI+Xni_$M5nw=^~K~$@I|e$eaA_D5rs1E2Y+FxBUW5Kc3esB>6r9txM$uD1;4rf zWV0)mIJesFYw01aKOFUELGckXp=HLDVffR`?fgOCWjLs=GkJ#zB5(7`z6K>4Y0;A@ z%x>oPUz(YuITZV;Tn0umVa#{mTA_baRie?nAZr!<(Px``5ht1d5R9K>c?k=ZXlZE+ z75WyJK?^4X5uIf+z$G;%`l4uO`=(|6*z)|1=Q%DAXSnozZ2XDfr^*6ypc}#Qf-&|~ z_FGQg$H0^}nEO1Ee67PrM!s`f%QNwS-nfs?Q?cBRHTE{w;#h2&agR;YRoWL1m@k~? zMQyGm?%g*hM50r9ydmKbYeuB^-{EOBO3QeVSoj!#ZS8;5wLTn7$~zDkoDCoMY9V@S z<&GzEG`Uho9EB68RGYD8*G-f^lL+)Xlv4Rh>m)0TwAyV3Bo1CT2y}7Z=!kk9=m#pf z*Fokk;aTUj+OQj%+eMRnr@@-S1fAZbJBvHS!=sGoT;4R_4vayc+$EFOeToA=f~o0F zB{ho1zewgb-Xk&Ce`R)?(0%N7RebI7=9-}}!&!{?;FCU{liwmJG%zddiU=?8UFpw4 z=7C>MkfEkjzDO2>BBSq9Hw@~>p|kBfgm`RHWvba}K=@g2DKd^53~CG2*2kh531cpU zKuyiOqRFGv;jyWUHc=)1t^Lv70)t@$*93`tWOo@6*?QKu8 zh;^yAl>F_7Cw9b7OQW+V4(Unm;HB!1NCQU!(4RK@uy(hW@i5K!%HBCys%BJTJ3>>t zbHn(5HqR0oi@)y4y}Kc7j8uQ2D;ocaTuGu0*JWV0baA`}(ZBVwVCF&Jsd;K@Q8M}?96nY;g$U-pS}+@RCvkqC zFpcUvk&e9Ho7Cd5^x^c-&e|O|6k2i76jx_)Ek2h!%Yit%~i}3VX zCdj$E5}j>0#Ao*|&KREV0VpG31CUPlGiepP=e@P+4QBqE5UdDzff$92hXWhgys~;F zZ##VB2YtJmmxAi9Q+v{R^H({0yQ`4ho=_P-Zg7zj6*K&?tqN1iLPT0^p?>I^$4v$+ zRLowwL8_hi=ySi6yh5B_9)?em@?Ya`!0?^U$58AxvZIBC9H9-6qMi4455ogONs}*7 zAt}v9?yk1pd{o|PJER`r=H*c3G~uK1rQha2dl*}E74KXB5X5EXkyR42K6^4`$8Dsv z;QDf{u}RGe!xM6VgueW#S>CSY$Y=X5k)glzM-IGcuJ`!*N&q}G zEyE0Ywh|Lk?|w>q2lomsApu19>|A#%>xMFP7ll8am{{o>-UlX# zP=Arwaa$Fi1=R5Sz#L?B(j(zL48xgX>T+{QC1q{&jrP((&c7(FHPV6d;yUZc#h|Nk zQc9X8$<@uhRbkmkAg$+ma3t+FiXlNdsoa#XO zv$Zriwj3-Ld6^}Bpn;1)?2ePTt*>Wg8r0x=9uY}&0>ziIIZ2yATqQJjq3@4hE1Oxl zg1U}>y3|fI!HZSoo6a)FjpXX}c;BD4(i^dU>U++eDz6e|$6v;hd5#(H@5Cq>ZoYf( z8HoCv0T<05Q)_kfE3xMda)2ojC!$7xJ9)x7s=iDCyoT2UZKQu))hZ9Zz_FF0(mSTV zasj&gHRP*Z@LR;A*|c6nd@7+?)s1@iCevz5(d zBIQK>;Dr9I>t2NoXBt6_@WnqI3)p2<5>wJCSr=Tq;-fW2PcrlZ%ntLyt9n$Lt?czm zshx325!Wp@ccc2ATZwbU&p*0LIIVGy5ubPw9ra=fG;7C9HK4?M@0hJVN@sR|cq0?y za(wdLzm?9R{q!e3I;gz*9Z$#726l`kcofZNJfG3}68j zR7Q(JWN$AN`1~|f<~NoC!+uTbNyN_Gs?0-CzJRbxkveqZI44*aX+*joJ>S~#%`Q=s z3G6q?f<}O%dA1aVcciBW^e?%!u%bMQZq*t5&L}O@mc*qUewAqk@1tmsCLHct9#~K_B#W}uAUG{zY zGw7Lay17f5hRt(E+H+cZ%f%f7#kY~v^vvDYypR1_#bpx4Mr{A9ykJekFR>_lm-p(k zB|pElnDRNSN4AKa#|~Eu4U4ZYwI(BHU}7H9rhPl-c+=;o6b_Y!)rMeLpCB%dF5pSG zRM>CkoIU}SC@qmNv1XwiLqybdYV_<&Ifr7OeX&qdMpINLhx|$xrIs4Si9_95+G8gx z#L4o4Ta;|frA^_@XcVgiH_)GgO;Oh32^UX6o-uSnH^mzPxZ;hF-2S9#jUPEK&QFg2 zglD|c6Cv9=!LZy;J0Tm^`1PKF2B)^#k)DZ(hSCv=l{%~voMlGZvHP8qY8*lD>um- znvn6&$a3OrTZGEq%CvyU_f975!V5pbh2dqTG?ifdQDePO(`K?{7Fv|GIFPmHV{Bkz zEhWz|!d};vH+Ay3_Puw1aLw_6n}rIjOCXGh%Rert>h%h@nFNW`yVR&^lym3=g$u?e zZo<(8C^>t=gHAUVb=UU0x-G{Jah4R>X^tfLrLOc?&L?fI>*&ES$bUVxg6U-E12uc8 zk_e6eo=S{rt5euUzP*W;n(vS3Yw!Z=P5$}y9nG-trGI5^X}C_ZEIcerGlApjJ4d1L za=V=s)CvEO3EZ?lv%Aq6*ru}{RS|d+IdCT$;ey@ z30qY(P1o8zQr#Y9mp5Z039mLHadZg2(OndZH;43m^Bdam@NmeBi=`!NfUadjh^xSkGp7TEVWQMj zu+YuYuM2hujAU4F8HyzE$&SKyu;vhup%UYoz5V$+Q!GU&K~S8RT>6u9N~ ziEkzAkYpjdqdpQ;_( zUFv(eKAu{5bXpQTO#g@o^#6hL@#e+ZEiLu?GfZY1{lGa=8(FWK0^d#>;Kj?7XTXeP z8kst`Sf*FoctJ!64dT+Mml9O_MX{i%$G(Bq%iA4>N~~I7*V?SHL6$sV?Gv~f^*CSr z&#Rm`)siw}g(`c8p|Ml%SzWK2Jp=M@MltBpU@={x#enf6a$#ZN*!1*p5{Vdi^h7io z)n9*7<%$Mp+jq@CGR9K)Wi#ErEdlU@GX4k})ncnx2ek}OB~lJ=q^=M+EJo5|H8OW= zmm>yWqg(&2@Lc!Qh3hDLXb_6qc}CO0DyGH=J?@Q;N8^)4PZsx%#<}GTV!$h2&Zw`e zbGgQ56P5j%^{*UICbrvVp~e5OY8s+TR)#wOCRI?1;zX=J+o+l^{2m{w)!7A46S=Z0 zy@@WLVD(KPJ)_Qb>DF#5#40l4wI2%H_&Unj%gBXYbo`UHYHUxHOo_J53_181F%HAx zmD)SR#@2-5YRC$v-#B-x`j_Hm+cF&N`yb6&D>Pwj^fnTedq`Qp0jwl}x>C=PBGWqNfFepUmQNZLncqVteg1R!a$rQkCHnc8b z8(}OgJxq=;K>dU}F}aG_Go>_MWo|;4@1m3T$&2(ssSw``j(_wyZRb`WTkMy|kFgAUpY)isyJx^}8)>JyOpUvP^_>)+gs{pZg7$U`xbBm}IB zeVNNbnIGBENN^CLFw67f=7r599MTLcqJ3R;r0|)=E z5^NzAB&WsBH`M*<|K4dQa2?cOB*Di|SKYrqhsMG~L-(C7l<%FNZ|=76gGd1fU;dWW z@&ezzMbe9DCd{XLii$}4y9`thS9H9cU}Bd=^D-wV~7QV)k8Ux{_fO~B)lO-^NsEOOT7sCs?Kk~UC=KaQyXRY-i`%>Umx7d}7+HjP2! zkl<1Y_|e|Kk2&gy^J2(;#6UBbZ36J~XgbY0Xahl9xd~wL-ptD}!5D}#>UDfNRxS~M z1gy42WUByp(nYf0XExv?DS-iVjsVB0&EtJb-e$iM0hm57D)|RYA_otduH0O^hL4RY zoA#kAb_>;miTaf?n+M90RQDZUp1?qGP$bz&;@@-15ga$soj9}d*U@7At7QJ@6xiEhDL^Pd3Cj| zE>-%%C@?=+LBbnE?(!AjiSy{tV~tmlqU1yBr1g3T8n^A$mpCa8Gt|NDLgD+MtB1SbWSpdP&(ju8^j;u#5_$dw*F97TMmpf#2_~t0)Dg zku$j!Q#Vb%1HVTM;N}54`B0RG=H|inoq$;9gOQ~uP+h;qVeDpzK0DBc;q4W}K!l70 zs03kO@xNxzKWB0Wdq3VBnGc%Xzp-%C5dE$j?2h}&>n^KQY5~ce2oN3cO#luGT|XF- zsYw)+sfm`(GM6g4t5vq4qT*}@O;qLpFv=F_zmb5Hxjj;}1e(wY@svp1mdD825fQEu z{P;epPJ=obEkB$~Q?sZ*4>VDUZ@v^>n8{FXj4qvesxr)<%9l}JnJ(cW>+N19j!8|} z#y)<^wVLH@iAY&TrME4ksR=t;Q_*O0TdcwPLD*U11%0Om1NZ&!Xm_F6K|nos{J8)x zZ{-^8<}w|JSS+Ryy83QVixOXea1z4JZ)))Ru;CbDCfN%93Mo}UK-`B7?b!RqN$7ou z!vAN}b@jWiXRaEs4iBQK@_q~}r^?#+`E~bo8yUD_qlLa@F*z zhU&8&Ln8f)MFlj-K$I;LzTq4vBV8wwqj-`Yhw6NAm9Vm`_QAoFWmQUcEyjN;4QkEB zIS#}J4i=v88Bql1gfScjXfQW3Q!OQJfR|9#M}U7e&^NlOjL6Q$IgjlQH&l&DW>z|_ zNE$a5i{U%QH&Zn_aQ?e<;6n}uJk9X|3bgR&&k1!8z{gMQt1;sIgh{Uz^!L>O*FBlZ z6k3_aDg#I9-VbanzLO$jV@K(CdF*8x2J{06f;iv{w|XE8Ap%&#z|S!fDpkvW5$g8_ z=o`lm!rJ0Nb$EOA&(6Ye9zS=o+{X=sm>gn_ z(>P*rck$v@&R3+RkI5>EWKY2-)CoFya(>DTLTzTXq;hBWOiQO2o17Jcsk|7IV{;?^H_uoXfj6N)#AYh?NYm;r0O9M97}#$Yz#+FX#LFizB^Z z&(|(ryPfI4QPtTj#Dmd9ENs^)4Y5BiWPrQn=~d&fUUD!g&eH2V?Q$@W(H? zxeVQM=l;)okwjq~@^7^C^zxQSp)73JXoJgbF|sv`mH~gCNhff2Xs6;Yo?eP$y)I~} z`4dty*~bUN9v3p4px-II_u|(aPWIKg6sw!WBLV|D!n%&t`})TTEt`CamDbY=s+3bw z&} zdyl2I()6`K@^!=!LEtKt2>g8R!$FM;H&ZRrDM~+}T4jSbZC|e6b$51ktYIc5=r}mJ zsdKZlA!=$vZr*Q^<~DZJEs>kKy_o_w?yv%OFcM)@%S>+v*65JqLa0012Nvs6P~HMOsjIib1;x9N0A=qQgP}IwWO9l(@L~ zB6j}HA#@8Bcoy$pxD3N;CGx379M3ULdF8;sw z@7$pcxGF%5+MSWyZ8P{1Rb)9e&w|83R**vDd?1e_TT9VUwsoIohw-fm^y~AOm9wEh zyiTcTfCf$7kY-8&XeAjsjgtu%3kWqe1}i>KR;(SE#Wzvogv+ESn8y(R?jf*zpW;w{aV zSdVUQpJ-*1a=kHNAXHMEfS!*bwXxVSCkxb>{8*{k!r>&%Mu93d4dS22+Ik&(37<{J zrKQP2WetK`(%@YTGd`=}z^0{Jm*}RE8z<*^;S4`KyAe}NBAJ3e~G~%1Tjp#uPR0p>1k=uDo6@2h^a;Rj>$c2%Rv3Heu784$V@m5Ool zkS3Lcj)gU^tKTL~3L>1eX0)4+x`PcBC6<%hiospMxfG?MF{MI$19PFbwr069&B1CS zQ8Xg4x7Cqc2ed~Eo{NLn9NkE~>m|ms^|p;KdW0UYuTVG|c+f~vwkdu}BrM04HQFC= zGvse_(lBu10hm=r&GFpd||!M%IVotZgX~pRI$`JU%f6 zYqPLF!)yw4AhFP&7uL~Bcu0b>HVYu{IzWMmAWBb36NH15QCh$dsGm*zGLqTR8EI?8 zycfa^j%;=Wrxd({47ToJ$I|d{6t@eg5Ec-{9=jdcSpqa;K~5OnLdqgG zOT6_7-=&a8#1}-+$K|564>G8j>FvfmhPM*=Ltn_EnoU(2BzJr*O}fwd=VPYk`Yp*t zUj?G9%~ebYqyO!$Hc_CNH?ZVMfn-_;*= z>$b-YUe;5y>qICUjy>d}Rj9Py@4=lDH%I6Ke6#13~q~C>Uq!j=UaY(o@QRD3^7t^kWJRFf^p{eycH#K;oF{T# zwtCzqD%;)VJ}k|t4Y~#SfrU9mR%_Hmk(VC7*i^b2T`d>-h;T9)gCN4pU3V0Ju9;;@ z41~0PSQe>4aOj+X6&~XV;2@(}zh7S)db2x3eRqKZ9~c<;`20LDy;dffj)Nd^anPGx ziuWwFwIvvJr1n?K1yWF8mC($5T`|`KcTi}gsl=9hg z%RSR6isJC+PtDXJX=&-ccs#Bl>W&wOY5YhQmMriH>hS&m9MiCWf8kFIs3jN0x$BJS zEtQI86fGhUOdJ zzFtYwaF!`~4fFgdT74W&=z@lK@R+ZbXfCd7fOUmC%;H=KOm?0O0@+1j|MDyZ3Jkk+?jZV>7-kyY|etrd&P3zk|;Z z3HlyeER~3f*eGt%8{*BM&F|ghM90mol@w(n5AOj06J_FP?F4aHnOAvKmG^>yt6KR& zs~PsU3gGfc;DaE54gsk^7TJA}Nddv$?qaN;|4Xs2m%Bxzj)H-*bb3*7=({>ITf6vb z6pz+|J^A}~7fSEz1Ju5%Om0aBG>#_H{+Gmek)Ac5?zdGV?hTFmJ=qApB@0}x3SQJ` z?A8?cv|kSFThSr{*{iJoNo#=E_6ps7J3)ud>qbo&@MzqiuYXLMj>Gg9=KH1B$p91x ze@bMaQ_xhtOv8QruK`%6{q6af%2{y0Cxt0QI=-QJN5eX4g`#Hs# zT}Igj-h`FPdLb7jJe*w%-^*)9=&En>=8}#_-^q#bj~sug0MIR=;%`DG1I%70<*W8s z6Z@)Owj-qY5{C32NBUvxtNms?ZS+2<`Bz{D@9)0iP@LSM>y650rayCplEf2$*e(?0 zj6&Et`duNvC6aN`>Lk0G@)(YnoxNqwd$ST|<6~V@7OWwAx0dYr+nSfrJ~aG^$p-Nn zly5z;xME;#f@YHc0j$USE5ZBQmc^kaZG-=y2tUU+B>X62W3xZ~^ibxv^yq0Rb5ynB zkvZSk72=1F;D$CE9JCUvzCeiFtLmNzu|rP5smdx(Oe$;j=`~c1PezfQ+tX?^v5KS=YRPz=jJtMeqw?$A^oR z)V8#!d3j1eLYi6(4G+UOXk<$fdqfqAMItW2`{J&TBOkOrJKkV)^PQ5MJ3^BfPbPVu zQVC!Bz$apf-)=CqgVegh_uO`tN%pRIWHzF7=mq_7b zb*!yRN2)1*Y|FZ_%QXT7H7PBvEM{sBIgoadG8>&5TJ#*TO7eR|9ZR;vEE}B;+S6>6 zO79VP?Y8pWO&Cwvx{Y*+$?6)&2(7JOaJdxihinxE5>6BOLy&q_!w#qRA+s-PB$0dj z|Hb}GX7I1MxsA>TlJo$JHX|cr!24?ZyE6b==Yxka&`%J45$T(m`J6Q5by}L);B#-I z*X@NOAtCX1L)i0rE5{FA3k#Fc1cFC^2<-%*R0n5gkqApZPr-*WYBhWc zd==r_k&&r2^oFD9*3E|v2CXcWjwY*`AZbTIjUS^H?1cnu9GgQ1);-IL8eh|+q;~r8 zGi1*F=(RHYW^mC>0`n;Bnj#?;q~se#R6^SjnyhieZA*uPYBKn1ts$0Dj<{^K0cN@A z`+24Uu~$U`P?I3tq*q!X)dZDu<*B2n6i(n&O45k1y%|ozNbw031@&aIbvv;#&&krh zH_>@b_@>VlhkX|}?=nXg>96GV7@y8DTb4$9X>L0qdu{Qr1gGTSyb4e zW^U(eY+4M)le=3l;JD>T&a6;h+hYzCb}K8AGnQkDt481ECTw;lDg-5rsh?i23r1ZN zeoB$j!16F&ZujPxTy);(Sa(^L?gs3(wk+IjgT;-X*XkS5$lagdq9SA*a(%N?K5(#3 z)`cQ z)GZ1JS)OoItF<2?0^~cF8lQ96ZNVCG2>Wxqh`SwEr>MWJlg*CL`-WGK#08WrG`a84 z42KA8)&s5(s+YtTpHhvP8MKUs$cT1NdRCN`oCv+2GOu551C0k=4i-^eslXosz{r4@ zshFrHi?grzDt-Bj{j0{o>X2GPLs)t)n1P=?f46jqm>JQkCvFhQR^ma)6(PRj_b@bF zrv0AUeB}O;He&}lNFjzo;&Fs@B`ALemzmnZtv`&FI`BrAi66d~C|hrYNc01iA3o5= zx$x&sS!;vsa*px=)adz+aZ~{I93;xG=YPBa=>7Ic=oibH8kz(Y4Q%}>H7c-Z*=pUD+Cj%yhIod^x?nP zbO3mkR5Ea|lw+LF(%v4S?0FcGMJ>hbG8_&oZnLK8th<|k@4Mx^hJ1X9x|otBxUVml zj!K`}x&Z0O-VL;>Y5TO-)9AAB4;zwO%wCUK22;$N-PkC~h+)iU-{Z;9j=M|dR62*s z)(=JAqon1QZC0Lg-Ne-lE8|v_72S3NwmdOD@jm$N&=MzS4l4hRPDGmX8U#BE>_s(E zQi!%0i?nS~?(>{r2uc!7{D-^*CvpLCDyW6_kLe3B=8qT0oA;1%r`f*C6Jdv+S&U?S zS#Z1_US7PWCu1|b<0vrSu$(E6Lm&0xn98E@U%-2w+>r8YHEIO2``P!tfOz7$_e8Cn_-2gapJB&snoDNbkXQH!lRXgP;Gj z)*v!rXy)QTJWjhlfcgdjh|;eBK`$ii=M7*@F8DXYh9mAHr(gALw^^#Ppo#y!^7ec` z2;?pCKqiRD!op(2`5U|W7SP~oXKRdF8Ewp##ybxBcY6C&Qzc2iI+YbGK&PoS2XaMUqr0IIN^J9GRsfh< za>UN*HmU5NmnT2!CPm|M)l~L71w(UjG`5FthexN-R5=`@(){Agj@)c8{tgve4^>#bPH*GJE?f*_*hOIk?5=DIK$%xIFgCxz#aRs%}ns zKVC8E0l{hK?VG`io4~fWC(M>^ zXo;wj*?i-zFMP6iD^_GWR=wx4Y2+O@a=>Y(m&M*0J1Kj%O77`%oh-M$lBzZxdXZrH zWn~|r9Gm!y>})1xak3a()y5`_VPQe>w8+g(yfPZ2VERjd`+ZtyPQRb_b?@CaAz8zu36Sn+Cx{EX zHmhwMi}AJY98xXM`8BlPxEl1&N=s!Q7~Hk}Co^oOz@&B`#QH$nJU){v@4+atW*i=$ zi?3vi5DHeQtuObZIMDYvbqwfKX&zOeE}hKhs7dMxe4p#zLU>S(PP!K$^&_Si4|TVwNt1)cK1+;BIqQ#>k>d;e8kPs+=ygG|t? z%Dj7j2)uoCpg_>df*`}v5B@+5l3ZSa0WGh_Pyl5#?&xZIZgfM4h{-?Dz;lb34@w+a z^}9uXBj#YTM$vL}dFi$VNGY0vR|*p1kWCm?nBPEWC)~`RnR*}i&^&#TP1BE}p9~|A zE}C8E3MR=`gYV%7EcY9#`=!@k{B9dAeeRNsw!R&QZKn0GNJq+O-SO9Kbwl}j*!6u` zxD%fuct=f8PcCwTCKLtC66R~Elw&tT>$+#i@`k#-9Zhu7E-{Oarv|&Z48=!v)WwQ! zwr|t5dc5)kQB=jA{L1^?2RB-sTd>jB^vW5AajfHAI>IDu3uY$&j`_b)x(_+{Xfm@} zhR9$Q;%u=p^%>YNMR*D^uNtdS_pisJ3fnoQwB{)v6A z6BcM+#agj~1s`Lz6_Vz{dL2Re#VUE40e|h!)vO7e30%nO?p6%Ny*Vt!bvj3%q3VVmRq7@*ve?1}ZIFFH$xlwk4j zP~{%oA!#U*i|z_@eTiMC<*=Qw-G zWvuf3(724*SG)eFA(Oj!=3rCfr08w_-hKt%cppbr(CXUlcm~&B^PI|Ezi!zmOC{>7 z)W1o{IUWyyf`K_7PvJa1J#o0o0KG7O-6;N}KXHS)lNmWWCbrtHz@lPKU7P}1D6nf0 z`gmoFKqS!dGTerHMUs2expkDH4AYlmOCw@rBT$K9?v6`5 zzvnqWyGm8|!LXVf8dmUyppb zxoLK@svjsi-aY+EG)#sTi6djMn1mK{Gy)%a`s`NQ7c?RR;8 zfT*l&%rD@==bK*<20F9j8d@Dk68kRggb_6DNr9Y zaxZ?COH5xQZ1EA>&Y%nachS9pXjavD7R1Ilsib}(Nv*P`AcF5QFdAd>72FqKG>ba6 z|1G8ePdf)4nUSR>)oKF(kfE@%w{NsvVZ8gb1P6tPZ)HB=bpynbzTDs-&`d<4WepB^ zTPDX*IV41}f_5l;yB1lBr?rmdG^0iGBPHdC7RXFvgge}YzO~oRHmi*Ddt@7f;$p7KU4hhaR6%a16ugh&8?(rSM=1gt^2lJj}2im`8TSduOK~8S)WUlxID4&X!s&JTA^4tDfa!PS6%|$FwieBy&Hcz6#-wgw* zeh&rbWFeFVN!td@Z4PA+NF_O@Nd_~-=FMdq-__K?;obHAWYiDCxpE|D`>HM`sL{(x z2q$D9J?b}v6>UR->2y+H?g2pA!f5d$Z|~U9KTQ4PtvT~%t8!@YrEg8Xj?syz3&fjz zaF!!J++j_0E^$vqE?)C z#mZ7tuM5bN`YDsu7^eD#ABwWL!`9^uW+`-e7~@o?fLUx5JPw{45sSF)80J?BMZ$>wLQ`fQ<)yIF zgVFui@6Nmy(%u2A0fy{wzgx*85n#Do*kC0vXJTJtPK>QOf`)z0AJ5QJ7MjkKIao?M zetE05+N%CgNpFiVbd8W0WI3SuUsDP6d+>nDGlY3_8c7ajca8zU9vmat|K)+hTbo}D zn8@7+QQOc^r!k^%;OPFax^=|vk3Ph4(b zU!nzr1ESFr1mBnqGu_c2YJBIT8h2Mb!x*>vRVzDP_lA7&dzR+#V!xS+8q>j0(Qw1q z>A&41B}AizGZ_%tr)ebnz;v~G7D@f7zhqsaZrcm*Df>-zAKE*MK4j&*EW{F5FT};J zMQajZdjYhS(lR`&=w6-!Pb)sJ8}wE0hv1=0vUd-J;-z}4MwA5STTT5|m!a*lkang>Se-3bLsg9?4x`kHLnkk{DiE5iXj&~H3 z?4?^e#uuTcTk}x&CPSG!PhUa%=CAse6y1avH0vVfc(&;M`COHLw{0^SR0RY3L#09^ zgM{q@G9^_t*--dyC8Wip=xWPh6;^2cl(R(RMrEFNwaQG zU^@vlH;p|ghzmRM{GNSygcbTKxM0*`U`A{=C=1rK<0Aig)T|ilx5vduQUtOA*>2Ms zP9tmca(|;5M3(cd_|@w&>S>ZP^Il`EHY!Ly%mp^eRwMD=9)pM03g8%+pcp5GhfxAG zs&N(vUt$1~RP+0Qm7}Seov7r#fPK!r&_!KQ87Q-lCp{ZG7jCSUmKV#ey`dE%UDd_)0;lz1Yc$3`P$o~8rLLf9^B=@7%z0?s(Mwz64k+Wj7) ziqZsd$sdCl!Jng>vGmHJTCk_>D~Y`g7$*{Tf&lQ$)VaM8M*-Zq4|h8SDh~tDIO(r( zw026podqtT46!K7mbI}lU}46!8qOo)_bu({iTarVqzaXXK7_A7pa5%%Af8GMfkac;mh8Z(ZVzY>ZC4SEbZ-ngZcD_RdPn6Y$-Tr zTLL!sgNXP&LQuLIn+x?Mo7008JR+eGQuT}g3M$JdGGDdqC5{pogt%&r@g(Or>VQqB zFts{9)8p8My}~bgT+GcHaX{Bx0+yj4iqPwIsPJ%wa&d#h7O$7LcPpv-w6+@sV!dum zs0{5RlFz3(F;HV$^X;z?oUD!%6R~IwCr{rVkA!J1zwlteZ3le1yl6<|Kn>#j0(eT` z<>lpp`nTVpmU~~9JI+73VkIlc8_3D=dAVIiAPZsXYd@J8N{O7Af7^u2+h#;Ci8)~{ zOugN=Bd01WPJ2i#_4o`8eH$uvAmk6JO_zk}iI|z%6kSLfp4-gU&F4u1R`kq^Espsa zZCN0^0EaH@cWdS(vwk^iQDP-AMpD+o&!m)Dr$+Kvj0|Y#1mRiaD_myjzR46~dh_w} zT8XvJQ{;4|JDz2C$tvzOjD|J~71m`ZXDh8=xULRvU7TPH9H?3!)0C%#_gD+OEaKy^ zH|_2fyQSOK&|`A6$exr?SAkgxbV+@o+{15FU1)87SdwV6M-LY-)ior7~4+D}_J4L~J<0Lxh|3Lw|LX zv@-Y|bWIBn2LbzMNYuR;bv~~pDA9@HM)W`;B}Gp>J^ynkU}IF$QnV6 z%d8#L!6N9ckKc~;`n_0O=A7)=v?2kOxKfNq{h@KG;qD&6t>54^5$gv>S~srG>h$1! ze!#q=Bu~Tnrf)7y$eK|Q3~Cy_V?8`1XFaODm}#vhrN0AFm@rQH=59gp0QeSDO?G58IZ4L62Hsq}msJNHh zJ=7e79D#iX6MVk>R2C>$r=tglB_y8kQ0>uMhPURsVx5I?=XAt3hX5SWz_;Jsn**1J z72C(%s+$&$CDBLV=Gqju7iJ|)|CE*#wl|d+Z-_g>lM7fI81D}CEhLiOac3lhy6SS+J zT;DlXULo3}h_RsFUms^soM@F#cg43lQF}vMVg|?WyA7U-*)lfQ1<#cKlMMWIo5*IN z0{54+NEM$II90`iOhYbHLwS1Mu=~L+PYGk`EM0qT&foqz8dar|)aSX=?o4Z^u0207oXNE3dT`n~0&NBXshuT>pxI;W02@R^Sff{OTXb|b zmM&-tV%6mCE;mn(O9>igKtq=y6$RI^ZPP|1m6XBC!w(Z48i(#(J>h6o7p?S6Vg@s7 z8afOLRM)^OQL{xnF>BNnw1}@k^NigS?o&O~On<*RPopVdYUVu5ASH20tKJqRwG67tOP8lL#=$Cpn2Ev)4mHBDGG_`oQJU9*_tERw^h;=w(q>pk)75C~nA! z5oizldjlc$q8*jGUOiK4Yw%8*kt&*mN?r&pf zA?(bR7RgXb3vZQ50xKI5-*zN1?<#8mCFL)dtDsbwqhqKu-diI@a)t_S9UTzrTgz-+ zEQ^=`cDBR>AnA-E+M%zYO;}pa!^YkUb~X~2YgvnoNg^!P z6$Pb)4ngS&ga8So*WHxez5j3SW@|POLPF6Q*z7G+&U`cX?wm8HctyEWn}7@vc`Sv| zuUsSRU-eQr`84V(?VJ&6JU!?OfNfRbVd)lkwG9-IxbTL?i?20>V?}TbS^`9@Txe^e z3Gjr5bD^fB=$W4km%=$ClE#z3QM1IHTwGX(H|rLcM0=cH0&I`y7zEz!i;9FIxO)22 zM}WocmAR<(Ta;s;T*&ZggjXawMAXxSNU7kA=9C7?j`Pnwfba8cq zi>Eyp8mhsXm}C2*wLW8?1a^cF8U_M)aeP386UQ(&1XVrp-vQq4#&b+pMg(vmBVd1E=SpkqbUB2W*S<+XH-}CcMG9!7>9O+K(B&W zmkSI{<1OZ%iyP;TCqH}-IVQzuoeS=5Xv59<>lR4W4vm;l953MuKs|OWS^IN6pG%R9 z6>*3PrRD;mE6sHgQ&U8(Lw?2Y|4K`85>hMvNAt~*WiynXn-wRvxO(Xe^p!i5+xU;rjen1J7Y`%N_w z5j)C4>*BnLlI2SFy^_zVcz1a7U1=z%@Jv_A-$n6EyTQjl5PpuB7%Tak$mfi6$lI6i zD7!`!RIm;`fB!V@o^a>$=gK)|6z?`VJQsd5g^IpXS&D)p@nb<8LIU9KVnj(n0e@F3 z;O-rWkU;*JI3{8setIg(O4V@?C`cD5#vjzehLMO<@^yA79$7`E{nt!RU?S4t>|MmzdIKDFy?m)vPz4wlSH)RH_~!dkbwPW}J)tN#RDvD5i*W1=7cu27h=}#a z4L1g%6CLxAZ?F{1K3dAH z!r&o($ojJc+Yge)+)9*JXwal>5PJ9WNBcNWF1B2t&DE)Gq-0QNK_+eYW;VuDMnTN_ zZ_lCfpu4)T7uPgwze$%64D4K;Stu_&fTat5LUExEVU6#?$ea5jI>PMLS(LU%(Yh_G_fu)98uujzL9-_OF0ulnT#w{94nw|eLgDB35HTIfUa;04}uQ` z0v@fy;1*1s_p0?^WlF}^)V|aCnfP08} zu&an9V_2D$i;Bb9(4{iwb)JYQKp|J8Kg(GLm;>sCfPrzU+P=Od589nsFysq~kr3ig zfJM9wo<*qe|++nj-Ewj4*RI5a49YtK0RaL z(TE|%JgjViV&vjKn_jt3=A$fy*kqN$h0YEs<$_0;b)oM<|05o4o5QD_llHBYoaab^ zp@2K(Nyj;d$$YdVJ#B0>M%ah)tTWJV$!8zPc-+>8jE|_!h~mvhVazkI{l*k3QJ8it z6XhohU|^&i7aw1E2tOsAqv@~2SH7()Ug=cIS z+Y5q63;JuIP*P147L}hP?OXF`o)G}wULh*}x+CW}<^q3rc$0RYp3xLS@mZ)Vufd03 zLxxI~o*47lUM2+vnL?JrL}sWW7r8WpQpg~>Hi<>(-K|Ko%{*f+V+A!8&J?1I^P}=y zAu9RYyHKDaI3^b|%c6e)16x*MuU4%7aUt{M66gY@NJz6x9q?!xSS57w=3Jpb#zDYk zEnT}Kcb^~xu{VXD`|dCC3;F*UD2^yL2&jr=Ny@%uw7mXc!W@HF02Y zYuAo7mjQ=1evbn=<%o#C9xVguvnTZk4jfKFc3wFjOMf(O9*-V9yP#Q4g?CLH5kAu?*)4~?Xd3B*lI%33r5wc3TvxP+i$%fV?MOO!nnKxFTsL$WT6vwr&ucdzEhCR}r8Djt zeH(wL?cdEBO$9Pf9>kgBhj8pfx;oyT{$Yq}+!Eb;bVaLX;qc;n*ODk@E#q&&;p9JX zC^ZEqPVs$CNqe31N@!#Z`u_dV}01>Asjz`5^1M$ zxK{8$U~n|L_3n)>9jPhAjSl-H>W{^PbDzh`k&|(%=mciZUWxt-{=txq*7;#yMxIFo zBm!3*0t{oCEPSg`FQRUKF1<1K8!Zwkm-Vk(1y>y;R}(?@tE8)mGD(X>;F1u~BkA+A znEqukLIwt*V}u?jc9mjNGIz#_2mr?8U2FkERWQKl1YM=tt|4#T3^&CrIL1}0yU1|0c4AMd=DflcSgD=?cu zXhLwY6dmh@Ux$Su*oi!#AWgKD!#dsY##;ecGc_N(__Fh)j6p}Q{)&==;SV>$8?VHm zQ*#ewpD4t~b5G;HKXRvu1dE+AkxGjU`1th<{MW{@r5ks55?}RjDzM1dy$(s-nJ!nn zaPO0Mpnb~-s&uPWIsK#Y@q|~aD7Zxlr<+{JioD!mcaJ7B#J1gntMB$&Dym|fEaw+E zFw!f2{;FS4^4~&JP6|teFhDSPks?&Ji z<#P!F3+}+4$wk4OEl|9I;d6(VgTR7)NK0iO4zo4>s1Ix-WG_q=DEwd>Do=5@v`-v) zWeQ>Tl(KK)o(UV#$%H@Mb;D%1YQW-IA%d%s5p8M<=Y61nPEpBnKaWhHZ?=R zO2w%RM&Q{E?JpE=iF-m&A?7SQDCUbUG7P?bTUKFjDklDFJObf$eGn@5>)|e*Nj`f* zBg!?>kKYs&StYF2LKk@y*t7?33J<^!Iq7(V_WL2oL4o4N9qgf~JiHz&H>BWfsTUr8 zdL#x8>4CuWyc%lPv=-}TKZj3_R3c{Zqv+$l9Y1Y3hD>^E*NEqom>88P47=-5+&^Iy z`gaJfCLpQgyY9FDEyh=`e}HtdtDY3vlv8s>q}!gHhnY{0K(ILu9LFj@V?S0dcpD$B z-ifn?#FCmu8V%p))EF?3fo;;VHzrS;ip1;N(jlb!%b^MbR?VDyah26x^vv#$OMewhV3* z5DmEJW&FSAMZi$hXU5}tkbS-HgR`0IH1o|{#HMPB+gY+3yQ7X4m^p1pct-^w*O%`&bO z7XFZ&hdZN_@YW~WEVUGecErE$q5#x#g@X93Ke24V2Uz~&0hDmwi}5xJ9M$oD49`4w zFWNQntr~A?pOqD0->+X|+LJSIfDep^&~%{+kd9`L;tA||=QOPq^*H_OQXD!v8jUE>l1dgf11b+C0#`W#Zr=X5XvGTKKJoXK%3Lxv00L5A(Ey;z zDz6R%e1t|c*NVyC7ho@WPcN!^x9sSSs6a9^DHYh2uSfP-9UdE%jvYr^Axdn9aJA%O z(eVnW#?&vU-Q8vgl z`%$oQd{;RbEQHC;N!TNCouw@L!3MnYd>*{%bT2B_9Yb$$!Ec+(aU$J_A3i>de$?n_ z5-CD7(LY8rA9U@erwX)!!-p!6l_S(-UAXJyfll#NOcqyX#&j2SY3gi;rwmCyt);p$ z!*IIYgyFs85FTW|5H?X~{j2fvgKy71@Lko%dJ%qznKj1d-1(skg+M~J-Nd|BuWH(W!=U~$Y`TLe%LK%4nLxr?5mWwZkW3g>KuPxmPJwDPGMGf|;hDYUCk zk};#IIweGIQJ&m6r&@1@4#uoPXunH_Pq)VKZehYjn1K}j`dgK-f?6YCR4pGF$2M|N z{wsIqsNEs>Z5JOeYLUdMJK zDEr3+EqxcbHFt+TwTJ?jO6b(5wCIDtStN&_k1*0ej-+y1 zih5T=^A^`A&MT_Z8*{m1Oa`&!yJM=BOz`;Ds=!6fScgAG-8r0l;a5}~*OEqexQ2Vd ztsTRKR+2GHFJ^nxP)N^4!Gg^cKn+Gf58mVaFw?FGHFye(RQ!W{;M&p+m48wjhs>#N zMG~}wTfw)3VDjrg9N8IH|0sk`>x|r)hd4RJgl6kPt7bwB3jecLeHJ_icR}EF;TEBr zxg~vJI!eB!;Do{o7cZL8gm8YepeaGL2aE+&T|ZIG`Nz3u9tLwk_1hECQ}4CA;@z?% znk?>UckNP6T-@UD(kHJXbMic#Eah{hL?YUy6CR$_6Pw?D2RS*Vh#UGW5^w5(4zXsT z+oiO~-6|?@xo}z?`RPL(=DSU3Iy8;%ga|E#B?aejvM2-JFZqUyzX9=U9!0o~;1(|B z488gr{`1D~@bC#ibZ`sw=pKmeTXx`FZZW=nZxRL#z8<%C<1@u)yFz;eZ$J7lzB@?4 zohE=n?N(^jx+x<4N|1W^Akwo-aVm8a=DzVF%BFsZaS2hTpB%>e*3;B!Y_yqA81DEeNS z^x`0Oy!E;qYI>}|(@)Mq1_cPAjbd=!&37P<8acl#{~jq>nb^562h|o+xh%Z6zdm+M$OuxX|yY}*=aa&vKS|h9l*kQ-=W)w4CyvBmxoviNK{HP^abCr8Rl^O1o6;?3%wXU(2$ks|kU? z#=-}LD~1jW$8-OTLI?o~-kpP!=M?PygIWiJ{1Hb}h&Sf*!zwG;#`kRFj`0NuVVJ~u zYdYf2PS!e+82kFrX)V`qCL;d^AGP&STc$=7;ckcfq%%NqAE_ zbQiKG?WReC0iHMYM5CwMQJvZJ%JhtBh3L0iRT0JzuB&z;e+d~;VNFu2M6D5;n|KcD zjHqY2s{RX#w)}?N*BIiGf;8=cT=?{9#2rFvV&rC{{6sc|D1NS%K`7n)#4+5S@K)?-*Q65Oa;D_tUfS-jv ztqA33bD%vI%dxeZvRoViM&zy9MPU*JMD!Qq(W?!@pX>(DrhXho?xr543je3OQMM_U zW);OKU3nD#U3)q-LBS90z*PMI*;N%x6#TLdd9S9aPGXC9oP~F*=47}WxI>3pYCPrp z#=lQb_%4g*_)(z59a_&uZpeINnJU211cf8~iB52JqJB_zG8+}yCha26KCR$CvNHmD z2?z6SeYmyh^9WpY1Ee zncp_zbm3?;rf}4a%E!Mhn~gtC9YYEI-weBRJSIK)7~-kHAmZSrjs6KAPJbSsZ7)Ld zcOPKa!$WXGtmTDh2kxj*qtLBeH`P>R*REZ-_10VQ!V53p_19mgd7|S|k6Hg(9^AE{THZ5HJ%l$OyczHPR*d~C-@^7s2jgZ& zt+DJE*tF{t5P~}&dIgCT*z{>1hNBxk#pU9U5l#Fskp3yeGOnCXIQQ(Q`GAwu zj61WdSG7U+7TmEX9~clDj-l7P(dniO{@BVzZ9bU?C*=fN?(DjgtW}qc7&txx^QSgJ z6onOnPnE!1tVRop?#NT@S~b|L$bJrlr-ee z+=~3ywjlovev1!}3;*vhDkz9?OfK}?=hLn=0=iOI;ZE%g?$Db)t_ZoUgC%?UaSDUX z0uakp`qSt_jgg665zwF8oqWT2(yra7W2`EZP@NyDW<90?7R0#Qr-^885mt!1zHK`} z)0`|9>!{pymb>B?8~_Uy7d|k;V?b9#KW`FNi2Qy%T5{)H2)_icLdxj8&vas;k{Td1 z_qeX9={iSE;}?v;QO$TR^3#E88f_!4bSiU2;0wbLd~;I@D@1>M5O{k$e0oKzl<70I zC_h?o2~^U5$g(_D)5gU$5}{+;!?T$XR){LOW8Atq{01=UMms*;bYiGKdzL;w?8i=+ zh*A8o#PN)woqcQoig#~=Zk^EBAwEwhz$26s zOL(f}f4d|cc2ezKB!vzMjr5;j)KNo;jvK`=i>+W+VYYOIlg2X;F)>Ziv}q&?{#b@L zm;Hgl3OC$I=CEhSCh&BjW{uFUGIgb1sMQyEWAd9z@W|c6$h-=HS0Fm{y&awU4i=9o z9cq^0;E~K~ZMjky+57f?;lqEMgcTw~i#|iqfkFsTPFHF&Edes{-FrVnOW!^k+=NH} zaVI*p5W)&kLW8I_{V+K39~j#woIZrIk)OH_N6!h7hfU(U)!nl>-u-fkDy$H9G@h{- zHnOK`TH>P7(e#354ZLnBO`!=#8M1Tfuc+fN3?4QTZ5blgz&T(rx+A(}FWmR+Q*4=L zLi)4Ve=NtQq3VY%TC~8HEnD!^Q%|X;GH<^5CT_g(Mw~iz$|18Be&Hdw?fw_=!UNYs z$YI@SCe^0*K*SRhVP+#RsgLC1;)kZOUGVVOo@CzXu$n!0rP_3>p|9X3Y7S8W&XODj zTy7M6KJ)exJa|iQ%Xr^3`YFqJpUx;y$6L{t;B5MF{J7p7o_?XY{;uh`ty`!GN4`fD zw^%&!>c7>o1_X{Y8Fcb|YiOo*`=hU6(!;l-AN?{>XAMo;bw<}42he0jPo=<9NXxA9 zo@=4ndf@8U5`DUv=0hP(O3IzIVa+X%8VZ3jI!tZTrj6>1wxQaP^-2UJ0+$y7N3r|m zZM}h8d*X>FAU-nvy%8AjKr}|*6NdOk*5#0fs_xOjo)|=EHuF=ZjN<^5D{YF~f-Wd* zk~{j33B;4lsB#|Qhv-Nl{-}@SiI@M`m=3+HwTXxrt=qZ4=lpBbi*DRV=Wv1-77ON1 z?a5e(XbKIO)Cds_vFNznst6{G@!y0wL$$<61V*Zxm!!}a0(a$2JdcJ^6)1f=Z}LXS z=yr6tNCvhl3D57s38E`6nd&j$5j-vz<>Vm+SEUq}P$l`>a?_fXYugt=|EAU}>l)q^Caxbp|joc+At2HK-5B7_N(*Bx|%8tJHN zpn+G%5IRJSLSAw@wO;6}C98}MC(ZL*XyF7Pl71>|lQ4U7rR9jRW`4vr`$ZiaOVg1v z`3TEU+p9MLXzK?vGN;D@EI<#+7WT)=Jk-{>#(*&bw zvu0@Ah!GQ zJ97gKod&m!sl=aWtPi1~q<|s|4ZTOWpvTzWXw@_nCJ~~!It_v&8=>2f1mL%2^dWQ> z>F0Fl9ZeIRs^rdSAOj}7i$QIy*Tk)e7VT-awMoroo{5DSCvdDx3n6B7b=4v(^9Z(W zJtf?*FtH!hvM4-T#=ZeLbUb`q2rLH1h?_Mg5s_owym_jL$U_f3gsoe*s*Ia35gE{v zLR^uI(P$Ra4-byLUUi?NUZWt)(-<+}c z_E(!d-yidM`_N~PIe7*}sN)^O7qjU#!gpp_2}8S{z!8p}Hw9%dp2e9euvhu?6HLcGWh1JS>2@HIvL6(g3ot^4nHNMmHO zX4l<~n>$I|>NFNt`TR5gZq})9vXDgJT0lUkWQvF#c8NRc-dyyHJzpNe%h%Z=#0{Yi zn#H1y&O=-9V^>?foK$}p-4RK17U8q4K2zc8{W#2>A0a1Ta6A5Wz@|PMV_jZ}{SoRr?PSvQhbADBYWiHf8@V&kq>7!Segk$v1E2gy2wuyM8BRre?F+xl$_}xOS z9{T+#+sp+w8P&>UMvmCW4+X96)LsZ0-y8m&LmbSZE<{NE-&g1`^1Ipp!cRpdl%e&& znCo@;?78vSb}R#Bu5^aU_e|qX!|>EI&*F}o+QGZ}P+b&JUaB^j{p1qPL<8uo$e*S% zWqeO~)zC=w_iHes8&l6L(@KKy-y)_aJ;xv{fRSyg`EVgGY$!)XsfnW(4xEF1?L``* zHfOKw;E*s1EbM8gO9|1Kno_o}!^~YjQ*dFQN8D7XnczoLw@@2K+El9Mzjxemhw95h zL=)MuV+Rrw6Ym!;X}l;P>39>S%(}_;at+7+=-jI}uxa&takImxwkKEl{HyhI zRNMZ_d=i0c0s+iT@(GP1(89q?a`bz_J1;OxrkG*3IVDwl50`b zrDYM!ivt5A9AxVD@3j+FxV2*h6H3g5HiD5M>vSvgF0Eag(juaRPv0iciZC!tW#1&> zBtLe^*a*RcVyGctzr450(|{+g+&y-Sq~lQv&xBLR%3~*x`M?Rg7F`%-Fn~@E&5L%s zn{_?ml&~@wwv}A`&xT?US|KJ?8bzqX+nD9p)?8o5E|l`QGQ6qEdGP@k4nM0o39YZj z&Q}kG6{QEa(~+vl^vQo>N0id3r_hvfp&&}rv>e7GdQ|+n7uw!j*iu`>)rbBTTDOAx zi;Yk^YfBkhzi zglI!14Z|CI`i84bh=>)%W%Q-uR77DswTId8d|ZYoBT;Gw4b&aMk(sH#Wb+3kzI zrU=tFg?@f$dgDQjxN74~jT#@XUbvOfqMRk2E*BBhW^(q6C+MtF-f~v77T1btDk`*S zeBdr*%+4CAMJ+PRwT-~RFCRdUr#G?_2}mm1fIjxhR@Zjtu14xQj#?x&R444}edwG} z$O?8J$UynvcHF(Ie@3E)=FfXs(N&^oQ>vOrgy;;OcBcbVb`Gg4zd3ImwZCbEq4+Fv zpEn6Bs+VJU%2AyV`AAQ=1zZ_ng`i1yG!>=0QYoycP9f+N(!EVI0w?xIV0Yo|r2dj% zs`Y2>?IVOA1>penVw0hy6b6;CyV#P|^OIMxq{>uA1ks8M_Vd?3Uj~)gRkU+gThrj$ zF&2_+4ya+E&BD2d_o>%hThkPzzY9t};Hl9Kk#p~1$XGqjJh=kp`%CCVHV=M%X*bUh z!XmjbHd^2f-PMnAaw$r7r_e!cGAg!Go1wS@MT-yc*|N?P6a^M7>z!1iu9yxoOOZ=`flK8N zg?X9~EeXxD)Dt-k#YaH52rKpNOvL5oo%D`;bQ){cpO0<$6HZ|Nfp=%&0^C2&@;65 zrJ`RuN({w0$fpTN5r4tL%teTyStt_BkiW0e{N>J0rsv3<9l(;~#wOM0KzKcV%rzka_;b z2wdg!uYP|S|Hk7iyMFZ|Ak;=R4$OSs*$Vu+HXD0Lb~sgtuj_m9J4fg1u~@p9SzL7) z*8oqN8y#{sql2~{C_qXj!>AtTPG5X&q|4s4j$llCehzNmkHYf)Jg1+B^L6NQ-6L2o zR3R3s7cQ=JD0zeZ4szc2V#MkxaVyP8=Uiev zccaFPfXSH2Q_5Ui#mj<9~F-X6CIWKUYh1m;fM83a^VjeF2ovljVAYKx$dz7Wb8g@T4vSrpKaxA*HtlLoU7lS?2{L*ECEs{F_I?ZnQ# zJCVnFwM*T!2LxHJe&O`tpkV7+1xMF?g&lX@fToRu;BR@Y<{{(c9<2WF&m6{R zxa)62C*kn5YLrDC7n1^;#G$RX4-T`?=JngrvGaDs-QUkaL~=1T)|^+Ege3j46oYSj zfI^F0?Af^-(_f6nf_LA9@Ox3al*y=73{l)Fh^8x|L=G0cG!iS0W)c7T{{?Y2-n-T= z<#=yV$Gdm)Q1swywN6x8YeDrP_W__xM8+TsY^bf^=S~2?P>y#e(d6nvi zF;r1W2J_@8u{%d!5sY$i{AekCIFJ=4)8`lBi^yQJIOBYEp8uY(hjyuoM_Q0wiPs)F ziqQYIz_9Cl81l3NXHS+w(?57JsLnvk8KUyUA>^)!L_l936s+5ZqW8{Ru*&lN)CftY7Ee5F_Fc-5_iYkN zmYsqzn|K6-!!s_5jJIV)cftCqX9$GfeT`7?b{e%#%8~Wbuc-KEF9ZyZ;dqpy=-WLg z{w76ro~eoQhj(Hd)l|l*bJyyX*UuH2g6NL!MClS!*j~RUZ-$4kBBt2Q}E9c>>)f@54t~2lqyAe&|??LZ4pG#<_5$<|)6ux=!3zX)a zz$@ckL-CuFFl10XBD{*RY2_TOUilR^<+vfdc_ZBN#3(RYZJm>=Pa6yx?1imqMjX%D zh6V4;K-Sp_xcBz!(5^`k{bguTQb zdp7@!r0XKkt7B7WjHTG~^XGVd-p@!$&xKd`jp)!}FnX!utstsdD~!0U0<*p?!qLsM zF*NZU=FFIY?r|ctsg4>TMaVpL2x<8-7@W`sp7r1a*0D>c_x*(AD#6Y5*SDtctcJnG zv0Zt7bs&(xe+@oddkAN@F0CSHDgHgR6RrzvjC=k$7ES5Wcf~s&;h@-Cy8S8K9(9~y z>|e#_i`L-an8hgmY$$&HXbp1#(LF~ZvAfCi%-TC0cCYyiAAGzFCSThD=rUj!?i)W2 z@s-I~xj7Y{{w+E8`@s6d0>vk{VaaD}u<6JFI;Sl{Ky()jyZb(jOKgqgH9sPi+CVM) zB%)7C+ft&{d~90sAr^l0IhJlD&GB^e6xR=TjJ_N9-Z_w)eIk!FWp7)71wYaESWp){ zHEB4GuXqQqPWv2dcxiF+HaytLT7Q1(CVcqbLVW&{Ndu8Jqu+=-aQ8p%#lW_X{Wi5x zd~y@!&sm6dKM^7ESO>Ji&7&W}BZ)qUw31P)qlTb0dpG<4J;wm$h!`*q5B6_bofdrt zmT(T77Iy-B;fcp?wi&C7z?=1)Z!Q%rdC*j^>X38+7Q8)89S1W(iIeAI>hpX*a?{h2 zSm&Da%s+khZhSQ3T|7R=Jil2oJ^@pndInF9ON4DzqRxy>OYqSK;?ZOXUVN|z=k7=9 z+})Crr8X(%(%la~%6ZrwHdLthoc+0kpIT?tU0v5^%$R{Mzx)zE{qz%}qN1)Y;v_Yf z4S_#4l;F_jQoJ?gw7R1y!WCA~_LZC&!b1J<&f75%dt}a1ZCQf+W9RVBd-<66iPp5) zPTOWZwI0Y;AUjisJNg|^!=kz~0>!9lE%3p_>U%%6ZOcL@nS;iy1995}nb`HI0eLwU zc;wC_44ubqTQ=?L;ez2)-0;P-_F4*Jx3~MiTj2T4$J_!%&mUY3g&S}GhO+JFsEJ|9s?fK9$`F?pqvVTCDE&&%Or#@% z5cSbrRPPsc)-8E952ny01Qq+Z6;G3y$~_s-@2}EwP-$ens@a1Fd>%+Z#68XG){t6? z4r-$U;6JPh@{du-P(=0jcYa0@cm73a+1e!I;ZyXna0a<#zJ*6gH6(3?c;39=KeQG6 zZ*EeXYR;P{Xh1KNu04zLouw246r$+;U+H|5?FrD$W5Jzk?;foXJVdN)E@u)xI`WY9 z${(CV6#n=%=6}Hr6IsjmQQ`naif-x>T`kNYTE{?j%mxcq(S@RX@>lzfQRF% zzO=+R3n7KU^bBYy#0Yx2O*1HwP|F@+58~;2x+2hzVV`|lp*Nj~W@=7g z&P)Hmd}_7n>9xJwNJcxVEqb1}(&CC6?wOAM>z+qh)*)o({(-L-K8MvGo5EFf`Z9;B(CRp7+Psm^^;9WxNf=DeIK{2 zSzJHJx;J9c%%_nseJy^Ndb1r9d-50dV~|Zh)T%eCwAfq~71b&=1g(|Xd)WV(ruH{P zVAO`ks=cp^53a#|oC9h~Tugz5>F}cQ=3F`7TvC5y^0#4ZNM9^9HDUL6&77wpK68Ih z!z8m{DLs3#n=nU9N40=_=92BX?qf1xt>F0Yyo8<4s_3yYRo=YuF~ zo=g8XeLa$=4!odqTCu|W%cfPUR^iV-|HR(Cd;hY1lpVV;0>Tknxz328rJ zlq%%^`LHQA?`?zbF^+OO^J78g?ZVZoIoxTUp%)I=iVbQz#l=SG%EThZv`{Lox?WOZ zfRCqj5yL%vHIERUIBQEIehavz=EdD~S0@=lWBu^x|7(qpX9S=H+t+Ei*b{wZU$v!1 z!~{Cuk89HVP7LO*r#F8*)_*V?+Yh9n)VhkUqUqAU)LnXZRYZj_Ns1in6MYvS-d9X? zx_FSO?Qa~tb!pI!z~uY)T~g=IJErMu~EBZrqjt_GM@!AZ_-({*wyFg7%@U?s)dt4 z6CDDN8yMcQPcSs`6f${GSi%sohI~4s`(z`E4_ulSimQJh0tWSk-#xU$75(8Zy66uS z4g4P)fYAR`G0F-(?K@HEP=u39(qkGoVpHx0&l}nz^o@ZCeY{h>_yciq4@S(4+Y#_s z3%EA&pi@mjqnIZg4-qP~YYPPbvp*u9>uj2TmNBRu{u*E6FF4dXB&DJpg*%gBIA@@# zkryK7++d}_B?yu8uOrQFG)FScYZz&wHEjsIyHO*BcmPi*)wHx%jD+!^e*%U{&uDje8Uxoy&+FufB+Gu^}8M`VT27=I=%^w7LL< zM-Rr78E<3Wv=Ioi3AZVlWiT8z$4Dj4LPaa4cY#qK!ux!OhyqNW!CmmLk3YoQ^ZtdK z;+w#&jNzP%i}(&NGWC<~RvIeMp+{sbTzi`8tm!X*MWEq3QXwr7;hnH9X&?Ia>xaU^ zLW~?a3NO6;1`Oie!#)U1%*jO)hAE$Yi*eUCr%gV8TZl(-aVb1wZ^QhrSL3xu2B1lZ z2P1a4GRlStQ}Mec-VY)Q7k_G)=u3wG0>q1w_50BjQ+)gQRSEO_X}+a47=T+|ejRt+ zdL5!!uFPhin4ooF1WR;-p1(L4`sOn;ymiezD1Y&O- zkI9p#U`&FQsDufVF?q^Uh!Sf#zJ&OzT9r29ppASmz7@69Z0-lxIgiZ8Cl11jF;iw? z?yM;kdN?Ml-;Cnj3+;s!31cv8?p#b6Q@xa(vr655jI$F~Bx2U$ud(`zxfD~F(;5tm z8;{*4B|`P0yZQn&-M1B1a2p<>B5XI){!hl)2`dsO&%)QMS7YuJYt0*{4Z}SPcbh8W z-{SqB*b6J-*^dPaFnvOTCC|kkGz6{DH{OE@WKOs%4gw>CfG25@^%~ zZ=1Oy1W=B1$(vebYhJVpE8-I;cH{r_4U@3VJcc%P+Ri@{{!d%zjvq4% zYm;^%Y5fZHV8kQtOWx?sqr%shA8JQJIevw`& zFE=Xm@?I?&4F;v4K(Brb)$Z1C!C+K$T5CCTd1v2RwrfroYO74ESBeVtidJW&ze95= z`)`I0B#&fJ@^j0S6DPDvTAG&q)G1lHdPQf`mP08eW#b&BTgNs^baWG?@4as+yN=~J z#lyZ_?az$ba>Y1)Xw$iR*V+F4lmMUc@vXrv4Y^5TjgxdZUbe7LC zGuK;TQ!k}bh7!>p^XP4^n`;&2N_n4k7^}+B5u|eNqvc9U@As9|8$MKWR%a*_PVmD# z{1}*@p;wi(5g#fkeco4&-}aMInN_CNXe{S_wLMcGML(T>N=T1w5_GD09j;g9G3re9 z7|S^G*bPH@mXiD7FG@<^`AW+5OO(@3Z&CC`f<7yqmDvSK_L7xKN-xrQ<07SSZ?SEQ zbvx#yGLV*va|Md-=sBhAWWJ&=;8<3WE~kCaDJLIWq@)t3Q&abHyo!ijA-{`Ru8d>G z@pU#Xb)(KwiqStfPCBI`GnexuODR8@r&MN_n((P~^7QI2fWNc3Wohjq4yaybo)3y6 z8Ab!=j#eo@ou`zaD&RaXCpOl)jg@+J{#E9bD&^^Urf1+pwwhNR1CD*QI<3VU{K%*; zQ^?@*Q%uDohvtlxd=C~VnWs5aCm6wWs6!JY;ts_chMY&R1oUfcYlcpR$ zo}#24Pg63^<|u{59G4240kdhh=KA4^itT49`GrM_w#@z=Y13@24~<5>Ql=|a$F799iC$M$Rw^&P_@Y7~g}Pqq(xtP) zO`htQaNg=li%4JSLjq+iut}ZCkm!Gk4lwd=FQ8?A_iwH<6X*URE&3xN$q2aQkZ|% zf@A8*lgjB+=af9wS1O(#hg7aD;_s}WU(8j9n-@GaD5ZJH%EY)B{+327(Geq*ec61^ zTy#AbY)EFPB?3B~P6-MMQetCcYgwXxvmIJIMtzomF-o%Sbkpuq##C)>kk4bDG0NIQ zS=PpF?kCROWw~CoYi?D2qRa#(Da)o(ZHh80(X?GcMv?aum0d-mXr8hl!Fqkd;v~hU zl#;T3mQ9^dY}-qkm1w;;VX<B>4`vNPitlPF9)?C$(8irOySwB6& zLeJ!{Q|ueAJfu*dQFVWUa!Bo;qSOg*o4InKxdcpCY-X8CQ_@`XSV5V)E6+ryO`TR8 z9q^y~yWKoeI6G|TL&B`zEq%6PEczU;EKaheB2QU7#xzgFyq&VvelA!lu{ofm{jZy( zz~ZkvwioW1RbX+;6Q`6lTeaNy<)X3bbK&yWW)7|$>5I|S&kOZHUzVeM_-qG!ePeWH z%@Xbx%*1vwv2Al=V`AI3ZQFJ-v5kps+qRwi&Zl$Ey=&bc`LVNh_wK5$s;;-HtDZv3 zaI!+NTb7Z8&)4t6LyYu$XcX?t7vYN8PgmZaq#$TB#%NT`^e-T&4~l>-)kF5S1g}cq{slhk(D5zpybhJfq;NU zZuUnrd(3mXf6!hvOFo4sd#?|q^DmClIVBo?8M#^fN=cocui)D+fqS0YlPQ0+*FPH4 z-C}>3u(q}~x|G*IPMM!nLbPnR$O4_Kv!#?>gMwe(KJH1`TcX&Bu5V4aeYl%hI+4Dxxh@zu4-iLj&iKs7H``$a%Im3@ho%`$hUp@aOmXiir%Ccj0#t z1ABi5Epeo%mkWG2*5=WS;3z9I2EXuf!in>8hg4_0y}o~?C7|$twURyxSBh0?>!XBJ z!=Aq}m#Gtjkt*3doU)p^ylA|#Lh0T~gx++84g7qM|M@=FtK8_0#<6i%{ZnkQGg4xl z+>2I}e?v9ld#t=6sU9ihI@ z3fia9U84&7m1Vg_q=BUJG8>DMg37$Tov$dysg1*8piFG zEzZUOf9jw(Y2hFzbCO3=l`c#A`87wt{sT4O@z)%NCl$LH$CPr)xlqaK?G1PU=jhQU zra##)wV%=Dj`XyT2Uow8c4+5|RI_&LSAJe8M6ranBa zmEr zUpW|=@^-pSk>5z60?nALye1si&gi z84N27B`OYR{N)T^B|Oo1(ES^u@Clm{n;^A(URT(QhifkGI@GC?-5xEh*c&;oplYvS z^qh-PN$*9%iN2Ds6!zZvtFYhZkW0K#L7SSdAeY0nXHF0| zCAk=$J5XV=R6zPu9HuB5Uqc{W9reB1UQ)l83AvY!(aG^_^>yGWiQaWEVMNDjAVnI|n zDLOdiyuF6GxaH3-HRExF89Z&!1qF7fR76nHf7Fhfg95P%Hgo`;kJKWdZ9lR`wev*+ zDQgQlp9i7SQoo_kG_!V4ua0}VA;+z7?E3g zH@3^(zve8=FQRQ=r}YvxBIFAt$mjInoZ;NntDpgL$!w-wGwQtjTdFKGbM?KpEyF;8}TlyEj zB_cK-9mn@S^z`^?#LcaSV(@=f>y4$*FE!idn^6$d(;hI4_@Fhk80aBmPJ%;wAAPCE zD$0@a7!%s2yp9K-g9Q2{t3Nzwz{IZ??r_EaSTfL~7wd_+Rrdsd0o2uz1MP#@FdKc+W*&= zrn6`?<6B05-B!jZy~RfArRq13v6_GkP5Q{3favtBruJEvB6+ z{Nl<&zuM!S&RyigNv{X_czn-JnbSObS}lIlJf-G9e{eo=Q({$nRriQ{(m&dj7k!XK ztp2)Lt`U^MuDAlGPtvmhBPOt2F}Vcj@LEwA9m5Ar81wYxv z;g1pNcV>9_Tl@K+iptH5UM1gKwh4}IDsSNM!l?So!W$JS+m17h@m2I!_pnV+xTrke zq%208I!1TTw6D6s2Y)CmSu7*PkEE=Wd)he8toMBmpaI4rSC;% zHNn;xv|lzPIw+i`+*~i$_0n0AoMt^E{s^Pk#$Cni;q(*5DwgV}#VFE0-GugFyC*(1 z?f5nyskB;@{~B3P^93n1+jY|u)jmcwdfnu(tswVm!SI4Oi19f5h`4+$+fX9S?M!Ao zJbk=-23JOEn6o?;EV{ef7bG?#1NvI?$B(?1CO`vbE{E0= z3z2j4M0R4}`)EF0-CPdds`kmmLY3EBtBkRsN~;&yQ>!-Jr@iB(HS?^CaD=MLZe0fZ z(%>fDZAK8AUWtbf2tlK!q9*-&Q2Uy~BP<3~%0_Gwq?9nrQ&c^^KD^nBR`&4yHgcTz zo*yw2^ufFFmZekEP6Ac50~gHJ1O=C~No)4Suub~i)T|lbn6N*{sF>x^8QxNfMaR)D z;kd&Xop#3&@O7%;OHQL zPx#9;DL*o=uMEUN&(=63yF)soY*{eBT7Qz22$hZ+(kbxjq4LDGh6U z>O9XhQw~`J{&W`PovZ)K8o=e}7+hC2f$_W_Z#*U7NN3i1H}>vXcA?p$Y|9`#$Rto_ zG!7qs1wqAt7))1uqW}=l-#q@aQt>y-WnqB)f8|UG3g#XJ+7#qF98G;^ce&DO@VN04 z+!DtJ00937F~Wm2?mDcv0qnf#!qc3KBt^l zU*2Bp{N;P28UR{bV3(yBMz~B0OQ`K&<(^r0;gZGK67JOJ(e)s#|ZUChVu$3-1Nu89IDfLz?=kQIamxEdyu1Oc z*PqR#>^hbX<5Yfujz1(d3G*O3MMQ`^#hlZmd`JytaO;t23}lbAFAUVR%>j(8#D0m z@SsspTuOjqLi?VbSpZOWOYXJVQAL8!&KQK}5Wd1`fmdF%)K0uFqRduiM>XHOaksy} zlQT`}ZiC~-vwx8y2V7=Q((Ko+5}>yPUR%2>7)+01QuX@eZ+7<5DAd2G0o0Cz^F=c-3bgIitE=ggXtSC@>#4|DxrtZ4)X53(7Ez;Nhhg<(Sb z&gB@DOsgIBa4w`GlnSWO!SyW@(2?_gm)5+7-nmHYh1|Us8lj1>JcidK)W=>Kgl0T< zk<&M&R8xC-P?diD~~bItoH(avzi{lgyRdsd~ z8pss!h84?ZS@XXvEfMg&uMd=fdnW)U@Ci@^iGYM8khtrAG+%~>N~@)TJ3q=n1nlh& zy2dpI|EyhBppYdbK=CuFCk~$fwXtj;&Av0 z#^iPl;&Pqbr#9P6WTlkvNt?>#z`wO{d)N)2V|192%srPE7g2x&r@Yiiivc|~HO!8E zCo&F>*T#B4CZ}^=eSQ4^MIQj7EfM~nO`;Z8Nny`Lr6%TfBp}d(r`(>O2Um$|?-oBB z?$m+~d-$+7OjLjimw=-e%eoy%u1QoN2wJXz8i27-0kisT_`2F}8h%%|V%_1+S@rT_ zwFwS@ny$#{xi888ZMrMKe%*^}XLcD8Q+^IPJ0>KgoO#K_k?0R18sUu-FzN%m;AAd8 zr$_2qQ#PE?Pz04aqx^AZ9iZ{=IQDw5ddXs00N0EOxQA%3Dh+@MyuF1#Kh62`_-#K~ z+X)iL+XKbg`ciGa#`#jK*b#vHQ=qi3-e9(0-`-62s_Q2l73>%`>$WRY$)tZvYM-*0&3KZ>KkwjxJjccvkRxJ;B$r4+!r{+0ye*B2EpZ9ClTuIobr~tCj+~%= zz>Z(m3u$X>L*w#%3x>tEbKCTgV|z56DF}U@yJ(Xymi`HLeru`{0^Az}Q%7m+PVlPB zF92At(*I|@S#T9f;f%%;c(k-K^Ll`dq#=>47RHY0I$V&8y`-c>y?^c$7Zt5m@`%K3 z5qcbqfPu`avU5w32@cNL73dm5uQ$hvF4~6mqMrM3%b)aT%vrnTdLX_0zH;orp@H20 zHr4FN$)RZ>{+JFh=an#^A2THQiv#eY6#wH!yfwfNjgJS0e7gYvWRzjIJ#M&P{{qA9 z&W?pKi2LUN+`0~9p2C9=o8X2Kw|19YD!DOR~Ff=(i2m;5|P4>ln&I!!{ea)c&8f|A8 znOvas-)DL^b1&_ z+#l`$JciwP86MxMzL?*&(Z4;Wk4|t=nDDO4rO_;j|5-Q%JbWi*=f{hAJ3z*M5>5cF zZj_P`tTtJ9Q5?k!{_SdYCw}LHq^g9zs8#%*4-TypQ9#4O!|8Q;ePqvk~uc;zYedBe8I$wB`hp6hEl6oE%`_TG+AfF}hpt4Gg1$ zOx>?Q--WVopW7$eGWOXw?KhMs?6Yn&jH`8fzj}tolSuUd`ppA*dw-S@0ipGRru>1V z1H9<>hq{Ue)QJWO;6c&{f)0qJ8#IN_9|9nUc=-50<_qP%Wo4AVVdC#DH*sGd&dqIb zzQz9ao*FKYl1hCii+*V8@45aOZX=KQJJ1+VA>G^CYrEa^B?6Nfax|ItkeGng(0WUp z5s1Ww5q^sppH+a;9{axsf*_QXm-hh>bAETi_pG>Rzm)zR64QIM#F{A>Q|9>3TB zeCqc~%ZT7{aib5jy>TD|f${P2pKeBJ@1xk&4GmRHev|SCQi*_1a=_g89@akz{)a|2 z5MK;Nqphu9{(!hRpOqDjFu>S+z8&&TnKe{=_elDiL>d7fYAPC#5M02r|D5elGBq|q zL7~DAv)o~`Js(oFrfhcybOCUE05xvy{9=6k*N1YnfXk~((BOCt{=?WmkW8pS0jvQG zQw8UtS!uK^+f)^_85{Qfo1>&aT8J|otC599|2?Rh;Oc2Nnvj_IONaZTu$C4M0B#|i zSR&~P5rIL zV^8!KMa?|m)jc~eCSXZ^e{cVv9L`CF02dDfSnvG2JYdU}`svpO_;MPyzXjwJuUmp& zO@y;1;U9+nNpvI;9U!9e8|4GQ4goJOp5L2DrB)Bl%*-4)7MlMT7rPtqx=~RV{Wy~U zXRS8#;0lV0x*u=PPe=LDcZN~l*bxDEiF~4>LyGafP zNp!2Ms+MNN1qn6vH&r(F@H&D07OKpk! zA2$B;poRt{c8Zu>_}8?*7f?w8j1$~$rjXoK z{buV!wh3g-WAXY>@qzWMJ9JR+>&b2-nZ)Z1(@7`;BLbswCs>3VLbmLCdYu>~oaG>^ zw-L~UZpzJ4z}LsZxcE7+XpEPwl#*?_DGP-LH4%doR?e=~NW`Hj&po0$%Z20$6ug`l zYwbsn+Mi_zEN8lsNACXhcadsUN>hbaE(ng_{!2S`g89TP3Zg#H&H4~Z!&hSsNt;d| z8(XqsTi^GM*ipWyw%*-2Xu1hUNn3WVhcGr+oxXvCbeWnv=9Oc`ir5P&o;LQ+jp$j- zhNx)T%Gc3uR)v?3lj=Qt$Hp&sis3TH9T=0RvUq{*Zg`zZ_>yIvEM?k~?+WH((2qZX^xEz@oMMPph>T`ML4Um(4e9IwpvG< zR2jX1R_oH2&Q@w_;CiFLMScgxG2oeE7W>p%>CJ7Yu8>DeOY{T1i=uKTxq^7KJ?jQ_ zzUwp+bW6Bm@qW(P>W6CD?AA4j(9rxpCI+AzDz$_lRU4QA6p{Bw(*AGyZP%9waC!pN zVPEul2jyFZz%BMoDlM*5{hnN9Fu(;XgytGFp@7LKzeB-7@Md`#LLx8HxOl#+ zaY;+?ufU$^jkV0bIkkMcO7>!&)KStOW(L=s-s8h0uj|t3nt z`FR$zdM%#toMj?v+)B=7pmDw90Kg+Fdq59Vg;Lf%9Ojq*b{gMPf3Ct|&hYKqw=t2b zVUIV09>)}C;|P6#%Mh|tzS=rM4qxy7 z46LIITNxZ`TPzt?X{G=&ZBZhQ;R*%Zm1D*Z4L)nit^t-ZOBbGR0d+@Gf|vR3B=CvJ z?24*Hd?uKiS%h=bJsUCwl@g%1`Hdk=ZPFSaQCW`H<<=8vw2%FB;W06wW5IIatJR(w zL#jbqd9s+vp%%P>nq~eaDvo#`g|D#(0c3w9{|wUsb~Uv}@4l4+E*&Zfn?{6o*l-8c z>&+gT^0qAZWh#BkbMni{f#Tf<pjlidK(p-d>y`O=3 zAAqLO(8J40*f|LA;x3a)v#g^}JV)N{kv%>0p(RyjHx|83R>=L>%z)~EM-+%*vT4!j z0kY1cCb7$4vJM7L)lY8t(Pi^FK#v>Rd;Zm2^3C0Wg;A$rk7NiYziyP5?ybFljCo2o zj3gejRod{jk;TaVx4r$NBoTCM2VqsVs=3(Fk$2naqV^-1M2^E)iN<4OB`SHr{Z(;) zQ`=Um*va06rIHm7>P%zII%=6v=0r-TC*w-v1e!oD{UWMprv5rA)}EhQ0~yrb^Q!5R z$N75lRJf4JCsf!{7wJ1HNxIFr7Dq|C77Fz)A7HLJBnaLC8FPBKJ}8x2(+ zX};I$X=apS>pA4DVDvY>5fA#It_kg5%Q86%UG4Wa9 zo8`ay#FV&n4f~dMcpSkKp97dihL5Deo4Azg+?a~)+2JgLZyGVI{)v>%VFFVgU+y+pe|DiFB$7Ve zfQULPL_{gwxP^H+o!%sBx(6TlR)0}{FbjJL8BLy(QhM1k#h;XysW^5dA{>V*(>M(> zX+*O4{<-ax&JK`akN#CJD(@gDJURH5f;Oe3Kzl1cT6f0l&AN_m@9sR?vMm_(MB2kg ziC`uGnpCwJl#uYQ(v=ak-hgkqY#>-6%Ay4vuz6GtFWLj)Z|d{mD%j-XmSNnQvj`MYqkOX!gtO6-e~if8@*CX@V;Y7ZNlg3hYjbX)0bBAUQ%t}xKg@0Z_J;rT%82l?sI$6EyJ#pW z1(IWRT?>jY7N_}E^@Qo&0KJCL#=C{njPeK_7V3(U>YQdB$QzKhcWR3*>sH)qb)usN zyn+}A$JOoAX3?pL$N14FM&x%T?ZS+m&kH5A&FeW*iX>1}?NkoUT`TCU4IyipY8WKR zW#~u$keodyuO4GFj&@OE{n0d-qQ%5$nX;c8=beEb}( zj=6UyHR_COK?JKw07Z2oWDj8yc~#%^va^OR{r8q@Wj@lfs?E!KhiH}6rNlSY8q`!a z$vRzoF?duGdJ+U57fDd%qS zAETpgdz~{pe^5=UJ_^_L2ON7MHJFy`Ob9M0yZ2d!fUmn@DM2Nn&}${0E~HTzfNo54 zNC;Hht&{9F_VhgJeJJ%l^bufKJW|%K!S7?Cv<;f6?|y@VBCl(RP;4bmQu@K}-gn94 z0OM*c$=vXKP5^};8z`nw?&_|KpGlx!cC;#?gxXVFB(}L*v^Yw}`^n&2gs1}*UUclr zhVsvGn;z9Y0v1d_RZiLJ)WGHTfE6Z!mU7rsV*tmKg=4i7|2(T!3q}`UYTi}KXue`Q zcAE3u*fIjWEV(+fbGbY6Axa)$oBz>$_Li@WEtpMz^Pnkd%u4mCOV;(=fpuLM$oLUO1RmI6VO!W5?bX_RC#Ucu~Q4J|@6B5N9>nN}KYg4>Sqx&2BVlaWOp(N#_z zY2k2(_WJPE9*j!$?&jcgVu*7j`L=qWF&#P53Vf$Rn&VXq5(f! z48K7CnAvo_fb;q?m%l@0H+|yk zKtE(O6FNGvq}M@6z@V4~r^SSWX{fLXL1)Qepre`|YGMv4BL2F|pLRLf93>Z?lW%Eq z@7q|BD33Kf8Xl9Z9MUt^t8$&-+9GN>UilFn$wA2oo0{w=7%k%7@Qv6GlReB2>fA{0 zL{p5@R-6Y-pl??f4mUoI@R|>koz^l4Tj6eBWT!@KVFdRC$cP&Kq}&L8m^wpi+&M?g zvwL1-kI#ec#Kp4*?wtlhwyfZU}oQvdpi0Zk5_ zb7{8F4ntvK7%=P9-4Gx2X~#Za|H2v@7H(Z(}<0lvc;CW70tLI zr;}|_H5&!iyG#bO*a8@_pRXm;&V8QbC!IA#UQ`n%dp*zv6D0-bb{*5}@}bsgSKL`{ zqw^KXDqcLsz~hg+*meI2c37DJ-(<3X#x5{PklZ5oBssM z`)G6Ct88tJ3L)7#LyUa+FTU8UKBX=WcatjeTOdz zt_B7Hq98Rw*73P8t8|%)B!?7SrgJLiLr51#Y+ZR(1om+Q+0FVfth5t5%UMas&L715 z0%fyQ*J?Z1)r2bYuY7|L;j$SuV59vwLgu4@1_)noalZNDH4QJBb|{y@u!9b^;gVn_ z1uVO0d{Wbueiq}d<<66F^@1Mah!H*^Yyg&7Ntw`N&TWfJZ;H{ZFJ(F_$A0AzUo_%gx4N-EPkjGd3 zu{UD=sZqth@KLbRLr_g!hQ8n5Pu9pYGMXz(b~ngc_nH@zRY>Zu<+|Vxm!v%i6=_-u+=xn2fc2hUF=4Y%h=){6)v|mNqs`>Zl?cV|J>l zsD8*nF*bXvU9J}-x*Q=_Advd~vTHIWP#J-*! z7M;GFV%X##O$n*_d>S5r=fNc3H~Zm~mZ*0dL^J>DUrlc8$vUdYa>Gg_o3fzm`YVVs zlB4CIFuV)tVa!0>#=||1ykZJv5_`k6weqsBo}#={RB6(p16Pmz+%uc`1`!!`W~P## z-OPkuOTKfET9vlU?1uPvU=;<`G3E;9--ud{7^n%I)tZ8WsPz-$<`F*GbH`@nx2WG0N)F{cUhhiL;%AZCzJX9^dO{wxUU)mlSbn~}`E<_H?rfEmVCRiG zh>VYMfy&UWGI2}j#$+RD3<`88v(-pq@U7D;PM$g669Nq?GwHgYsBE#KmQp2oZg!iB zYF~n%Qzq1R1m$foiz6{atb1f|C;P8DHQ|p=Ar{qwTF@lvBj~HPnx0pLcaU`Ab_Jaf zh$^!jUy`b-K)b4PJMTBW_2-k2+x*q=3DrkIB)@3f$5VQOVba*hzxt3k(1d&Vc|SeINfWs5hab@4r!3_B&f{`HY?7&H{?cQ3|x#x+Y67f<({2Y+-MA3 zwYQ{#vy%G-eOq;h>8nQpszZ^kH{y|Ak5>IyTt7d*qY!>^q1n#Cae3N0Nsj?g%-?$w zncHvsZ(!Xw2!T!&u37JDeOJm9=lv0Ng|wF=Ve2-TMDMz`-+_wn>v*$Y&~tE?ijU!F zlI}10(4iV>{z|G~nGBsyN}?KYtgAx^2}>Umcg8H1b738X79cd7m=#%{C22BW05l&l z3``!w_{S?*D`k%x1`1oKi@T=XI@R8=N(Wo%gBQ^S?na?ihEWG0GrM<$#bQyEHMG=) z6P21y!r+6>Pum?P?bT){4Cq-Ba4ekuq<9uLWjijCiTuunl$V(SNfl=lLnpNgclnOA zGFwFiJ*;FpW}bb+9~#G;HmPUSdsb?=vIp2j&Q2H|#ZcA66*f8MUO zTjUXHNk%eq%Mg^6KE1z9GBPJQEe<*j>cY*kY)eJv zCq=EwGh|g_PdXFQs4&OZA@k!-d*JS@bu*PsNsk0v_t=X|1Op?PQUO>d6Ds>{(wO(zuE7Y$cwlyr8 zr8D=uMcG|ZV+@jUwaEAvCVn1q1KgDeQW5met7yJQ*dV!(F;y2UaQm;^q2s|OO3GRx z^_kZxhz~mNUic9ZmLFE#joNVdoK4jm7;XYTl<#m{-7RTC+3b|wL|Q?SvRGS*3OwZw zVEXSASMNEY!66b_hFxrjWM}`tywx6*Rab|ZJJ|Tfi>QndF)AHr5`)skiX^^5E-1DZ zhor2laU@8T3W@ktej%S=w%1KYt;mvN2V>eUQN&%E-=h|f_nzhk#E#7N%UjR{<1k8_ z$q(#6$zL^0v$xw1--y%nQe+}LxoxcxVGWJ)rp%lX?Z=9;n9s4BHB1}#tjut)EX;9o zsxsIBI3QU}~TGlgBj~BW~OtGD9ut2b^(S%tqV+FqN#rjrS5_ zfse=i^PI8j1i?R4{q_O^!P70gHudx`8de$xm4pp4P#91z$5=<=LJ+*_s^1H0HfJY9 zu4(nAGGL1OXn9x^ylD#5jb?I)buEq@h>G@4x-dq1n99>|AmcA46IN_y3LXhsznD^a1i0}QlP~ZZC@TsVvb7QX^8&Mj2x|1by zXn?T6{xzZinZS?1WR()}=01vEO3>r1iD(LUw2cjF=1*IC|1tp9y7O*ZE$c=~O#i$o1#5CVW%=xC6jEmJ2pC*Hbzpz5Zd?X)IQts2w#me?EF= zg~ba-fN|m>KQivAvGw!P7!{?ulWegfvejetbd}64URqa@Chq&%*MY$o?YKbXiX_MU z%jkPn(~lR0aD!GotmE?ATyuQf@Y?hnFK}YY8Ap1+vG*X+YaKx&F!!8($8BZS5yI?B zMsE`lHXF)onzZ#422eskzUB_q{y)(o3#1s=)^wa9nl=)@B>sSuJpGMYTuJi<&)~75 z`?FPe%M55H5ylK5>R%sz+VmuX^%s6daPhO$vl(o1oSN$CUS8iSz_bmttYO9Lh#fnY zpNmT}LXOx93M`npZ;|Ik4lK$^so&uXk6*6{Qb^S9AEnGS1#3>ST7#nVs8wMeSv{^L zGFAIK8F_>U8dc(-o8jd(8itGUZ6#%pX*D@JA+bMvEFEMUC1VVf)Sk?9M_8w+l1j>v+C&(O}9i zRzQq@RI0147U$s#@Iq}kF(6sB!S?0dGf5%`;^i~S#3#yNrLU%muK_NVwERktu74{b zzw{9;;wX072WaFUL9{D=mZ)bkDtCs06N6C^f)VtV6{cEl@`vd=?C@ocX)PoZ&)}wx z%FlD9Ngu)n&;GWHIyI_I~%rHmFM5BZI7$GQKXm@dGU3{@2tMP%4MUM?+ zP@Vj(3RL*#dh2|qjg~@^1-gt1;Sbh3O;DBFF7lay4Y%a-SojpePf6{i+O>KfYXz6K z@~1+Z5W9;g2hvu^f!?Xzh7fGvZV{scJK+C@VgdP5RA9eSJBpb)$l>Ha-tkX&{bOJ= zr-=(X9pNwyiVN}XFQH_I4Pr_aVj7MEI^SRM1HL^h!-^vt9THTPC*VkIV^2X~3ygsX zt(u;#7q&x|ZS4?H>Dl^L726t^(ROZo8TC*(csfLW=y|pR&)!RDme0g2yQ!UM4^hYp zkjUrq??+t#$_cCoUtCo7rz4Vy_a?Vhgmbg%+MMy+9H4@$YFg6}4bV(NiZClXzdLN= zypBT5WmxK~CKVB(l@89xsfcw*oWN#J!8=?m^=p8Q9t}x*#3n(bqdr$j(GEsj4csG0 ziQ$VA(Zs|+bcPpK4U3g3(vRCLCpdR|)!YxS&++8s662A37w+&wbY$OU&2$~jbEU*wA^2aP_5wl~?3rSn{I3jrZe6+%<}I?S*?clRiul@i*;XnGu$fE+jXx$1P{ zmfEU5vNUyB^Mti=<(5h@L@Jph2dbhwO{Vef&y4Ng*>)JB9IjARD{lM}DuM&+-s?v} zX@0DfFCyk*z%44%;U19S@&~gE0!%b;;gnQ{%eMgPxnq21OpOR*cHc!8mNiA!U{ ze#b@rNVF0y|KI`RLGs`8!2V8_bC`gl{++S^{Xcq$07(~wJ>(dE#1EvV$ANN#vT{X; z&M5jb97KdVnAtY;#g6)sk?JX6-=LOcT_u1R)-(L}1PoyYb{PK6D_4a2kQ((_&T=kF zs=cwmhB~uZO3gR_GEmHVgSC$f5SQvjqiRs?DMW;M=ixSZNOE(XX_ zuVtK1H2jm(17wN`fFggu5>sCSFs}d^C6l;k zFh7sxO9EH3s6s}Q1<1S^5!zs@sF>u+n&2guQAdlzBv4q^Dc%WBiWXOVPvnofvHS9q zS*ps?RSqExGF+~((@!!@t9?9yBQxwpn0#+vktJJuleJVEt$4d@60WC?(#v<}1$Cj( zfrFZ99x@>@DfdR4Bl&zyeP2@(#`T7l*1w$}D6!~zvy@OhImftZu;%sBZtjXls-hOI zdP*{<29$`2&d5+mzqG-^Ls>>ish(=PEWWMG8jqi!Uq#i{(Z-%!a_c8j^8sY|&TNaz z-L}Ksh3DLki_L>;`Fn#R?4?=(!uO<#5A^c){lf`9MSq(xc#b@3E4h zxVpbz-LOiN118V96T#8u&4{PVwy*b&ixvuI)s2aZ>$mv-zKa2n7bW1u zwbV)_P5-xWiVRNUXHsvz+%zg$s|()UhI#fUM{@$NCo9INjKusj;agZ(Sepm)X#A$g z{t!85ohM6#t0K?s24=;WWKF*BLh57`b5cjqiA(qWXhR&Hu!i$KJJAjrX&kU8va~C< zD^63Uiey7e{>^Zm1C{lRA)%{b5ZgG7gzD?mY`3J1-q|5is~>#d#R9b;2VU0kr(%O2o%gVjJhSvf-<3|jrtd|&BSJkepinC~nt zo95S2i&}dNzdEnrmoQ~F|0xlZ*@y&nNXoa+e_K0G#i1Fc`uPgJ`}#%GQHjEk84?Q8 zR%{;COa$hBHV8~a6%@6T2`Bp99x9CH-RJP&vA1XvM+A%2H2=obCG`Jcx-{^jhVJ%a z1$E~e2Rw>6c0ny>fyMaX&n1Z$jL`vTU^P;&*B{AQuQ0dSDglInLS$7j0)-2y<=Efs zXbDJf(Zv-Y9nKbDJo(MUG^a&pV7R~D`8X^J<*0oLO~)!gBxPGhL*S5NzNv%5-+slO z63dX&GPK0irL-mXiqsC7$ATkxKO$pK;48st10&- zQi$nFIk2nK6}Jn!(pR00{9AqGY(fbo)YaidqzmabJcW7t^llWp3Y$AuyJ4fdqtJ%- z($#xaL3?bzLT->!RcZ}(lP`+NfeBIggl|$1lYL{}S-lxCb@nsXYaQ$_{KFf|3Ptl+ z^~yYA6#i8WOQoQj`g%L@GE;+P*a`HHp{^RPTe2h~1A@~n&L4>!lctUcKSnV1GCTn!Hz-WxKSMQ%JdF3+@ogdpNuWKMCr^gn`W}%Y6 zfng_-{?;IaqjmM^s!%+5&ZyL=L^N99=j@nJTK25eH8wFOHhP1`Pait;4kx}Yw$m=n zb0%iVvTQm!q_{&FG*xRR^tocnGstDQrDbpzZbtY{R%wBnD2AdDgc7m%lyn8@xCM*V z9Ml7$jQ=s}e@4Ki@TM3bp z6Hs9tv(yDqHBFeAn-i+|F8V2u(H4*1aYZ!^j^GcXs+}gPS-loa8^~yX>}WIs_Y-A{ zpZ)pcXTx zFkY`7I9@ynQX`PmZc%QilOmguzU!;Kqw%Q5CZ?s(?5}O^;w%O{_~20Rs1_;M1{~mT zLk#@t`2+ps4hC71B8G{mT0oi8mq}JOO$@11KgDhFi_y(Gc6wnD{4e=fZ-9?3ut8Z< z6%8oFE-k9h5FsVX?od%vF;rjF%!ZE-m&fXwx6bn)>vFrqKAWF|2$mLwHj5MkhgGhJ zSqidf{M4``M6$9YgD)@wAAWUS;XSCObyoeP67b^Tc80F0uPhmt-O2OEyS>gIjFnjc z$IQgycsvTOJomI{Y>Hw_Ks}WIBqJz`)FVsi8um5r5LVj(`C_yAArHf_TSr<|N$P}s(d*CY!1S}#2Zvk%+&%%06Ob!(&Wfpg$ z`#mJoCQtqq;2vI2;D+|EB7X47tc#d1z^$n~o zJsmjX@!fkK1Ev{mDtxfsfmQ^8Q0&7{&}QKe*e0P-@g;Zy*DRkX8wkYYn>S%YVA1}B zL^PdlBnz?cE75%!-qQWJpN}XsnKJxq%CFPA{kGsgtG3CIegT<%v_S7A{5zGYB-)iOK`yB;0*VewQ1 ztO_Y!JpCP=lw(GvFVx6PfYWU&KT>nr_l;lWD@y0*6P1#OQsjvEO00k(Is?RU0TVID zP3c3*V+kSHw5x{v;-80GWtHWxHp0gc!?KN1;mEo(*%L&=iOYbV0kCloyqb4F*+19SK7 zk_^{!G;s^I`z2aF3$7LJTkNjq6%7}Jmiu-E&79a@oYj40YtH3N^fWRj=|LSGF#6qZ zFm9enyTWV->u;YW&rivqJ0dYk?lyyClOW|%#4M)`*dF(ZZ!=}dz2%Gvw|$&Iwj#Wu zGQmoL#U3KP;`IHq-Py=*{a(fC>B_h>bI?%-J3GPNh3Q1Ec|C0ToOd&o&etzu3l9`)OVWC-!_7e;9~y9iDz z2}h5FU~;If_k^&a27C85MXwM(iE>k^s2piL3EgZz2~4#F@8R%bM+m#Sr-a(Ybwi5; zKOnz~Tio)6ZC$ykr6mQ3$!hgNKyh;({#Z}8ogH&~+}toP^uk>@QX4ZOTz(=G+Vew* zrJ~TRc8hLbZf}ECG}8XV2!4NNZ|w8MF-#}hD^}*dQ}d-Ad!MBAk%ZTsioopXlG3M% z5!OlQ2NW;Bf!y-|clnU1_@=&(KNw~H6`NuuDkCPV7gHh+AJz$Pnl1&@Nl6CU93B7b z<)Es-RGxsr#7V4Q_!NPVkT5%nZpTx64l-h`jgYbtMp5iTlaz`hA~N|0MCK&6uGizx zUZeg{0)d~1i}K8DM_f*k;9jqcVE@TDjFOcqs*IP;#NbdnW%Gm5I?5qrV$W!bo=@St z%C77<OLshJP!M?&d=h9S&8QZsd}yH( zWw*18^$pA8!Ootm>9cR|c3D5N?`!fRRBRQ^#F)uP8wUQZg^*6wV7=U0##xfH|Bgyv zkzc-m!64TC70KkTc1*;>U+$I63)K|N-IdK6E_Fw^iZW@-FhQ)<9K4j7KD{zUgS#oJ zE+>8UWE2Tnxca#He9TKWqUHRIchlok@Qo-NyXA6n=l9=*GF)zSuy%HA1~p9QUc?$2 zI(*r2(yJtsSXPo1NG#%-*5nFdO{0?h1pvV}fq)KX7%Fug1w5*iM{Puks zDNwhnw?Y7rN(yj{P4-tocSo@I(60J*L*>L1v)!@T)bhEdASWa9*US1N1ukxsx{^RX zQkf;iJZ6WcLP@T9K14vRaL1wHVNi_=s#JX6!8DGWPj6&xONoaEn~+%(0y}1kCowK# z+B8J@VA{Cy%4+M1{d?+Q_xP{UZ-$eoyjFOTRwDH(4Hqd;OLk4r;u1wm6&>ydmaw36 zWd)CfP|+F|=_-Z3DtYFMQX{o=s&OYr;yw9y&to!^;(RD5gn|+ko|K%gY`i)U6l!Xd zC0nsNJ67b=?+irifZOKPht%yeF~%nqg5VQ>CXS8-k4-uS&5Sg9a~jyQgl&aW+W1Oc zqAFQ#FB?%1!07)O3cFC`wf+fYclI}ojvyxGyu^0hx9C$w=7AHnJX=Caj%H2?ap#8` z_xmW#z*;n3U!}ByLjjbNl7-$Vt++v_Hu91mOVCD)tmK$hS|Fp~2TCtFVX+_hGnlLR z8xrHI-I!v=7bi=UuH=mbhN^3HQ&&;V@K6lk`SGr@KEE6cf zvw7rLvDHZV6qqV1ZikFo1=59LC;~scN=wNF{pceWAR&L;=rqmPKg&v9U0)9|vHbsg zQIs{x$Ex3Frnx_A-)m;t17Kw?`exd!D<&_&QA84>;g*@(a!hi^IZ%7J%XTm*zK}Yrr zU*Fdg>Ip6v4s&|Sw1iFY-7OuybT%2kU7G#WU}OVojJ8 zRp`u>><*j>LkaeHJ%pbOF#r&6btu;Sl56D;OV=lW79MeCHpblL{FIYsgctuAIc&Sr zZ<1rUHtoe>*l3Y-!X9+cB7pr!%x2Z7|wik@Y43yg5aukX|-_;1v3)i_=$D6AB%tw*gg|wv`}T1 zBYXi3k1sZ~KNUOwWJd;E;)HLnZrFgGMFq`w-n?*jN>)IwBR{ZR;&B9^VdRxpR0+`mPprtgLl^y^sjfVcxzVpA0H2+nMv-<(^055~1sE2&wa@r?amMJ#;ve~=t2fxh&+{TESrrM*}>hF>mi*4 z-QpebBH`);VYFA=nFMz7)2J1N{GEnDqot@3s-u(#<@{HaWSz;>3W>gz2@wQ5*x$9e zUOUp&(JF_>D#Pg!%4xVdT?84~fM7~u)}<-V43G_Y`c+u%R;r5|MUXpsQo=8Rw?vXv zn=7e_9OF&skHGpXE68D2S=SSi4fApP+5Je-<(?63&Zbe(aA$W5mNoB6HM;|(vR7^I z9{kRxmwIg#wIZAGb(1+0p+7eaPr{w{9tO4|1r0;_XnRMyt0wul&f4N{P!Jjs$dFy@ z6PZ688&DJW{O3=J+eRc5W?=cmt7r+z^+G}wiN{pKttsQt!VAZFd2|tWz8D^ZngJ1% z2xvq>h{#UPKP}z^v1sZdGqc<8E!RlLQ-lC@OX>aMl7jMAfhK2YVU_!dSFWH?vA;{L z-}mq|lbFG546)j_e1g^N_($-zkPmHM5!~FPBdpNeyb)vsFZ!Oi>AmphV6F%CfJ5uG z1w2X*Z@#)lOwKJ^DZH)31kJfN&2!UPxbPkTiFp&Zf8Tka4MemkYA|2GkL&u4MJq`u zAf@O9115TIy^)H-8p|+$&42&H!{!vpBhel~l&Azr6B>G=GNeRqDNwCeNIWZMTn793 z#lPxW*oWlS5?Z3p@M(9*lhJV3_M$pjXYtJK;|j31!}9zLiR^7zW26$D#Or0eHNBNI zKu6#t20SOHJCI8eDh4kd5t(0&G+>KLFru8?iNkRJm1K(O%UWDq{Tn?8sw?@MlKtk_ zUmLqcLhOi$CeV-N;*FN~>kf{@-qM;jbBMprEtsh-7s0d4mCNmzPV*8pP#G`uCLIcPPgt9Kf(~#3@$)pyiOExWp4R+A3j;PN;+(V6pW$lZ8j&L(C=x{m`ma zEH4oJ9E;Jx%AIAUye$$^`*>AwQ4~AeOLZvwyX$6a>Q$B>Br00bSR-5rY5@xBTi#YY z?57n)g1;R^jEe7SvleOl5)U?1c=E_J^pBOke(%(iiCI+=$;egT7&HddRpZ3 zUR{Wn@m;i@#bk)N|=)G*D z^4ios;;tn4!fo_EVgO2ntGdgHiZ}h}=1hKNWsn0Gmsj7PB3h8Kzczcpi6tMy?ru-P z4#%U=RZ7GX;Pi=e^OPe61jH9R!s;da5yW-*lZ76X=BN72jQuY$MO-PT!I!zB+oCW+ zoL=H#gl@rco9L8F>V^{#ko~- z8ntrxA*_AbF~jPVB_&%~9*>4L_Qw@1jSyPa3h)<|`neI6np-q@`{G1c?MIM&g4d?K zE%)sde*a@IWP|USJ(cUtj~~5jj?=;M9Fm&EXlKXg@&}g0+%gcvreV4M=iO3q`x$I> z?{1S>J7LL36G$rsjCL27#%RC%|HrfOUq!P%c(Hs>G>oQ}_c<}}x$k3d6=~jwJnl-K zx}AUNIBx}PN97{q?cFs{)a6R@E5VKCu#tuYh@d1x!%D&8~#yM3lGR9W@ z`sSp;s5#`_%+BJ)Qkujj+f;Yrtw{8bFjJ7#py(85xRmW#x7! z+wY1qhY#3(4lO;e3#G z6v`aQHKi1i`dHQ^M^|4G=~gA}UGZNT_bVpUK~i%c+r*dYd&9DhxLfBh(Bsx*rM>0| zdShgoPa!4qt=cVbr>K@u_lG)DKFFwtTqR{hl4d4>xnql`F>R54$O5-?eX;m`7-E%f z@M8E5@REK@{P;XBL=-LV_?43~{iV&+uTj+H=57{(ShLRhfNs&rtF;4b`cTM=+P!TL zsX7;w(d8%nM8&OB0Ru4n^yy6F|5d8~ancN2lFipd!x+$C4r*^h3K?4i>OCA=N-*B= z2ak@cZUX7*Jm=foDn^zTlZ4cQ5so_r;~r;7QkKJ==*R{AxUN&Ln(das=J1u*20fft ziuXm=c(w+&MLZ4>5Z$<5xkp;}ZbcLsB`)LUG8}--y*aILmVBzPUrO`H*EI zbMM=yd}sb>Fu1iAUTg8nOpnT#=RB-dbAaT2na|gBUTtaIoQ+mp8W57;BSIyB<@NV? z(cRP3z>&mtK|`RTr4wbpSS6-1ImOL@{;eI}5e3e;`di8R)c^3Aor=dFNWV@uSibjf ze6)$~j$9mFw`Hdsbj`-BMWrNyBW|`b$45*HHhFmQfmjJyJg;rEWf3X|zq7J*|2Ns8 zX=9~<9rWdcECs1k*++?36Fj|W7KB_)*mGih!|1tL4dXK@7hxNo)Gq{X`4t5T6E za`bN}U2gD$(^mBiPb#*ypos2g^So$Np0#M4r>a!W@!^!BWc85&G>>b#L!^D#L~MkDPu# zmj+E$=!pvoi*bW?=cNhU|JQvB0+!)w- zd-Af(9@iYTRhg~hw+^RsISH<5#!fzntji}sVnb8XCPSXS(qMZ zn2|#rlhaapP*97xFYhC7x5%hk!Hzp$Ap1txUc9fM;XJ3d16>i=!L4+Pp5)L`!+ziW zK59bGJBs)tOuYvro<(eo{+l7z8Y}D;1wr_@A?D`GwtDsZvI&@{R!v!hDpTenmYOA2 zn%Ung`WBRB|2i^jOpNXaXs0`woy&vxyw>%&yp1s{vJ|)V^7aTDt;OirsbD>WIx3*DezPX#r` z6LPat_?V7829yGCPE>li>?ptl@CJp`mETJrCoX$tXd*4v2d@!B;Pih<1pkwo_WhCh z$0jl;kL3g9Z@s)Q6Tnu2^7NG6KQ-k8Ur4`i?))wjoJXVe>ywt67SEE1P+%3Y*Opu# zcsOh3E^oi#tI8LqJf$T%X;!H4sve1FRYbRB_+-JT`t6v68(AfZ+aJEg&lQ?g%Fu9A zyFViymBfxTJlkcGQYQAE^hJZe4Cri*ovJXG{Zm?f_fcDCXLt6#K%-39eU2oQ%xU#F zfF8HS_;oby7)w%Hhl5|H=sNbD+&Xn4P0G0=Jm_png7)zwgr0H4Vv0{8JSF;r}p z;@!^z&&%CD+|7?ATDZZrqf~EM#cXF=c(t^75V(i`a!8q#aF8{A9R^^R#*=bH5Gr#* zD5;liSO!&@&8pd|;E)0(1B-)^Ge#KXV1X7&RcfnuH+E9h`eZ6Rr-b08Uk>Fv>BR-3 zgqFu&A}GnybjAV%yQLZ1`I>C5Nj$@nF&7m4%>1a0#nHpI-txIz^wT`%*DW7@qvMGP zgx^*R^4FUO>gUwAas$ZZ@_B{}OBxX2+jnzt5q4lrV*h~{J*tRg@e z?D0YLS*M4*+huVznRPlbLV%H>RoL5x{sQs`1NY*L2GuLbiTU6ibqDXh9=s`Nx8H5A z*qd1zA?j93j%Ra|^NqCG%fM^~KJFWMtVGb!0Ss$1T!iEk+^)(ZZ` zaWPc=2HBU5hOxVZhK5l@As9<=;9XDsN^=i+Ooc1k0Sy&5+dibIsDCvq z0&yrC4^9DW_)gDGJ98{3cRN}R156qusQ7D8REglDYq{FYQbk!*gDEYyKogCGq_!vp zX4E(il@tSeD)r=}w1oq<027kDEc{R~n#2lze`;Ej1`gm%L23ytie^QELmZ#{ngrkG@En!UEhd7Iu}R4pBD=N@|eR z&;m&KBkR$!(Hqh-g#s01^@5EtVN&i6G%nmAPH{Ouq#lCHSKzI(=)IeZC&)x)Nl->7 ztcr@+88~P3W_fM~bPS?i11%Lq!)n)Wkxm?~Q70{ml$!cg?71&_+=MTKOD zlyT>PJl~*Ybnn3T+;x^8U)M^viaBHSV$JoqaE>z;3*{7<4@)3wV{L(PyB(yeINS9(2=ZjzhlTgxo|JV+g z(5AhiOM2WyE9ZD{VnyYzl+R!V8xI zHjNm2m&hCGVJ9Lp-3)A&6}g+wJ;*1c0Q7;hp=a8KLP|S6qu6v`F)yXz!1I{Ky&Iq0 z(b_E4>Qv!o=RjYsifj{G5u}%k9i;qsHt$<6tr({4{~6fb`w=zoYF#`%kCEJm(0zT~ z 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 literal 0 HcmV?d00001 From 61de5fe1d643daa565d5df51d6af83ec6421756d Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 11 Aug 2015 22:18:16 +0000 Subject: [PATCH 50/55] Updated from global requirements Change-Id: If5ac273fa9a93b6ee2f7ec1d190be3482ab67eb8 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a8bf5a76..a9120368 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,7 @@ monotonic>=0.3 # Apache-2.0 jsonschema!=2.5.0,<3.0.0,>=2.0.0 # For the state machine we run with -automaton>=0.2.0 # Apache-2.0 +automaton>=0.5.0 # Apache-2.0 # For common utilities oslo.utils>=1.9.0 # Apache-2.0 From 0f83585e6751a34a787e89379d5602cb519d3518 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 13 Aug 2015 20:22:35 +0000 Subject: [PATCH 51/55] Updated from global requirements Change-Id: I6431079cadca673c43e10ea96dfe692eaaf038d7 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a9120368..0aec8adb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,7 +41,7 @@ jsonschema!=2.5.0,<3.0.0,>=2.0.0 automaton>=0.5.0 # Apache-2.0 # For common utilities -oslo.utils>=1.9.0 # Apache-2.0 +oslo.utils>=2.0.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 # For lru caches and such From 022e330f075f78d61f89aad8705e78e389e8ee89 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 17 Aug 2015 22:41:20 +0000 Subject: [PATCH 52/55] Updated from global requirements Change-Id: Ib758b1fa69948a417595e2b2f5c6c737ed8a3b8b --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 37ee6bcc..2cb540c9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -23,7 +23,7 @@ redis>=2.10.0 # Used for testing database persistence backends. SQLAlchemy<1.1.0,>=0.9.7 -alembic>=0.7.2 +alembic>=0.8.0 psycopg2 PyMySQL>=0.6.2 # MIT License From af33579a85671f5eb43bb583bd108287015c4897 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 26 Aug 2015 14:14:31 +0000 Subject: [PATCH 53/55] Updated from global requirements Change-Id: Ieaf59b115064e4b67ab022fb464a68bf14a5b359 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0aec8adb..760dfcfa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. # See: https://bugs.launchpad.net/pbr/+bug/1384919 for why this is here... -pbr<2.0,>=1.4 +pbr<2.0,>=1.6 # Packages needed for using this library. From 9965e2e37ac2df19fcf9f0f5353b329e906299ee Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 1 Sep 2015 23:26:01 +0000 Subject: [PATCH 54/55] Updated from global requirements Change-Id: I8cc17030b5feff7c51bfca38c1d658fb54c39524 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 2cb540c9..5ab78eb4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -22,7 +22,7 @@ kazoo>=2.2 redis>=2.10.0 # Used for testing database persistence backends. -SQLAlchemy<1.1.0,>=0.9.7 +SQLAlchemy<1.1.0,>=0.9.9 alembic>=0.8.0 psycopg2 PyMySQL>=0.6.2 # MIT License From 52bd5e89fdddec49907a45e83a7a4b1abd1d1291 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 7 Sep 2015 15:18:38 +0000 Subject: [PATCH 55/55] Updated from global requirements Change-Id: I83143a099a633f6b79d23fabc45d96d881a2fd35 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 5ab78eb4..e9bca9bc 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -24,7 +24,7 @@ redis>=2.10.0 # Used for testing database persistence backends. SQLAlchemy<1.1.0,>=0.9.9 alembic>=0.8.0 -psycopg2 +psycopg2>=2.5 PyMySQL>=0.6.2 # MIT License # Used for making sure we still work with eventlet.