mirror of https://github.com/nicolargo/glances.git
Implement a basic memory cache with TTL for API call (set to ~1 second) #3202
This commit is contained in:
parent
b888dc55e8
commit
7c13ae17fa
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -197,6 +197,15 @@ body {
|
|||
width: 8em;
|
||||
}
|
||||
|
||||
#system {
|
||||
span {
|
||||
padding-left: 10px;
|
||||
}
|
||||
span:nth-child(1) {
|
||||
padding-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
#ip {
|
||||
span {
|
||||
padding-left: 10px;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue