|
1 | | -"""Python Response Time Benchmark (production-safe, Docker/K8s friendly).""" |
| 1 | +"""Run the Python Response Time Benchmark (K8s-safe, single-threaded).""" |
2 | 2 |
|
3 | | -import signal |
4 | 3 | import threading |
5 | 4 | import time |
| 5 | +from threading import Event |
6 | 6 |
|
7 | | -import requests |
8 | 7 | from loguru import logger |
| 8 | +from requests import Session |
| 9 | +from requests.exceptions import ConnectTimeout, ReadTimeout, RequestException, SSLError |
9 | 10 | from rich.console import Console |
10 | 11 |
|
11 | 12 | from python_response_time.core import ( |
12 | 13 | REQUEST_COUNT, |
13 | 14 | REQUEST_LATENCY, |
14 | 15 | app_settings, |
| 16 | + register_signals, |
15 | 17 | setup_logger, |
| 18 | + sleep_interruptible, |
16 | 19 | start_metrics_server, |
17 | 20 | ) |
18 | 21 |
|
19 | | -setup_logger(app_settings.LOG_LEVEL) |
20 | | -console = Console() |
21 | 22 |
|
22 | | -shutdown_event = threading.Event() |
23 | | - |
24 | | - |
25 | | -def handle_shutdown(*_): |
26 | | - """Handle SIGTERM / SIGINT.""" |
27 | | - shutdown_event.set() |
28 | | - console.print("\n[yellow]Shutdown signal received... stopping gracefully[/yellow]") |
29 | | - |
30 | | - |
31 | | -def register_signals(): |
32 | | - """Register signal handlers for graceful shutdown.""" |
33 | | - signal.signal(signal.SIGTERM, handle_shutdown) |
34 | | - signal.signal(signal.SIGINT, handle_shutdown) |
35 | | - |
36 | | - |
37 | | -def sleep_interruptible(seconds: float): |
38 | | - """Sleep in small increments, checking for shutdown signal.""" |
39 | | - step = 0.1 |
40 | | - elapsed = 0.0 |
41 | | - |
42 | | - while elapsed < seconds: |
43 | | - if shutdown_event.is_set(): |
44 | | - return |
45 | | - try: |
46 | | - time.sleep(step) |
47 | | - except KeyboardInterrupt: |
48 | | - shutdown_event.set() |
49 | | - return |
50 | | - elapsed += step |
51 | | - |
52 | | - |
53 | | -def run_app(): |
54 | | - """Run the main application logic.""" |
| 23 | +def run_app(console: Console, shutdown_event: Event) -> None: |
| 24 | + """Run the main benchmark loop.""" |
55 | 25 | console.print("[bold cyan]HTTP Benchmark Starting...[/bold cyan]") |
56 | 26 | console.print(f"Target: {app_settings.TARGET_URL}") |
57 | 27 | console.print(f"Requests: {app_settings.NUM_REQUESTS}") |
58 | | - console.print(f"Concurrency: {app_settings.CONCURRENCY}") |
59 | | - console.print(f"Timeout: {app_settings.TIMEOUT}s") |
60 | 28 | console.print(f"Delay: {app_settings.REQUEST_DELAY}s") |
61 | 29 | console.print(f"SSL Verify: {app_settings.VERIFY_SSL}\n") |
62 | 30 |
|
63 | | - session = requests.Session() |
| 31 | + session = Session() |
64 | 32 |
|
65 | 33 | try: |
66 | 34 | for i in range(app_settings.NUM_REQUESTS): |
67 | 35 | if shutdown_event.is_set(): |
68 | 36 | break |
| 37 | + request_id = i + 1 |
| 38 | + logger.info({"event": "request_start", "request": request_id}) |
69 | 39 | start_time = time.perf_counter() |
70 | 40 | try: |
71 | 41 | response = session.get( |
72 | 42 | str(app_settings.TARGET_URL), |
73 | | - timeout=app_settings.TIMEOUT, |
| 43 | + timeout=( |
| 44 | + app_settings.CONNECT_TIMEOUT, |
| 45 | + app_settings.READ_TIMEOUT, |
| 46 | + ), |
74 | 47 | verify=app_settings.VERIFY_SSL, |
75 | 48 | ) |
76 | 49 | elapsed = time.perf_counter() - start_time |
77 | 50 | console.print( |
78 | | - f"{i + 1:>4} | {response.status_code} | {elapsed * 1000:.2f} ms" |
| 51 | + f"{request_id:>4} | " |
| 52 | + f"{response.status_code} | " |
| 53 | + f"{elapsed * 1000:.2f} ms" |
79 | 54 | ) |
80 | 55 | logger.info( |
81 | 56 | { |
82 | | - "request": i + 1, |
| 57 | + "event": "request_complete", |
| 58 | + "request": request_id, |
83 | 59 | "status": response.status_code, |
84 | 60 | "response_time_ms": elapsed * 1000, |
85 | | - "url": str(app_settings.TARGET_URL), |
86 | 61 | } |
87 | 62 | ) |
88 | 63 | REQUEST_COUNT.labels(status=str(response.status_code)).inc() |
89 | 64 | REQUEST_LATENCY.labels(status=str(response.status_code)).observe( |
90 | 65 | elapsed |
91 | 66 | ) |
92 | | - except requests.exceptions.SSLError as e: |
93 | | - console.print(f"{i + 1:>4} | SSL ERROR") |
| 67 | + except ConnectTimeout: |
| 68 | + console.print(f"{request_id:>4} | CONNECT_TIMEOUT") |
| 69 | + logger.warning( |
| 70 | + { |
| 71 | + "event": "connect_timeout", |
| 72 | + "request": request_id, |
| 73 | + } |
| 74 | + ) |
| 75 | + REQUEST_COUNT.labels(status="connect_timeout").inc() |
| 76 | + except ReadTimeout: |
| 77 | + console.print(f"{request_id:>4} | READ_TIMEOUT") |
| 78 | + logger.warning( |
| 79 | + { |
| 80 | + "event": "read_timeout", |
| 81 | + "request": request_id, |
| 82 | + } |
| 83 | + ) |
| 84 | + REQUEST_COUNT.labels(status="read_timeout").inc() |
| 85 | + except SSLError as e: |
| 86 | + console.print(f"{request_id:>4} | SSL_ERROR") |
94 | 87 | logger.error( |
95 | 88 | { |
96 | | - "request": i + 1, |
97 | | - "error": "ssl_error", |
| 89 | + "event": "ssl_error", |
| 90 | + "request": request_id, |
98 | 91 | "details": str(e), |
99 | 92 | } |
100 | 93 | ) |
101 | | - except requests.RequestException as e: |
102 | | - REQUEST_COUNT.labels(status="error").inc() |
103 | | - console.print(f"{i + 1:>4} | ERROR") |
| 94 | + REQUEST_COUNT.labels(status="ssl_error").inc() |
| 95 | + except RequestException as e: |
| 96 | + console.print(f"{request_id:>4} | ERROR") |
104 | 97 | logger.error( |
105 | 98 | { |
106 | | - "request": i + 1, |
107 | | - "error": "request_error", |
| 99 | + "event": "request_error", |
| 100 | + "request": request_id, |
108 | 101 | "details": str(e), |
109 | 102 | } |
110 | 103 | ) |
| 104 | + REQUEST_COUNT.labels(status="error").inc() |
111 | 105 | if app_settings.REQUEST_DELAY > 0: |
112 | | - sleep_interruptible(app_settings.REQUEST_DELAY) |
| 106 | + sleep_interruptible(app_settings.REQUEST_DELAY, shutdown_event) |
113 | 107 | finally: |
114 | 108 | session.close() |
115 | 109 | console.print("\n[green]Benchmark stopped gracefully[/green]") |
116 | 110 |
|
117 | 111 |
|
118 | | -if __name__ == "__main__": |
119 | | - """Run the application.""" |
120 | | - register_signals() |
| 112 | +def main() -> None: |
| 113 | + """Application entrypoint.""" |
| 114 | + setup_logger(app_settings.LOG_LEVEL) |
| 115 | + console = Console() |
| 116 | + shutdown_event = threading.Event() |
| 117 | + register_signals(shutdown_event, console) |
121 | 118 | start_metrics_server(port=8000) |
122 | 119 | try: |
123 | | - run_app() |
124 | | - except KeyboardInterrupt: |
125 | | - shutdown_event.set() |
126 | | - console.print("\n[yellow]Interrupted (Ctrl+C). Exiting cleanly...[/yellow]") |
| 120 | + run_app(console, shutdown_event) |
127 | 121 | finally: |
128 | 122 | shutdown_event.set() |
129 | 123 | console.print("[green]Cleanup complete[/green]") |
| 124 | + |
| 125 | + |
| 126 | +if __name__ == "__main__": |
| 127 | + main() |
0 commit comments