Skip to content

Commit

Permalink
[FIX] fastapi: Avoid zombie threads
Browse files Browse the repository at this point in the history
Each time a fastapi app is created, a new event loop thread is created by the ASGIMiddleware. Unfortunately, every time the cache is cleared, a new app is created with a new even loop thread. This leads to an increase in the number of threads created to manage the asyncio event loop, even though many of them are no longer in use. To avoid this problem, the thread in charge of the event loop is now created only once per thread / process and the result is stored in the thread's local storage. If a new instance of an app needs to be created following a cache reset, this ensures that the same event loop is reused.

refs #484
  • Loading branch information
lmignon committed Jan 8, 2025
1 parent dbbfea0 commit 696a7c0
Showing 1 changed file with 24 additions and 1 deletion.
25 changes: 24 additions & 1 deletion fastapi/models/fastapi_endpoint.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Copyright 2022 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).

import asyncio
import logging
import threading
from functools import partial
from itertools import chain
from typing import Any, Callable, Dict, List, Tuple
Expand All @@ -19,6 +21,26 @@
_logger = logging.getLogger(__name__)


# Thread-local storage for event loops
# Using a thread-local storage allows to have a dedicated event loop per thread
# and avoid the need to create a new event loop for each request. It's also
# compatible with the multi-worker mode of Odoo.
_event_loop_storage = threading.local()


def get_or_create_event_loop() -> asyncio.AbstractEventLoop:
"""
Get or create a reusable event loop for the current thread.
"""
if not hasattr(_event_loop_storage, "loop"):
loop = asyncio.new_event_loop()
loop_thread = threading.Thread(target=loop.run_forever, daemon=True)
loop_thread.start()
_event_loop_storage.loop = loop
_event_loop_storage.thread = loop_thread
return _event_loop_storage.loop


class FastapiEndpoint(models.Model):

_name = "fastapi.endpoint"
Expand Down Expand Up @@ -213,7 +235,8 @@ def get_app(self, root_path):
app = FastAPI()
app.mount(record.root_path, record._get_app())
self._clear_fastapi_exception_handlers(app)
return ASGIMiddleware(app)
event_loop = get_or_create_event_loop()
return ASGIMiddleware(app, loop=event_loop)

def _clear_fastapi_exception_handlers(self, app: FastAPI) -> None:
"""
Expand Down

0 comments on commit 696a7c0

Please sign in to comment.