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:
Winson Chan 2016-07-23 00:10:09 +00:00
parent 001598237c
commit 82ab51b0c1
14 changed files with 763 additions and 38 deletions

View File

@ -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
])
)
)

View File

@ -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"
)
)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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]))

View File

@ -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',

View File

@ -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):

View 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)

View File

@ -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)

View File

@ -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.

View File

@ -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):

View File

@ -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)

View File

@ -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