Validate each flow state change
When engine changing state, the transition is now validated. This helps to avoid complex and error prone checks in engine._change_state and make validating code more reusable in different engines. Change-Id: I2a06823c532926bb3bd034f7252b14bdbbc1fa1d Implements: bp:transition-control
This commit is contained in:
@@ -104,11 +104,11 @@ class ActionEngine(base.EngineBase):
|
||||
|
||||
@decorators.locked(lock='_state_lock')
|
||||
def _change_state(self, state):
|
||||
if (state == states.SUSPENDING and not (self.is_running or
|
||||
self.is_reverting)):
|
||||
old_state = self.storage.get_flow_state()
|
||||
if not states.check_flow_transition(old_state, state):
|
||||
return
|
||||
self.storage.set_flow_state(state)
|
||||
details = dict(engine=self)
|
||||
details = dict(engine=self, old_state=old_state)
|
||||
self.notifier.notify(state, details)
|
||||
|
||||
def on_task_state_change(self, task_action, state, result=None):
|
||||
|
@@ -131,4 +131,4 @@ class WrappedFailure(TaskFlowException):
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return 'WrappedFailure: %s' % self._causes
|
||||
return 'WrappedFailure: %s' % [str(cause) for cause in self._causes]
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (C) 2012 Yahoo! Inc. All Rights Reserved.
|
||||
# Copyright (C) 2012-2013 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
|
||||
@@ -16,6 +16,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from taskflow import exceptions as exc
|
||||
|
||||
# Job states.
|
||||
CLAIMED = 'CLAIMED'
|
||||
FAILURE = 'FAILURE'
|
||||
@@ -33,6 +35,7 @@ RUNNING = RUNNING
|
||||
SUCCESS = SUCCESS
|
||||
SUSPENDING = 'SUSPENDING'
|
||||
SUSPENDED = 'SUSPENDED'
|
||||
RESUMING = 'RESUMING'
|
||||
|
||||
# Task states.
|
||||
FAILURE = FAILURE
|
||||
@@ -42,3 +45,80 @@ REVERTING = REVERTING
|
||||
|
||||
# TODO(harlowja): use when we can timeout tasks??
|
||||
TIMED_OUT = 'TIMED_OUT'
|
||||
|
||||
|
||||
## Flow state transitions
|
||||
# https://wiki.openstack.org/wiki/TaskFlow/States_of_Task_and_Flow#Flow_States
|
||||
|
||||
_ALLOWED_FLOW_TRANSITIONS = frozenset((
|
||||
(PENDING, RUNNING), # run it!
|
||||
|
||||
(RUNNING, SUCCESS), # all tasks finished successfully
|
||||
(RUNNING, FAILURE), # some of task failed
|
||||
(RUNNING, SUSPENDING), # engine.suspend was called
|
||||
|
||||
(SUCCESS, RUNNING), # see note below
|
||||
|
||||
(FAILURE, RUNNING), # see note below
|
||||
(FAILURE, REVERTING), # flow failed, do cleanup now
|
||||
|
||||
(REVERTING, REVERTED), # revert done
|
||||
(REVERTING, FAILURE), # revert failed
|
||||
(REVERTING, SUSPENDING), # engine.suspend was called
|
||||
|
||||
(REVERTED, RUNNING), # try again
|
||||
|
||||
(SUSPENDING, SUSPENDED), # suspend finished
|
||||
(SUSPENDING, SUCCESS), # all tasks finished while we were waiting
|
||||
(SUSPENDING, FAILURE), # some tasks failed while we were waiting
|
||||
(SUSPENDING, REVERTED), # all tasks were reverted while we were waiting
|
||||
|
||||
(SUSPENDED, RUNNING), # restart from suspended
|
||||
(SUSPENDED, REVERTING), # revert from suspended
|
||||
|
||||
(RESUMING, SUSPENDED), # after flow resumed, it is suspended
|
||||
))
|
||||
|
||||
|
||||
# NOTE(imelnikov) SUCCESS->RUNNING and FAILURE->RUNNING transitions are
|
||||
# useful when flow or flowdetails baciking it were altered after the flow
|
||||
# was finished; then, client code may want to run through flow again
|
||||
# to ensure all tasks from updated flow had a chance to run.
|
||||
|
||||
|
||||
# NOTE(imelnikov): Engine cannot transition flow from SUSPENDING to
|
||||
# SUSPENDED while some tasks from the flow are running and some results
|
||||
# from them are not retrieved and saved properly, so while flow is
|
||||
# in SUSPENDING state it may wait for some of the tasks to stop. Then,
|
||||
# flow can go to SUSPENDED, SUCCESS, FAILURE or REVERTED state depending
|
||||
# of actual state of the tasks -- e.g. if all tasks were finished
|
||||
# successfully while we were waiting, flow can be transitioned from
|
||||
# SUSPENDING to SUCCESS state.
|
||||
|
||||
|
||||
_IGNORED_FLOW_TRANSITIONS = frozenset(
|
||||
(a, b)
|
||||
for a in (PENDING, FAILURE, SUCCESS, SUSPENDED, REVERTED)
|
||||
for b in (SUSPENDING, SUSPENDED, RESUMING)
|
||||
if a != b
|
||||
)
|
||||
|
||||
|
||||
def check_flow_transition(old_state, new_state):
|
||||
"""Check that flow can transition from old_state to new_state.
|
||||
|
||||
If transition can be performed, it returns True. If transition
|
||||
should be ignored, it returns False. If transition is not
|
||||
invalid, it raises InvalidStateException.
|
||||
"""
|
||||
if old_state == new_state:
|
||||
return False
|
||||
pair = (old_state, new_state)
|
||||
if pair in _ALLOWED_FLOW_TRANSITIONS:
|
||||
return True
|
||||
if pair in _IGNORED_FLOW_TRANSITIONS:
|
||||
return False
|
||||
if new_state == RESUMING:
|
||||
return True
|
||||
raise exc.InvalidStateException(
|
||||
"Flow transition from %s to %s is not allowed" % pair)
|
||||
|
@@ -278,7 +278,10 @@ class Storage(object):
|
||||
|
||||
def get_flow_state(self):
|
||||
"""Set state from flowdetails"""
|
||||
return self._flowdetail.state
|
||||
state = self._flowdetail.state
|
||||
if state is None:
|
||||
state = states.PENDING
|
||||
return state
|
||||
|
||||
|
||||
class ThreadSafeStorage(Storage):
|
||||
|
45
taskflow/tests/unit/test_check_transition.py
Normal file
45
taskflow/tests/unit/test_check_transition.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (C) 2013 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 taskflow import exceptions as exc
|
||||
from taskflow import states
|
||||
from taskflow import test
|
||||
|
||||
|
||||
class CheckFlowTransitionTest(test.TestCase):
|
||||
|
||||
def test_same_state(self):
|
||||
self.assertFalse(
|
||||
states.check_flow_transition(states.SUCCESS, states.SUCCESS))
|
||||
|
||||
def test_rerunning_allowed(self):
|
||||
self.assertTrue(
|
||||
states.check_flow_transition(states.SUCCESS, states.RUNNING))
|
||||
|
||||
def test_no_resuming_from_pending(self):
|
||||
self.assertFalse(
|
||||
states.check_flow_transition(states.PENDING, states.RESUMING))
|
||||
|
||||
def test_resuming_from_running(self):
|
||||
self.assertTrue(
|
||||
states.check_flow_transition(states.RUNNING, states.RESUMING))
|
||||
|
||||
def test_bad_transition_raises(self):
|
||||
with self.assertRaisesRegexp(exc.InvalidStateException,
|
||||
'^Flow transition.*not allowed'):
|
||||
states.check_flow_transition(states.FAILURE, states.SUCCESS)
|
@@ -185,6 +185,10 @@ class StorageTest(test.TestCase):
|
||||
'^Unknown task name:'):
|
||||
s.get_uuid_by_name('42')
|
||||
|
||||
def test_initial_flow_state(self):
|
||||
s = self._get_storage()
|
||||
self.assertEquals(s.get_flow_state(), states.PENDING)
|
||||
|
||||
def test_get_flow_state(self):
|
||||
_lb, fd = p_utils.temporary_flow_detail(backend=self.backend)
|
||||
fd.state = states.FAILURE
|
||||
|
Reference in New Issue
Block a user