From 583aa66e64a7e4414980accbdb6578690074a933 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Sun, 8 Feb 2026 11:07:36 -0800 Subject: [PATCH] feat: added Client.subscribe_notify_error() for error monitoring --- switcher_client/client.py | 8 ++++++++ switcher_client/lib/utils/execution_logger.py | 5 +++++ switcher_client/switcher.py | 7 +++++++ tests/test_switcher_silent_mode.py | 12 +++++++++++- 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/switcher_client/client.py b/switcher_client/client.py index 906fc14..b8ffe1e 100644 --- a/switcher_client/client.py +++ b/switcher_client/client.py @@ -178,6 +178,14 @@ def clear_resources() -> None: ExecutionLogger.clear_logger() GlobalSnapshot.clear() TimedMatch.terminate_worker() + + @staticmethod + def subscribe_notify_error(callback: Callable[[Exception], None]) -> None: + """ + Subscribe to notify when an asynchronous error is thrown. + It is usually used when throttle and silent mode are enabled. + """ + ExecutionLogger.subscribe_notify_error(callback) @staticmethod def _is_check_snapshot_available(fetch_remote = False) -> bool: diff --git a/switcher_client/lib/utils/execution_logger.py b/switcher_client/lib/utils/execution_logger.py index da7ed43..86a6577 100644 --- a/switcher_client/lib/utils/execution_logger.py +++ b/switcher_client/lib/utils/execution_logger.py @@ -62,6 +62,11 @@ def clear_logger() -> None: """Clear all results""" global _logger _logger.clear() + + @staticmethod + def subscribe_notify_error(callback: Callable[[Exception], None]) -> None: + """Subscribe to notify when an asynchronous error is thrown.""" + ExecutionLogger._callback_error = callback @staticmethod def _has_execution(log: 'ExecutionLogger', key: str, input: Optional[List[List[str]]]) -> bool: diff --git a/switcher_client/switcher.py b/switcher_client/switcher.py index 112b94e..9e3da82 100644 --- a/switcher_client/switcher.py +++ b/switcher_client/switcher.py @@ -44,6 +44,8 @@ def _submit(self) -> ResultDetail: return self._execute_remote_criteria() except Exception as e: + self._notify_error(e) + if self._context.options.silent_mode: RemoteAuth.update_silent_token() return self._execute_local_criteria() @@ -81,6 +83,11 @@ def _execute_api_checks(self): if RemoteAuth.is_token_expired(): self.prepare(self._key) + def _notify_error(self, error: Exception): + """ Notify asynchronous error to the subscribed callback """ + if ExecutionLogger._callback_error: + ExecutionLogger._callback_error(error) + def _execute_remote_criteria(self): """ Execute remote criteria """ token = GlobalAuth.get_token() diff --git a/tests/test_switcher_silent_mode.py b/tests/test_switcher_silent_mode.py index 61051ae..84e0849 100644 --- a/tests/test_switcher_silent_mode.py +++ b/tests/test_switcher_silent_mode.py @@ -6,6 +6,8 @@ from switcher_client import Client from switcher_client.lib.globals.global_context import ContextOptions +async_error = None + def test_silent_mode_for_check_criteria(httpx_mock): """ Should use the silent mode when the remote API is not available for check criteria """ @@ -15,15 +17,19 @@ def test_silent_mode_for_check_criteria(httpx_mock): given_check_health(httpx_mock, status=500) given_context(silent_mode='1s') + Client.subscribe_notify_error(lambda error: globals().update(async_error=str(error))) switcher = Client.get_switcher('FF2FOR2022') # test # assert silent mode being used while registering the error assert switcher.is_on('FF2FOR2022') + assert async_error == '[check_criteria] failed with status: 429' # assert silent mode being used in the next call time.sleep(1.5) + globals().update(async_error=None) assert switcher.is_on('FF2FOR2022') + assert async_error is None def test_silent_mode_for_check_criteria_restabilished(httpx_mock): """ Should retry check criteria once the remote API is restabilished and the token is renewed """ @@ -33,17 +39,22 @@ def test_silent_mode_for_check_criteria_restabilished(httpx_mock): given_check_criteria(httpx_mock, key='FF2FOR2022', response={'error': 'Too many requests'}, status=429) given_context(silent_mode='1s') + Client.subscribe_notify_error(lambda error: globals().update(async_error=str(error))) switcher = Client.get_switcher('FF2FOR2022') # test # assert silent mode being used while registering the error assert switcher.is_on('FF2FOR2022') + assert async_error == '[check_criteria] failed with status: 429' # assert from remote API once the API is restabilished and the token is renewed given_check_criteria(httpx_mock, key='FF2FOR2022', response={'result': True}, status=200) given_check_health(httpx_mock, status=200) + time.sleep(1.5) + globals().update(async_error=None) assert switcher.is_on('FF2FOR2022') + assert async_error is None # Helpers @@ -85,4 +96,3 @@ def given_check_health(httpx_mock: HTTPXMock, status=200): method='GET', status_code=status, ) -