diff --git a/keystone/revoke/backends/sql.py b/keystone/revoke/backends/sql.py index 5f0f8b3a7d..bb23d7b300 100644 --- a/keystone/revoke/backends/sql.py +++ b/keystone/revoke/backends/sql.py @@ -14,6 +14,8 @@ from keystone.common import sql from keystone.models import revoke_model from keystone.revoke.backends import base +from oslo_db import api as oslo_db_api + class RevocationEvent(sql.ModelBase, sql.ModelDictMixin): __tablename__ = 'revocation_event' @@ -90,6 +92,7 @@ class Revoke(base.RevokeDriverV8): return events + @oslo_db_api.wrap_db_retry(retry_on_deadlock=True) def revoke(self, event): kwargs = dict() for attr in revoke_model.REVOKE_KEYS: diff --git a/keystone/tests/unit/test_v3_os_revoke.py b/keystone/tests/unit/test_v3_os_revoke.py index d735300e61..082d8d1c4f 100644 --- a/keystone/tests/unit/test_v3_os_revoke.py +++ b/keystone/tests/unit/test_v3_os_revoke.py @@ -11,9 +11,11 @@ # under the License. import datetime +import mock import uuid import freezegun +from oslo_db import exception as oslo_db_exception from oslo_utils import timeutils import six from six.moves import http_client @@ -165,3 +167,41 @@ class OSRevokeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin): # `OS-OAUTH1:access_token_id` is None and won't be returned to # end user. self.assertNotIn('OS-OAUTH1:access_token_id', event) + + def test_retries_on_deadlock(self): + patcher = mock.patch('sqlalchemy.orm.query.Query.delete', + autospec=True) + + # NOTE(mnikolaenko): raise 2 deadlocks and back to normal work of + # method. Two attempts is enough to check that retry decorator works. + # Otherwise it will take very much time to pass this test + class FakeDeadlock(object): + def __init__(self, mock_patcher): + self.deadlock_count = 2 + self.mock_patcher = mock_patcher + self.patched = True + + def __call__(self, *args, **kwargs): + if self.deadlock_count > 1: + self.deadlock_count -= 1 + else: + self.mock_patcher.stop() + self.patched = False + raise oslo_db_exception.DBDeadlock + + sql_delete_mock = patcher.start() + side_effect = FakeDeadlock(patcher) + sql_delete_mock.side_effect = side_effect + + try: + self.revoke_api.revoke(revoke_model.RevokeEvent( + user_id=uuid.uuid4().hex)) + finally: + if side_effect.patched: + patcher.stop() + + call_count = sql_delete_mock.call_count + + # initial attempt + 1 retry + revoke_attempt_count = 2 + self.assertEqual(call_count, revoke_attempt_count)