Release locks when action is cancelled

When an action is cancelled, it should release any associated cluster or
node locks so that new operations can be performed on the cluster or
node.
Remove action status check for action force operation.

Change-Id: I6a520a90feed7ab1e9c99a595863ca3ff90aef40
This commit is contained in:
Duc Truong 2020-11-05 00:44:38 +00:00
parent d4ec93ae55
commit 8fc8ee6549
4 changed files with 32 additions and 18 deletions

View File

@ -379,14 +379,11 @@ class Action(object):
:raises: `ActionImmutable` if the action is in an unchangeable state :raises: `ActionImmutable` if the action is in an unchangeable state
""" """
expected = (self.INIT, self.WAITING, self.READY, self.RUNNING,
self.WAITING_LIFECYCLE_COMPLETION)
if self.status not in expected:
raise exception.ActionImmutable(id=self.id[:8], expected=expected,
actual=self.status)
LOG.debug('Forcing action %s to cancel.', self.id) LOG.debug('Forcing action %s to cancel.', self.id)
self.set_status(self.RES_CANCEL, 'Action execution force cancelled') self.set_status(self.RES_CANCEL, 'Action execution force cancelled')
self.release_lock()
depended = dobj.Dependency.get_depended(self.context, self.id) depended = dobj.Dependency.get_depended(self.context, self.id)
if not depended: if not depended:
return return
@ -400,6 +397,7 @@ class Action(object):
LOG.debug('Forcing action %s to cancel.', action.id) LOG.debug('Forcing action %s to cancel.', action.id)
action.set_status(action.RES_CANCEL, action.set_status(action.RES_CANCEL,
'Action execution force cancelled') 'Action execution force cancelled')
action.release_lock()
def execute(self, **kwargs): def execute(self, **kwargs):
"""Execute the action. """Execute the action.
@ -411,6 +409,10 @@ class Action(object):
""" """
raise NotImplementedError raise NotImplementedError
def release_lock(self):
"""Release the lock associated with the action."""
raise NotImplementedError
def set_status(self, result, reason=None): def set_status(self, result, reason=None):
"""Set action status based on return value from execute.""" """Set action status based on return value from execute."""

View File

@ -1215,6 +1215,12 @@ class ClusterAction(base.Action):
"""Handler to cancel the execution of action.""" """Handler to cancel the execution of action."""
return self.RES_OK return self.RES_OK
def release_lock(self):
"""Handler to release the lock."""
senlin_lock.cluster_lock_release(self.target, self.id,
senlin_lock.CLUSTER_SCOPE)
return self.RES_OK
def CompleteLifecycleProc(context, action_id): def CompleteLifecycleProc(context, action_id):
"""Complete lifecycle process.""" """Complete lifecycle process."""

View File

@ -283,3 +283,15 @@ class NodeAction(base.Action):
def cancel(self): def cancel(self):
"""Handler for cancelling the action.""" """Handler for cancelling the action."""
return self.RES_OK return self.RES_OK
def release_lock(self):
"""Handler to release the lock."""
senlin_lock.node_lock_release(self.entity.id, self.id)
# only release cluster lock if it was locked as part of this
# action (i.e. it's a user intiated action aka CAUSE_RPC from
# senlin API and a not a CAUSED_DERIVED)
if self.cause == consts.CAUSE_RPC:
senlin_lock.cluster_lock_release(self.entity.cluster_id, self.id,
senlin_lock.NODE_SCOPE)
return self.RES_OK

View File

@ -635,6 +635,7 @@ class ActionBaseTest(base.SenlinTestCase):
action = ab.Action(OBJID, 'OBJECT_ACTION', self.ctx, id=ACTION_ID) action = ab.Action(OBJID, 'OBJECT_ACTION', self.ctx, id=ACTION_ID)
action.load = mock.Mock() action.load = mock.Mock()
action.set_status = mock.Mock() action.set_status = mock.Mock()
action.release_lock = mock.Mock()
mock_dobj.return_value = None mock_dobj.return_value = None
action.status = action.RUNNING action.status = action.RUNNING
@ -643,19 +644,23 @@ class ActionBaseTest(base.SenlinTestCase):
action.load.assert_not_called() action.load.assert_not_called()
action.set_status.assert_called_once_with( action.set_status.assert_called_once_with(
action.RES_CANCEL, 'Action execution force cancelled') action.RES_CANCEL, 'Action execution force cancelled')
self.assertEqual(1, action.release_lock.call_count)
@mock.patch.object(dobj.Dependency, 'get_depended') @mock.patch.object(dobj.Dependency, 'get_depended')
def test_force_cancel_children(self, mock_dobj): def test_force_cancel_children(self, mock_dobj):
action = ab.Action(OBJID, 'OBJECT_ACTION', self.ctx, id=ACTION_ID) action = ab.Action(OBJID, 'OBJECT_ACTION', self.ctx, id=ACTION_ID)
child_status_mock = mock.Mock() child_status_mock = mock.Mock()
child_release_mock = mock.Mock()
children = [] children = []
for child_id in CHILD_IDS: for child_id in CHILD_IDS:
child = ab.Action(OBJID, 'OBJECT_ACTION', self.ctx, id=child_id) child = ab.Action(OBJID, 'OBJECT_ACTION', self.ctx, id=child_id)
child.status = child.WAITING_LIFECYCLE_COMPLETION child.status = child.WAITING_LIFECYCLE_COMPLETION
child.set_status = child_status_mock child.set_status = child_status_mock
child.release_lock = child_release_mock
children.append(child) children.append(child)
mock_dobj.return_value = CHILD_IDS mock_dobj.return_value = CHILD_IDS
action.set_status = mock.Mock() action.set_status = mock.Mock()
action.release_lock = mock.Mock()
action.load = mock.Mock() action.load = mock.Mock()
action.load.side_effect = children action.load.side_effect = children
@ -664,20 +669,9 @@ class ActionBaseTest(base.SenlinTestCase):
mock_dobj.assert_called_once_with(action.context, action.id) mock_dobj.assert_called_once_with(action.context, action.id)
self.assertEqual(2, child_status_mock.call_count) self.assertEqual(2, child_status_mock.call_count)
self.assertEqual(2, child_release_mock.call_count)
self.assertEqual(2, action.load.call_count) self.assertEqual(2, action.load.call_count)
self.assertEqual(1, action.release_lock.call_count)
@mock.patch.object(dobj.Dependency, 'get_depended')
def test_force_cancel_immutable(self, mock_dobj):
action = ab.Action(OBJID, 'OBJECT_ACTION', self.ctx, id=ACTION_ID)
action.load = mock.Mock()
action.set_status = mock.Mock()
mock_dobj.return_value = None
action.status = action.FAILED
self.assertRaises(exception.ActionImmutable, action.force_cancel)
action.load.assert_not_called()
action.set_status.assert_not_called()
def test_execute_default(self): def test_execute_default(self):
action = ab.Action.__new__(DummyAction, OBJID, 'BOOM', self.ctx) action = ab.Action.__new__(DummyAction, OBJID, 'BOOM', self.ctx)