From 82c100e46f6509b1b82e28b9254e5e61331cc7c9 Mon Sep 17 00:00:00 2001 From: Dmitry Ermakovich Date: Tue, 19 May 2026 15:16:24 +0300 Subject: [PATCH] feat: add keep-alive sync mechanism --- testit-adapter-behave/setup.py | 2 +- testit-adapter-nose/setup.py | 2 +- testit-adapter-pytest/setup.py | 2 +- testit-adapter-robotframework/setup.py | 2 +- testit-python-commons/setup.py | 4 +- .../client/api_client.py | 7 ++- .../sync_storage/sync_storage_runner.py | 53 ++++++++++++++++++- .../services/test_sync_storage_runner.py | 41 ++++++++++++++ update_versions.sh | 4 +- 9 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 testit-python-commons/tests/services/test_sync_storage_runner.py diff --git a/testit-adapter-behave/setup.py b/testit-adapter-behave/setup.py index e4f2348a..8c1cbd01 100644 --- a/testit-adapter-behave/setup.py +++ b/testit-adapter-behave/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -VERSION = "4.2.2" +VERSION = "4.2.3" setup( name='testit-adapter-behave', diff --git a/testit-adapter-nose/setup.py b/testit-adapter-nose/setup.py index 4bd05cff..17853d33 100644 --- a/testit-adapter-nose/setup.py +++ b/testit-adapter-nose/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -VERSION = "4.2.2" +VERSION = "4.2.3" setup( name='testit-adapter-nose', diff --git a/testit-adapter-pytest/setup.py b/testit-adapter-pytest/setup.py index bb0fad38..7c4fa616 100644 --- a/testit-adapter-pytest/setup.py +++ b/testit-adapter-pytest/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -VERSION = "4.2.2" +VERSION = "4.2.3" setup( name='testit-adapter-pytest', diff --git a/testit-adapter-robotframework/setup.py b/testit-adapter-robotframework/setup.py index 211ae727..773fd3c8 100644 --- a/testit-adapter-robotframework/setup.py +++ b/testit-adapter-robotframework/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -VERSION = "4.2.2" +VERSION = "4.2.3" setup( name='testit-adapter-robotframework', diff --git a/testit-python-commons/setup.py b/testit-python-commons/setup.py index f905e7b8..1148b8cf 100644 --- a/testit-python-commons/setup.py +++ b/testit-python-commons/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -VERSION = "4.2.2" +VERSION = "4.2.3" setup( name='testit-python-commons', @@ -25,5 +25,5 @@ py_modules=['testit', 'testit_python_commons'], packages=find_packages(where='src'), package_dir={'': 'src'}, - install_requires=['pluggy', 'tomli', 'testit-api-client==7.5.6'] + install_requires=['pluggy', 'tomli', 'testit-api-client==7.5.10'] ) diff --git a/testit-python-commons/src/testit_python_commons/client/api_client.py b/testit-python-commons/src/testit_python_commons/client/api_client.py index 20ddeda0..25922b48 100644 --- a/testit-python-commons/src/testit_python_commons/client/api_client.py +++ b/testit-python-commons/src/testit_python_commons/client/api_client.py @@ -24,7 +24,6 @@ TestRunV2ApiResult, LinkAutoTestToWorkItemRequest, AutoTestWorkItemIdentifierApiResult, - ProjectModel, WorkflowApiResult, ) @@ -547,7 +546,7 @@ def get_configuration_id(self): @adapter_logger @retry - def __get_project(self) -> ProjectModel: + def __get_project(self): return self.__projects_api.get_project_by_id(id=self.__config.get_project_id()) @adapter_logger @@ -557,7 +556,7 @@ def __get_workflow_by_id(self, workflow_id: str) -> WorkflowApiResult: @adapter_logger def __get_status_codes(self) -> List[str]: - project: ProjectModel = self.__get_project() - workflow: WorkflowApiResult = self.__get_workflow_by_id(project.workflow_id) + project = self.__get_project() + workflow = self.__get_workflow_by_id(project.workflow_id) return [status.code for status in workflow.statuses] diff --git a/testit-python-commons/src/testit_python_commons/services/sync_storage/sync_storage_runner.py b/testit-python-commons/src/testit_python_commons/services/sync_storage/sync_storage_runner.py index 85ef3a5a..c1fc1e66 100644 --- a/testit-python-commons/src/testit_python_commons/services/sync_storage/sync_storage_runner.py +++ b/testit-python-commons/src/testit_python_commons/services/sync_storage/sync_storage_runner.py @@ -1,3 +1,4 @@ +import json import logging import os import platform @@ -39,13 +40,15 @@ class SyncStorageRunner: across multiple workers. """ - SYNC_STORAGE_VERSION = "v0.3.2" + SYNC_STORAGE_VERSION = "v0.3.3" SYNC_STORAGE_REPO_URL = ( "https://github.com/testit-tms/sync-storage-public/releases/download/" ) AMD64 = "amd64" ARM64 = "arm64" SYNC_STORAGE_STARTUP_TIMEOUT = 5 # seconds + KEEP_ALIVE_INTERVAL_SECONDS = 30 + KEEP_ALIVE_REQUEST_TIMEOUT_SECONDS = 5 def __init__( self, @@ -82,6 +85,9 @@ def __init__( self.workers_api: Optional[WorkersApi] = None self.test_results_api: Optional[TestResultsApi] = None + self._keep_alive_stop_event = threading.Event() + self._keep_alive_thread: Optional[threading.Thread] = None + logger.debug( f"Initialized SyncStorageRunner with test_run_id={test_run_id}, port={self.port}" ) @@ -113,6 +119,7 @@ def start(self) -> bool: except Exception as e: logger.error(f"Error registering worker: {e}") + self._start_keep_alive() return True # Get executable file name for current platform @@ -170,6 +177,7 @@ def start(self) -> bool: except Exception as e: logger.error(f"Error registering worker: {e}") + self._start_keep_alive() return True else: raise RuntimeError("Cannot start the SyncStorage until timeout") @@ -213,6 +221,49 @@ def get_url(self) -> str: """Get the Sync Storage URL.""" return f"http://localhost:{self.port}" + def _keep_alive(self) -> None: + try: + payload = json.dumps({ + "pid": self.worker_pid, + "testRunId": self.test_run_id, + }).encode("utf-8") + request = urllib.request.Request( + f"{self.get_url()}/keep_alive", + data=payload, + headers={"Content-Type": "application/json"}, + method="POST", + ) + urllib.request.urlopen( + request, + timeout=self.KEEP_ALIVE_REQUEST_TIMEOUT_SECONDS, + ) + except Exception: + pass + + def _keep_alive_loop(self) -> None: + while not self._keep_alive_stop_event.is_set(): + self._keep_alive() + if self._keep_alive_stop_event.wait(self.KEEP_ALIVE_INTERVAL_SECONDS): + break + + def _start_keep_alive(self) -> None: + if self._keep_alive_thread and self._keep_alive_thread.is_alive(): + return + + self._keep_alive_stop_event.clear() + self._keep_alive_thread = threading.Thread( + target=self._keep_alive_loop, + name="sync-storage-keep-alive", + daemon=True, + ) + self._keep_alive_thread.start() + logger.debug("Sync Storage keep-alive thread started") + + def _stop_keep_alive(self) -> None: + self._keep_alive_stop_event.set() + if self._keep_alive_thread and self._keep_alive_thread.is_alive(): + self._keep_alive_thread.join(timeout=1) + def send_in_progress_test_result( self, model: TestResultCutApiModel ) -> bool: diff --git a/testit-python-commons/tests/services/test_sync_storage_runner.py b/testit-python-commons/tests/services/test_sync_storage_runner.py new file mode 100644 index 00000000..ee7278b3 --- /dev/null +++ b/testit-python-commons/tests/services/test_sync_storage_runner.py @@ -0,0 +1,41 @@ +import json +from unittest.mock import MagicMock, patch + +from testit_python_commons.services.sync_storage.sync_storage_runner import SyncStorageRunner + + +class TestSyncStorageKeepAlive: + def test_keep_alive_posts_expected_payload(self): + runner = SyncStorageRunner( + test_run_id="run-id", + port="49152", + ) + runner.worker_pid = "worker-1" + + with patch("urllib.request.urlopen") as urlopen_mock: + runner._keep_alive() + + urlopen_mock.assert_called_once() + request = urlopen_mock.call_args[0][0] + assert request.full_url == "http://localhost:49152/keep_alive" + assert request.method == "POST" + assert json.loads(request.data.decode("utf-8")) == { + "pid": "worker-1", + "testRunId": "run-id", + } + + def test_keep_alive_ignores_errors(self): + runner = SyncStorageRunner(test_run_id="run-id", port="49152") + + with patch("urllib.request.urlopen", side_effect=OSError("down")): + runner._keep_alive() + + def test_start_keep_alive_starts_background_thread(self): + runner = SyncStorageRunner(test_run_id="run-id", port="49152") + + with patch.object(runner, "_keep_alive_loop") as loop_mock: + runner._start_keep_alive() + runner._keep_alive_stop_event.set() + runner._keep_alive_thread.join(timeout=1) + + loop_mock.assert_called_once() diff --git a/update_versions.sh b/update_versions.sh index 6aa9dac2..32c0dacb 100644 --- a/update_versions.sh +++ b/update_versions.sh @@ -1,7 +1,7 @@ #!/bin/bash -NEW_VERSION="4.2.2" -TESTIT_API_CLIENT_VERSION="7.5.6" +NEW_VERSION="4.2.3" +TESTIT_API_CLIENT_VERSION="7.5.10" echo "Updating all adapters to version: $NEW_VERSION" echo "Updating testit-api-client to version: $TESTIT_API_CLIENT_VERSION"