11# Copyright 2025 ACSONE SA/NV
22# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
3-
3+ import logging
44import queue
55import threading
66from collections import defaultdict
1111
1212from fastapi import FastAPI
1313
14+ _logger = logging .getLogger (__name__ )
15+
1416
1517class FastApiAppPool :
18+ """Pool of FastAPI apps.
19+
20+ This class manages a pool of FastAPI apps. The pool is organized by database name
21+ and root path. Each pool is a queue of FastAPI apps.
22+
23+ The pool is used to reuse FastAPI apps across multiple requests. This is useful
24+ to avoid the overhead of creating a new FastAPI app for each request. The pool
25+ ensures that only one request at a time uses an app.
26+
27+ The proper way to use the pool is to use the get_app method as a context manager.
28+ This ensures that the app is returned to the pool after the context manager exits.
29+ The get_app method is designed to ensure that the app made available to the
30+ caller is unique and not used by another caller at the same time.
31+
32+ .. code-block:: python
33+
34+ with fastapi_app_pool.get_app(env=request.env, root_path=root_path) as app:
35+ # use the app
36+
37+ The pool is invalidated when the cache registry is updated. This ensures that
38+ the pool is always up-to-date with the latest app configuration. It also
39+ ensures that the invalidation is done even in the case of a modification occurring
40+ in a different worker process or thread or server instance. This mechanism
41+ works because every time an attribute of the fastapi.endpoint model is modified
42+ and this attribute is part of the list returned by the `_fastapi_app_fields`,
43+ or `_routing_impacting_fields` methods, we reset the cache of a marker method
44+ `_reset_app_cache_marker`. As side effect, the cache registry is marked to be
45+ updated by the increment of the `cache_sequence` SQL sequence. This cache sequence
46+ on the registry is reloaded from the DB on each request made to a specific database.
47+ When an app is retrieved from the pool, we always compare the cache sequence of
48+ the pool with the cache sequence of the registry. If the two sequences are different,
49+ we invalidate the pool and save the new cache sequence on the pool.
50+
51+ The cache is based on a defaultdict of defaultdict of queue.Queue. We are cautious
52+ that the use of defaultdict is not thread-safe for operations that modify the
53+ dictionary. However the only operation that modifies the dictionary is the
54+ first access to a new key. If two threads access the same key at the same time,
55+ the two threads will create two different queues. This is not a problem since
56+ at the time of returning an app to the pool, we are sure that a queue exists
57+ for the key into the cache and all the created apps are returned to the same
58+ valid queue. And the end, the lack of thread-safety for the defaultdict could
59+ only lead to a negligible overhead of creating a new queue that will never be
60+ used. This is why we consider that the use of defaultdict is safe in this context.
61+ """
62+
1663 def __init__ (self ):
1764 self ._queue_by_db_by_root_path : dict [
1865 str , dict [str , queue .Queue [FastAPI ]]
1966 ] = defaultdict (lambda : defaultdict (queue .Queue ))
67+ self .__cache_sequence = 0
2068 self ._lock = threading .Lock ()
2169
2270 def __get_pool (self , env : Environment , root_path : str ) -> queue .Queue [FastAPI ]:
2371 db_name = env .cr .dbname
24- with self ._lock :
25- # default dict is not thread safe but the use
26- return self ._queue_by_db_by_root_path [db_name ][root_path ]
72+ return self ._queue_by_db_by_root_path [db_name ][root_path ]
2773
2874 def __get_app (self , env : Environment , root_path : str ) -> FastAPI :
2975 pool = self .__get_pool (env , root_path )
@@ -39,7 +85,7 @@ def __return_app(self, env: Environment, app: FastAPI, root_path: str) -> None:
3985
4086 @contextmanager
4187 def get_app (
42- self , root_path : str , env : Environment
88+ self , env : Environment , root_path : str
4389 ) -> Generator [FastAPI , None , None ]:
4490 """Return a FastAPI app to be used in a context manager.
4591
@@ -49,13 +95,36 @@ def get_app(
4995 When used into the FastApiDispatcher class this ensures that the app is reused
5096 across multiple requests but only one request at a time uses an app.
5197 """
98+ self ._check_cache (env )
5299 app = self .__get_app (env , root_path )
53100 try :
54101 yield app
55102 finally :
56103 self .__return_app (env , app , root_path )
57104
58- def invalidate (self , root_path : str , env : Environment ) -> None :
59- with self ._lock :
60- db_name = env .cr .dbname
105+ @property
106+ def cache_sequence (self ) -> int :
107+ return self .__cache_sequence
108+
109+ @cache_sequence .setter
110+ def cache_sequence (self , value : int ) -> None :
111+ if value != self .__cache_sequence :
112+ with self ._lock :
113+ self .__cache_sequence = value
114+
115+ def _check_cache (self , env : Environment ) -> None :
116+ cache_sequence = env .registry .cache_sequence
117+ if cache_sequence != self .cache_sequence and self .cache_sequence != 0 :
118+ _logger .info (
119+ "Cache registry updated, reset fastapi_app pool for the current "
120+ "database"
121+ )
122+ self .invalidate (env )
123+ self .cache_sequence = cache_sequence
124+
125+ def invalidate (self , env : Environment , root_path : str | None = None ) -> None :
126+ db_name = env .cr .dbname
127+ if root_path :
61128 self ._queue_by_db_by_root_path [db_name ][root_path ] = queue .Queue ()
129+ elif db_name in self ._queue_by_db_by_root_path :
130+ del self ._queue_by_db_by_root_path [db_name ]
0 commit comments