diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index 427c435d61..994aac20e2 100644 --- a/edi_oca/models/edi_backend.py +++ b/edi_oca/models/edi_backend.py @@ -8,6 +8,7 @@ import base64 import logging import traceback +from datetime import timedelta from io import StringIO from odoo import _, exceptions, fields, models, tools @@ -322,10 +323,20 @@ def exchange_send(self, exchange_record): _logger.debug("%s sent", exchange_record.identifier) except self._send_retryable_exceptions() as err: error = _get_exception_msg() - _logger.debug("%s send failed. To be retried.", exchange_record.identifier) - raise RetryableJobError( - error, **exchange_record._job_retry_params() - ) from err + if self._send_should_retry(exchange_record): + _logger.debug( + "%s send failed. To be retried.", exchange_record.identifier + ) + raise RetryableJobError( + error, **exchange_record._job_retry_params() + ) from err + else: + state = "output_error_on_send" + message = exchange_record._exchange_status_message("send_ko") + res = f"Error: {error}" + _logger.debug( + "%s send failed. Marked as errored.", exchange_record.identifier + ) except self._swallable_exceptions(): if self.env.context.get("_edi_send_break_on_error"): raise @@ -374,6 +385,10 @@ def _send_retryable_exceptions(self): # when dealing w/ internal or external systems or filesystems return (IOError, OSError) + def _send_should_retry(self, exc_record): + # Safety check not to retry indefinitely + return exc_record.create_date >= (fields.Datetime.now() - timedelta(days=1)) + def _output_check_send(self, exchange_record): if exchange_record.direction != "output": raise exceptions.UserError( diff --git a/edi_oca/tests/test_backend_jobs.py b/edi_oca/tests/test_backend_jobs.py index b33eb1fbbd..f74a832588 100644 --- a/edi_oca/tests/test_backend_jobs.py +++ b/edi_oca/tests/test_backend_jobs.py @@ -3,6 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import mock +from freezegun import freeze_time from requests.exceptions import ConnectionError as ReqConnectionError from odoo.addons.queue_job.exception import RetryableJobError @@ -75,6 +76,38 @@ def test_output_fail_retry(self): mocked.side_effect = ReqConnectionError("Connection broken") with self.assertRaises(RetryableJobError): job.perform() + self.assertEqual(record.edi_exchange_state, "output_pending") + + def test_output_fail_too_many_retries(self): + job_counter = self.job_counter() + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + "edi_exchange_state": "output_pending", + } + record = self.backend.create_record("test_csv_output", vals) + record._write({"create_date": "2024-01-10 09:00:00"}) + record._set_file_content("ABC") + with mock.patch.object(type(self.backend), "_exchange_send") as mocked: + mocked.side_effect = ReqConnectionError("Connection broken") + with freeze_time("2024-01-10 11:00:00"): + # + 2 hours + job = self.backend.with_delay().exchange_send(record) + with self.assertRaises(RetryableJobError): + job.perform() + with freeze_time("2024-01-11 08:50:00"): + # + 23 hours and 50 minutes + job = self.backend.with_delay().exchange_send(record) + with self.assertRaises(RetryableJobError): + job.perform() + self.assertEqual(record.edi_exchange_state, "output_pending") + with freeze_time("2024-01-11 09:20:00"): + # + 24 hours and 20 minutes + job = self.backend.with_delay().exchange_send(record) + res = job.perform() + self.assertIn("Error", res) + job_counter.search_created() + self.assertEqual(record.edi_exchange_state, "output_error_on_send") def test_input(self): job_counter = self.job_counter()