diff options
author | Zuul <zuul@review.openstack.org> | 2018-08-09 12:56:24 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2018-08-09 12:56:24 +0000 |
commit | 811505417343144ac8cdb7f802ca60fd759dcb15 (patch) | |
tree | dba993e7fa4a24c80eb389dc500984348852278d | |
parent | b0a72cba7ac143ae7b47daa69c2cf06af03ca9b7 (diff) | |
parent | d8cfd8a4a5d5750a8f86553ef4f1fb87a0f4d834 (diff) | |
download | heat-811505417343144ac8cdb7f802ca60fd759dcb15.tar.gz |
Merge "Refactor deferral of stack state persistence"
-rw-r--r-- | heat/engine/service.py | 5 | ||||
-rw-r--r-- | heat/engine/stack.py | 44 | ||||
-rw-r--r-- | heat/tests/test_convg_stack.py | 31 | ||||
-rw-r--r-- | heat/tests/test_stack.py | 3 |
4 files changed, 43 insertions, 40 deletions
diff --git a/heat/engine/service.py b/heat/engine/service.py index cf9528bf8..aca026c5e 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -180,10 +180,7 @@ class ThreadGroupManager(object): Persist the stack state to COMPLETE and FAILED close to releasing the lock to avoid race conditions. """ - if (stack is not None and stack.status != stack.IN_PROGRESS - and stack.action not in (stack.DELETE, - stack.ROLLBACK, - stack.UPDATE)): + if stack is not None and stack.defer_state_persist(): stack.persist_state_and_release_lock(lock.engine_id) notify = kwargs.get('notify') diff --git a/heat/engine/stack.py b/heat/engine/stack.py index 1ad5f26ba..65367097e 100644 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -957,6 +957,29 @@ class Stack(collections.Mapping): self.env.get_event_sinks(), ev.as_dict()) + def defer_state_persist(self): + """Return whether to defer persisting the state. + + If persistence is deferred, the new state will not be written to the + database until the stack lock is released (by calling + persist_state_and_release_lock()). This prevents races in the legacy + path where an observer sees the stack COMPLETE but an engine still + holds the lock. + """ + if self.status == self.IN_PROGRESS: + # Always persist IN_PROGRESS immediately + return False + + if (self.convergence and + self.action in {self.UPDATE, self.DELETE, self.CREATE, + self.ADOPT, self.ROLLBACK, self.RESTORE}): + # These operations do not use the stack lock in convergence, so + # never defer. + return False + + return self.action not in {self.UPDATE, self.DELETE, self.ROLLBACK, + self.RESTORE} + @profiler.trace('Stack.state_set', hide_args=False) def state_set(self, action, status, reason): """Update the stack state.""" @@ -971,13 +994,9 @@ class Stack(collections.Mapping): self.status_reason = reason self._log_status() - if self.convergence and action in ( - self.UPDATE, self.DELETE, self.CREATE, - self.ADOPT, self.ROLLBACK, self.RESTORE): - # if convergence and stack operation is create/update/rollback/ - # delete, stack lock is not used, hence persist state + if not self.defer_state_persist(): updated = self._persist_state() - if not updated: + if self.convergence and not updated: LOG.info("Stack %(name)s traversal %(trvsl_id)s no longer " "active; not setting state to %(action)s_%(status)s", {'name': self.name, @@ -985,13 +1004,6 @@ class Stack(collections.Mapping): 'action': action, 'status': status}) return updated - # Persist state to db only if status == IN_PROGRESS - # or action == UPDATE/DELETE/ROLLBACK. Else, it would - # be done before releasing the stack lock. - if status == self.IN_PROGRESS or action in ( - self.UPDATE, self.DELETE, self.ROLLBACK, self.RESTORE): - self._persist_state() - def _log_status(self): LOG.info('Stack %(action)s %(status)s (%(name)s): %(reason)s', {'action': self.action, @@ -1148,8 +1160,10 @@ class Stack(collections.Mapping): 'Failed stack pre-ops: %s' % six.text_type(e)) if callable(post_func): post_func() - # No need to call notify.signal(), because persistence of the - # state is always deferred here. + if notify is not None: + # No need to call notify.signal(), because persistence of the + # state is always deferred here. + assert self.defer_state_persist() return self.state_set(action, self.IN_PROGRESS, 'Stack %s started' % action) diff --git a/heat/tests/test_convg_stack.py b/heat/tests/test_convg_stack.py index 5aa681615..f1d1d5e9a 100644 --- a/heat/tests/test_convg_stack.py +++ b/heat/tests/test_convg_stack.py @@ -587,38 +587,31 @@ class TestConvgStackStateSet(common.HeatTestCase): def test_state_set_stack_suspend(self, mock_ps): mock_ps.return_value = 'updated' - ret_val = self.stack.state_set( - self.stack.SUSPEND, self.stack.IN_PROGRESS, 'Suspend started') + self.stack.state_set(self.stack.SUSPEND, self.stack.IN_PROGRESS, + 'Suspend started') self.assertTrue(mock_ps.called) - # Ensure that state_set returns None for other actions in convergence - self.assertIsNone(ret_val) mock_ps.reset_mock() - ret_val = self.stack.state_set( - self.stack.SUSPEND, self.stack.COMPLETE, 'Suspend complete') + self.stack.state_set(self.stack.SUSPEND, self.stack.COMPLETE, + 'Suspend complete') self.assertFalse(mock_ps.called) - self.assertIsNone(ret_val) def test_state_set_stack_resume(self, mock_ps): - ret_val = self.stack.state_set( - self.stack.RESUME, self.stack.IN_PROGRESS, 'Resume started') + self.stack.state_set(self.stack.RESUME, self.stack.IN_PROGRESS, + 'Resume started') self.assertTrue(mock_ps.called) - self.assertIsNone(ret_val) mock_ps.reset_mock() - ret_val = self.stack.state_set(self.stack.RESUME, self.stack.COMPLETE, - 'Resume complete') + self.stack.state_set(self.stack.RESUME, self.stack.COMPLETE, + 'Resume complete') self.assertFalse(mock_ps.called) - self.assertIsNone(ret_val) def test_state_set_stack_snapshot(self, mock_ps): - ret_val = self.stack.state_set( - self.stack.SNAPSHOT, self.stack.IN_PROGRESS, 'Snapshot started') + self.stack.state_set(self.stack.SNAPSHOT, self.stack.IN_PROGRESS, + 'Snapshot started') self.assertTrue(mock_ps.called) - self.assertIsNone(ret_val) mock_ps.reset_mock() - ret_val = self.stack.state_set( - self.stack.SNAPSHOT, self.stack.COMPLETE, 'Snapshot complete') + self.stack.state_set(self.stack.SNAPSHOT, self.stack.COMPLETE, + 'Snapshot complete') self.assertFalse(mock_ps.called) - self.assertIsNone(ret_val) def test_state_set_stack_restore(self, mock_ps): mock_ps.return_value = 'updated' diff --git a/heat/tests/test_stack.py b/heat/tests/test_stack.py index 2aec4ad91..5d7102356 100644 --- a/heat/tests/test_stack.py +++ b/heat/tests/test_stack.py @@ -3091,8 +3091,7 @@ class StackStateSetTest(common.HeatTestCase): self.assertRaises(ValueError, self.stack.state_set, self.action, self.status, 'test') else: - self.assertIsNone(self.stack.state_set(self.action, - self.status, 'test')) + self.stack.state_set(self.action, self.status, 'test') self.assertEqual((self.action, self.status), self.stack.state) self.assertEqual('test', self.stack.status_reason) self.assertEqual(self.persist_count, persist_state.call_count) |