Add cancelled state to executions
Allow workflow executions to be cancelled and propagate state up for subworkflows and with-items task. Cancelled workflow executions are subject to expiration. Change-Id: I74925105420e84c0b164f83b76edaa9b1612b5d5 Implements: blueprint mistral-cancel-state
This commit is contained in:
parent
001598237c
commit
82ab51b0c1
@ -1,6 +1,7 @@
|
||||
# Copyright 2013 - Mirantis, Inc.
|
||||
# Copyright 2015 - StackStorm, Inc.
|
||||
# Copyright 2015 Huawei Technologies Co., Ltd.
|
||||
# Copyright 2016 - Brocade Communications Systems, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -33,8 +34,17 @@ from mistral.workflow import states
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
STATE_TYPES = wtypes.Enum(str, states.IDLE, states.RUNNING, states.SUCCESS,
|
||||
states.ERROR, states.PAUSED)
|
||||
|
||||
STATE_TYPES = wtypes.Enum(
|
||||
str,
|
||||
states.IDLE,
|
||||
states.RUNNING,
|
||||
states.SUCCESS,
|
||||
states.ERROR,
|
||||
states.PAUSED,
|
||||
states.CANCELLED
|
||||
)
|
||||
|
||||
|
||||
# TODO(rakhmerov): Make sure to make all needed renaming on public API.
|
||||
|
||||
@ -118,14 +128,14 @@ class ExecutionsController(rest.RestController):
|
||||
)
|
||||
|
||||
if delta.get('state'):
|
||||
if delta.get('state') == states.PAUSED:
|
||||
if states.is_paused(delta.get('state')):
|
||||
wf_ex = rpc.get_engine_client().pause_workflow(id)
|
||||
elif delta.get('state') == states.RUNNING:
|
||||
wf_ex = rpc.get_engine_client().resume_workflow(
|
||||
id,
|
||||
env=delta.get('env')
|
||||
)
|
||||
elif delta.get('state') in [states.SUCCESS, states.ERROR]:
|
||||
elif states.is_completed(delta.get('state')):
|
||||
msg = wf_ex.state_info if wf_ex.state_info else None
|
||||
wf_ex = rpc.get_engine_client().stop_workflow(
|
||||
id,
|
||||
@ -141,7 +151,8 @@ class ExecutionsController(rest.RestController):
|
||||
states.RUNNING,
|
||||
states.PAUSED,
|
||||
states.SUCCESS,
|
||||
states.ERROR
|
||||
states.ERROR,
|
||||
states.CANCELLED
|
||||
])
|
||||
)
|
||||
)
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright 2015 - Mirantis, Inc.
|
||||
# Copyright 2015 - StackStorm, Inc.
|
||||
# Copyright 2016 - Brocade Communications Systems, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -940,7 +941,8 @@ def get_expired_executions(time, session=None):
|
||||
query = query.filter(
|
||||
sa.or_(
|
||||
models.WorkflowExecution.state == "SUCCESS",
|
||||
models.WorkflowExecution.state == "ERROR"
|
||||
models.WorkflowExecution.state == "ERROR",
|
||||
models.WorkflowExecution.state == "CANCELLED"
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -193,8 +193,13 @@ class PythonAction(Action):
|
||||
|
||||
prev_state = self.action_ex.state
|
||||
|
||||
self.action_ex.state = (states.SUCCESS if result.is_success()
|
||||
else states.ERROR)
|
||||
if result.is_success():
|
||||
self.action_ex.state = states.SUCCESS
|
||||
elif result.is_cancel():
|
||||
self.action_ex.state = states.CANCELLED
|
||||
else:
|
||||
self.action_ex.state = states.ERROR
|
||||
|
||||
self.action_ex.output = self._prepare_output(result)
|
||||
self.action_ex.accepted = True
|
||||
|
||||
|
@ -370,20 +370,24 @@ class WithItemsTask(RegularTask):
|
||||
assert self.task_ex
|
||||
|
||||
state = action_ex.state
|
||||
|
||||
# TODO(rakhmerov): Here we can define more informative messages
|
||||
# cases when action is successful and when it's not. For example,
|
||||
# in state_info we can specify the cause action.
|
||||
state_info = (None if state == states.SUCCESS
|
||||
else action_ex.output.get('result'))
|
||||
# The use of action_ex.output.get('result') for state_info is not
|
||||
# accurate because there could be action executions that had
|
||||
# failed or was cancelled prior to this action execution.
|
||||
state_info = {
|
||||
states.SUCCESS: None,
|
||||
states.ERROR: 'One or more action executions had failed.',
|
||||
states.CANCELLED: 'One or more action executions was cancelled.'
|
||||
}
|
||||
|
||||
with_items.increase_capacity(self.task_ex)
|
||||
|
||||
if with_items.is_completed(self.task_ex):
|
||||
self.complete(
|
||||
with_items.get_final_state(self.task_ex),
|
||||
state_info
|
||||
)
|
||||
|
||||
state = with_items.get_final_state(self.task_ex)
|
||||
self.complete(state, state_info[state])
|
||||
return
|
||||
|
||||
if (with_items.has_more_iterations(self.task_ex)
|
||||
|
@ -50,11 +50,26 @@ def stop_workflow(wf_ex, state, msg=None):
|
||||
# with ERROR state.
|
||||
wf.stop(state, msg)
|
||||
|
||||
# Cancels subworkflows.
|
||||
if state == states.CANCELLED:
|
||||
for task_ex in wf_ex.task_executions:
|
||||
sub_wf_exs = db_api.get_workflow_executions(
|
||||
task_execution_id=task_ex.id
|
||||
)
|
||||
|
||||
for sub_wf_ex in sub_wf_exs:
|
||||
if not states.is_completed(sub_wf_ex.state):
|
||||
stop_workflow(sub_wf_ex, state, msg=msg)
|
||||
|
||||
|
||||
def fail_workflow(wf_ex, msg=None):
|
||||
stop_workflow(wf_ex, states.ERROR, msg)
|
||||
|
||||
|
||||
def cancel_workflow(wf_ex, msg=None):
|
||||
stop_workflow(wf_ex, states.CANCELLED, msg)
|
||||
|
||||
|
||||
@profiler.trace('workflow-handler-on-task-complete')
|
||||
def on_task_complete(task_ex):
|
||||
wf_ex = task_ex.workflow_execution
|
||||
|
@ -109,6 +109,8 @@ class Workflow(object):
|
||||
return self._succeed_workflow(final_context, msg)
|
||||
elif state == states.ERROR:
|
||||
return self._fail_workflow(msg)
|
||||
elif state == states.CANCELLED:
|
||||
return self._cancel_workflow(msg)
|
||||
|
||||
@profiler.trace('workflow-on-task-complete')
|
||||
def on_task_complete(self, task_ex):
|
||||
@ -212,7 +214,7 @@ class Workflow(object):
|
||||
|
||||
# Workflow result should be accepted by parent workflows (if any)
|
||||
# only if it completed successfully or failed.
|
||||
self.wf_ex.accepted = state in (states.SUCCESS, states.ERROR)
|
||||
self.wf_ex.accepted = states.is_completed(state)
|
||||
|
||||
if recursive and self.wf_ex.task_execution_id:
|
||||
parent_task_ex = db_api.get_task_execution(
|
||||
@ -275,7 +277,11 @@ class Workflow(object):
|
||||
|
||||
wf_ctrl = wf_base.get_controller(self.wf_ex, self.wf_spec)
|
||||
|
||||
if wf_ctrl.all_errors_handled():
|
||||
if wf_ctrl.any_cancels():
|
||||
self._cancel_workflow(
|
||||
_build_cancel_info_message(wf_ctrl, self.wf_ex)
|
||||
)
|
||||
elif wf_ctrl.all_errors_handled():
|
||||
self._succeed_workflow(wf_ctrl.evaluate_workflow_final_context())
|
||||
else:
|
||||
self._fail_workflow(_build_fail_info_message(wf_ctrl, self.wf_ex))
|
||||
@ -310,6 +316,24 @@ class Workflow(object):
|
||||
if self.wf_ex.task_execution_id:
|
||||
self._schedule_send_result_to_parent_workflow()
|
||||
|
||||
def _cancel_workflow(self, msg):
|
||||
if states.is_completed(self.wf_ex.state):
|
||||
return
|
||||
|
||||
self.set_state(states.CANCELLED, state_info=msg)
|
||||
|
||||
# When we set an ERROR state we should safely set output value getting
|
||||
# w/o exceptions due to field size limitations.
|
||||
msg = utils.cut_by_kb(
|
||||
msg,
|
||||
cfg.CONF.engine.execution_field_size_limit_kb
|
||||
)
|
||||
|
||||
self.wf_ex.output = {'result': msg}
|
||||
|
||||
if self.wf_ex.task_execution_id:
|
||||
self._schedule_send_result_to_parent_workflow()
|
||||
|
||||
def _schedule_send_result_to_parent_workflow(self):
|
||||
scheduler.schedule_call(
|
||||
None,
|
||||
@ -359,6 +383,16 @@ def _send_result_to_parent_workflow(wf_ex_id):
|
||||
wf_ex.id,
|
||||
wf_utils.Result(error=err_msg)
|
||||
)
|
||||
elif wf_ex.state == states.CANCELLED:
|
||||
err_msg = (
|
||||
wf_ex.state_info or
|
||||
'Cancelled subworkflow [execution_id=%s]' % wf_ex.id
|
||||
)
|
||||
|
||||
rpc.get_engine_client().on_action_complete(
|
||||
wf_ex.id,
|
||||
wf_utils.Result(error=err_msg, cancel=True)
|
||||
)
|
||||
|
||||
|
||||
def _build_fail_info_message(wf_ctrl, wf_ex):
|
||||
@ -389,3 +423,14 @@ def _build_fail_info_message(wf_ctrl, wf_ex):
|
||||
)
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def _build_cancel_info_message(wf_ctrl, wf_ex):
|
||||
# Try to find where cancel is exactly.
|
||||
cancelled_tasks = sorted(
|
||||
wf_utils.find_cancelled_task_executions(wf_ex),
|
||||
key=lambda t: t.name
|
||||
)
|
||||
|
||||
return ('Cancelled tasks: %s' %
|
||||
', '.join([t.name for t in cancelled_tasks]))
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Copyright 2013 - Mirantis, Inc.
|
||||
# Copyright 2015 - StackStorm, Inc.
|
||||
# Copyright 2015 Huawei Technologies Co., Ltd.
|
||||
# Copyright 2016 - Brocade Communications Systems, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -201,6 +202,39 @@ class TestExecutionsController(base.APITest):
|
||||
self.assertDictEqual(expected_exec, resp.json)
|
||||
mock_stop_wf.assert_called_once_with('123', 'ERROR', 'Force')
|
||||
|
||||
@mock.patch.object(
|
||||
db_api,
|
||||
'ensure_workflow_execution_exists',
|
||||
mock.MagicMock(return_value=None)
|
||||
)
|
||||
@mock.patch.object(rpc.EngineClient, 'stop_workflow')
|
||||
def test_put_state_cancelled(self, mock_stop_wf):
|
||||
update_exec = {
|
||||
'id': WF_EX['id'],
|
||||
'state': states.CANCELLED,
|
||||
'state_info': 'Cancelled by user.'
|
||||
}
|
||||
|
||||
wf_ex = copy.deepcopy(WF_EX)
|
||||
wf_ex['state'] = states.CANCELLED
|
||||
wf_ex['state_info'] = 'Cancelled by user.'
|
||||
mock_stop_wf.return_value = wf_ex
|
||||
|
||||
resp = self.app.put_json('/v2/executions/123', update_exec)
|
||||
|
||||
expected_exec = copy.deepcopy(WF_EX_JSON_WITH_DESC)
|
||||
expected_exec['state'] = states.CANCELLED
|
||||
expected_exec['state_info'] = 'Cancelled by user.'
|
||||
|
||||
self.assertEqual(200, resp.status_int)
|
||||
self.assertDictEqual(expected_exec, resp.json)
|
||||
|
||||
mock_stop_wf.assert_called_once_with(
|
||||
'123',
|
||||
'CANCELLED',
|
||||
'Cancelled by user.'
|
||||
)
|
||||
|
||||
@mock.patch.object(
|
||||
db_api,
|
||||
'ensure_workflow_execution_exists',
|
||||
@ -228,6 +262,33 @@ class TestExecutionsController(base.APITest):
|
||||
self.assertDictEqual(expected_exec, resp.json)
|
||||
mock_resume_wf.assert_called_once_with('123', env=None)
|
||||
|
||||
@mock.patch.object(
|
||||
db_api,
|
||||
'ensure_workflow_execution_exists',
|
||||
mock.MagicMock(return_value=None)
|
||||
)
|
||||
def test_put_invalid_state(self):
|
||||
invalid_states = [states.IDLE, states.WAITING, states.RUNNING_DELAYED]
|
||||
|
||||
for state in invalid_states:
|
||||
update_exec = {
|
||||
'id': WF_EX['id'],
|
||||
'state': state
|
||||
}
|
||||
|
||||
resp = self.app.put_json(
|
||||
'/v2/executions/123',
|
||||
update_exec,
|
||||
expect_errors=True
|
||||
)
|
||||
|
||||
self.assertEqual(400, resp.status_int)
|
||||
|
||||
self.assertIn(
|
||||
'Cannot change state to %s.' % state,
|
||||
resp.json['faultstring']
|
||||
)
|
||||
|
||||
@mock.patch.object(
|
||||
db_api,
|
||||
'ensure_workflow_execution_exists',
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright 2014 - Mirantis, Inc.
|
||||
# Copyright 2015 - StackStorm, Inc.
|
||||
# Copyright 2016 - Brocade Communications Systems, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -219,6 +220,10 @@ class EngineTestCase(base.DbTestCase):
|
||||
timeout=DEFAULT_TIMEOUT):
|
||||
self.await_execution_state(ex_id, states.PAUSED, delay, timeout)
|
||||
|
||||
def await_execution_cancelled(self, ex_id, delay=DEFAULT_DELAY,
|
||||
timeout=DEFAULT_TIMEOUT):
|
||||
self.await_execution_state(ex_id, states.CANCELLED, delay, timeout)
|
||||
|
||||
# Various methods for action execution objects.
|
||||
|
||||
def is_action_success(self, a_ex_id):
|
||||
|
516
mistral/tests/unit/engine/test_workflow_cancel.py
Normal file
516
mistral/tests/unit/engine/test_workflow_cancel.py
Normal file
@ -0,0 +1,516 @@
|
||||
# Copyright 2015 - StackStorm, Inc.
|
||||
# Copyright 2016 - Brocade Communications Systems, Inc.
|
||||
#
|
||||
# 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 mistral.db.v2 import api as db_api
|
||||
from mistral.services import workbooks as wb_service
|
||||
from mistral.services import workflows as wf_service
|
||||
from mistral.tests.unit.engine import base
|
||||
from mistral.workflow import states
|
||||
|
||||
|
||||
class WorkflowCancelTest(base.EngineTestCase):
|
||||
|
||||
def test_cancel_workflow(self):
|
||||
workflow = """
|
||||
version: '2.0'
|
||||
|
||||
wf:
|
||||
type: direct
|
||||
tasks:
|
||||
task1:
|
||||
action: std.echo output="Echo"
|
||||
on-complete:
|
||||
- task2
|
||||
|
||||
task2:
|
||||
action: std.echo output="foo"
|
||||
wait-before: 3
|
||||
"""
|
||||
|
||||
wf_service.create_workflows(workflow)
|
||||
wf_ex = self.engine.start_workflow('wf', {})
|
||||
|
||||
self.engine.stop_workflow(
|
||||
wf_ex.id,
|
||||
states.CANCELLED,
|
||||
"Cancelled by user."
|
||||
)
|
||||
|
||||
self.await_execution_cancelled(wf_ex.id)
|
||||
|
||||
wf_ex = db_api.get_execution(wf_ex.id)
|
||||
|
||||
task_1_ex = self._assert_single_item(
|
||||
wf_ex.task_executions,
|
||||
name='task1'
|
||||
)
|
||||
|
||||
self.await_execution_success(task_1_ex.id)
|
||||
|
||||
wf_ex = db_api.get_execution(wf_ex.id)
|
||||
|
||||
task_1_ex = self._assert_single_item(
|
||||
wf_ex.task_executions,
|
||||
name='task1'
|
||||
)
|
||||
|
||||
self.assertEqual(states.CANCELLED, wf_ex.state)
|
||||
self.assertEqual("Cancelled by user.", wf_ex.state_info)
|
||||
self.assertEqual(1, len(wf_ex.task_executions))
|
||||
self.assertEqual(states.SUCCESS, task_1_ex.state)
|
||||
|
||||
def test_cancel_paused_workflow(self):
|
||||
workflow = """
|
||||
version: '2.0'
|
||||
|
||||
wf:
|
||||
type: direct
|
||||
tasks:
|
||||
task1:
|
||||
action: std.echo output="Echo"
|
||||
on-complete:
|
||||
- task2
|
||||
|
||||
task2:
|
||||
action: std.echo output="foo"
|
||||
wait-before: 3
|
||||
"""
|
||||
|
||||
wf_service.create_workflows(workflow)
|
||||
wf_ex = self.engine.start_workflow('wf', {})
|
||||
|
||||
self.engine.pause_workflow(wf_ex.id)
|
||||
|
||||
self.await_execution_paused(wf_ex.id)
|
||||
|
||||
self.engine.stop_workflow(
|
||||
wf_ex.id,
|
||||
states.CANCELLED,
|
||||
"Cancelled by user."
|
||||
)
|
||||
|
||||
self.await_execution_cancelled(wf_ex.id)
|
||||
|
||||
wf_ex = db_api.get_execution(wf_ex.id)
|
||||
|
||||
task_1_ex = self._assert_single_item(
|
||||
wf_ex.task_executions,
|
||||
name='task1'
|
||||
)
|
||||
|
||||
self.await_execution_success(task_1_ex.id)
|
||||
|
||||
wf_ex = db_api.get_execution(wf_ex.id)
|
||||
|
||||
task_1_ex = self._assert_single_item(
|
||||
wf_ex.task_executions,
|
||||
name='task1'
|
||||
)
|
||||
|
||||
self.assertEqual(states.CANCELLED, wf_ex.state)
|
||||
self.assertEqual("Cancelled by user.", wf_ex.state_info)
|
||||
self.assertEqual(1, len(wf_ex.task_executions))
|
||||
self.assertEqual(states.SUCCESS, task_1_ex.state)
|
||||
|
||||
def test_cancel_completed_workflow(self):
|
||||
workflow = """
|
||||
version: '2.0'
|
||||
|
||||
wf:
|
||||
type: direct
|
||||
tasks:
|
||||
task1:
|
||||
action: std.echo output="Echo"
|
||||
"""
|
||||
|
||||
wf_service.create_workflows(workflow)
|
||||
wf_ex = self.engine.start_workflow('wf', {})
|
||||
|
||||
self.await_execution_success(wf_ex.id)
|
||||
|
||||
self.engine.stop_workflow(
|
||||
wf_ex.id,
|
||||
states.CANCELLED,
|
||||
"Cancelled by user."
|
||||
)
|
||||
|
||||
wf_ex = db_api.get_execution(wf_ex.id)
|
||||
|
||||
task_1_ex = self._assert_single_item(
|
||||
wf_ex.task_executions,
|
||||
name='task1'
|
||||
)
|
||||
|
||||
self.assertEqual(states.SUCCESS, wf_ex.state)
|
||||
self.assertIsNone(wf_ex.state_info)
|
||||
self.assertEqual(1, len(wf_ex.task_executions))
|
||||
self.assertEqual(states.SUCCESS, task_1_ex.state)
|
||||
|
||||
def test_cancel_parent_workflow(self):
|
||||
workbook = """
|
||||
version: '2.0'
|
||||
|
||||
name: wb
|
||||
|
||||
workflows:
|
||||
wf:
|
||||
type: direct
|
||||
tasks:
|
||||
taskx:
|
||||
workflow: subwf
|
||||
|
||||
subwf:
|
||||
type: direct
|
||||
tasks:
|
||||
task1:
|
||||
action: std.echo output="Echo"
|
||||
on-complete:
|
||||
- task2
|
||||
|
||||
task2:
|
||||
action: std.echo output="foo"
|
||||
wait-before: 2
|
||||
"""
|
||||
|
||||
wb_service.create_workbook_v2(workbook)
|
||||
wf_ex = self.engine.start_workflow('wb.wf', {})
|
||||
|
||||
self.engine.stop_workflow(
|
||||
wf_ex.id,
|
||||
states.CANCELLED,
|
||||
"Cancelled by user."
|
||||
)
|
||||
|
||||
self.await_execution_cancelled(wf_ex.id)
|
||||
|
||||
wf_ex = db_api.get_execution(wf_ex.id)
|
||||
task_ex = self._assert_single_item(wf_ex.task_executions, name='taskx')
|
||||
|
||||
self.await_execution_cancelled(task_ex.id)
|
||||
|
||||
wf_ex = db_api.get_execution(wf_ex.id)
|
||||
task_ex = self._assert_single_item(wf_ex.task_executions, name='taskx')
|
||||
action_exs = db_api.get_action_executions(task_execution_id=task_ex.id)
|
||||
|
||||
self.assertEqual(states.CANCELLED, wf_ex.state)
|
||||
self.assertEqual("Cancelled by user.", wf_ex.state_info)
|
||||
self.assertEqual(states.CANCELLED, task_ex.state)
|
||||
self.assertEqual("Cancelled by user.", task_ex.state_info)
|
||||
self.assertEqual(1, len(action_exs))
|
||||
self.assertEqual(states.CANCELLED, action_exs[0].state)
|
||||
self.assertEqual("Cancelled by user.", action_exs[0].state_info)
|
||||
|
||||
def test_cancel_child_workflow(self):
|
||||
workbook = """
|
||||
version: '2.0'
|
||||
|
||||
name: wb
|
||||
|
||||
workflows:
|
||||
wf:
|
||||
type: direct
|
||||
tasks:
|
||||
taskx:
|
||||
workflow: subwf
|
||||
|
||||
subwf:
|
||||
type: direct
|
||||
tasks:
|
||||
task1:
|
||||
action: std.echo output="Echo"
|
||||
on-complete:
|
||||
- task2
|
||||
|
||||
task2:
|
||||
action: std.echo output="foo"
|
||||
wait-before: 3
|
||||
"""
|
||||
|
||||
wb_service.create_workbook_v2(workbook)
|
||||
wf_ex = self.engine.start_workflow('wb.wf', {})
|
||||
|
||||
wf_execs = db_api.get_workflow_executions()
|
||||
wf_ex = self._assert_single_item(wf_execs, name='wb.wf')
|
||||
task_ex = self._assert_single_item(wf_ex.task_executions, name='taskx')
|
||||
subwf_ex = self._assert_single_item(wf_execs, name='wb.subwf')
|
||||
|
||||
self.engine.stop_workflow(
|
||||
subwf_ex.id,
|
||||
states.CANCELLED,
|
||||
"Cancelled by user."
|
||||
)
|
||||
|
||||
self.await_execution_cancelled(subwf_ex.id)
|
||||
self.await_execution_cancelled(task_ex.id)
|
||||
self.await_execution_cancelled(wf_ex.id)
|
||||
|
||||
wf_execs = db_api.get_workflow_executions()
|
||||
wf_ex = self._assert_single_item(wf_execs, name='wb.wf')
|
||||
task_ex = self._assert_single_item(wf_ex.task_executions, name='taskx')
|
||||
subwf_ex = self._assert_single_item(wf_execs, name='wb.subwf')
|
||||
|
||||
self.assertEqual(states.CANCELLED, subwf_ex.state)
|
||||
self.assertEqual("Cancelled by user.", subwf_ex.state_info)
|
||||
self.assertEqual(states.CANCELLED, task_ex.state)
|
||||
self.assertIn("Cancelled by user.", task_ex.state_info)
|
||||
self.assertEqual(states.CANCELLED, wf_ex.state)
|
||||
self.assertEqual("Cancelled tasks: taskx", wf_ex.state_info)
|
||||
|
||||
def test_cancel_with_items_parent_workflow(self):
|
||||
workbook = """
|
||||
version: '2.0'
|
||||
|
||||
name: wb
|
||||
|
||||
workflows:
|
||||
wf:
|
||||
type: direct
|
||||
tasks:
|
||||
taskx:
|
||||
with-items: i in [1, 2]
|
||||
workflow: subwf
|
||||
|
||||
subwf:
|
||||
type: direct
|
||||
tasks:
|
||||
task1:
|
||||
action: std.echo output="Echo"
|
||||
on-complete:
|
||||
- task2
|
||||
|
||||
task2:
|
||||
action: std.echo output="foo"
|
||||
wait-before: 1
|
||||
"""
|
||||
wb_service.create_workbook_v2(workbook)
|
||||
wf_ex = self.engine.start_workflow('wb.wf', {})
|
||||
|
||||
self.engine.stop_workflow(
|
||||
wf_ex.id,
|
||||
states.CANCELLED,
|
||||
"Cancelled by user."
|
||||
)
|
||||
|
||||
wf_ex = db_api.get_execution(wf_ex.id)
|
||||
task_ex = self._assert_single_item(wf_ex.task_executions, name='taskx')
|
||||
|
||||
self.await_execution_cancelled(wf_ex.id)
|
||||
self.await_execution_cancelled(task_ex.id)
|
||||
|
||||
wf_execs = db_api.get_workflow_executions()
|
||||
wf_ex = self._assert_single_item(wf_execs, name='wb.wf')
|
||||
task_ex = self._assert_single_item(wf_ex.task_executions, name='taskx')
|
||||
subwf_exs = self._assert_multiple_items(wf_execs, 2, name='wb.subwf')
|
||||
|
||||
self.assertEqual(states.CANCELLED, subwf_exs[0].state)
|
||||
self.assertEqual("Cancelled by user.", subwf_exs[0].state_info)
|
||||
self.assertEqual(states.CANCELLED, subwf_exs[1].state)
|
||||
self.assertEqual("Cancelled by user.", subwf_exs[1].state_info)
|
||||
self.assertEqual(states.CANCELLED, task_ex.state)
|
||||
self.assertIn("cancelled", task_ex.state_info)
|
||||
self.assertEqual(states.CANCELLED, wf_ex.state)
|
||||
self.assertEqual("Cancelled by user.", wf_ex.state_info)
|
||||
|
||||
def test_cancel_with_items_child_workflow(self):
|
||||
workbook = """
|
||||
version: '2.0'
|
||||
|
||||
name: wb
|
||||
|
||||
workflows:
|
||||
wf:
|
||||
type: direct
|
||||
tasks:
|
||||
taskx:
|
||||
with-items: i in [1, 2]
|
||||
workflow: subwf
|
||||
|
||||
subwf:
|
||||
type: direct
|
||||
tasks:
|
||||
task1:
|
||||
action: std.echo output="Echo"
|
||||
on-complete:
|
||||
- task2
|
||||
|
||||
task2:
|
||||
action: std.echo output="foo"
|
||||
wait-before: 1
|
||||
"""
|
||||
|
||||
wb_service.create_workbook_v2(workbook)
|
||||
wf_ex = self.engine.start_workflow('wb.wf', {})
|
||||
|
||||
wf_execs = db_api.get_workflow_executions()
|
||||
wf_ex = self._assert_single_item(wf_execs, name='wb.wf')
|
||||
task_ex = self._assert_single_item(wf_ex.task_executions, name='taskx')
|
||||
subwf_exs = self._assert_multiple_items(wf_execs, 2, name='wb.subwf')
|
||||
|
||||
self.engine.stop_workflow(
|
||||
subwf_exs[0].id,
|
||||
states.CANCELLED,
|
||||
"Cancelled by user."
|
||||
)
|
||||
|
||||
self.await_execution_cancelled(subwf_exs[0].id)
|
||||
self.await_execution_success(subwf_exs[1].id)
|
||||
self.await_execution_cancelled(task_ex.id)
|
||||
self.await_execution_cancelled(wf_ex.id)
|
||||
|
||||
wf_execs = db_api.get_workflow_executions()
|
||||
wf_ex = self._assert_single_item(wf_execs, name='wb.wf')
|
||||
task_ex = self._assert_single_item(wf_ex.task_executions, name='taskx')
|
||||
subwf_exs = self._assert_multiple_items(wf_execs, 2, name='wb.subwf')
|
||||
|
||||
self.assertEqual(states.CANCELLED, subwf_exs[0].state)
|
||||
self.assertEqual("Cancelled by user.", subwf_exs[0].state_info)
|
||||
self.assertEqual(states.SUCCESS, subwf_exs[1].state)
|
||||
self.assertIsNone(subwf_exs[1].state_info)
|
||||
self.assertEqual(states.CANCELLED, task_ex.state)
|
||||
self.assertIn("cancelled", task_ex.state_info)
|
||||
self.assertEqual(states.CANCELLED, wf_ex.state)
|
||||
self.assertEqual("Cancelled tasks: taskx", wf_ex.state_info)
|
||||
|
||||
def test_cancel_then_fail_with_items_child_workflow(self):
|
||||
workbook = """
|
||||
version: '2.0'
|
||||
|
||||
name: wb
|
||||
|
||||
workflows:
|
||||
wf:
|
||||
type: direct
|
||||
tasks:
|
||||
taskx:
|
||||
with-items: i in [1, 2]
|
||||
workflow: subwf
|
||||
|
||||
subwf:
|
||||
type: direct
|
||||
tasks:
|
||||
task1:
|
||||
action: std.echo output="Echo"
|
||||
on-complete:
|
||||
- task2
|
||||
|
||||
task2:
|
||||
action: std.echo output="foo"
|
||||
wait-before: 1
|
||||
"""
|
||||
|
||||
wb_service.create_workbook_v2(workbook)
|
||||
wf_ex = self.engine.start_workflow('wb.wf', {})
|
||||
|
||||
wf_execs = db_api.get_workflow_executions()
|
||||
wf_ex = self._assert_single_item(wf_execs, name='wb.wf')
|
||||
task_ex = self._assert_single_item(wf_ex.task_executions, name='taskx')
|
||||
subwf_exs = self._assert_multiple_items(wf_execs, 2, name='wb.subwf')
|
||||
|
||||
self.engine.stop_workflow(
|
||||
subwf_exs[0].id,
|
||||
states.CANCELLED,
|
||||
"Cancelled by user."
|
||||
)
|
||||
|
||||
self.engine.stop_workflow(
|
||||
subwf_exs[1].id,
|
||||
states.ERROR,
|
||||
"Failed by user."
|
||||
)
|
||||
|
||||
self.await_execution_cancelled(subwf_exs[0].id)
|
||||
self.await_execution_error(subwf_exs[1].id)
|
||||
self.await_execution_error(task_ex.id)
|
||||
self.await_execution_error(wf_ex.id)
|
||||
|
||||
wf_execs = db_api.get_workflow_executions()
|
||||
wf_ex = self._assert_single_item(wf_execs, name='wb.wf')
|
||||
task_ex = self._assert_single_item(wf_ex.task_executions, name='taskx')
|
||||
subwf_exs = self._assert_multiple_items(wf_execs, 2, name='wb.subwf')
|
||||
|
||||
self.assertEqual(states.CANCELLED, subwf_exs[0].state)
|
||||
self.assertEqual("Cancelled by user.", subwf_exs[0].state_info)
|
||||
self.assertEqual(states.ERROR, subwf_exs[1].state)
|
||||
self.assertEqual("Failed by user.", subwf_exs[1].state_info)
|
||||
self.assertEqual(states.ERROR, task_ex.state)
|
||||
self.assertIn("failed", task_ex.state_info)
|
||||
self.assertEqual(states.ERROR, wf_ex.state)
|
||||
self.assertIn("Failed by user.", wf_ex.state_info)
|
||||
|
||||
def test_fail_then_cancel_with_items_child_workflow(self):
|
||||
workbook = """
|
||||
version: '2.0'
|
||||
|
||||
name: wb
|
||||
|
||||
workflows:
|
||||
wf:
|
||||
type: direct
|
||||
tasks:
|
||||
taskx:
|
||||
with-items: i in [1, 2]
|
||||
workflow: subwf
|
||||
|
||||
subwf:
|
||||
type: direct
|
||||
tasks:
|
||||
task1:
|
||||
action: std.echo output="Echo"
|
||||
on-complete:
|
||||
- task2
|
||||
|
||||
task2:
|
||||
action: std.echo output="foo"
|
||||
wait-before: 1
|
||||
"""
|
||||
|
||||
wb_service.create_workbook_v2(workbook)
|
||||
wf_ex = self.engine.start_workflow('wb.wf', {})
|
||||
|
||||
wf_execs = db_api.get_workflow_executions()
|
||||
wf_ex = self._assert_single_item(wf_execs, name='wb.wf')
|
||||
task_ex = self._assert_single_item(wf_ex.task_executions, name='taskx')
|
||||
subwf_exs = self._assert_multiple_items(wf_execs, 2, name='wb.subwf')
|
||||
|
||||
self.engine.stop_workflow(
|
||||
subwf_exs[1].id,
|
||||
states.ERROR,
|
||||
"Failed by user."
|
||||
)
|
||||
|
||||
self.engine.stop_workflow(
|
||||
subwf_exs[0].id,
|
||||
states.CANCELLED,
|
||||
"Cancelled by user."
|
||||
)
|
||||
|
||||
self.await_execution_cancelled(subwf_exs[0].id)
|
||||
self.await_execution_error(subwf_exs[1].id)
|
||||
self.await_execution_error(task_ex.id)
|
||||
self.await_execution_error(wf_ex.id)
|
||||
|
||||
wf_execs = db_api.get_workflow_executions()
|
||||
wf_ex = self._assert_single_item(wf_execs, name='wb.wf')
|
||||
task_ex = self._assert_single_item(wf_ex.task_executions, name='taskx')
|
||||
subwf_exs = self._assert_multiple_items(wf_execs, 2, name='wb.subwf')
|
||||
|
||||
self.assertEqual(states.CANCELLED, subwf_exs[0].state)
|
||||
self.assertEqual("Cancelled by user.", subwf_exs[0].state_info)
|
||||
self.assertEqual(states.ERROR, subwf_exs[1].state)
|
||||
self.assertEqual("Failed by user.", subwf_exs[1].state_info)
|
||||
self.assertEqual(states.ERROR, task_ex.state)
|
||||
self.assertIn("failed", task_ex.state_info)
|
||||
self.assertEqual(states.ERROR, wf_ex.state)
|
||||
self.assertIn("Failed by user.", wf_ex.state_info)
|
@ -1,5 +1,6 @@
|
||||
# Copyright 2015 - Alcatel-lucent, Inc.
|
||||
# Copyright 2015 - StackStorm, Inc.
|
||||
# Copyright 2016 - Brocade Communications Systems, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -65,8 +66,25 @@ def _load_executions():
|
||||
'workflow_name': 'test_exec',
|
||||
'state': "SUCCESS",
|
||||
'task_execution_id': '789'
|
||||
},
|
||||
{
|
||||
'id': 'abc',
|
||||
'name': 'cancelled_expired',
|
||||
'created_at': time_now - datetime.timedelta(minutes=60),
|
||||
'updated_at': time_now - datetime.timedelta(minutes=59),
|
||||
'workflow_name': 'test_exec',
|
||||
'state': "CANCELLED",
|
||||
},
|
||||
{
|
||||
'id': 'def',
|
||||
'name': 'cancelled_not_expired',
|
||||
'created_at': time_now - datetime.timedelta(minutes=15),
|
||||
'updated_at': time_now - datetime.timedelta(minutes=5),
|
||||
'workflow_name': 'test_exec',
|
||||
'state': "CANCELLED",
|
||||
}
|
||||
]
|
||||
|
||||
for wf_exec in wf_execs:
|
||||
db_api.create_workflow_execution(wf_exec)
|
||||
|
||||
@ -105,9 +123,9 @@ class ExpirationPolicyTest(base.DbTestCase):
|
||||
# Call for all expired wfs execs.
|
||||
execs = db_api.get_expired_executions(now)
|
||||
|
||||
# Should be only 3, the RUNNING execution shouldn't return,
|
||||
# Should be only 5, the RUNNING execution shouldn't return,
|
||||
# so the child wf (that has parent task id).
|
||||
self.assertEqual(3, len(execs))
|
||||
self.assertEqual(5, len(execs))
|
||||
|
||||
# Switch context to Admin since expiration policy running as Admin.
|
||||
_switch_context(None, True)
|
||||
@ -118,8 +136,8 @@ class ExpirationPolicyTest(base.DbTestCase):
|
||||
# Only non_expired available (update_at < older_than).
|
||||
execs = db_api.get_expired_executions(now)
|
||||
|
||||
self.assertEqual(1, len(execs))
|
||||
self.assertEqual('987', execs[0].id)
|
||||
self.assertEqual(2, len(execs))
|
||||
self.assertListEqual(['987', 'def'], sorted([ex.id for ex in execs]))
|
||||
|
||||
_set_expiration_policy_config(1, 5)
|
||||
expiration_policy.run_execution_expiration_policy(self, ctx)
|
||||
|
@ -140,6 +140,13 @@ class WorkflowController(object):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def any_cancels(self):
|
||||
"""Determines if there are any task cancellations.
|
||||
|
||||
:return: True if there is one or more tasks in cancelled state.
|
||||
"""
|
||||
return len(wf_utils.find_cancelled_task_executions(self.wf_ex)) > 0
|
||||
|
||||
@abc.abstractmethod
|
||||
def evaluate_workflow_final_context(self):
|
||||
"""Evaluates final workflow context assuming that workflow has finished.
|
||||
|
@ -1,6 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 - Mirantis, Inc.
|
||||
# Copyright 2016 - Brocade Communications Systems, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -22,17 +21,28 @@ RUNNING = 'RUNNING'
|
||||
RUNNING_DELAYED = 'DELAYED'
|
||||
PAUSED = 'PAUSED'
|
||||
SUCCESS = 'SUCCESS'
|
||||
CANCELLED = 'CANCELLED'
|
||||
ERROR = 'ERROR'
|
||||
|
||||
_ALL = [IDLE, WAITING, RUNNING, SUCCESS, ERROR, PAUSED, RUNNING_DELAYED]
|
||||
_ALL = [
|
||||
IDLE,
|
||||
WAITING,
|
||||
RUNNING,
|
||||
RUNNING_DELAYED,
|
||||
PAUSED,
|
||||
SUCCESS,
|
||||
CANCELLED,
|
||||
ERROR
|
||||
]
|
||||
|
||||
_VALID_TRANSITIONS = {
|
||||
IDLE: [RUNNING, ERROR],
|
||||
IDLE: [RUNNING, ERROR, CANCELLED],
|
||||
WAITING: [RUNNING],
|
||||
RUNNING: [PAUSED, RUNNING_DELAYED, SUCCESS, ERROR],
|
||||
RUNNING_DELAYED: [RUNNING, ERROR],
|
||||
PAUSED: [RUNNING, ERROR],
|
||||
RUNNING: [PAUSED, RUNNING_DELAYED, SUCCESS, ERROR, CANCELLED],
|
||||
RUNNING_DELAYED: [RUNNING, ERROR, CANCELLED],
|
||||
PAUSED: [RUNNING, ERROR, CANCELLED],
|
||||
SUCCESS: [],
|
||||
CANCELLED: [],
|
||||
ERROR: [RUNNING]
|
||||
}
|
||||
|
||||
@ -46,7 +56,7 @@ def is_invalid(state):
|
||||
|
||||
|
||||
def is_completed(state):
|
||||
return state in [SUCCESS, ERROR]
|
||||
return state in [SUCCESS, ERROR, CANCELLED]
|
||||
|
||||
|
||||
def is_running(state):
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright 2014 - Mirantis, Inc.
|
||||
# Copyright 2015 - StackStorm, Inc.
|
||||
# Copyright 2016 - Brocade Communications Systems, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -20,30 +21,47 @@ from mistral.workflow import states
|
||||
class Result(object):
|
||||
"""Explicit data structure containing a result of task execution."""
|
||||
|
||||
def __init__(self, data=None, error=None):
|
||||
def __init__(self, data=None, error=None, cancel=False):
|
||||
self.data = data
|
||||
self.error = error
|
||||
self.cancel = cancel
|
||||
|
||||
def __repr__(self):
|
||||
return 'Result [data=%s, error=%s]' % (
|
||||
repr(self.data), repr(self.error))
|
||||
return 'Result [data=%s, error=%s, cancel=%s]' % (
|
||||
repr(self.data), repr(self.error), str(self.cancel)
|
||||
)
|
||||
|
||||
def is_cancel(self):
|
||||
return self.cancel
|
||||
|
||||
def is_error(self):
|
||||
return self.error is not None
|
||||
return self.error is not None and not self.is_cancel()
|
||||
|
||||
def is_success(self):
|
||||
return not self.is_error()
|
||||
return not self.is_error() and not self.is_cancel()
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.data == other.data and self.error == other.error
|
||||
return (
|
||||
self.data == other.data and
|
||||
self.error == other.error and
|
||||
self.cancel == other.cancel
|
||||
)
|
||||
|
||||
|
||||
class ResultSerializer(serializers.Serializer):
|
||||
def serialize(self, entity):
|
||||
return {'data': entity.data, 'error': entity.error}
|
||||
return {
|
||||
'data': entity.data,
|
||||
'error': entity.error,
|
||||
'cancel': entity.cancel
|
||||
}
|
||||
|
||||
def deserialize(self, entity):
|
||||
return Result(entity['data'], entity['error'])
|
||||
return Result(
|
||||
entity['data'],
|
||||
entity['error'],
|
||||
entity.get('cancel', False)
|
||||
)
|
||||
|
||||
|
||||
def find_task_execution_not_state(wf_ex, task_spec, state):
|
||||
@ -106,3 +124,7 @@ def find_incomplete_task_executions(wf_ex):
|
||||
|
||||
def find_error_task_executions(wf_ex):
|
||||
return find_task_executions_with_state(wf_ex, states.ERROR)
|
||||
|
||||
|
||||
def find_cancelled_task_executions(wf_ex):
|
||||
return find_task_executions_with_state(wf_ex, states.CANCELLED)
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Copyright 2014 - Mirantis, Inc.
|
||||
# Copyright 2016 - Brocade Communications Systems, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# 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
|
||||
#
|
||||
@ -69,9 +70,12 @@ def get_concurrency(task_ex):
|
||||
|
||||
def get_final_state(task_ex):
|
||||
find_error = lambda x: x.accepted and x.state == states.ERROR
|
||||
find_cancel = lambda x: x.accepted and x.state == states.CANCELLED
|
||||
|
||||
if list(filter(find_error, task_ex.executions)):
|
||||
return states.ERROR
|
||||
elif list(filter(find_cancel, task_ex.executions)):
|
||||
return states.CANCELLED
|
||||
else:
|
||||
return states.SUCCESS
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user