Skip to content
Open
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
85 changes: 56 additions & 29 deletions spp_api_v2/models/ir_http_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
This workaround should be removed when Odoo core fixes the cache bug.
"""

import hashlib
import logging
import threading

Expand All @@ -22,6 +23,16 @@

_logger = logging.getLogger(__name__)

# Stable 64-bit signed key for the transaction-scoped advisory lock that
# serializes FastAPI endpoint sync attempts across workers. Derived from a
# SHA-256 of the qualified name so it is deterministic and unlikely to collide
# with other modules' advisory locks in the same database.
_FASTAPI_SYNC_ADVISORY_LOCK_KEY = int.from_bytes(
hashlib.sha256(b"spp_api_v2.fastapi_endpoint_sync").digest()[:8],
byteorder="big",
signed=True,
)


class IrHttp(models.AbstractModel):
"""Patch ir.http to fix routing_map cache bug"""
Expand Down Expand Up @@ -80,37 +91,53 @@ def routing_map(self, key=None):
from odoo.api import Environment

with registry.cursor() as cr:
env = Environment(cr, SUPERUSER_ID, {})

# First check for endpoints with registry_sync=False (never synced)
unsynced_endpoints = env["fastapi.endpoint"].search([("registry_sync", "=", False)])

# Also check for endpoints that claim to be synced but have no routes
# This catches cases where routes were deleted or DB was reset
synced_endpoints = env["fastapi.endpoint"].search([("registry_sync", "=", True)])
if synced_endpoints and "endpoint.route" in env:
for endpoint in synced_endpoints:
route_exists = env["endpoint.route"].search_count(
[("endpoint_id", "=", endpoint.id)], limit=1
)
if not route_exists:
_logger.warning(
"Endpoint '%s' (id=%d) claims to be synced but has no routes - forcing re-sync",
endpoint.name,
endpoint.id,
)
# Reset flag to trigger re-sync
endpoint.registry_sync = False
unsynced_endpoints |= endpoint

if unsynced_endpoints:
unsynced_endpoints.action_sync_registry()
cr.commit()
_logger.info(
"Synced %d FastAPI endpoints for database %s",
len(unsynced_endpoints),
# Serialize concurrent sync attempts across workers. After a
# registry reload (e.g. -u all) every worker's routing_map()
# races to update the same fastapi_endpoint rows; without
# this lock all but one fail with SerializationFailure.
# Transaction-scoped — released automatically at COMMIT/ROLLBACK.
cr.execute(
"SELECT pg_try_advisory_xact_lock(%s)",
(_FASTAPI_SYNC_ADVISORY_LOCK_KEY,),
)
(got_lock,) = cr.fetchone()
if not got_lock:
_logger.debug(
"FastAPI endpoint sync skipped for %s — another worker is syncing",
registry.db_name,
)
else:
env = Environment(cr, SUPERUSER_ID, {})

# First check for endpoints with registry_sync=False (never synced)
unsynced_endpoints = env["fastapi.endpoint"].search([("registry_sync", "=", False)])

# Also check for endpoints that claim to be synced but have no routes
# This catches cases where routes were deleted or DB was reset
synced_endpoints = env["fastapi.endpoint"].search([("registry_sync", "=", True)])
if synced_endpoints and "endpoint.route" in env:
for endpoint in synced_endpoints:
route_exists = env["endpoint.route"].search_count(
[("endpoint_id", "=", endpoint.id)], limit=1
)
if not route_exists:
_logger.warning(
"Endpoint '%s' (id=%d) claims to be synced but has no routes - forcing re-sync",
endpoint.name,
endpoint.id,
)
# Reset flag to trigger re-sync
endpoint.registry_sync = False
unsynced_endpoints |= endpoint

if unsynced_endpoints:
unsynced_endpoints.action_sync_registry()
cr.commit()
_logger.info(
"Synced %d FastAPI endpoints for database %s",
len(unsynced_endpoints),
registry.db_name,
)
except Exception as e:
# If endpoint model doesn't exist or sync fails, continue anyway
_logger.debug("Could not sync FastAPI endpoints: %s", e)
Expand Down
Loading