diff --git a/src/fgai/graylog_mcp.py b/src/fgai/graylog_mcp.py index 5f2c8e9..6602f66 100644 --- a/src/fgai/graylog_mcp.py +++ b/src/fgai/graylog_mcp.py @@ -6,6 +6,7 @@ import json import signal import ssl import threading +import time from urllib import error, request @@ -16,6 +17,7 @@ def _hard_timeout(seconds: int): return previous_handler = signal.getsignal(signal.SIGALRM) previous_timer = signal.getitimer(signal.ITIMER_REAL) + started = time.monotonic() def _raise_timeout(_signum, _frame): raise TimeoutError(f"mcp_call_timeout_{seconds}s") @@ -25,7 +27,9 @@ def _hard_timeout(seconds: int): try: yield finally: - signal.setitimer(signal.ITIMER_REAL, previous_timer[0], previous_timer[1]) + elapsed = time.monotonic() - started + remaining = max(0.0, previous_timer[0] - elapsed) if previous_timer[0] else 0.0 + signal.setitimer(signal.ITIMER_REAL, remaining, previous_timer[1]) signal.signal(signal.SIGALRM, previous_handler) diff --git a/src/fgai/monitor.py b/src/fgai/monitor.py index c6ce85d..24b9ddb 100644 --- a/src/fgai/monitor.py +++ b/src/fgai/monitor.py @@ -38,6 +38,7 @@ def _cycle_timeout(seconds: int): return previous_handler = signal.getsignal(signal.SIGALRM) previous_timer = signal.getitimer(signal.ITIMER_REAL) + started = time.monotonic() def _raise_timeout(_signum, _frame): raise TimeoutError(f"status_cycle_timeout_{seconds}s") @@ -47,7 +48,9 @@ def _cycle_timeout(seconds: int): try: yield finally: - signal.setitimer(signal.ITIMER_REAL, previous_timer[0], previous_timer[1]) + elapsed = time.monotonic() - started + remaining = max(0.0, previous_timer[0] - elapsed) if previous_timer[0] else 0.0 + signal.setitimer(signal.ITIMER_REAL, remaining, previous_timer[1]) signal.signal(signal.SIGALRM, previous_handler) diff --git a/tests/test_graylog_mcp.py b/tests/test_graylog_mcp.py index 5c78c99..e734ac6 100644 --- a/tests/test_graylog_mcp.py +++ b/tests/test_graylog_mcp.py @@ -1,9 +1,10 @@ import json +import signal import unittest from urllib.error import HTTPError from unittest.mock import Mock, patch -from fgai.graylog_mcp import GraylogMcpClient +from fgai.graylog_mcp import GraylogMcpClient, _hard_timeout class _Response: @@ -83,6 +84,21 @@ class GraylogMcpTests(unittest.TestCase): self.assertEqual(opener.open.call_args.kwargs["timeout"], 15) self.assertEqual(build_opener.call_args.args[0].proxies, {}) + def test_hard_timeout_preserves_outer_timer_remaining_time(self): + previous_handler = signal.getsignal(signal.SIGALRM) + previous_timer = signal.getitimer(signal.ITIMER_REAL) + try: + signal.setitimer(signal.ITIMER_REAL, 20) + with patch("fgai.graylog_mcp.time.monotonic", side_effect=[100.0, 103.0]): + with _hard_timeout(5): + pass + remaining = signal.getitimer(signal.ITIMER_REAL)[0] + self.assertGreater(remaining, 16) + self.assertLessEqual(remaining, 17.5) + finally: + signal.setitimer(signal.ITIMER_REAL, previous_timer[0], previous_timer[1]) + signal.signal(signal.SIGALRM, previous_handler) + if __name__ == "__main__": unittest.main()