diff --git a/kernelci/api/__init__.py b/kernelci/api/__init__.py index 360d40c211..22bc4d804d 100644 --- a/kernelci/api/__init__.py +++ b/kernelci/api/__init__.py @@ -382,7 +382,11 @@ def find( @abc.abstractmethod def stats(self, attributes: Dict[str, str]) -> list: - """Get aggregated telemetry statistics""" + """Return aggregated telemetry statistics for matched events. + + The result contains grouped counters only; it does not run any + anomaly detection. + """ @property def telemetry(self) -> Telemetry: diff --git a/kernelci/api/latest.py b/kernelci/api/latest.py index 488e32ef51..637217c52f 100644 --- a/kernelci/api/latest.py +++ b/kernelci/api/latest.py @@ -168,7 +168,11 @@ def find( return self._get_paginated(params, 'telemetry', offset, limit) def stats(self, attributes: Dict[str, str]) -> list: - """Get aggregated telemetry statistics""" + """Return aggregated telemetry statistics for matched events. + + This endpoint only performs grouped aggregation of telemetry counters + and does not perform anomaly detection. + """ params = attributes.copy() if attributes else {} return self._get('telemetry/stats', params=params).json() diff --git a/kernelci/kbuild.py b/kernelci/kbuild.py index 0dacb8c683..dfba730529 100644 --- a/kernelci/kbuild.py +++ b/kernelci/kbuild.py @@ -903,6 +903,9 @@ def _get_storage(self): ''' Get storage object ''' + if hasattr(self, '_storage') and self._storage is not None: + return self._storage + storage_config = kernelci.config.storage.StorageFactory.from_yaml( name='storage', config=yaml.safe_load(self._storage_config) ) @@ -911,7 +914,8 @@ def _get_storage(self): storage_cred = os.getenv('KCI_STORAGE_CREDENTIALS') if not storage_cred: raise ValueError("KCI_STORAGE_CREDENTIALS not set") - return kernelci.storage.get_storage(storage_config, storage_cred) + self._storage = kernelci.storage.get_storage(storage_config, storage_cred) + return self._storage def map_artifact_name(self, artifact): ''' diff --git a/kernelci/storage/backend.py b/kernelci/storage/backend.py index e164198f7c..ae68a31c9f 100644 --- a/kernelci/storage/backend.py +++ b/kernelci/storage/backend.py @@ -11,6 +11,7 @@ import time from urllib.parse import urljoin import requests +from requests.adapters import HTTPAdapter from . import Storage @@ -20,6 +21,8 @@ class StorageBackend(Storage): This class implements the Storage interface for the kernelci-backend API. It requires an API token as credentials. """ + _HTTP_TIMEOUT = 300 + _HTTP_POOL_SIZE = 20 def _close_files(self, files): """Helper to close all file handles in the files dictionary.""" @@ -27,6 +30,10 @@ def _close_files(self, files): if hasattr(file_tuple[1], 'close'): file_tuple[1].close() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._session = None + def _handle_http_error(self, exc, attempt, max_retries, retry_delay): """Handle HTTP errors during upload with retry logic.""" # Only retry on server errors (5xx status codes) @@ -57,7 +64,17 @@ def _handle_network_error(self, exc, attempt, max_retries, retry_delay): print(f"Upload failed after {max_retries} attempts") return exc + def _connect(self): + if self._session is not None: + return + self._session = requests.Session() + adapter = HTTPAdapter(pool_connections=self._HTTP_POOL_SIZE, + pool_maxsize=self._HTTP_POOL_SIZE) + self._session.mount('https://', adapter) + self._session.mount('http://', adapter) + def _upload(self, file_paths, dest_path): + self._connect() headers = { 'Authorization': self.credentials, } @@ -78,8 +95,9 @@ def _upload(self, file_paths, dest_path): } url = urljoin(self.config.api_url, 'upload') - resp = requests.post( - url, headers=headers, data=data, files=files, timeout=300 + resp = self._session.post( + url, headers=headers, data=data, files=files, + timeout=self._HTTP_TIMEOUT ) resp.raise_for_status()