Add XML-RPC timeout to odoo_client (hung Odoo leaked call slots)

Reliability drill finding: xmlrpc.client has no default timeout, and the call
slot is released only after run_agent returns (which awaits post-call
persistence). A hung -- not refused -- Odoo would block forever; two hung
persists would leave the phone line permanently at capacity ("busy") until a
restart. All XML-RPC round-trips now use a 15s socket timeout
(ODOO_TIMEOUT_SECS), http and https transports both covered.
Drilled: black-hole address fails cleanly at the timeout; real auth unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tocmo0nlord
2026-07-04 04:57:04 +00:00
parent 54d707ceac
commit 80e0bbe899

View File

@@ -26,18 +26,45 @@ ODOO_TEAM_ID = int(os.environ["ODOO_TEAM_ID"]) if os.environ.get("ODOO_TEAM_ID")
ODOO_USER_ID = int(os.environ["ODOO_USER_ID"]) if os.environ.get("ODOO_USER_ID") else None
# Hard cap on any single XML-RPC round-trip. Without it, xmlrpc.client uses the OS
# default (no timeout): a HUNG Odoo (as opposed to a refused connection) would block
# post-call persistence forever — and since the call slot is released only after
# run_agent returns, two hung persists would leave the line permanently "busy".
ODOO_TIMEOUT_SECS = float(os.environ.get("ODOO_TIMEOUT_SECS", "15"))
class OdooError(RuntimeError):
pass
class _TimeoutTransport(xmlrpc.client.Transport):
def make_connection(self, host):
conn = super().make_connection(host)
conn.timeout = ODOO_TIMEOUT_SECS
return conn
class _TimeoutSafeTransport(xmlrpc.client.SafeTransport):
def make_connection(self, host):
conn = super().make_connection(host)
conn.timeout = ODOO_TIMEOUT_SECS
return conn
def _proxy(path):
transport = (_TimeoutSafeTransport() if ODOO_URL.lower().startswith("https")
else _TimeoutTransport())
return xmlrpc.client.ServerProxy(f"{ODOO_URL}{path}", transport=transport)
def _connect():
if not (ODOO_USER and ODOO_API_KEY):
raise OdooError("ODOO_USER / ODOO_API_KEY not set")
common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common")
common = _proxy("/xmlrpc/2/common")
uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_API_KEY, {})
if not uid:
raise OdooError("Odoo authentication failed (check db/user/key)")
models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object")
models = _proxy("/xmlrpc/2/object")
return uid, models