dory-sdk 2.1.0__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.
- dory/__init__.py +70 -0
- dory/auto_instrument.py +142 -0
- dory/cli/__init__.py +5 -0
- dory/cli/main.py +290 -0
- dory/cli/templates.py +333 -0
- dory/config/__init__.py +23 -0
- dory/config/defaults.py +50 -0
- dory/config/loader.py +361 -0
- dory/config/presets.py +325 -0
- dory/config/schema.py +152 -0
- dory/core/__init__.py +27 -0
- dory/core/app.py +404 -0
- dory/core/context.py +209 -0
- dory/core/lifecycle.py +214 -0
- dory/core/meta.py +121 -0
- dory/core/modes.py +479 -0
- dory/core/processor.py +654 -0
- dory/core/signals.py +122 -0
- dory/decorators.py +142 -0
- dory/errors/__init__.py +117 -0
- dory/errors/classification.py +362 -0
- dory/errors/codes.py +495 -0
- dory/health/__init__.py +10 -0
- dory/health/probes.py +210 -0
- dory/health/server.py +306 -0
- dory/k8s/__init__.py +11 -0
- dory/k8s/annotation_watcher.py +184 -0
- dory/k8s/client.py +251 -0
- dory/k8s/pod_metadata.py +182 -0
- dory/logging/__init__.py +9 -0
- dory/logging/logger.py +175 -0
- dory/metrics/__init__.py +7 -0
- dory/metrics/collector.py +301 -0
- dory/middleware/__init__.py +36 -0
- dory/middleware/connection_tracker.py +608 -0
- dory/middleware/request_id.py +321 -0
- dory/middleware/request_tracker.py +501 -0
- dory/migration/__init__.py +11 -0
- dory/migration/configmap.py +260 -0
- dory/migration/serialization.py +167 -0
- dory/migration/state_manager.py +301 -0
- dory/monitoring/__init__.py +23 -0
- dory/monitoring/opentelemetry.py +462 -0
- dory/py.typed +2 -0
- dory/recovery/__init__.py +60 -0
- dory/recovery/golden_image.py +480 -0
- dory/recovery/golden_snapshot.py +561 -0
- dory/recovery/golden_validator.py +518 -0
- dory/recovery/partial_recovery.py +479 -0
- dory/recovery/recovery_decision.py +242 -0
- dory/recovery/restart_detector.py +142 -0
- dory/recovery/state_validator.py +187 -0
- dory/resilience/__init__.py +45 -0
- dory/resilience/circuit_breaker.py +454 -0
- dory/resilience/retry.py +389 -0
- dory/sidecar/__init__.py +6 -0
- dory/sidecar/main.py +75 -0
- dory/sidecar/server.py +329 -0
- dory/simple.py +342 -0
- dory/types.py +75 -0
- dory/utils/__init__.py +25 -0
- dory/utils/errors.py +59 -0
- dory/utils/retry.py +115 -0
- dory/utils/timeout.py +80 -0
- dory_sdk-2.1.0.dist-info/METADATA +663 -0
- dory_sdk-2.1.0.dist-info/RECORD +69 -0
- dory_sdk-2.1.0.dist-info/WHEEL +5 -0
- dory_sdk-2.1.0.dist-info/entry_points.txt +3 -0
- dory_sdk-2.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Connection Tracker Middleware
|
|
3
|
+
|
|
4
|
+
Automatically tracks and manages connections (database, HTTP, etc.).
|
|
5
|
+
Eliminates manual connection lifecycle management.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Automatic connection registration
|
|
9
|
+
- Health checking
|
|
10
|
+
- Auto-close on shutdown
|
|
11
|
+
- Connection pooling awareness
|
|
12
|
+
- Leak detection
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from typing import Any, Dict, Optional, List, Callable, Set
|
|
22
|
+
from functools import wraps
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConnectionStatus(Enum):
|
|
28
|
+
"""Connection status."""
|
|
29
|
+
CONNECTING = "connecting"
|
|
30
|
+
OPEN = "open"
|
|
31
|
+
CLOSING = "closing"
|
|
32
|
+
CLOSED = "closed"
|
|
33
|
+
ERROR = "error"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ConnectionType(Enum):
|
|
37
|
+
"""Type of connection."""
|
|
38
|
+
DATABASE = "database"
|
|
39
|
+
HTTP = "http"
|
|
40
|
+
WEBSOCKET = "websocket"
|
|
41
|
+
QUEUE = "queue"
|
|
42
|
+
CACHE = "cache"
|
|
43
|
+
CUSTOM = "custom"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ConnectionInfo:
|
|
48
|
+
"""
|
|
49
|
+
Information about a tracked connection.
|
|
50
|
+
"""
|
|
51
|
+
connection_id: str
|
|
52
|
+
connection_type: ConnectionType
|
|
53
|
+
name: str
|
|
54
|
+
status: ConnectionStatus
|
|
55
|
+
created_at: float
|
|
56
|
+
last_used: float
|
|
57
|
+
use_count: int = 0
|
|
58
|
+
health_check_count: int = 0
|
|
59
|
+
last_health_check: Optional[float] = None
|
|
60
|
+
health_status: bool = True
|
|
61
|
+
error: Optional[str] = None
|
|
62
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
63
|
+
|
|
64
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
65
|
+
"""Convert to dictionary."""
|
|
66
|
+
return {
|
|
67
|
+
"connection_id": self.connection_id,
|
|
68
|
+
"connection_type": self.connection_type.value,
|
|
69
|
+
"name": self.name,
|
|
70
|
+
"status": self.status.value,
|
|
71
|
+
"created_at": self.created_at,
|
|
72
|
+
"last_used": self.last_used,
|
|
73
|
+
"use_count": self.use_count,
|
|
74
|
+
"health_check_count": self.health_check_count,
|
|
75
|
+
"last_health_check": self.last_health_check,
|
|
76
|
+
"health_status": self.health_status,
|
|
77
|
+
"error": self.error,
|
|
78
|
+
"age_seconds": time.time() - self.created_at,
|
|
79
|
+
"idle_seconds": time.time() - self.last_used,
|
|
80
|
+
"metadata": self.metadata,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
def is_idle(self, idle_threshold: float = 300.0) -> bool:
|
|
84
|
+
"""
|
|
85
|
+
Check if connection is idle.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
idle_threshold: Idle threshold in seconds
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True if idle for more than threshold
|
|
92
|
+
"""
|
|
93
|
+
return (time.time() - self.last_used) > idle_threshold
|
|
94
|
+
|
|
95
|
+
def is_healthy(self) -> bool:
|
|
96
|
+
"""Check if connection is healthy."""
|
|
97
|
+
return self.health_status and self.status == ConnectionStatus.OPEN
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class ConnectionMetrics:
|
|
102
|
+
"""Connection metrics."""
|
|
103
|
+
total_connections: int = 0
|
|
104
|
+
open_connections: int = 0
|
|
105
|
+
closed_connections: int = 0
|
|
106
|
+
failed_connections: int = 0
|
|
107
|
+
total_health_checks: int = 0
|
|
108
|
+
failed_health_checks: int = 0
|
|
109
|
+
total_use_count: int = 0
|
|
110
|
+
|
|
111
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
112
|
+
"""Convert to dictionary."""
|
|
113
|
+
return {
|
|
114
|
+
"total_connections": self.total_connections,
|
|
115
|
+
"open_connections": self.open_connections,
|
|
116
|
+
"closed_connections": self.closed_connections,
|
|
117
|
+
"failed_connections": self.failed_connections,
|
|
118
|
+
"total_health_checks": self.total_health_checks,
|
|
119
|
+
"failed_health_checks": self.failed_health_checks,
|
|
120
|
+
"total_use_count": self.total_use_count,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ConnectionTracker:
|
|
125
|
+
"""
|
|
126
|
+
Tracks and manages connections automatically.
|
|
127
|
+
|
|
128
|
+
Features:
|
|
129
|
+
- Auto-register connections
|
|
130
|
+
- Health checking
|
|
131
|
+
- Auto-close on shutdown
|
|
132
|
+
- Idle connection detection
|
|
133
|
+
- Connection metrics
|
|
134
|
+
|
|
135
|
+
Usage:
|
|
136
|
+
tracker = ConnectionTracker()
|
|
137
|
+
|
|
138
|
+
# Register connection
|
|
139
|
+
conn_id = await tracker.register_connection(
|
|
140
|
+
db_connection,
|
|
141
|
+
name="database",
|
|
142
|
+
connection_type=ConnectionType.DATABASE,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Use connection (auto-tracked)
|
|
146
|
+
async with tracker.use_connection(conn_id):
|
|
147
|
+
result = await db_connection.execute(query)
|
|
148
|
+
|
|
149
|
+
# Health check
|
|
150
|
+
healthy = await tracker.health_check(conn_id)
|
|
151
|
+
|
|
152
|
+
# Auto-close on shutdown
|
|
153
|
+
await tracker.close_all_connections()
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def __init__(
|
|
157
|
+
self,
|
|
158
|
+
enable_health_checks: bool = True,
|
|
159
|
+
health_check_interval: float = 60.0,
|
|
160
|
+
idle_timeout: float = 300.0,
|
|
161
|
+
auto_close_on_idle: bool = True,
|
|
162
|
+
on_connection_open: Optional[Callable] = None,
|
|
163
|
+
on_connection_close: Optional[Callable] = None,
|
|
164
|
+
):
|
|
165
|
+
"""
|
|
166
|
+
Initialize connection tracker.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
enable_health_checks: Enable automatic health checks
|
|
170
|
+
health_check_interval: Interval between health checks (seconds)
|
|
171
|
+
idle_timeout: Timeout for idle connections (seconds)
|
|
172
|
+
auto_close_on_idle: Automatically close idle connections
|
|
173
|
+
on_connection_open: Callback when connection opens
|
|
174
|
+
on_connection_close: Callback when connection closes
|
|
175
|
+
"""
|
|
176
|
+
self.enable_health_checks = enable_health_checks
|
|
177
|
+
self.health_check_interval = health_check_interval
|
|
178
|
+
self.idle_timeout = idle_timeout
|
|
179
|
+
self.auto_close_on_idle = auto_close_on_idle
|
|
180
|
+
self.on_connection_open = on_connection_open
|
|
181
|
+
self.on_connection_close = on_connection_close
|
|
182
|
+
|
|
183
|
+
# Tracked connections
|
|
184
|
+
self._connections: Dict[str, ConnectionInfo] = {}
|
|
185
|
+
|
|
186
|
+
# Connection objects
|
|
187
|
+
self._connection_objects: Dict[str, Any] = {}
|
|
188
|
+
|
|
189
|
+
# Health check functions
|
|
190
|
+
self._health_check_funcs: Dict[str, Callable] = {}
|
|
191
|
+
|
|
192
|
+
# Close functions
|
|
193
|
+
self._close_funcs: Dict[str, Callable] = {}
|
|
194
|
+
|
|
195
|
+
# Metrics
|
|
196
|
+
self._metrics = ConnectionMetrics()
|
|
197
|
+
|
|
198
|
+
# Health check task
|
|
199
|
+
self._health_check_task: Optional[asyncio.Task] = None
|
|
200
|
+
|
|
201
|
+
# Counter
|
|
202
|
+
self._connection_counter = 0
|
|
203
|
+
|
|
204
|
+
# Lock
|
|
205
|
+
self._lock = asyncio.Lock()
|
|
206
|
+
|
|
207
|
+
logger.info(
|
|
208
|
+
f"ConnectionTracker initialized: health_checks={enable_health_checks}, "
|
|
209
|
+
f"health_interval={health_check_interval}s, idle_timeout={idle_timeout}s"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Start health check loop
|
|
213
|
+
if enable_health_checks:
|
|
214
|
+
self._start_health_check_loop()
|
|
215
|
+
|
|
216
|
+
def _generate_connection_id(self, name: str) -> str:
|
|
217
|
+
"""Generate unique connection ID."""
|
|
218
|
+
self._connection_counter += 1
|
|
219
|
+
timestamp = int(time.time() * 1000)
|
|
220
|
+
return f"{name}_{timestamp}_{self._connection_counter}"
|
|
221
|
+
|
|
222
|
+
async def register_connection(
|
|
223
|
+
self,
|
|
224
|
+
connection: Any,
|
|
225
|
+
name: str,
|
|
226
|
+
connection_type: ConnectionType = ConnectionType.CUSTOM,
|
|
227
|
+
health_check_func: Optional[Callable] = None,
|
|
228
|
+
close_func: Optional[Callable] = None,
|
|
229
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
230
|
+
) -> str:
|
|
231
|
+
"""
|
|
232
|
+
Register a connection for tracking.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
connection: Connection object
|
|
236
|
+
name: Connection name
|
|
237
|
+
connection_type: Type of connection
|
|
238
|
+
health_check_func: Optional health check function
|
|
239
|
+
close_func: Optional close function
|
|
240
|
+
metadata: Optional metadata
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Connection ID
|
|
244
|
+
|
|
245
|
+
Example:
|
|
246
|
+
conn_id = await tracker.register_connection(
|
|
247
|
+
db_connection,
|
|
248
|
+
name="postgres",
|
|
249
|
+
connection_type=ConnectionType.DATABASE,
|
|
250
|
+
health_check_func=lambda conn: conn.is_alive(),
|
|
251
|
+
close_func=lambda conn: conn.close(),
|
|
252
|
+
)
|
|
253
|
+
"""
|
|
254
|
+
async with self._lock:
|
|
255
|
+
connection_id = self._generate_connection_id(name)
|
|
256
|
+
|
|
257
|
+
# Create connection info
|
|
258
|
+
conn_info = ConnectionInfo(
|
|
259
|
+
connection_id=connection_id,
|
|
260
|
+
connection_type=connection_type,
|
|
261
|
+
name=name,
|
|
262
|
+
status=ConnectionStatus.OPEN,
|
|
263
|
+
created_at=time.time(),
|
|
264
|
+
last_used=time.time(),
|
|
265
|
+
metadata=metadata or {},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Store
|
|
269
|
+
self._connections[connection_id] = conn_info
|
|
270
|
+
self._connection_objects[connection_id] = connection
|
|
271
|
+
|
|
272
|
+
if health_check_func:
|
|
273
|
+
self._health_check_funcs[connection_id] = health_check_func
|
|
274
|
+
if close_func:
|
|
275
|
+
self._close_funcs[connection_id] = close_func
|
|
276
|
+
|
|
277
|
+
# Update metrics
|
|
278
|
+
self._metrics.total_connections += 1
|
|
279
|
+
self._metrics.open_connections += 1
|
|
280
|
+
|
|
281
|
+
# Call open callback
|
|
282
|
+
if self.on_connection_open:
|
|
283
|
+
try:
|
|
284
|
+
if asyncio.iscoroutinefunction(self.on_connection_open):
|
|
285
|
+
await self.on_connection_open(conn_info)
|
|
286
|
+
else:
|
|
287
|
+
self.on_connection_open(conn_info)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.error(f"Connection open callback failed: {e}")
|
|
290
|
+
|
|
291
|
+
logger.info(f"Connection registered: {connection_id} ({name})")
|
|
292
|
+
|
|
293
|
+
return connection_id
|
|
294
|
+
|
|
295
|
+
async def use_connection(self, connection_id: str):
|
|
296
|
+
"""
|
|
297
|
+
Context manager to use a connection.
|
|
298
|
+
|
|
299
|
+
Updates last_used timestamp and use_count.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
connection_id: Connection ID
|
|
303
|
+
|
|
304
|
+
Example:
|
|
305
|
+
async with tracker.use_connection(conn_id):
|
|
306
|
+
result = await connection.execute(query)
|
|
307
|
+
"""
|
|
308
|
+
if connection_id not in self._connections:
|
|
309
|
+
raise ValueError(f"Connection not found: {connection_id}")
|
|
310
|
+
|
|
311
|
+
conn_info = self._connections[connection_id]
|
|
312
|
+
|
|
313
|
+
# Update usage
|
|
314
|
+
conn_info.last_used = time.time()
|
|
315
|
+
conn_info.use_count += 1
|
|
316
|
+
self._metrics.total_use_count += 1
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
yield self._connection_objects[connection_id]
|
|
320
|
+
except Exception as e:
|
|
321
|
+
logger.error(f"Error using connection {connection_id}: {e}")
|
|
322
|
+
conn_info.error = str(e)
|
|
323
|
+
conn_info.health_status = False
|
|
324
|
+
raise
|
|
325
|
+
|
|
326
|
+
async def health_check(self, connection_id: str) -> bool:
|
|
327
|
+
"""
|
|
328
|
+
Perform health check on a connection.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
connection_id: Connection ID
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
True if healthy
|
|
335
|
+
"""
|
|
336
|
+
if connection_id not in self._connections:
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
conn_info = self._connections[connection_id]
|
|
340
|
+
conn_info.health_check_count += 1
|
|
341
|
+
conn_info.last_health_check = time.time()
|
|
342
|
+
self._metrics.total_health_checks += 1
|
|
343
|
+
|
|
344
|
+
# Get health check function
|
|
345
|
+
health_check_func = self._health_check_funcs.get(connection_id)
|
|
346
|
+
if not health_check_func:
|
|
347
|
+
# No health check function, assume healthy if open
|
|
348
|
+
conn_info.health_status = conn_info.status == ConnectionStatus.OPEN
|
|
349
|
+
return conn_info.health_status
|
|
350
|
+
|
|
351
|
+
# Run health check
|
|
352
|
+
try:
|
|
353
|
+
connection = self._connection_objects[connection_id]
|
|
354
|
+
if asyncio.iscoroutinefunction(health_check_func):
|
|
355
|
+
healthy = await health_check_func(connection)
|
|
356
|
+
else:
|
|
357
|
+
healthy = health_check_func(connection)
|
|
358
|
+
|
|
359
|
+
conn_info.health_status = bool(healthy)
|
|
360
|
+
|
|
361
|
+
if not healthy:
|
|
362
|
+
self._metrics.failed_health_checks += 1
|
|
363
|
+
logger.warning(f"Health check failed: {connection_id}")
|
|
364
|
+
|
|
365
|
+
return conn_info.health_status
|
|
366
|
+
|
|
367
|
+
except Exception as e:
|
|
368
|
+
logger.error(f"Health check error for {connection_id}: {e}")
|
|
369
|
+
conn_info.health_status = False
|
|
370
|
+
conn_info.error = str(e)
|
|
371
|
+
self._metrics.failed_health_checks += 1
|
|
372
|
+
return False
|
|
373
|
+
|
|
374
|
+
async def close_connection(self, connection_id: str) -> bool:
|
|
375
|
+
"""
|
|
376
|
+
Close a connection.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
connection_id: Connection ID
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
True if closed successfully
|
|
383
|
+
"""
|
|
384
|
+
if connection_id not in self._connections:
|
|
385
|
+
logger.warning(f"Connection not found: {connection_id}")
|
|
386
|
+
return False
|
|
387
|
+
|
|
388
|
+
conn_info = self._connections[connection_id]
|
|
389
|
+
conn_info.status = ConnectionStatus.CLOSING
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
# Get close function
|
|
393
|
+
close_func = self._close_funcs.get(connection_id)
|
|
394
|
+
if close_func:
|
|
395
|
+
connection = self._connection_objects[connection_id]
|
|
396
|
+
if asyncio.iscoroutinefunction(close_func):
|
|
397
|
+
await close_func(connection)
|
|
398
|
+
else:
|
|
399
|
+
close_func(connection)
|
|
400
|
+
|
|
401
|
+
# Update status
|
|
402
|
+
conn_info.status = ConnectionStatus.CLOSED
|
|
403
|
+
|
|
404
|
+
# Update metrics
|
|
405
|
+
async with self._lock:
|
|
406
|
+
self._metrics.open_connections -= 1
|
|
407
|
+
self._metrics.closed_connections += 1
|
|
408
|
+
|
|
409
|
+
# Remove from tracking
|
|
410
|
+
del self._connections[connection_id]
|
|
411
|
+
del self._connection_objects[connection_id]
|
|
412
|
+
self._health_check_funcs.pop(connection_id, None)
|
|
413
|
+
self._close_funcs.pop(connection_id, None)
|
|
414
|
+
|
|
415
|
+
# Call close callback
|
|
416
|
+
if self.on_connection_close:
|
|
417
|
+
try:
|
|
418
|
+
if asyncio.iscoroutinefunction(self.on_connection_close):
|
|
419
|
+
await self.on_connection_close(conn_info)
|
|
420
|
+
else:
|
|
421
|
+
self.on_connection_close(conn_info)
|
|
422
|
+
except Exception as e:
|
|
423
|
+
logger.error(f"Connection close callback failed: {e}")
|
|
424
|
+
|
|
425
|
+
logger.info(f"Connection closed: {connection_id}")
|
|
426
|
+
return True
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.error(f"Failed to close connection {connection_id}: {e}")
|
|
430
|
+
conn_info.status = ConnectionStatus.ERROR
|
|
431
|
+
conn_info.error = str(e)
|
|
432
|
+
self._metrics.failed_connections += 1
|
|
433
|
+
return False
|
|
434
|
+
|
|
435
|
+
async def close_all_connections(self) -> int:
|
|
436
|
+
"""
|
|
437
|
+
Close all tracked connections.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
Number of connections closed
|
|
441
|
+
"""
|
|
442
|
+
logger.info("Closing all connections")
|
|
443
|
+
|
|
444
|
+
# Get list of connection IDs
|
|
445
|
+
connection_ids = list(self._connections.keys())
|
|
446
|
+
|
|
447
|
+
closed_count = 0
|
|
448
|
+
for conn_id in connection_ids:
|
|
449
|
+
if await self.close_connection(conn_id):
|
|
450
|
+
closed_count += 1
|
|
451
|
+
|
|
452
|
+
logger.info(f"Closed {closed_count} connections")
|
|
453
|
+
return closed_count
|
|
454
|
+
|
|
455
|
+
def get_connections(
|
|
456
|
+
self,
|
|
457
|
+
connection_type: Optional[ConnectionType] = None,
|
|
458
|
+
only_open: bool = False,
|
|
459
|
+
) -> List[ConnectionInfo]:
|
|
460
|
+
"""
|
|
461
|
+
Get tracked connections.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
connection_type: Optional filter by type
|
|
465
|
+
only_open: Only return open connections
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
List of connection info
|
|
469
|
+
"""
|
|
470
|
+
connections = list(self._connections.values())
|
|
471
|
+
|
|
472
|
+
if connection_type:
|
|
473
|
+
connections = [c for c in connections if c.connection_type == connection_type]
|
|
474
|
+
|
|
475
|
+
if only_open:
|
|
476
|
+
connections = [c for c in connections if c.status == ConnectionStatus.OPEN]
|
|
477
|
+
|
|
478
|
+
return connections
|
|
479
|
+
|
|
480
|
+
def get_idle_connections(self, idle_threshold: Optional[float] = None) -> List[ConnectionInfo]:
|
|
481
|
+
"""
|
|
482
|
+
Get idle connections.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
idle_threshold: Optional threshold (uses default if None)
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
List of idle connection info
|
|
489
|
+
"""
|
|
490
|
+
threshold = idle_threshold or self.idle_timeout
|
|
491
|
+
return [
|
|
492
|
+
conn for conn in self._connections.values()
|
|
493
|
+
if conn.is_idle(threshold)
|
|
494
|
+
]
|
|
495
|
+
|
|
496
|
+
def get_metrics(self) -> ConnectionMetrics:
|
|
497
|
+
"""Get connection metrics."""
|
|
498
|
+
return self._metrics
|
|
499
|
+
|
|
500
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
501
|
+
"""
|
|
502
|
+
Get tracker statistics.
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
Dictionary of statistics
|
|
506
|
+
"""
|
|
507
|
+
return {
|
|
508
|
+
"total_tracked": len(self._connections),
|
|
509
|
+
"metrics": self._metrics.to_dict(),
|
|
510
|
+
"health_checks_enabled": self.enable_health_checks,
|
|
511
|
+
"auto_close_on_idle": self.auto_close_on_idle,
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
def _start_health_check_loop(self) -> None:
|
|
515
|
+
"""Start health check background loop."""
|
|
516
|
+
if self._health_check_task and not self._health_check_task.done():
|
|
517
|
+
return
|
|
518
|
+
|
|
519
|
+
self._health_check_task = asyncio.create_task(self._health_check_loop())
|
|
520
|
+
|
|
521
|
+
async def _health_check_loop(self) -> None:
|
|
522
|
+
"""Background health check loop."""
|
|
523
|
+
logger.info("Starting health check loop")
|
|
524
|
+
|
|
525
|
+
while True:
|
|
526
|
+
try:
|
|
527
|
+
await asyncio.sleep(self.health_check_interval)
|
|
528
|
+
|
|
529
|
+
# Health check all connections
|
|
530
|
+
connection_ids = list(self._connections.keys())
|
|
531
|
+
for conn_id in connection_ids:
|
|
532
|
+
await self.health_check(conn_id)
|
|
533
|
+
|
|
534
|
+
# Auto-close idle connections
|
|
535
|
+
if self.auto_close_on_idle:
|
|
536
|
+
idle_connections = self.get_idle_connections()
|
|
537
|
+
for conn_info in idle_connections:
|
|
538
|
+
logger.info(
|
|
539
|
+
f"Closing idle connection: {conn_info.connection_id} "
|
|
540
|
+
f"(idle for {conn_info.idle_seconds:.1f}s)"
|
|
541
|
+
)
|
|
542
|
+
await self.close_connection(conn_info.connection_id)
|
|
543
|
+
|
|
544
|
+
except asyncio.CancelledError:
|
|
545
|
+
logger.info("Health check loop cancelled")
|
|
546
|
+
break
|
|
547
|
+
except Exception as e:
|
|
548
|
+
logger.error(f"Health check loop error: {e}")
|
|
549
|
+
|
|
550
|
+
async def stop(self) -> None:
|
|
551
|
+
"""Stop tracker and close all connections."""
|
|
552
|
+
logger.info("Stopping connection tracker")
|
|
553
|
+
|
|
554
|
+
# Stop health check loop
|
|
555
|
+
if self._health_check_task:
|
|
556
|
+
self._health_check_task.cancel()
|
|
557
|
+
try:
|
|
558
|
+
await self._health_check_task
|
|
559
|
+
except asyncio.CancelledError:
|
|
560
|
+
pass
|
|
561
|
+
|
|
562
|
+
# Close all connections
|
|
563
|
+
await self.close_all_connections()
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
# Decorator for automatic connection tracking
|
|
567
|
+
|
|
568
|
+
def track_connection(
|
|
569
|
+
tracker: ConnectionTracker,
|
|
570
|
+
name: str,
|
|
571
|
+
connection_type: ConnectionType = ConnectionType.CUSTOM,
|
|
572
|
+
):
|
|
573
|
+
"""
|
|
574
|
+
Decorator to automatically track connections.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
tracker: ConnectionTracker instance
|
|
578
|
+
name: Connection name
|
|
579
|
+
connection_type: Type of connection
|
|
580
|
+
|
|
581
|
+
Example:
|
|
582
|
+
tracker = ConnectionTracker()
|
|
583
|
+
|
|
584
|
+
@track_connection(tracker, "database", ConnectionType.DATABASE)
|
|
585
|
+
async def create_db_connection():
|
|
586
|
+
return await create_connection()
|
|
587
|
+
|
|
588
|
+
# Connection automatically tracked
|
|
589
|
+
conn = await create_db_connection()
|
|
590
|
+
"""
|
|
591
|
+
def decorator(func):
|
|
592
|
+
@wraps(func)
|
|
593
|
+
async def wrapper(*args, **kwargs):
|
|
594
|
+
# Create connection
|
|
595
|
+
connection = await func(*args, **kwargs)
|
|
596
|
+
|
|
597
|
+
# Register with tracker
|
|
598
|
+
await tracker.register_connection(
|
|
599
|
+
connection,
|
|
600
|
+
name=name,
|
|
601
|
+
connection_type=connection_type,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
return connection
|
|
605
|
+
|
|
606
|
+
return wrapper
|
|
607
|
+
|
|
608
|
+
return decorator
|