-
-
Notifications
You must be signed in to change notification settings - Fork 305
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[FIX] fastapi: Transactions handling refactoring
This change is a complete rewrite of the way the transactions are managed when integrating a fastapi application into Odoo. In the previous implementation, specifics error handlers were put in place to catch exception occurring in the handling of requests made to a fastapi application and to rollback the transaction in case of error. This was done by registering specifics error handlers methods to the fastapi application using the 'add_exception_handler' method of the fastapi application. In this implementation, the transaction was rolled back in the error handler method. This approach was not working as expected for several reasons: - The handling of the error at the fastapi level prevented the retry mechanism to be triggered in case of a DB concurrency error. This is because the error was catch at the fastapi level and never bubbled up to the early stage of the processing of the request where the retry mechanism is implemented. - The cleanup of the environment and the registry was not properly done in case of error. In the **'odoo.service.model.retrying'** method, you can see that the cleanup process is different in case of error raised by the database and in case of error raised by the application. This change fix these issues by ensuring that errors are no more catch at the fastapi level and bubble up the fastapi processing stack through the event loop required to transform WSGI to ASGI. As result the transactional nature of the requests to the fastapi applications is now properly managed by the Odoo framework.
- Loading branch information
Showing
11 changed files
with
276 additions
and
433 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
from . import models | ||
from . import fastapi_dispatcher | ||
from . import error_handlers |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,119 +1,36 @@ | ||
# Copyright 2022 ACSONE SA/NV | ||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). | ||
|
||
import logging | ||
|
||
from psycopg2.errors import IntegrityError, OperationalError | ||
from starlette.exceptions import WebSocketException | ||
from starlette.middleware.errors import ServerErrorMiddleware | ||
from starlette.middleware.exceptions import ExceptionMiddleware | ||
from starlette.responses import JSONResponse | ||
from starlette.status import ( | ||
HTTP_400_BAD_REQUEST, | ||
HTTP_403_FORBIDDEN, | ||
HTTP_404_NOT_FOUND, | ||
HTTP_500_INTERNAL_SERVER_ERROR, | ||
) | ||
|
||
import odoo | ||
from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY, _as_validation_error | ||
from starlette.websockets import WebSocket | ||
|
||
from fastapi import Request | ||
from fastapi.exception_handlers import http_exception_handler | ||
from fastapi.exceptions import HTTPException | ||
|
||
from .context import odoo_env_ctx | ||
|
||
_logger = logging.getLogger(__name__) | ||
|
||
|
||
def _rollback(request: Request, reason: str) -> None: | ||
cr = odoo_env_ctx.get().cr | ||
if cr is not None: | ||
_logger.debug("rollback on %s", reason) | ||
cr.rollback() | ||
|
||
|
||
async def _odoo_user_error_handler( | ||
request: Request, exc: odoo.exceptions.UserError | ||
) -> JSONResponse: | ||
_rollback(request, "UserError") | ||
return await http_exception_handler( | ||
request, HTTPException(HTTP_400_BAD_REQUEST, exc.args[0]) | ||
) | ||
|
||
|
||
async def _odoo_access_error_handler( | ||
request: Request, _exc: odoo.exceptions.AccessError | ||
) -> JSONResponse: | ||
_rollback(request, "AccessError") | ||
return await http_exception_handler( | ||
request, HTTPException(HTTP_403_FORBIDDEN, "AccessError") | ||
) | ||
|
||
|
||
async def _odoo_missing_error_handler( | ||
request: Request, _exc: odoo.exceptions.MissingError | ||
) -> JSONResponse: | ||
_rollback(request, "MissingError") | ||
return await http_exception_handler( | ||
request, HTTPException(HTTP_404_NOT_FOUND, "MissingError") | ||
) | ||
|
||
|
||
async def _odoo_validation_error_handler( | ||
request: Request, exc: odoo.exceptions.ValidationError | ||
) -> JSONResponse: | ||
_rollback(request, "ValidationError") | ||
return await http_exception_handler( | ||
request, HTTPException(HTTP_400_BAD_REQUEST, exc.args[0]) | ||
) | ||
|
||
|
||
async def _odoo_http_exception_handler( | ||
request: Request, exc: HTTPException | ||
) -> JSONResponse: | ||
_rollback(request, "HTTPException") | ||
return await http_exception_handler(request, exc) | ||
|
||
|
||
async def _odoo_exception_handler(request: Request, exc: Exception) -> JSONResponse: | ||
# let the OperationalError bubble up to the retrying mechanism | ||
# We can't define a specific handler for OperationalError because since we | ||
# want to let it bubble up to the retrying mechanism, it will be handled by | ||
# the default handler at the end of the chain. | ||
if ( | ||
isinstance(exc, OperationalError) | ||
and exc.pgcode in PG_CONCURRENCY_ERRORS_TO_RETRY | ||
): | ||
raise exc | ||
# we need to monkey patch the ServerErrorMiddleware and ExceptionMiddleware classes | ||
# to ensure that all the exceptions that are handled by these specific | ||
# middlewares are let to bubble up to the retrying mechanism and the | ||
# dispatcher error handler to ensure that appropriate action are taken | ||
# regarding the transaction, environment, and registry. These middlewares | ||
# are added by default by FastAPI when creating an application and it's not | ||
# possible to remove them. So we need to monkey patch them. | ||
|
||
_rollback(request, "Exception") | ||
_logger.exception("Unhandled exception", exc_info=exc) | ||
return await http_exception_handler( | ||
request, HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, str(exc)) | ||
) | ||
|
||
|
||
async def _pg_integrity_error_handler( | ||
request: Request, exc: IntegrityError | ||
def pass_through_exception_handler( | ||
self, request: Request, exc: Exception | ||
) -> JSONResponse: | ||
"""Handle IntegrityError from the database as a ValidationError""" | ||
validation_error = _as_validation_error(odoo_env_ctx.get(), exc) | ||
return await _odoo_validation_error_handler(request, validation_error) | ||
|
||
|
||
# we need to monkey patch the ServerErrorMiddleware to ensure that the | ||
# OperationalError is not handled by the default handler but is let to bubble up | ||
# to the retrying mechanism | ||
original_error_response_method = ServerErrorMiddleware.error_response | ||
raise exc | ||
|
||
|
||
def error_response(self, request: Request, exc: Exception) -> JSONResponse: | ||
if ( | ||
isinstance(exc, OperationalError) | ||
and exc.pgcode in PG_CONCURRENCY_ERRORS_TO_RETRY | ||
): | ||
raise exc | ||
return original_error_response_method(self, request, exc) | ||
def pass_through_websocket_exception_handler( | ||
self, websocket: WebSocket, exc: WebSocketException | ||
) -> None: | ||
raise exc | ||
|
||
|
||
ServerErrorMiddleware.error_response = error_response | ||
ServerErrorMiddleware.error_response = pass_through_exception_handler | ||
ExceptionMiddleware.http_exception = pass_through_exception_handler | ||
ExceptionMiddleware.websocket_exception = pass_through_websocket_exception_handler |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.