Coverage for src / moai_adk / utils / timeout.py: 27.54%
69 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1#!/usr/bin/env python3
2# # REMOVED_ORPHAN_CODE:TIMEOUT-001 | SPEC: SPEC-TIMEOUT-HANDLING-001
3"""Cross-Platform Timeout Handler for Windows & Unix Compatibility
5Provides a unified timeout mechanism that works on both Windows (threading-based)
6and Unix/POSIX systems (signal-based).
8Architecture:
9 - Windows: threading.Timer with exception injection
10 - Unix/POSIX: signal.SIGALRM (traditional approach)
11 - Both: Context manager for clean cancellation
12"""
14import platform
15import signal
16import threading
17from contextlib import contextmanager
18from typing import Callable, Optional
21class TimeoutError(Exception):
22 """Timeout exception raised when deadline exceeded"""
23 pass
26class CrossPlatformTimeout:
27 """Cross-platform timeout handler supporting Windows and Unix.
29 Windows: Uses threading.Timer to schedule timeout exception
30 Unix: Uses signal.SIGALRM for timeout handling
32 Usage:
33 # Context manager (recommended)
34 with CrossPlatformTimeout(5):
35 long_running_operation()
37 # Manual control
38 timeout = CrossPlatformTimeout(5)
39 timeout.start()
40 try:
41 long_running_operation()
42 finally:
43 timeout.cancel()
44 """
46 def __init__(self, timeout_seconds: float, callback: Optional[Callable] = None):
47 """Initialize timeout with duration in seconds.
49 Args:
50 timeout_seconds: Timeout duration in seconds (float or int)
51 callback: Optional callback to execute before raising TimeoutError
52 """
53 # Convert float to int for signal.alarm()
54 self.timeout_seconds = timeout_seconds
55 self.timeout_seconds_int = max(1, int(timeout_seconds)) # signal.alarm requires >=1
56 self.callback = callback
57 self.timer: Optional[threading.Timer] = None
58 self._is_windows = platform.system() == "Windows"
59 self._old_handler = None
61 def start(self) -> None:
62 """Start timeout countdown."""
63 # Handle zero/negative timeouts
64 if self.timeout_seconds <= 0:
65 if self.timeout_seconds == 0:
66 # Zero timeout: raise immediately
67 if self.callback:
68 self.callback()
69 raise TimeoutError("Timeout of 0 seconds exceeded immediately")
70 else:
71 # Negative timeout: don't timeout at all
72 return
74 if self._is_windows:
75 self._start_windows_timeout()
76 else:
77 self._start_unix_timeout()
79 def cancel(self) -> None:
80 """Cancel timeout (must call before timeout expires)."""
81 if self._is_windows:
82 self._cancel_windows_timeout()
83 else:
84 self._cancel_unix_timeout()
86 def _start_windows_timeout(self) -> None:
87 """Windows: Use threading.Timer to raise exception."""
88 def timeout_handler():
89 if self.callback:
90 self.callback()
91 raise TimeoutError(
92 f"Operation exceeded {self.timeout_seconds}s timeout (Windows threading)"
93 )
95 self.timer = threading.Timer(self.timeout_seconds, timeout_handler)
96 self.timer.daemon = True
97 self.timer.start()
99 def _cancel_windows_timeout(self) -> None:
100 """Windows: Cancel timer thread."""
101 if self.timer:
102 self.timer.cancel()
103 self.timer = None
105 def _start_unix_timeout(self) -> None:
106 """Unix/POSIX: Use signal.SIGALRM for timeout."""
107 def signal_handler(signum, frame):
108 if self.callback:
109 try:
110 self.callback()
111 except Exception:
112 # Ignore callback exceptions, timeout error takes precedence
113 pass
114 raise TimeoutError(
115 f"Operation exceeded {self.timeout_seconds}s timeout (Unix signal)"
116 )
118 # Save old handler to restore later
119 self._old_handler = signal.signal(signal.SIGALRM, signal_handler)
120 # Use integer timeout_seconds_int for signal.alarm()
121 signal.alarm(self.timeout_seconds_int)
123 def _cancel_unix_timeout(self) -> None:
124 """Unix/POSIX: Cancel alarm and restore old handler."""
125 signal.alarm(0) # Cancel pending alarm
126 if self._old_handler is not None:
127 signal.signal(signal.SIGALRM, self._old_handler)
128 self._old_handler = None
130 def __enter__(self):
131 """Context manager entry."""
132 self.start()
133 return self
135 def __exit__(self, exc_type, exc_val, exc_tb):
136 """Context manager exit - always cancel."""
137 self.cancel()
138 return False # Don't suppress exceptions
141@contextmanager
142def timeout_context(timeout_seconds: float, callback: Optional[Callable] = None):
143 """Decorator/context manager for timeout.
145 Usage:
146 with timeout_context(5):
147 slow_function()
149 Args:
150 timeout_seconds: Timeout duration in seconds (float or int)
151 callback: Optional callback to execute before raising TimeoutError
153 Yields:
154 CrossPlatformTimeout instance
155 """
156 timeout = CrossPlatformTimeout(timeout_seconds, callback=callback)
157 timeout.start()
158 try:
159 yield timeout
160 finally:
161 timeout.cancel()