dataquery-sdk 0.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataquery/__init__.py +215 -0
- dataquery/auth.py +380 -0
- dataquery/auto_download.py +448 -0
- dataquery/cli.py +244 -0
- dataquery/client.py +2825 -0
- dataquery/config.py +571 -0
- dataquery/connection_pool.py +538 -0
- dataquery/dataquery.py +2467 -0
- dataquery/exceptions.py +169 -0
- dataquery/logging_config.py +415 -0
- dataquery/models.py +1303 -0
- dataquery/py.typed +2 -0
- dataquery/rate_limiter.py +520 -0
- dataquery/retry.py +406 -0
- dataquery/utils.py +525 -0
- dataquery_sdk-0.0.7.dist-info/METADATA +996 -0
- dataquery_sdk-0.0.7.dist-info/RECORD +21 -0
- dataquery_sdk-0.0.7.dist-info/WHEEL +5 -0
- dataquery_sdk-0.0.7.dist-info/entry_points.txt +2 -0
- dataquery_sdk-0.0.7.dist-info/licenses/LICENSE +201 -0
- dataquery_sdk-0.0.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Connection pool monitoring and management for the DATAQUERY SDK.
|
|
3
|
+
|
|
4
|
+
Provides connection pool health monitoring, cleanup, and metrics collection.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import time
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
import aiohttp
|
|
15
|
+
import structlog
|
|
16
|
+
|
|
17
|
+
logger = structlog.get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ConnectionPoolStats:
|
|
22
|
+
"""Connection pool statistics."""
|
|
23
|
+
|
|
24
|
+
total_connections: int = 0
|
|
25
|
+
active_connections: int = 0
|
|
26
|
+
idle_connections: int = 0
|
|
27
|
+
connection_errors: int = 0
|
|
28
|
+
connection_timeouts: int = 0
|
|
29
|
+
last_cleanup: Optional[datetime] = None
|
|
30
|
+
cleanup_count: int = 0
|
|
31
|
+
max_connections_reached: int = 0
|
|
32
|
+
connection_wait_time: float = 0.0
|
|
33
|
+
avg_connection_lifetime: float = 0.0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ConnectionPoolConfig:
|
|
38
|
+
"""Configuration for connection pool monitoring."""
|
|
39
|
+
|
|
40
|
+
max_connections: int = 64
|
|
41
|
+
max_keepalive_connections: int = (
|
|
42
|
+
16 # Renamed for test compatibility; tuned for high parallelism
|
|
43
|
+
)
|
|
44
|
+
keepalive_timeout: int = 300
|
|
45
|
+
connection_timeout: int = 300 # Increased for better reliability
|
|
46
|
+
enable_cleanup: bool = True
|
|
47
|
+
cleanup_interval: int = 300 # 5 minutes
|
|
48
|
+
max_idle_time: int = 60 # 1 minute
|
|
49
|
+
health_check_interval: int = 60 # 1 minute
|
|
50
|
+
enable_metrics: bool = True
|
|
51
|
+
log_connection_events: bool = True
|
|
52
|
+
enable_monitoring: bool = True # Added for test compatibility
|
|
53
|
+
|
|
54
|
+
def __post_init__(self):
|
|
55
|
+
"""Validate configuration parameters."""
|
|
56
|
+
if self.max_connections <= 0:
|
|
57
|
+
raise ValueError("max_connections must be positive")
|
|
58
|
+
if self.max_keepalive_connections <= 0:
|
|
59
|
+
raise ValueError("max_keepalive_connections must be positive")
|
|
60
|
+
if self.keepalive_timeout <= 0:
|
|
61
|
+
raise ValueError("keepalive_timeout must be positive")
|
|
62
|
+
if self.connection_timeout <= 0:
|
|
63
|
+
raise ValueError("connection_timeout must be positive")
|
|
64
|
+
if self.cleanup_interval <= 0:
|
|
65
|
+
raise ValueError("cleanup_interval must be positive")
|
|
66
|
+
if self.health_check_interval <= 0:
|
|
67
|
+
raise ValueError("health_check_interval must be positive")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ConnectionPoolMonitor:
|
|
71
|
+
"""Monitor and manage connection pool health."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, config: ConnectionPoolConfig):
|
|
74
|
+
self.config = config
|
|
75
|
+
self.stats = ConnectionPoolStats()
|
|
76
|
+
self.connection_times: List[float] = []
|
|
77
|
+
self.last_health_check = time.time()
|
|
78
|
+
self._cleanup_task: Optional[asyncio.Task] = None
|
|
79
|
+
self._health_check_task: Optional[asyncio.Task] = None
|
|
80
|
+
self._running = False
|
|
81
|
+
self._shutdown_event: Optional[asyncio.Event] = (
|
|
82
|
+
None # Added for test compatibility
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
logger.info(
|
|
86
|
+
"Connection pool monitor initialized",
|
|
87
|
+
max_connections=config.max_connections,
|
|
88
|
+
cleanup_interval=config.cleanup_interval,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _get_shutdown_event(self) -> asyncio.Event:
|
|
92
|
+
"""Get the shutdown event, creating it if necessary."""
|
|
93
|
+
if self._shutdown_event is None:
|
|
94
|
+
self._shutdown_event = asyncio.Event()
|
|
95
|
+
return self._shutdown_event
|
|
96
|
+
|
|
97
|
+
def start_monitoring(
|
|
98
|
+
self, connector: Optional[aiohttp.TCPConnector] = None
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Start monitoring the connection pool."""
|
|
101
|
+
if self._running:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
self._running = True
|
|
105
|
+
self.connector = connector
|
|
106
|
+
|
|
107
|
+
# Start background tasks only if we have an event loop
|
|
108
|
+
try:
|
|
109
|
+
loop = asyncio.get_running_loop()
|
|
110
|
+
if self.config.enable_cleanup:
|
|
111
|
+
self._cleanup_task = loop.create_task(self._cleanup_loop())
|
|
112
|
+
|
|
113
|
+
if self.config.health_check_interval > 0:
|
|
114
|
+
self._health_check_task = loop.create_task(self._health_check_loop())
|
|
115
|
+
except RuntimeError:
|
|
116
|
+
# No event loop running, tasks will be started later if needed
|
|
117
|
+
logger.debug("No event loop running, deferring task creation")
|
|
118
|
+
|
|
119
|
+
logger.info("Connection pool monitoring started")
|
|
120
|
+
|
|
121
|
+
def stop_monitoring(self) -> None:
|
|
122
|
+
"""Stop monitoring the connection pool."""
|
|
123
|
+
if not self._running:
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
self._running = False
|
|
127
|
+
|
|
128
|
+
# Cancel background tasks
|
|
129
|
+
if self._cleanup_task:
|
|
130
|
+
self._cleanup_task.cancel()
|
|
131
|
+
self._cleanup_task = None
|
|
132
|
+
|
|
133
|
+
if self._health_check_task:
|
|
134
|
+
self._health_check_task.cancel()
|
|
135
|
+
self._health_check_task = None
|
|
136
|
+
|
|
137
|
+
logger.info("Connection pool monitoring stopped")
|
|
138
|
+
|
|
139
|
+
async def _cleanup_loop(self) -> None:
|
|
140
|
+
"""Background cleanup loop."""
|
|
141
|
+
while self._running:
|
|
142
|
+
try:
|
|
143
|
+
await asyncio.sleep(self.config.cleanup_interval)
|
|
144
|
+
await self.cleanup_idle_connections()
|
|
145
|
+
except asyncio.CancelledError:
|
|
146
|
+
logger.info("Cleanup loop cancelled")
|
|
147
|
+
break
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.error("Error in cleanup loop", error=str(e))
|
|
150
|
+
# Continue running despite errors
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
async def _health_check_loop(self) -> None:
|
|
154
|
+
"""Background health check loop."""
|
|
155
|
+
while self._running:
|
|
156
|
+
try:
|
|
157
|
+
await asyncio.sleep(self.config.health_check_interval)
|
|
158
|
+
await self.perform_health_check()
|
|
159
|
+
except asyncio.CancelledError:
|
|
160
|
+
logger.info("Health check loop cancelled")
|
|
161
|
+
break
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.error("Error in health check loop", error=str(e))
|
|
164
|
+
# Continue running despite errors
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
async def cleanup_idle_connections(self) -> None:
|
|
168
|
+
"""Clean up idle connections."""
|
|
169
|
+
if not hasattr(self, "connector"):
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
# Get current pool stats
|
|
174
|
+
pool_stats = self._get_pool_stats()
|
|
175
|
+
|
|
176
|
+
# Clean up idle connections
|
|
177
|
+
if self.connector is not None and hasattr(self.connector, "_resolver"):
|
|
178
|
+
resolver = self.connector._resolver
|
|
179
|
+
# Try different possible cache attribute names
|
|
180
|
+
cache_attr = None
|
|
181
|
+
for attr_name in ["_cache", "_resolver_cache", "cache"]:
|
|
182
|
+
if hasattr(resolver, attr_name):
|
|
183
|
+
try:
|
|
184
|
+
cache_attr = getattr(resolver, attr_name)
|
|
185
|
+
# Verify it's actually a cache-like object
|
|
186
|
+
if hasattr(cache_attr, "clear") and hasattr(
|
|
187
|
+
cache_attr, "__len__"
|
|
188
|
+
):
|
|
189
|
+
break
|
|
190
|
+
else:
|
|
191
|
+
cache_attr = None
|
|
192
|
+
except (AttributeError, TypeError):
|
|
193
|
+
cache_attr = None
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
if cache_attr:
|
|
197
|
+
try:
|
|
198
|
+
cache_attr.clear()
|
|
199
|
+
logger.debug("Resolver cache cleared successfully")
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logger.debug("Could not clear resolver cache", error=str(e))
|
|
202
|
+
|
|
203
|
+
# Update stats
|
|
204
|
+
self.stats.last_cleanup = datetime.now()
|
|
205
|
+
self.stats.cleanup_count += 1
|
|
206
|
+
|
|
207
|
+
# Log with simple values to avoid structlog formatting issues
|
|
208
|
+
logger.info(
|
|
209
|
+
"Connection pool cleanup completed",
|
|
210
|
+
total_connections=pool_stats.get("total_connections", 0),
|
|
211
|
+
active_connections=pool_stats.get("active_connections", 0),
|
|
212
|
+
idle_connections=pool_stats.get("idle_connections", 0),
|
|
213
|
+
cleanup_count=self.stats.cleanup_count,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.error("Error during connection cleanup", error=str(e))
|
|
218
|
+
self.stats.connection_errors += 1
|
|
219
|
+
|
|
220
|
+
async def perform_health_check(self) -> None:
|
|
221
|
+
"""Perform health check on connection pool."""
|
|
222
|
+
if not hasattr(self, "connector"):
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
pool_stats = self._get_pool_stats()
|
|
227
|
+
|
|
228
|
+
# Check for potential issues
|
|
229
|
+
issues = []
|
|
230
|
+
|
|
231
|
+
if (
|
|
232
|
+
pool_stats.get("active_connections", 0)
|
|
233
|
+
> self.config.max_connections * 0.8
|
|
234
|
+
):
|
|
235
|
+
issues.append("High connection usage")
|
|
236
|
+
|
|
237
|
+
if self.stats.connection_errors > 10:
|
|
238
|
+
issues.append("High error rate")
|
|
239
|
+
|
|
240
|
+
if (
|
|
241
|
+
pool_stats.get("idle_connections", 0)
|
|
242
|
+
> self.config.max_connections * 0.5
|
|
243
|
+
):
|
|
244
|
+
issues.append("Many idle connections")
|
|
245
|
+
|
|
246
|
+
# Log health status with simplified logging
|
|
247
|
+
if issues:
|
|
248
|
+
logger.warning(
|
|
249
|
+
"Connection pool health issues detected",
|
|
250
|
+
issues=issues,
|
|
251
|
+
total_connections=pool_stats.get("total_connections", 0),
|
|
252
|
+
active_connections=pool_stats.get("active_connections", 0),
|
|
253
|
+
idle_connections=pool_stats.get("idle_connections", 0),
|
|
254
|
+
)
|
|
255
|
+
else:
|
|
256
|
+
logger.debug(
|
|
257
|
+
"Connection pool health check passed",
|
|
258
|
+
total_connections=pool_stats.get("total_connections", 0),
|
|
259
|
+
active_connections=pool_stats.get("active_connections", 0),
|
|
260
|
+
idle_connections=pool_stats.get("idle_connections", 0),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
self.last_health_check = time.time()
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.error("Error during health check", error=str(e))
|
|
267
|
+
|
|
268
|
+
def _get_pool_stats(self) -> Dict[str, Any]:
|
|
269
|
+
"""Get current connection pool statistics with optimized performance."""
|
|
270
|
+
if not hasattr(self, "connector"):
|
|
271
|
+
return {}
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
# Get basic connector stats efficiently
|
|
275
|
+
connector_stats = {
|
|
276
|
+
"limit": getattr(self.connector, "limit", 0),
|
|
277
|
+
"limit_per_host": getattr(self.connector, "limit_per_host", 0),
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# Use aiohttp's built-in stats if available (more efficient)
|
|
281
|
+
if (
|
|
282
|
+
self.connector is not None
|
|
283
|
+
and hasattr(self.connector, "closed")
|
|
284
|
+
and not self.connector.closed
|
|
285
|
+
):
|
|
286
|
+
# Try to get stats from aiohttp's internal structures
|
|
287
|
+
total_connections = 0
|
|
288
|
+
active_connections = 0
|
|
289
|
+
idle_connections = 0
|
|
290
|
+
|
|
291
|
+
# Check if we have the newer aiohttp stats API
|
|
292
|
+
if (
|
|
293
|
+
hasattr(self.connector, "_conns")
|
|
294
|
+
and self.connector._conns is not None
|
|
295
|
+
):
|
|
296
|
+
try:
|
|
297
|
+
# More efficient iteration
|
|
298
|
+
for host_connections in self.connector._conns.values():
|
|
299
|
+
if isinstance(host_connections, (list, tuple)):
|
|
300
|
+
total_connections += len(host_connections)
|
|
301
|
+
# For performance, assume most connections are idle
|
|
302
|
+
# Only check a sample for active status
|
|
303
|
+
sample_size = min(5, len(host_connections))
|
|
304
|
+
for conn in host_connections[:sample_size]:
|
|
305
|
+
if (
|
|
306
|
+
hasattr(conn, "_request_count")
|
|
307
|
+
and conn._request_count > 0
|
|
308
|
+
):
|
|
309
|
+
active_connections += 1
|
|
310
|
+
else:
|
|
311
|
+
idle_connections += 1
|
|
312
|
+
# Estimate remaining connections as idle
|
|
313
|
+
remaining = len(host_connections) - sample_size
|
|
314
|
+
idle_connections += remaining
|
|
315
|
+
except Exception:
|
|
316
|
+
# Fallback to simple counting
|
|
317
|
+
total_connections = 0
|
|
318
|
+
try:
|
|
319
|
+
for conns in self.connector._conns.values():
|
|
320
|
+
if isinstance(conns, (list, tuple)):
|
|
321
|
+
total_connections += len(conns)
|
|
322
|
+
except Exception:
|
|
323
|
+
total_connections = 0
|
|
324
|
+
idle_connections = total_connections
|
|
325
|
+
|
|
326
|
+
# Update our internal stats
|
|
327
|
+
self.stats.total_connections = total_connections
|
|
328
|
+
self.stats.active_connections = active_connections
|
|
329
|
+
self.stats.idle_connections = idle_connections
|
|
330
|
+
|
|
331
|
+
# Add connection stats to the result
|
|
332
|
+
connector_stats.update(
|
|
333
|
+
{
|
|
334
|
+
"total_connections": total_connections,
|
|
335
|
+
"active_connections": active_connections,
|
|
336
|
+
"idle_connections": idle_connections,
|
|
337
|
+
"connection_utilization": (
|
|
338
|
+
active_connections / max(total_connections, 1)
|
|
339
|
+
)
|
|
340
|
+
* 100,
|
|
341
|
+
"pool_utilization": (
|
|
342
|
+
total_connections / max(self.config.max_connections, 1)
|
|
343
|
+
)
|
|
344
|
+
* 100,
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return connector_stats
|
|
349
|
+
|
|
350
|
+
# Fallback for older aiohttp versions or when connector is closed
|
|
351
|
+
return {
|
|
352
|
+
"total_connections": 0,
|
|
353
|
+
"active_connections": 0,
|
|
354
|
+
"idle_connections": 0,
|
|
355
|
+
"connection_utilization": 0.0,
|
|
356
|
+
"pool_utilization": 0.0,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
except Exception as e:
|
|
360
|
+
logger.debug("Could not get pool stats", error=str(e))
|
|
361
|
+
return {
|
|
362
|
+
"total_connections": 0,
|
|
363
|
+
"active_connections": 0,
|
|
364
|
+
"idle_connections": 0,
|
|
365
|
+
"connection_utilization": 0.0,
|
|
366
|
+
"pool_utilization": 0.0,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
def record_connection_event(self, event_type: str, duration: float = 0.0) -> None:
|
|
370
|
+
"""Record a connection event for metrics."""
|
|
371
|
+
if not self.config.enable_metrics:
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
if event_type == "connection_created":
|
|
375
|
+
self.connection_times.append(duration)
|
|
376
|
+
# Keep only last 100 connection times
|
|
377
|
+
if len(self.connection_times) > 100:
|
|
378
|
+
self.connection_times.pop(0)
|
|
379
|
+
|
|
380
|
+
if self.connection_times:
|
|
381
|
+
self.stats.avg_connection_lifetime = sum(self.connection_times) / len(
|
|
382
|
+
self.connection_times
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
elif event_type == "connection_error":
|
|
386
|
+
self.stats.connection_errors += 1
|
|
387
|
+
|
|
388
|
+
elif event_type == "connection_timeout":
|
|
389
|
+
self.stats.connection_timeouts += 1
|
|
390
|
+
|
|
391
|
+
elif event_type == "max_connections_reached":
|
|
392
|
+
self.stats.max_connections_reached += 1
|
|
393
|
+
|
|
394
|
+
if self.config.log_connection_events:
|
|
395
|
+
# Log with simple values to avoid structlog formatting issues
|
|
396
|
+
logger.debug(
|
|
397
|
+
"Connection event",
|
|
398
|
+
event_type=event_type,
|
|
399
|
+
duration=duration,
|
|
400
|
+
total_connections=self.stats.total_connections,
|
|
401
|
+
active_connections=self.stats.active_connections,
|
|
402
|
+
idle_connections=self.stats.idle_connections,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def _cleanup_idle_connections(self) -> None:
|
|
406
|
+
"""Clean up idle connections."""
|
|
407
|
+
# Implementation for cleaning up idle connections
|
|
408
|
+
current_time = time.time()
|
|
409
|
+
# This would normally clean up actual connections
|
|
410
|
+
logger.debug("Cleaning up idle connections", timestamp=current_time)
|
|
411
|
+
|
|
412
|
+
def _perform_health_checks(self) -> None:
|
|
413
|
+
"""Perform health checks on connections."""
|
|
414
|
+
# Implementation for performing health checks
|
|
415
|
+
self.last_health_check = time.time()
|
|
416
|
+
logger.debug("Performing health checks", timestamp=self.last_health_check)
|
|
417
|
+
|
|
418
|
+
def get_pool_summary(self) -> Dict[str, Any]:
|
|
419
|
+
"""Get a concise summary of connection pool status."""
|
|
420
|
+
pool_stats = self._get_pool_stats()
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
"connections": {
|
|
424
|
+
"total": self.stats.total_connections,
|
|
425
|
+
"active": self.stats.active_connections,
|
|
426
|
+
"idle": self.stats.idle_connections,
|
|
427
|
+
"available": max(
|
|
428
|
+
0, self.config.max_connections - self.stats.total_connections
|
|
429
|
+
),
|
|
430
|
+
},
|
|
431
|
+
"utilization": {
|
|
432
|
+
"connection_utilization": pool_stats.get("connection_utilization", 0.0),
|
|
433
|
+
"pool_utilization": pool_stats.get("pool_utilization", 0.0),
|
|
434
|
+
},
|
|
435
|
+
"health": {
|
|
436
|
+
"errors": self.stats.connection_errors,
|
|
437
|
+
"timeouts": self.stats.connection_timeouts,
|
|
438
|
+
"max_reached": self.stats.max_connections_reached,
|
|
439
|
+
"monitoring_active": self._running,
|
|
440
|
+
},
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
444
|
+
"""Get comprehensive connection pool statistics."""
|
|
445
|
+
pool_stats = self._get_pool_stats()
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
"monitor_config": {
|
|
449
|
+
"max_connections": self.config.max_connections,
|
|
450
|
+
"max_keepalive_connections": self.config.max_keepalive_connections,
|
|
451
|
+
"cleanup_interval": self.config.cleanup_interval,
|
|
452
|
+
"health_check_interval": self.config.health_check_interval,
|
|
453
|
+
"enable_cleanup": self.config.enable_cleanup,
|
|
454
|
+
"enable_metrics": self.config.enable_metrics,
|
|
455
|
+
},
|
|
456
|
+
"connection_stats": {
|
|
457
|
+
"total_connections": self.stats.total_connections,
|
|
458
|
+
"active_connections": self.stats.active_connections,
|
|
459
|
+
"idle_connections": self.stats.idle_connections,
|
|
460
|
+
"connection_errors": self.stats.connection_errors,
|
|
461
|
+
"connection_timeouts": self.stats.connection_timeouts,
|
|
462
|
+
"max_connections_reached": self.stats.max_connections_reached,
|
|
463
|
+
"avg_connection_lifetime": self.stats.avg_connection_lifetime,
|
|
464
|
+
"connection_wait_time": self.stats.connection_wait_time,
|
|
465
|
+
},
|
|
466
|
+
"pool_stats": pool_stats,
|
|
467
|
+
"monitor_stats": {
|
|
468
|
+
"last_cleanup": (
|
|
469
|
+
self.stats.last_cleanup.isoformat()
|
|
470
|
+
if self.stats.last_cleanup
|
|
471
|
+
else None
|
|
472
|
+
),
|
|
473
|
+
"cleanup_count": self.stats.cleanup_count,
|
|
474
|
+
"last_health_check": (
|
|
475
|
+
datetime.fromtimestamp(self.last_health_check).isoformat()
|
|
476
|
+
if self.last_health_check
|
|
477
|
+
else None
|
|
478
|
+
),
|
|
479
|
+
"monitoring_active": self._running,
|
|
480
|
+
},
|
|
481
|
+
"utilization": {
|
|
482
|
+
"connection_utilization": pool_stats.get("connection_utilization", 0.0),
|
|
483
|
+
"pool_utilization": pool_stats.get("pool_utilization", 0.0),
|
|
484
|
+
"available_connections": max(
|
|
485
|
+
0, self.config.max_connections - self.stats.total_connections
|
|
486
|
+
),
|
|
487
|
+
},
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
def reset_stats(self) -> None:
|
|
491
|
+
"""Reset connection pool statistics."""
|
|
492
|
+
self.stats = ConnectionPoolStats()
|
|
493
|
+
self.connection_times.clear()
|
|
494
|
+
logger.info("Connection pool statistics reset")
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@asynccontextmanager
|
|
498
|
+
async def managed_connection_pool(
|
|
499
|
+
config: ConnectionPoolConfig, connector: aiohttp.TCPConnector
|
|
500
|
+
):
|
|
501
|
+
"""Context manager for connection pool monitoring."""
|
|
502
|
+
monitor = ConnectionPoolMonitor(config)
|
|
503
|
+
try:
|
|
504
|
+
monitor.start_monitoring(connector)
|
|
505
|
+
yield monitor
|
|
506
|
+
finally:
|
|
507
|
+
monitor.stop_monitoring()
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def create_connection_pool_config(
|
|
511
|
+
max_connections: int = 300,
|
|
512
|
+
max_connections_per_host: int = 100,
|
|
513
|
+
enable_cleanup: bool = True,
|
|
514
|
+
cleanup_interval: int = 300,
|
|
515
|
+
) -> ConnectionPoolConfig:
|
|
516
|
+
"""
|
|
517
|
+
Create connection pool configuration.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
max_connections: Maximum total connections
|
|
521
|
+
max_connections_per_host: Maximum connections per host
|
|
522
|
+
enable_cleanup: Whether to enable automatic cleanup
|
|
523
|
+
cleanup_interval: Cleanup interval in seconds
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
Connection pool configuration
|
|
527
|
+
"""
|
|
528
|
+
# Map legacy arg max_connections_per_host to our config.max_keepalive_connections
|
|
529
|
+
cfg = ConnectionPoolConfig(
|
|
530
|
+
max_connections=max_connections,
|
|
531
|
+
enable_cleanup=enable_cleanup,
|
|
532
|
+
cleanup_interval=cleanup_interval,
|
|
533
|
+
)
|
|
534
|
+
try:
|
|
535
|
+
cfg.max_keepalive_connections = max_connections_per_host
|
|
536
|
+
except Exception:
|
|
537
|
+
pass
|
|
538
|
+
return cfg
|