Implement a basic memory cache with TTL for API call (set to ~1 second) #3202

This commit is contained in:
nicolargo 2025-06-14 10:47:00 +02:00
parent b888dc55e8
commit 7c13ae17fa
8 changed files with 76 additions and 23 deletions

View File

@ -476,18 +476,32 @@ def folder_size(path, errno=0):
return ret_size, ret_err
def weak_lru_cache(maxsize=128, typed=False):
def _get_ttl_hash(ttl):
"""A simple (dummy) function to return a hash based on the current second.
TODO: Implement a real TTL mechanism.
"""
if ttl is None:
return 0
now = datetime.now()
return now.second
def weak_lru_cache(maxsize=1, typed=False, ttl=None):
"""LRU Cache decorator that keeps a weak reference to self
Warning: When used in a class, the class should implement __eq__(self, other) and __hash__(self) methods
Source: https://stackoverflow.com/a/55990799"""
def wrapper(func):
@functools.lru_cache(maxsize, typed)
def _func(_self, *args, **kwargs):
def _func(_self, *args, ttl_hash=None, **kwargs):
del ttl_hash # Unused parameter, but kept for compatibility
return func(_self(), *args, **kwargs)
@functools.wraps(func)
def inner(self, *args, **kwargs):
return _func(weakref.ref(self), *args, **kwargs)
return _func(weakref.ref(self), *args, ttl_hash=_get_ttl_hash(ttl), **kwargs)
return inner

View File

@ -18,7 +18,7 @@ from urllib.parse import urljoin
from glances import __apiversion__, __version__
from glances.events_list import glances_events
from glances.globals import json_dumps
from glances.globals import json_dumps, weak_lru_cache
from glances.logger import logger
from glances.password import GlancesPassword
from glances.processes import glances_processes
@ -141,6 +141,12 @@ class GlancesRestfulApi:
self.TEMPLATE_PATH = os.path.join(webui_root_path, 'static/templates')
self._templates = Jinja2Templates(directory=self.TEMPLATE_PATH)
# FastAPI Enable GZIP compression
# https://fastapi.tiangolo.com/advanced/middleware/
# Should be done before other middlewares to avoid
# LocalProtocolError("Too much data for declared Content-Length")
self._app.add_middleware(GZipMiddleware, minimum_size=1000)
# FastAPI Enable CORS
# https://fastapi.tiangolo.com/tutorial/cors/
self._app.add_middleware(
@ -152,10 +158,6 @@ class GlancesRestfulApi:
allow_headers=config.get_list_value('outputs', 'cors_headers', default=["*"]),
)
# FastAPI Enable GZIP compression
# https://fastapi.tiangolo.com/advanced/middleware/
self._app.add_middleware(GZipMiddleware, minimum_size=1000)
# FastAPI Define routes
self._app.include_router(self._router())
@ -196,7 +198,7 @@ class GlancesRestfulApi:
def authentication(self, creds: Annotated[HTTPBasicCredentials, Depends(security)]):
"""Check if a username/password combination is valid."""
if creds.username == self.args.username:
# check_password and get_hash are (lru) cached to optimize the requests
# check_password
if self._password.check_password(self.args.password, self._password.get_hash(creds.password)):
return creds.username
@ -453,6 +455,7 @@ class GlancesRestfulApi:
return GlancesJSONResponse(self.servers_list.get_servers_list() if self.servers_list else [])
@weak_lru_cache(maxsize=1, ttl=1)
def _api_all(self):
"""Glances API RESTful implementation.
@ -480,6 +483,7 @@ class GlancesRestfulApi:
return GlancesJSONResponse(statval)
@weak_lru_cache(maxsize=1, ttl=1)
def _api_all_limits(self):
"""Glances API RESTful implementation.
@ -496,6 +500,7 @@ class GlancesRestfulApi:
return GlancesJSONResponse(limits)
@weak_lru_cache(maxsize=1, ttl=1)
def _api_all_views(self):
"""Glances API RESTful implementation.
@ -512,6 +517,7 @@ class GlancesRestfulApi:
return GlancesJSONResponse(limits)
@weak_lru_cache(maxsize=1, ttl=1)
def _api(self, plugin: str):
"""Glances API RESTful implementation.
@ -541,6 +547,7 @@ class GlancesRestfulApi:
status.HTTP_400_BAD_REQUEST, f"Unknown plugin {plugin} (available plugins: {self.plugins_list})"
)
@weak_lru_cache(maxsize=1, ttl=1)
def _api_top(self, plugin: str, nb: int = 0):
"""Glances API RESTful implementation.
@ -569,6 +576,7 @@ class GlancesRestfulApi:
return GlancesJSONResponse(statval)
@weak_lru_cache(maxsize=1, ttl=1)
def _api_history(self, plugin: str, nb: int = 0):
"""Glances API RESTful implementation.
@ -591,6 +599,7 @@ class GlancesRestfulApi:
return statval
@weak_lru_cache(maxsize=1, ttl=1)
def _api_limits(self, plugin: str):
"""Glances API RESTful implementation.
@ -609,6 +618,7 @@ class GlancesRestfulApi:
return GlancesJSONResponse(ret)
@weak_lru_cache(maxsize=1, ttl=1)
def _api_views(self, plugin: str):
"""Glances API RESTful implementation.

View File

@ -197,6 +197,15 @@ body {
width: 8em;
}
#system {
span {
padding-left: 10px;
}
span:nth-child(1) {
padding-left: 0px;
}
}
#ip {
span {
padding-left: 10px;

View File

@ -2,7 +2,7 @@
<section id="system" class="plugin">
<span v-if="isDisconnected" class="critical">Disconnected from</span>
<span class="title">{{ hostname }}</span>
<span class="text-truncate">{{ humanReadableName }}</span>
<span v-if="!isDisconnected" class="text-truncate">{{ humanReadableName }}</span>
</section>
</template>

View File

@ -1176,9 +1176,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1878,9 +1878,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -22,6 +22,7 @@ import requests
from glances import __version__
from glances.globals import text_type
from glances.outputs.glances_restful_api import GlancesRestfulApi
from glances.timer import Counter
SERVER_PORT = 61234
API_VERSION = GlancesRestfulApi.API_VERSION
@ -71,10 +72,29 @@ class TestGlances(unittest.TestCase):
method = "all"
print('INFO: [TEST_001] Get all stats')
print(f"HTTP RESTful request: {URL}/{method}")
req = self.http_get(f"{URL}/{method}")
self.assertTrue(req.ok)
self.assertTrue(req.json(), dict)
# First call is not cached
counter_first_call = Counter()
first_req = self.http_get(f"{URL}/{method}")
self.assertTrue(first_req.ok)
self.assertTrue(first_req.json(), dict)
counter_first_call_result = counter_first_call.get()
# Second call (if it is in the same second) is cached
counter_second_call = Counter()
second_req = self.http_get(f"{URL}/{method}")
self.assertTrue(second_req.ok)
self.assertTrue(second_req.json(), dict)
counter_second_call_result = counter_second_call.get()
# Check if result of first call is equal to second call
self.assertEqual(first_req.json(), second_req.json(), "The result of the first and second call should be equal")
# Check cache result
print(
f"First API call took {counter_first_call_result:.2f} seconds"
f" and second API call (cached) took {counter_second_call_result:.2f} seconds"
)
self.assertTrue(
counter_second_call_result < counter_first_call_result,
"The second call should be cached (faster than the first one)",
)
def test_002_pluginslist(self):
"""Plugins list."""