Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion connector_oxigesti/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

{
"name": "Oxigesti-Odoo connector",
"version": "14.0.1.1.11",
"version": "14.0.1.1.13",
"author": "NuoBiT Solutions, S.L.",
"license": "AGPL-3",
"category": "Connector",
Expand Down
114 changes: 68 additions & 46 deletions connector_oxigesti/components/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,24 @@ def api_handle_errors(message=""):
)


@contextmanager
def mssql_connection_retryable():
"""Re-raise transient MSSQL connection errors as NetworkRetryableError.

Without this, a MSSQL restart or network blip while an export job is
running raises ``pymssql.OperationalError`` / ``InterfaceError``, which
queue_job does not recognize as retryable — the job moves straight to
``failed`` on its first attempt and the per-backend ``since_date``
cursor has already advanced past it, making the missing record
invisible to subsequent cron cycles. Wrapping as
``NetworkRetryableError`` lets the configured ``retry_pattern`` apply.
"""
try:
yield
except (pymssql.OperationalError, pymssql.InterfaceError) as err:
raise NetworkRetryableError("MSSQL connection error: %s" % err) from err


class CRUDAdapter(AbstractComponent):
"""External Records Adapter for Oxigesti"""

Expand Down Expand Up @@ -157,14 +175,15 @@ def _exec_sql(self, sql, params, as_dict=False, commit=False):
# Convert params
params = self._convert_dict(params, to_backend=True)
# Execute sql
conn = self.conn()
cr = conn.cursor(as_dict=as_dict)
cr.execute(sql, params)
res = cr.fetchall()
if commit:
conn.commit()
cr.close()
conn.close()
with mssql_connection_retryable():
conn = self.conn()
cr = conn.cursor(as_dict=as_dict)
cr.execute(sql, params)
res = cr.fetchall()
if commit:
conn.commit()
cr.close()
conn.close()
# Convert result
if as_dict:
for r in res:
Expand Down Expand Up @@ -313,26 +332,27 @@ def write(self, _id, values_d): # pylint: disable=W8106
params[k9] = v
params = self._convert_dict(params, to_backend=True)

conn = self.conn()
cr = conn.cursor()
cr.execute(sql, params) # pylint: disable=E8103
count = cr.rowcount
if count == 0:
raise Exception(
_(
"Impossible to update external record with ID '%s': "
"Register not found on Backend"
with mssql_connection_retryable():
conn = self.conn()
cr = conn.cursor()
cr.execute(sql, params) # pylint: disable=E8103
count = cr.rowcount
if count == 0:
raise Exception(
_(
"Impossible to update external record with ID '%s': "
"Register not found on Backend"
)
% (id_d,)
)
% (id_d,)
)
elif count > 1:
conn.rollback()
raise pymssql.IntegrityError(
"Unexpected error: Returned more the one row with ID: %s" % (id_d,)
)
conn.commit()
cr.close()
conn.close()
elif count > 1:
conn.rollback()
raise pymssql.IntegrityError(
"Unexpected error: Returned more the one row with ID: %s" % (id_d,)
)
conn.commit()
cr.close()
conn.close()

return count

Expand Down Expand Up @@ -428,26 +448,28 @@ def delete(self, _id):
params = dict(zip(self._id, _id))
params = self._convert_dict(params, to_backend=True)

conn = self.conn()
cr = conn.cursor()
cr.execute(sql, params) # pylint: disable=E8103
count = cr.rowcount
if count == 0:
raise Exception(
_(
"Impossible to delete external record with ID '%s': "
"Register not found on Backend"
with mssql_connection_retryable():
conn = self.conn()
cr = conn.cursor()
cr.execute(sql, params) # pylint: disable=E8103
count = cr.rowcount
if count == 0:
raise Exception(
_(
"Impossible to delete external record with ID '%s': "
"Register not found on Backend"
)
% (params,)
)
% (params,)
)
elif count > 1:
conn.rollback()
raise pymssql.IntegrityError(
"Unexpected error: Returned more the one row with ID: %s" % (params,)
)
conn.commit()
cr.close()
conn.close()
elif count > 1:
conn.rollback()
raise pymssql.IntegrityError(
"Unexpected error: Returned more the one row with ID: %s"
% (params,)
)
conn.commit()
cr.close()
conn.close()

return count

Expand Down
29 changes: 29 additions & 0 deletions connector_oxigesti/models/oxigesti_binding/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@

from ...common.tools import idhash

# Max attempts for a job before it transitions to ``state='failed'``.
# Combined with ``retry_pattern`` on every queue.job.function of this
# connector (``{1: 10, 5: 30, 10: 60, 15: 300}``) this yields a retry
# window of ~33 minutes — enough to cover transient MSSQL restarts,
# VPN blips and short planned maintenance on the Oxigesti server
# without spamming it. OCA queue_job default is 5 which clipped the
# progression at the first bucket (10s × 4 ≈ 40s).
MAX_RETRIES_NETWORK = 20


class OxigestiBinding(models.AbstractModel):
_name = "oxigesti.binding"
Expand All @@ -17,6 +26,26 @@ class OxigestiBinding(models.AbstractModel):

active = fields.Boolean(default=True)

def with_delay(
self,
priority=None,
eta=None,
max_retries=None,
description=None,
channel=None,
identity_key=None,
):
if max_retries is None:
max_retries = MAX_RETRIES_NETWORK
return super().with_delay(
priority=priority,
eta=eta,
max_retries=max_retries,
description=description,
channel=channel,
identity_key=identity_key,
)

backend_id = fields.Many2one(
comodel_name="oxigesti.backend",
string="Oxigesti Backend",
Expand Down
2 changes: 2 additions & 0 deletions connector_oxigesti/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import test_mssql_retryable
from . import test_with_delay_max_retries
Loading
Loading