Skip to content

Commit 9cd7d4d

Browse files
committed
[FIX] rest_log: Don't prevent the retrying mechanism
In case of a retryable error, the initial error must bubble up to the retrying mechanism. If this kind of error is wrapped into another one, the retrying mechanism no more works
1 parent 78bdf55 commit 9cd7d4d

3 files changed

Lines changed: 85 additions & 1 deletion

File tree

rest_log/components/service.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
import logging
88
import traceback
99

10+
from psycopg2.errors import OperationalError
1011
from werkzeug.urls import url_encode, url_join
1112

1213
from odoo import exceptions
1314
from odoo.http import Response, request
1415
from odoo.modules.registry import Registry
16+
from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY
1517

1618
from odoo.addons.base_rest.http import JSONEncoder
1719
from odoo.addons.component.core import AbstractComponent
@@ -112,6 +114,15 @@ def _dispatch_exception(
112114
log_entry_url = self._get_log_entry_url(log_entry)
113115
except Exception as e:
114116
_logger.exception("Rest Log Error Creation: %s", e)
117+
# let the OperationalError bubble up to the retrying mechanism
118+
# We can't wrap the OperationalError because we want to let it
119+
# bubble up to the retrying mechanism, it will be handled by
120+
# the default handler at the end of the chain.
121+
if (
122+
isinstance(orig_exception, OperationalError)
123+
and orig_exception.pgcode in PG_CONCURRENCY_ERRORS_TO_RETRY
124+
):
125+
raise orig_exception
115126
raise exception_klass(exc_msg, log_entry_url) from orig_exception
116127

117128
def _get_exception_message(self, exception):

rest_log/tests/common.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
import contextlib
66

7+
from psycopg2 import errorcodes
8+
from psycopg2.errors import OperationalError
9+
710
from odoo import exceptions
811

912
from odoo.addons.base_rest import restapi
@@ -42,6 +45,7 @@ def fail(self, how):
4245
"value": ValueError,
4346
"validation": exceptions.ValidationError,
4447
"user": exceptions.UserError,
48+
"retryable": FakeConcurrentUpdateError,
4549
}
4650
raise exc[how]("Failed as you wanted!")
4751

@@ -61,3 +65,9 @@ def _get_mocked_request(self, env=None, httprequest=None, extra_headers=None):
6165
headers.update(extra_headers or {})
6266
mocked_request.httprequest.headers = headers
6367
yield mocked_request
68+
69+
70+
class FakeConcurrentUpdateError(OperationalError):
71+
@property
72+
def pgcode(self):
73+
return errorcodes.SERIALIZATION_FAILURE

rest_log/tests/test_db_logging.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from odoo.addons.component.tests.common import new_rollbacked_env
1414
from odoo.addons.rest_log import exceptions as log_exceptions # pylint: disable=W7950
1515

16-
from .common import TestDBLoggingMixin
16+
from .common import FakeConcurrentUpdateError, TestDBLoggingMixin
1717

1818

1919
class TestDBLogging(TransactionRestServiceRegistryCase, TestDBLoggingMixin):
@@ -374,3 +374,66 @@ def test_log_exception_value(self):
374374
self._test_exception(
375375
"value", log_exceptions.RESTServiceDispatchException, "ValueError", "severe"
376376
)
377+
378+
379+
class TestDBLoggingRetryableError(
380+
TransactionRestServiceRegistryCase, TestDBLoggingMixin
381+
):
382+
@classmethod
383+
def setUpClass(cls):
384+
super().setUpClass()
385+
cls._setup_registry(cls)
386+
387+
@classmethod
388+
def tearDownClass(cls):
389+
# pylint: disable=W8110
390+
cls._teardown_registry(cls)
391+
super().tearDownClass()
392+
393+
def _test_exception(self, test_type, wrapping_exc, exc_name, severity):
394+
log_model = self.env["rest.log"].sudo()
395+
initial_entries = log_model.search([])
396+
# Context: we are running in a transaction case which uses savepoints.
397+
# The log machinery is going to rollback the transation when catching errors.
398+
# Hence we need a completely separated env for the service.
399+
with new_rollbacked_env() as new_env:
400+
# Init fake collection w/ new env
401+
collection = _PseudoCollection(self._collection_name, new_env)
402+
service = self._get_service(self, collection=collection)
403+
with self._get_mocked_request(env=new_env):
404+
try:
405+
service.dispatch("fail", test_type)
406+
except Exception as err:
407+
# Not using `assertRaises` to inspect the exception directly
408+
self.assertTrue(isinstance(err, wrapping_exc))
409+
self.assertEqual(
410+
service._get_exception_message(err), "Failed as you wanted!"
411+
)
412+
413+
with new_rollbacked_env() as new_env:
414+
log_model = new_env["rest.log"].sudo()
415+
entry = log_model.search([]) - initial_entries
416+
expected = {
417+
"collection": service._collection,
418+
"state": "failed",
419+
"result": "null",
420+
"exception_name": exc_name,
421+
"exception_message": "Failed as you wanted!",
422+
"severity": severity,
423+
}
424+
self.assertRecordValues(entry, [expected])
425+
426+
@staticmethod
427+
def _get_test_controller(class_or_instance, root_path=None):
428+
return super()._get_test_controller(
429+
class_or_instance, root_path="/test_log_exception_retryable/"
430+
)
431+
432+
def test_log_exception_retryable(self):
433+
# retryable error must bubble up to the retrying mechanism
434+
self._test_exception(
435+
"retryable",
436+
FakeConcurrentUpdateError,
437+
"odoo.addons.rest_log.tests.common.FakeConcurrentUpdateError",
438+
"warning",
439+
)

0 commit comments

Comments
 (0)