In order to move away from the existing flows having their own implementation of running, start moving the existing flows to be patterns that only structure tasks (and impose constraints about how the group of tasks can run) in useful ways. Let the concept of running those patterns be handled by an engine instead of being handled by the flow itself. This will allow for varying engines to be able to run flows in whichever way the engine chooses (as long as the constraints set up by the flow are observed). Currently threaded flow and graph flow are broken by this commit, since they have not been converted to being a structure of tasks + constraints. The existing engine has not yet been modified to run those structures either, work is underway to remediate this. Part of: blueprint patterns-and-engines Followup bugs that must be addressed: Bug: 1221448 Bug: 1221505 Change-Id: I3a8b96179f336d1defe269728ebae0caa3d832d7
546 lines
19 KiB
Python
546 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright (C) 2012 Yahoo! Inc. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
from multiprocessing import pool
|
|
import time
|
|
|
|
from taskflow.patterns import linear_flow as lf
|
|
from taskflow.patterns import unordered_flow as uf
|
|
|
|
from taskflow import exceptions
|
|
from taskflow.persistence import taskdetail
|
|
from taskflow import states
|
|
from taskflow import storage
|
|
from taskflow import task
|
|
from taskflow import test
|
|
|
|
from taskflow.engines.action_engine import engine as eng
|
|
|
|
|
|
class TestTask(task.Task):
|
|
|
|
def __init__(self, values=None, name=None,
|
|
sleep=None, provides=None, rebind=None):
|
|
super(TestTask, self).__init__(name=name, provides=provides,
|
|
rebind=rebind)
|
|
if values is None:
|
|
self.values = []
|
|
else:
|
|
self.values = values
|
|
self._sleep = sleep
|
|
|
|
def execute(self, **kwargs):
|
|
if self._sleep:
|
|
time.sleep(self._sleep)
|
|
self.values.append(self.name)
|
|
return 5
|
|
|
|
def revert(self, **kwargs):
|
|
if self._sleep:
|
|
time.sleep(self._sleep)
|
|
self.values.append(self.name + ' reverted(%s)'
|
|
% kwargs.get('result'))
|
|
|
|
|
|
class FailingTask(TestTask):
|
|
|
|
def execute(self, **kwargs):
|
|
if self._sleep:
|
|
time.sleep(self._sleep)
|
|
raise RuntimeError('Woot!')
|
|
|
|
|
|
class NeverRunningTask(task.Task):
|
|
def execute(self, **kwargs):
|
|
assert False, 'This method should not be called'
|
|
|
|
def revert(self, **kwargs):
|
|
assert False, 'This method should not be called'
|
|
|
|
|
|
class NastyTask(task.Task):
|
|
def execute(self, **kwargs):
|
|
pass
|
|
|
|
def revert(self, **kwargs):
|
|
raise RuntimeError('Gotcha!')
|
|
|
|
|
|
class MultiReturnTask(task.Task):
|
|
def execute(self, **kwargs):
|
|
return 12, 2, 1
|
|
|
|
|
|
class MultiargsTask(task.Task):
|
|
def execute(self, a, b, c):
|
|
return a + b + c
|
|
|
|
|
|
class EngineTestBase(object):
|
|
def setUp(self):
|
|
super(EngineTestBase, self).setUp()
|
|
self.values = []
|
|
|
|
def _make_engine(self, _flow, _flow_detail=None):
|
|
raise NotImplementedError()
|
|
|
|
|
|
class EngineTaskTest(EngineTestBase):
|
|
|
|
def test_run_task_as_flow(self):
|
|
flow = lf.Flow('test-1')
|
|
flow.add(TestTask(self.values, name='task1'))
|
|
engine = self._make_engine(flow)
|
|
engine.compile()
|
|
engine.run()
|
|
self.assertEquals(self.values, ['task1'])
|
|
|
|
@staticmethod
|
|
def _callback(state, values, details):
|
|
name = details.get('task_name', '<unknown>')
|
|
values.append('%s %s' % (name, state))
|
|
|
|
@staticmethod
|
|
def _flow_callback(state, values, details):
|
|
values.append('flow %s' % state)
|
|
|
|
def test_run_task_with_notifications(self):
|
|
flow = TestTask(self.values, name='task1')
|
|
engine = self._make_engine(flow)
|
|
engine.notifier.register('*', self._flow_callback,
|
|
kwargs={'values': self.values})
|
|
engine.task_notifier.register('*', self._callback,
|
|
kwargs={'values': self.values})
|
|
engine.run()
|
|
self.assertEquals(self.values,
|
|
['flow RUNNING',
|
|
'task1 RUNNING',
|
|
'task1',
|
|
'task1 SUCCESS',
|
|
'flow SUCCESS'])
|
|
|
|
def test_failing_task_with_notifications(self):
|
|
flow = FailingTask(self.values, 'fail')
|
|
engine = self._make_engine(flow)
|
|
engine.notifier.register('*', self._flow_callback,
|
|
kwargs={'values': self.values})
|
|
engine.task_notifier.register('*', self._callback,
|
|
kwargs={'values': self.values})
|
|
with self.assertRaisesRegexp(RuntimeError, '^Woot'):
|
|
engine.run()
|
|
self.assertEquals(self.values,
|
|
['flow RUNNING',
|
|
'fail RUNNING',
|
|
'fail FAILURE',
|
|
'flow REVERTING',
|
|
'fail REVERTING',
|
|
'fail reverted(Failure: RuntimeError: Woot!)',
|
|
'fail REVERTED',
|
|
'fail PENDING',
|
|
'flow REVERTED',
|
|
'flow FAILURE'])
|
|
|
|
def test_invalid_block_raises(self):
|
|
value = 'i am string, not task/flow, sorry'
|
|
with self.assertRaises(TypeError) as err:
|
|
engine = self._make_engine(value)
|
|
engine.compile()
|
|
self.assertIn(value, str(err.exception))
|
|
|
|
def test_save_as(self):
|
|
flow = TestTask(self.values, name='task1', provides='first_data')
|
|
engine = self._make_engine(flow)
|
|
engine.run()
|
|
self.assertEquals(self.values, ['task1'])
|
|
self.assertEquals(engine.storage.fetch_all(), {'first_data': 5})
|
|
|
|
def test_save_all_in_one(self):
|
|
flow = MultiReturnTask(provides='all_data')
|
|
engine = self._make_engine(flow)
|
|
engine.run()
|
|
self.assertEquals(engine.storage.fetch_all(),
|
|
{'all_data': (12, 2, 1)})
|
|
|
|
def test_save_several_values(self):
|
|
flow = MultiReturnTask(provides=('badger', 'mushroom', 'snake'))
|
|
engine = self._make_engine(flow)
|
|
engine.run()
|
|
self.assertEquals(engine.storage.fetch_all(), {
|
|
'badger': 12,
|
|
'mushroom': 2,
|
|
'snake': 1
|
|
})
|
|
|
|
def test_bad_save_as_value(self):
|
|
with self.assertRaises(TypeError):
|
|
TestTask(name='task1', provides=object())
|
|
|
|
def test_arguments_passing(self):
|
|
flow = MultiargsTask(provides='result')
|
|
engine = self._make_engine(flow)
|
|
engine.storage.inject({'a': 1, 'b': 4, 'c': 9, 'x': 17})
|
|
engine.run()
|
|
self.assertEquals(engine.storage.fetch_all(), {
|
|
'a': 1, 'b': 4, 'c': 9, 'x': 17,
|
|
'result': 14,
|
|
})
|
|
|
|
def test_arguments_missing(self):
|
|
flow = MultiargsTask(provides='result')
|
|
engine = self._make_engine(flow)
|
|
engine.storage.inject({'a': 1, 'b': 4, 'x': 17})
|
|
with self.assertRaisesRegexp(exceptions.NotFound,
|
|
"^Name 'c' is not mapped"):
|
|
engine.run()
|
|
|
|
def test_partial_arguments_mapping(self):
|
|
flow = MultiargsTask(name='task1',
|
|
provides='result',
|
|
rebind={'b': 'x'})
|
|
engine = self._make_engine(flow)
|
|
engine.storage.inject({'a': 1, 'b': 4, 'c': 9, 'x': 17})
|
|
engine.run()
|
|
self.assertEquals(engine.storage.fetch_all(), {
|
|
'a': 1, 'b': 4, 'c': 9, 'x': 17,
|
|
'result': 27,
|
|
})
|
|
|
|
def test_all_arguments_mapping(self):
|
|
flow = MultiargsTask(name='task1',
|
|
provides='result',
|
|
rebind=['x', 'y', 'z'])
|
|
engine = self._make_engine(flow)
|
|
engine.storage.inject({
|
|
'a': 1, 'b': 2, 'c': 3, 'x': 4, 'y': 5, 'z': 6
|
|
})
|
|
engine.run()
|
|
self.assertEquals(engine.storage.fetch_all(), {
|
|
'a': 1, 'b': 2, 'c': 3, 'x': 4, 'y': 5, 'z': 6,
|
|
'result': 15,
|
|
})
|
|
|
|
def test_invalid_argument_name_map(self):
|
|
flow = MultiargsTask(name='task1', provides='result',
|
|
rebind={'b': 'z'})
|
|
engine = self._make_engine(flow)
|
|
engine.storage.inject({'a': 1, 'b': 4, 'c': 9, 'x': 17})
|
|
with self.assertRaisesRegexp(exceptions.NotFound,
|
|
"Name 'z' is not mapped"):
|
|
engine.run()
|
|
|
|
def test_invalid_argument_name_list(self):
|
|
flow = MultiargsTask(name='task1',
|
|
provides='result',
|
|
rebind=['a', 'z', 'b'])
|
|
engine = self._make_engine(flow)
|
|
engine.storage.inject({'a': 1, 'b': 4, 'c': 9, 'x': 17})
|
|
with self.assertRaisesRegexp(exceptions.NotFound,
|
|
"Name 'z' is not mapped"):
|
|
engine.run()
|
|
|
|
def test_bad_rebind_args_value(self):
|
|
with self.assertRaises(TypeError):
|
|
TestTask(name='task1',
|
|
rebind=object())
|
|
|
|
|
|
class EngineLinearFlowTest(EngineTestBase):
|
|
|
|
def test_sequential_flow_one_task(self):
|
|
flow = lf.Flow('flow-1').add(
|
|
TestTask(self.values, name='task1')
|
|
)
|
|
self._make_engine(flow).run()
|
|
self.assertEquals(self.values, ['task1'])
|
|
|
|
def test_sequential_flow_two_tasks(self):
|
|
flow = lf.Flow('flow-2').add(
|
|
TestTask(self.values, name='task1'),
|
|
TestTask(self.values, name='task2')
|
|
)
|
|
self._make_engine(flow).run()
|
|
self.assertEquals(self.values, ['task1', 'task2'])
|
|
|
|
def test_revert_removes_data(self):
|
|
flow = lf.Flow('revert-removes').add(
|
|
TestTask(provides='one'),
|
|
MultiReturnTask(provides=('a', 'b', 'c')),
|
|
FailingTask(name='fail')
|
|
)
|
|
engine = self._make_engine(flow)
|
|
with self.assertRaisesRegexp(RuntimeError, '^Woot'):
|
|
engine.run()
|
|
self.assertEquals(engine.storage.fetch_all(), {})
|
|
|
|
def test_sequential_flow_nested_blocks(self):
|
|
flow = lf.Flow('nested-1').add(
|
|
TestTask(self.values, 'task1'),
|
|
lf.Flow('inner-1').add(
|
|
TestTask(self.values, 'task2')
|
|
)
|
|
)
|
|
self._make_engine(flow).run()
|
|
self.assertEquals(self.values, ['task1', 'task2'])
|
|
|
|
def test_revert_exception_is_reraised(self):
|
|
flow = lf.Flow('revert-1').add(
|
|
NastyTask(),
|
|
FailingTask(name='fail')
|
|
)
|
|
engine = self._make_engine(flow)
|
|
with self.assertRaisesRegexp(RuntimeError, '^Gotcha'):
|
|
engine.run()
|
|
|
|
def test_revert_not_run_task_is_not_reverted(self):
|
|
flow = lf.Flow('revert-not-run').add(
|
|
FailingTask(self.values, 'fail'),
|
|
NeverRunningTask(),
|
|
)
|
|
engine = self._make_engine(flow)
|
|
with self.assertRaisesRegexp(RuntimeError, '^Woot'):
|
|
engine.run()
|
|
self.assertEquals(self.values,
|
|
['fail reverted(Failure: RuntimeError: Woot!)'])
|
|
|
|
def test_correctly_reverts_children(self):
|
|
flow = lf.Flow('root-1').add(
|
|
TestTask(self.values, 'task1'),
|
|
lf.Flow('child-1').add(
|
|
TestTask(self.values, 'task2'),
|
|
FailingTask(self.values, 'fail')
|
|
)
|
|
)
|
|
engine = self._make_engine(flow)
|
|
with self.assertRaisesRegexp(RuntimeError, '^Woot'):
|
|
engine.run()
|
|
self.assertEquals(self.values,
|
|
['task1', 'task2',
|
|
'fail reverted(Failure: RuntimeError: Woot!)',
|
|
'task2 reverted(5)', 'task1 reverted(5)'])
|
|
|
|
|
|
class EngineParallelFlowTest(EngineTestBase):
|
|
|
|
def test_parallel_flow_one_task(self):
|
|
flow = uf.Flow('p-1').add(
|
|
TestTask(self.values, name='task1', sleep=0.01)
|
|
)
|
|
self._make_engine(flow).run()
|
|
self.assertEquals(self.values, ['task1'])
|
|
|
|
def test_parallel_flow_two_tasks(self):
|
|
flow = uf.Flow('p-2').add(
|
|
TestTask(self.values, name='task1', sleep=0.01),
|
|
TestTask(self.values, name='task2', sleep=0.01)
|
|
)
|
|
self._make_engine(flow).run()
|
|
|
|
result = set(self.values)
|
|
self.assertEquals(result, set(['task1', 'task2']))
|
|
|
|
def test_parallel_revert_common(self):
|
|
flow = uf.Flow('p-r-3').add(
|
|
TestTask(self.values, name='task1'),
|
|
FailingTask(self.values, sleep=0.01),
|
|
TestTask(self.values, name='task2')
|
|
)
|
|
engine = self._make_engine(flow)
|
|
with self.assertRaisesRegexp(RuntimeError, '^Woot'):
|
|
engine.run()
|
|
|
|
def test_parallel_revert_exception_is_reraised(self):
|
|
flow = uf.Flow('p-r-r').add(
|
|
TestTask(self.values, name='task1'),
|
|
NastyTask(),
|
|
FailingTask(self.values, sleep=0.1)
|
|
)
|
|
engine = self._make_engine(flow)
|
|
with self.assertRaisesRegexp(RuntimeError, '^Gotcha'):
|
|
engine.run()
|
|
|
|
def test_sequential_flow_two_tasks_with_resumption(self):
|
|
flow = lf.Flow('lf-2-r').add(
|
|
TestTask(self.values, name='task1', provides='x1'),
|
|
TestTask(self.values, name='task2', provides='x2')
|
|
)
|
|
|
|
# Create FlowDetail as if we already run task1
|
|
fd = storage.temporary_flow_detail()
|
|
td = taskdetail.TaskDetail(name='task1', uuid='42')
|
|
td.state = states.SUCCESS
|
|
td.results = 17
|
|
fd.add(td)
|
|
fd.save()
|
|
td.save()
|
|
|
|
engine = self._make_engine(flow, fd)
|
|
engine.run()
|
|
self.assertEquals(self.values, ['task2'])
|
|
self.assertEquals(engine.storage.fetch_all(),
|
|
{'x1': 17, 'x2': 5})
|
|
|
|
|
|
class SingleThreadedEngineTest(EngineTaskTest,
|
|
EngineLinearFlowTest,
|
|
EngineParallelFlowTest,
|
|
test.TestCase):
|
|
def _make_engine(self, flow, flow_detail=None):
|
|
return eng.SingleThreadedActionEngine(flow, flow_detail=flow_detail)
|
|
|
|
|
|
class MultiThreadedEngineTest(EngineTaskTest,
|
|
EngineLinearFlowTest,
|
|
EngineParallelFlowTest,
|
|
test.TestCase):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.thread_pool = pool.ThreadPool()
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.thread_pool.close()
|
|
cls.thread_pool.join()
|
|
|
|
def _make_engine(self, flow, flow_detail=None):
|
|
return eng.MultiThreadedActionEngine(flow, flow_detail=flow_detail,
|
|
thread_pool=self.thread_pool)
|
|
|
|
def test_using_common_pool(self):
|
|
flow = TestTask(self.values, name='task1')
|
|
thread_pool = pool.ThreadPool()
|
|
e1 = eng.MultiThreadedActionEngine(flow, thread_pool=thread_pool)
|
|
e2 = eng.MultiThreadedActionEngine(flow, thread_pool=thread_pool)
|
|
self.assertIs(e1.thread_pool, e2.thread_pool)
|
|
|
|
def test_parallel_revert_specific(self):
|
|
flow = uf.Flow('p-r-r').add(
|
|
TestTask(self.values, name='task1', sleep=0.01),
|
|
FailingTask(sleep=0.01),
|
|
TestTask(self.values, name='task2', sleep=0.01)
|
|
)
|
|
engine = self._make_engine(flow)
|
|
with self.assertRaisesRegexp(RuntimeError, '^Woot'):
|
|
engine.run()
|
|
result = set(self.values)
|
|
self.assertEquals(result,
|
|
set(['task1', 'task2',
|
|
'task2 reverted(5)', 'task1 reverted(5)']))
|
|
|
|
def test_parallel_revert_exception_is_reraised_(self):
|
|
flow = uf.Flow('p-r-reraise').add(
|
|
TestTask(self.values, name='task1', sleep=0.01),
|
|
NastyTask(),
|
|
FailingTask(sleep=0.01),
|
|
TestTask() # this should not get reverted
|
|
)
|
|
engine = self._make_engine(flow)
|
|
with self.assertRaisesRegexp(RuntimeError, '^Gotcha'):
|
|
engine.run()
|
|
result = set(self.values)
|
|
self.assertEquals(result, set(['task1', 'task1 reverted(5)']))
|
|
|
|
def test_nested_parallel_revert_exception_is_reraised(self):
|
|
flow = uf.Flow('p-root').add(
|
|
TestTask(self.values, name='task1'),
|
|
TestTask(self.values, name='task2'),
|
|
uf.Flow('p-inner').add(
|
|
TestTask(self.values, name='task3', sleep=0.1),
|
|
NastyTask(),
|
|
FailingTask(sleep=0.01)
|
|
)
|
|
)
|
|
engine = self._make_engine(flow)
|
|
with self.assertRaisesRegexp(RuntimeError, '^Gotcha'):
|
|
engine.run()
|
|
result = set(self.values)
|
|
self.assertEquals(result, set(['task1', 'task1 reverted(5)',
|
|
'task2', 'task2 reverted(5)',
|
|
'task3', 'task3 reverted(5)']))
|
|
|
|
def test_parallel_revert_exception_do_not_revert_linear_tasks(self):
|
|
flow = lf.Flow('l-root').add(
|
|
TestTask(self.values, name='task1'),
|
|
TestTask(self.values, name='task2'),
|
|
uf.Flow('p-inner').add(
|
|
TestTask(self.values, name='task3', sleep=0.1),
|
|
NastyTask(),
|
|
FailingTask(sleep=0.01)
|
|
)
|
|
)
|
|
engine = self._make_engine(flow)
|
|
with self.assertRaisesRegexp(RuntimeError, '^Gotcha'):
|
|
engine.run()
|
|
result = set(self.values)
|
|
self.assertEquals(result, set(['task1', 'task2',
|
|
'task3', 'task3 reverted(5)']))
|
|
|
|
def test_parallel_nested_to_linear_revert(self):
|
|
flow = lf.Flow('l-root').add(
|
|
TestTask(self.values, name='task1'),
|
|
TestTask(self.values, name='task2'),
|
|
uf.Flow('p-inner').add(
|
|
TestTask(self.values, name='task3', sleep=0.1),
|
|
FailingTask(sleep=0.01)
|
|
)
|
|
)
|
|
engine = self._make_engine(flow)
|
|
with self.assertRaisesRegexp(RuntimeError, '^Woot'):
|
|
engine.run()
|
|
result = set(self.values)
|
|
self.assertEquals(result, set(['task1', 'task1 reverted(5)',
|
|
'task2', 'task2 reverted(5)',
|
|
'task3', 'task3 reverted(5)']))
|
|
|
|
def test_linear_nested_to_parallel_revert(self):
|
|
flow = uf.Flow('p-root').add(
|
|
TestTask(self.values, name='task1'),
|
|
TestTask(self.values, name='task2'),
|
|
lf.Flow('l-inner').add(
|
|
TestTask(self.values, name='task3', sleep=0.1),
|
|
FailingTask(self.values, name='fail', sleep=0.01)
|
|
)
|
|
)
|
|
engine = self._make_engine(flow)
|
|
with self.assertRaisesRegexp(RuntimeError, '^Woot'):
|
|
engine.run()
|
|
result = set(self.values)
|
|
self.assertEquals(result,
|
|
set(['task1', 'task1 reverted(5)',
|
|
'task2', 'task2 reverted(5)',
|
|
'task3', 'task3 reverted(5)',
|
|
'fail reverted(Failure: RuntimeError: Woot!)']))
|
|
|
|
def test_linear_nested_to_parallel_revert_exception(self):
|
|
flow = uf.Flow('p-root').add(
|
|
TestTask(self.values, name='task1', sleep=0.01),
|
|
TestTask(self.values, name='task2', sleep=0.01),
|
|
lf.Flow('l-inner').add(
|
|
TestTask(self.values, name='task3'),
|
|
NastyTask(),
|
|
FailingTask(sleep=0.01)
|
|
)
|
|
)
|
|
engine = self._make_engine(flow)
|
|
with self.assertRaisesRegexp(RuntimeError, '^Gotcha'):
|
|
engine.run()
|
|
result = set(self.values)
|
|
self.assertEquals(result, set(['task1', 'task1 reverted(5)',
|
|
'task2', 'task2 reverted(5)',
|
|
'task3']))
|