kailash 0.5.0__py3-none-any.whl → 0.6.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.
- kailash/__init__.py +1 -1
- kailash/client/__init__.py +12 -0
- kailash/client/enhanced_client.py +306 -0
- kailash/core/actors/__init__.py +16 -0
- kailash/core/actors/connection_actor.py +566 -0
- kailash/core/actors/supervisor.py +364 -0
- kailash/edge/__init__.py +16 -0
- kailash/edge/compliance.py +834 -0
- kailash/edge/discovery.py +659 -0
- kailash/edge/location.py +582 -0
- kailash/gateway/__init__.py +33 -0
- kailash/gateway/api.py +289 -0
- kailash/gateway/enhanced_gateway.py +357 -0
- kailash/gateway/resource_resolver.py +217 -0
- kailash/gateway/security.py +227 -0
- kailash/middleware/auth/models.py +2 -2
- kailash/middleware/database/base_models.py +1 -7
- kailash/middleware/gateway/__init__.py +22 -0
- kailash/middleware/gateway/checkpoint_manager.py +398 -0
- kailash/middleware/gateway/deduplicator.py +382 -0
- kailash/middleware/gateway/durable_gateway.py +417 -0
- kailash/middleware/gateway/durable_request.py +498 -0
- kailash/middleware/gateway/event_store.py +459 -0
- kailash/nodes/admin/permission_check.py +817 -33
- kailash/nodes/admin/role_management.py +1242 -108
- kailash/nodes/admin/schema_manager.py +438 -0
- kailash/nodes/admin/user_management.py +1124 -1582
- kailash/nodes/code/__init__.py +8 -1
- kailash/nodes/code/async_python.py +1035 -0
- kailash/nodes/code/python.py +1 -0
- kailash/nodes/data/async_sql.py +9 -3
- kailash/nodes/data/sql.py +20 -11
- kailash/nodes/data/workflow_connection_pool.py +643 -0
- kailash/nodes/rag/__init__.py +1 -4
- kailash/resources/__init__.py +40 -0
- kailash/resources/factory.py +533 -0
- kailash/resources/health.py +319 -0
- kailash/resources/reference.py +288 -0
- kailash/resources/registry.py +392 -0
- kailash/runtime/async_local.py +711 -302
- kailash/testing/__init__.py +34 -0
- kailash/testing/async_test_case.py +353 -0
- kailash/testing/async_utils.py +345 -0
- kailash/testing/fixtures.py +458 -0
- kailash/testing/mock_registry.py +495 -0
- kailash/workflow/__init__.py +8 -0
- kailash/workflow/async_builder.py +621 -0
- kailash/workflow/async_patterns.py +766 -0
- kailash/workflow/cyclic_runner.py +107 -16
- kailash/workflow/graph.py +7 -2
- kailash/workflow/resilience.py +11 -1
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/METADATA +7 -4
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/RECORD +57 -22
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,566 @@
|
|
1
|
+
"""Actor-based connection management for Kailash SDK.
|
2
|
+
|
3
|
+
This module implements an actor-based approach to database connections,
|
4
|
+
providing better isolation, fault tolerance, and concurrent handling
|
5
|
+
compared to traditional thread-based models.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import asyncio
|
9
|
+
import logging
|
10
|
+
import time
|
11
|
+
import uuid
|
12
|
+
from dataclasses import dataclass, field
|
13
|
+
from datetime import UTC, datetime, timedelta
|
14
|
+
from enum import Enum
|
15
|
+
from typing import Any, Callable, Dict, Optional, Union
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class ConnectionState(Enum):
|
21
|
+
"""Connection lifecycle states."""
|
22
|
+
|
23
|
+
INITIALIZING = "initializing"
|
24
|
+
HEALTHY = "healthy"
|
25
|
+
DEGRADED = "degraded"
|
26
|
+
RECYCLING = "recycling"
|
27
|
+
FAILED = "failed"
|
28
|
+
TERMINATED = "terminated"
|
29
|
+
|
30
|
+
|
31
|
+
class MessageType(Enum):
|
32
|
+
"""Actor message types."""
|
33
|
+
|
34
|
+
QUERY = "query"
|
35
|
+
HEALTH_CHECK = "health_check"
|
36
|
+
RECYCLE = "recycle"
|
37
|
+
TERMINATE = "terminate"
|
38
|
+
GET_STATS = "get_stats"
|
39
|
+
PING = "ping"
|
40
|
+
|
41
|
+
|
42
|
+
@dataclass
|
43
|
+
class Message:
|
44
|
+
"""Message for actor communication."""
|
45
|
+
|
46
|
+
id: str = field(default_factory=lambda: uuid.uuid4().hex)
|
47
|
+
type: MessageType = MessageType.QUERY
|
48
|
+
payload: Any = None
|
49
|
+
reply_to: Optional[asyncio.Queue] = None
|
50
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
51
|
+
|
52
|
+
|
53
|
+
@dataclass
|
54
|
+
class QueryResult:
|
55
|
+
"""Result of a database query."""
|
56
|
+
|
57
|
+
success: bool
|
58
|
+
data: Optional[Any] = None
|
59
|
+
error: Optional[str] = None
|
60
|
+
execution_time: float = 0.0
|
61
|
+
connection_id: Optional[str] = None
|
62
|
+
|
63
|
+
|
64
|
+
@dataclass
|
65
|
+
class ConnectionStats:
|
66
|
+
"""Statistics for a connection."""
|
67
|
+
|
68
|
+
queries_executed: int = 0
|
69
|
+
errors_encountered: int = 0
|
70
|
+
total_execution_time: float = 0.0
|
71
|
+
health_checks_passed: int = 0
|
72
|
+
health_checks_failed: int = 0
|
73
|
+
created_at: datetime = field(default_factory=datetime.utcnow)
|
74
|
+
last_used_at: datetime = field(default_factory=datetime.utcnow)
|
75
|
+
health_score: float = 100.0
|
76
|
+
|
77
|
+
|
78
|
+
class ActorConnection:
|
79
|
+
"""
|
80
|
+
Actor-based database connection with isolated state and message passing.
|
81
|
+
|
82
|
+
This class represents a single database connection as an independent actor
|
83
|
+
with its own mailbox, state, and lifecycle management.
|
84
|
+
"""
|
85
|
+
|
86
|
+
def __init__(
|
87
|
+
self,
|
88
|
+
connection_id: str,
|
89
|
+
db_config: Dict[str, Any],
|
90
|
+
health_check_query: str = "SELECT 1",
|
91
|
+
health_check_interval: float = 30.0,
|
92
|
+
max_lifetime: float = 3600.0,
|
93
|
+
max_idle_time: float = 600.0,
|
94
|
+
):
|
95
|
+
"""
|
96
|
+
Initialize an actor connection.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
connection_id: Unique identifier for this connection
|
100
|
+
db_config: Database configuration parameters
|
101
|
+
health_check_query: Query to run for health checks
|
102
|
+
health_check_interval: Seconds between health checks
|
103
|
+
max_lifetime: Maximum connection lifetime in seconds
|
104
|
+
max_idle_time: Maximum idle time before recycling
|
105
|
+
"""
|
106
|
+
self.id = connection_id
|
107
|
+
self.db_config = db_config
|
108
|
+
self.health_check_query = health_check_query
|
109
|
+
self.health_check_interval = health_check_interval
|
110
|
+
self.max_lifetime = max_lifetime
|
111
|
+
self.max_idle_time = max_idle_time
|
112
|
+
|
113
|
+
# Actor state
|
114
|
+
self.state = ConnectionState.INITIALIZING
|
115
|
+
self.mailbox = asyncio.Queue(maxsize=100)
|
116
|
+
self.stats = ConnectionStats()
|
117
|
+
self.supervisor = None
|
118
|
+
|
119
|
+
# Physical connection (set during connect)
|
120
|
+
self._connection = None
|
121
|
+
self._adapter = None
|
122
|
+
|
123
|
+
# Background tasks
|
124
|
+
self._message_handler_task = None
|
125
|
+
self._health_monitor_task = None
|
126
|
+
|
127
|
+
# Lifecycle tracking
|
128
|
+
self._created_at = time.time()
|
129
|
+
self._last_activity = time.time()
|
130
|
+
|
131
|
+
async def start(self):
|
132
|
+
"""Start the actor and its background tasks."""
|
133
|
+
try:
|
134
|
+
# Establish connection
|
135
|
+
await self._connect()
|
136
|
+
|
137
|
+
# Start message handler
|
138
|
+
self._message_handler_task = asyncio.create_task(self._handle_messages())
|
139
|
+
|
140
|
+
# Start health monitor
|
141
|
+
self._health_monitor_task = asyncio.create_task(self._monitor_health())
|
142
|
+
|
143
|
+
self.state = ConnectionState.HEALTHY
|
144
|
+
logger.info(f"Connection actor {self.id} started successfully")
|
145
|
+
|
146
|
+
except Exception as e:
|
147
|
+
self.state = ConnectionState.FAILED
|
148
|
+
logger.error(f"Failed to start connection actor {self.id}: {e}")
|
149
|
+
raise
|
150
|
+
|
151
|
+
async def stop(self):
|
152
|
+
"""Stop the actor gracefully."""
|
153
|
+
self.state = ConnectionState.TERMINATED
|
154
|
+
|
155
|
+
# Cancel background tasks
|
156
|
+
if self._message_handler_task:
|
157
|
+
self._message_handler_task.cancel()
|
158
|
+
try:
|
159
|
+
await self._message_handler_task
|
160
|
+
except asyncio.CancelledError:
|
161
|
+
pass
|
162
|
+
|
163
|
+
if self._health_monitor_task:
|
164
|
+
self._health_monitor_task.cancel()
|
165
|
+
try:
|
166
|
+
await self._health_monitor_task
|
167
|
+
except asyncio.CancelledError:
|
168
|
+
pass
|
169
|
+
|
170
|
+
# Close connection
|
171
|
+
await self._disconnect()
|
172
|
+
|
173
|
+
logger.info(f"Connection actor {self.id} stopped")
|
174
|
+
|
175
|
+
async def send_message(self, message: Message) -> Any:
|
176
|
+
"""
|
177
|
+
Send a message to the actor and wait for response.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
message: Message to send
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
Response from the actor
|
184
|
+
"""
|
185
|
+
if self.state == ConnectionState.TERMINATED:
|
186
|
+
raise RuntimeError(f"Actor {self.id} is terminated")
|
187
|
+
|
188
|
+
# Create reply queue if not provided
|
189
|
+
if not message.reply_to:
|
190
|
+
message.reply_to = asyncio.Queue(maxsize=1)
|
191
|
+
|
192
|
+
# Send message
|
193
|
+
await self.mailbox.put(message)
|
194
|
+
|
195
|
+
# Wait for reply
|
196
|
+
response = await message.reply_to.get()
|
197
|
+
return response
|
198
|
+
|
199
|
+
async def _handle_messages(self):
|
200
|
+
"""Main message handling loop."""
|
201
|
+
message = None
|
202
|
+
while self.state != ConnectionState.TERMINATED:
|
203
|
+
try:
|
204
|
+
# Check if event loop is still running
|
205
|
+
try:
|
206
|
+
asyncio.get_running_loop()
|
207
|
+
except RuntimeError:
|
208
|
+
# Event loop closed, terminate gracefully
|
209
|
+
break
|
210
|
+
|
211
|
+
# Wait for message with timeout
|
212
|
+
message = await asyncio.wait_for(self.mailbox.get(), timeout=1.0)
|
213
|
+
|
214
|
+
# Update activity timestamp
|
215
|
+
self._last_activity = time.time()
|
216
|
+
|
217
|
+
# Process message based on type
|
218
|
+
if message.type == MessageType.QUERY:
|
219
|
+
response = await self._handle_query(message.payload)
|
220
|
+
elif message.type == MessageType.HEALTH_CHECK:
|
221
|
+
response = await self._handle_health_check()
|
222
|
+
elif message.type == MessageType.RECYCLE:
|
223
|
+
response = await self._handle_recycle()
|
224
|
+
elif message.type == MessageType.GET_STATS:
|
225
|
+
response = self._get_stats()
|
226
|
+
elif message.type == MessageType.PING:
|
227
|
+
response = {"status": "pong", "state": self.state.value}
|
228
|
+
else:
|
229
|
+
response = {"error": f"Unknown message type: {message.type}"}
|
230
|
+
|
231
|
+
# Send reply if requested (check if we can still send replies)
|
232
|
+
if message and message.reply_to:
|
233
|
+
try:
|
234
|
+
await message.reply_to.put(response)
|
235
|
+
except RuntimeError:
|
236
|
+
# Queue is bound to different event loop, skip reply
|
237
|
+
pass
|
238
|
+
|
239
|
+
# Clear message reference
|
240
|
+
message = None
|
241
|
+
|
242
|
+
except asyncio.TimeoutError:
|
243
|
+
# Check if connection should be recycled
|
244
|
+
if self._should_recycle():
|
245
|
+
await self._initiate_recycle()
|
246
|
+
except (asyncio.CancelledError, GeneratorExit):
|
247
|
+
# Handle graceful shutdown
|
248
|
+
break
|
249
|
+
except Exception as e:
|
250
|
+
logger.error(f"Error handling message in actor {self.id}: {e}")
|
251
|
+
if message and message.reply_to:
|
252
|
+
try:
|
253
|
+
await message.reply_to.put({"error": str(e)})
|
254
|
+
except RuntimeError:
|
255
|
+
# Queue is bound to different event loop, skip reply
|
256
|
+
pass
|
257
|
+
message = None
|
258
|
+
|
259
|
+
async def _handle_query(self, query_params: Dict[str, Any]) -> QueryResult:
|
260
|
+
"""Execute a database query."""
|
261
|
+
start_time = time.time()
|
262
|
+
|
263
|
+
try:
|
264
|
+
if self.state != ConnectionState.HEALTHY:
|
265
|
+
return QueryResult(
|
266
|
+
success=False,
|
267
|
+
error=f"Connection in {self.state.value} state",
|
268
|
+
connection_id=self.id,
|
269
|
+
)
|
270
|
+
|
271
|
+
# Import FetchMode enum
|
272
|
+
from kailash.nodes.data.async_sql import FetchMode
|
273
|
+
|
274
|
+
# Execute query using adapter
|
275
|
+
fetch_mode_str = query_params.get("fetch_mode", "all")
|
276
|
+
fetch_mode = FetchMode(fetch_mode_str.lower())
|
277
|
+
|
278
|
+
result = await self._adapter.execute(
|
279
|
+
query=query_params.get("query"),
|
280
|
+
params=query_params.get("params"),
|
281
|
+
fetch_mode=fetch_mode,
|
282
|
+
)
|
283
|
+
|
284
|
+
execution_time = time.time() - start_time
|
285
|
+
|
286
|
+
# Update stats
|
287
|
+
self.stats.queries_executed += 1
|
288
|
+
self.stats.total_execution_time += execution_time
|
289
|
+
self.stats.last_used_at = datetime.now(UTC)
|
290
|
+
|
291
|
+
return QueryResult(
|
292
|
+
success=True,
|
293
|
+
data=result,
|
294
|
+
execution_time=execution_time,
|
295
|
+
connection_id=self.id,
|
296
|
+
)
|
297
|
+
|
298
|
+
except Exception as e:
|
299
|
+
execution_time = time.time() - start_time
|
300
|
+
self.stats.errors_encountered += 1
|
301
|
+
|
302
|
+
# Degrade connection on errors
|
303
|
+
self._update_health_score(-10)
|
304
|
+
|
305
|
+
return QueryResult(
|
306
|
+
success=False,
|
307
|
+
error=str(e),
|
308
|
+
execution_time=execution_time,
|
309
|
+
connection_id=self.id,
|
310
|
+
)
|
311
|
+
|
312
|
+
async def _handle_health_check(self) -> Dict[str, Any]:
|
313
|
+
"""Perform a health check."""
|
314
|
+
try:
|
315
|
+
start_time = time.time()
|
316
|
+
|
317
|
+
# Import FetchMode enum
|
318
|
+
from kailash.nodes.data.async_sql import FetchMode
|
319
|
+
|
320
|
+
# Run health check query
|
321
|
+
await self._adapter.execute(
|
322
|
+
query=self.health_check_query, params=None, fetch_mode=FetchMode.ONE
|
323
|
+
)
|
324
|
+
|
325
|
+
check_time = time.time() - start_time
|
326
|
+
|
327
|
+
# Update stats
|
328
|
+
self.stats.health_checks_passed += 1
|
329
|
+
self._update_health_score(5) # Boost score on success
|
330
|
+
|
331
|
+
return {
|
332
|
+
"healthy": True,
|
333
|
+
"check_time": check_time,
|
334
|
+
"health_score": self.stats.health_score,
|
335
|
+
}
|
336
|
+
|
337
|
+
except Exception as e:
|
338
|
+
self.stats.health_checks_failed += 1
|
339
|
+
self._update_health_score(-20) # Significant penalty on failure
|
340
|
+
|
341
|
+
return {
|
342
|
+
"healthy": False,
|
343
|
+
"error": str(e),
|
344
|
+
"health_score": self.stats.health_score,
|
345
|
+
}
|
346
|
+
|
347
|
+
async def _handle_recycle(self) -> Dict[str, Any]:
|
348
|
+
"""Handle connection recycling request."""
|
349
|
+
if self.state == ConnectionState.RECYCLING:
|
350
|
+
return {"status": "already_recycling"}
|
351
|
+
|
352
|
+
self.state = ConnectionState.RECYCLING
|
353
|
+
|
354
|
+
# Notify supervisor if present
|
355
|
+
if self.supervisor:
|
356
|
+
await self.supervisor.notify_recycling(self.id)
|
357
|
+
|
358
|
+
return {"status": "recycling_initiated"}
|
359
|
+
|
360
|
+
def _get_stats(self) -> Dict[str, Any]:
|
361
|
+
"""Get connection statistics."""
|
362
|
+
uptime = time.time() - self._created_at
|
363
|
+
idle_time = time.time() - self._last_activity
|
364
|
+
|
365
|
+
return {
|
366
|
+
"connection_id": self.id,
|
367
|
+
"state": self.state.value,
|
368
|
+
"stats": {
|
369
|
+
"queries_executed": self.stats.queries_executed,
|
370
|
+
"errors_encountered": self.stats.errors_encountered,
|
371
|
+
"avg_execution_time": (
|
372
|
+
self.stats.total_execution_time / self.stats.queries_executed
|
373
|
+
if self.stats.queries_executed > 0
|
374
|
+
else 0
|
375
|
+
),
|
376
|
+
"health_checks_passed": self.stats.health_checks_passed,
|
377
|
+
"health_checks_failed": self.stats.health_checks_failed,
|
378
|
+
"health_score": self.stats.health_score,
|
379
|
+
"uptime_seconds": uptime,
|
380
|
+
"idle_seconds": idle_time,
|
381
|
+
},
|
382
|
+
}
|
383
|
+
|
384
|
+
async def _monitor_health(self):
|
385
|
+
"""Background health monitoring task."""
|
386
|
+
while self.state not in [ConnectionState.TERMINATED, ConnectionState.RECYCLING]:
|
387
|
+
try:
|
388
|
+
await asyncio.sleep(self.health_check_interval)
|
389
|
+
|
390
|
+
# Send health check message to self
|
391
|
+
health_result = await self.send_message(
|
392
|
+
Message(type=MessageType.HEALTH_CHECK)
|
393
|
+
)
|
394
|
+
|
395
|
+
# Update state based on health
|
396
|
+
if not health_result.get("healthy"):
|
397
|
+
if self.stats.health_score < 30:
|
398
|
+
self.state = ConnectionState.FAILED
|
399
|
+
if self.supervisor:
|
400
|
+
await self.supervisor.notify_failure(self.id)
|
401
|
+
elif self.stats.health_score < 50:
|
402
|
+
self.state = ConnectionState.DEGRADED
|
403
|
+
else:
|
404
|
+
if (
|
405
|
+
self.state == ConnectionState.DEGRADED
|
406
|
+
and self.stats.health_score > 70
|
407
|
+
):
|
408
|
+
self.state = ConnectionState.HEALTHY
|
409
|
+
|
410
|
+
except Exception as e:
|
411
|
+
logger.error(f"Health monitor error in actor {self.id}: {e}")
|
412
|
+
|
413
|
+
def _should_recycle(self) -> bool:
|
414
|
+
"""Check if connection should be recycled."""
|
415
|
+
# Check lifetime
|
416
|
+
if time.time() - self._created_at > self.max_lifetime:
|
417
|
+
return True
|
418
|
+
|
419
|
+
# Check idle time
|
420
|
+
if time.time() - self._last_activity > self.max_idle_time:
|
421
|
+
return True
|
422
|
+
|
423
|
+
# Check health score
|
424
|
+
if self.stats.health_score < 20:
|
425
|
+
return True
|
426
|
+
|
427
|
+
return False
|
428
|
+
|
429
|
+
async def _initiate_recycle(self):
|
430
|
+
"""Initiate connection recycling."""
|
431
|
+
await self.send_message(Message(type=MessageType.RECYCLE))
|
432
|
+
|
433
|
+
def _update_health_score(self, delta: float):
|
434
|
+
"""Update health score with bounds checking."""
|
435
|
+
self.stats.health_score = max(0, min(100, self.stats.health_score + delta))
|
436
|
+
|
437
|
+
# Update state based on score
|
438
|
+
if self.stats.health_score < 30:
|
439
|
+
self.state = ConnectionState.DEGRADED
|
440
|
+
elif self.stats.health_score > 70 and self.state == ConnectionState.DEGRADED:
|
441
|
+
self.state = ConnectionState.HEALTHY
|
442
|
+
|
443
|
+
async def _connect(self):
|
444
|
+
"""Establish physical database connection."""
|
445
|
+
# Import here to avoid circular dependencies
|
446
|
+
from kailash.nodes.data.async_sql import (
|
447
|
+
DatabaseConfig,
|
448
|
+
DatabaseType,
|
449
|
+
MySQLAdapter,
|
450
|
+
PostgreSQLAdapter,
|
451
|
+
SQLiteAdapter,
|
452
|
+
)
|
453
|
+
|
454
|
+
# Determine database type
|
455
|
+
db_type = DatabaseType(self.db_config["type"].lower())
|
456
|
+
|
457
|
+
# Create configuration
|
458
|
+
config = DatabaseConfig(
|
459
|
+
type=db_type,
|
460
|
+
host=self.db_config.get("host"),
|
461
|
+
port=self.db_config.get("port"),
|
462
|
+
database=self.db_config.get("database"),
|
463
|
+
user=self.db_config.get("user"),
|
464
|
+
password=self.db_config.get("password"),
|
465
|
+
connection_string=self.db_config.get("connection_string"),
|
466
|
+
pool_size=1, # Single connection per actor
|
467
|
+
max_pool_size=1,
|
468
|
+
)
|
469
|
+
|
470
|
+
# Create adapter
|
471
|
+
if db_type == DatabaseType.POSTGRESQL:
|
472
|
+
self._adapter = PostgreSQLAdapter(config)
|
473
|
+
elif db_type == DatabaseType.MYSQL:
|
474
|
+
self._adapter = MySQLAdapter(config)
|
475
|
+
elif db_type == DatabaseType.SQLITE:
|
476
|
+
self._adapter = SQLiteAdapter(config)
|
477
|
+
else:
|
478
|
+
raise ValueError(f"Unsupported database type: {db_type}")
|
479
|
+
|
480
|
+
# Connect
|
481
|
+
await self._adapter.connect()
|
482
|
+
logger.info(f"Connection actor {self.id} connected to {db_type.value}")
|
483
|
+
|
484
|
+
async def _disconnect(self):
|
485
|
+
"""Close physical database connection."""
|
486
|
+
if self._adapter:
|
487
|
+
await self._adapter.disconnect()
|
488
|
+
self._adapter = None
|
489
|
+
logger.info(f"Connection actor {self.id} disconnected")
|
490
|
+
|
491
|
+
|
492
|
+
class ConnectionActor:
|
493
|
+
"""
|
494
|
+
High-level interface for interacting with actor connections.
|
495
|
+
|
496
|
+
This class provides a convenient API for sending messages to
|
497
|
+
connection actors without dealing with low-level message passing.
|
498
|
+
"""
|
499
|
+
|
500
|
+
def __init__(self, actor: ActorConnection):
|
501
|
+
"""
|
502
|
+
Initialize connection actor interface.
|
503
|
+
|
504
|
+
Args:
|
505
|
+
actor: The underlying actor connection
|
506
|
+
"""
|
507
|
+
self.actor = actor
|
508
|
+
|
509
|
+
async def execute(
|
510
|
+
self,
|
511
|
+
query: str,
|
512
|
+
params: Optional[Union[tuple, dict]] = None,
|
513
|
+
fetch_mode: str = "all",
|
514
|
+
) -> QueryResult:
|
515
|
+
"""
|
516
|
+
Execute a database query.
|
517
|
+
|
518
|
+
Args:
|
519
|
+
query: SQL query to execute
|
520
|
+
params: Query parameters
|
521
|
+
fetch_mode: How to fetch results (one, all, many)
|
522
|
+
|
523
|
+
Returns:
|
524
|
+
Query result with data or error
|
525
|
+
"""
|
526
|
+
return await self.actor.send_message(
|
527
|
+
Message(
|
528
|
+
type=MessageType.QUERY,
|
529
|
+
payload={"query": query, "params": params, "fetch_mode": fetch_mode},
|
530
|
+
)
|
531
|
+
)
|
532
|
+
|
533
|
+
async def health_check(self) -> Dict[str, Any]:
|
534
|
+
"""Perform a health check on the connection."""
|
535
|
+
return await self.actor.send_message(Message(type=MessageType.HEALTH_CHECK))
|
536
|
+
|
537
|
+
async def get_stats(self) -> Dict[str, Any]:
|
538
|
+
"""Get connection statistics."""
|
539
|
+
return await self.actor.send_message(Message(type=MessageType.GET_STATS))
|
540
|
+
|
541
|
+
async def recycle(self) -> Dict[str, Any]:
|
542
|
+
"""Request connection recycling."""
|
543
|
+
return await self.actor.send_message(Message(type=MessageType.RECYCLE))
|
544
|
+
|
545
|
+
async def ping(self) -> Dict[str, Any]:
|
546
|
+
"""Ping the connection actor."""
|
547
|
+
return await self.actor.send_message(Message(type=MessageType.PING))
|
548
|
+
|
549
|
+
@property
|
550
|
+
def id(self) -> str:
|
551
|
+
"""Get connection ID."""
|
552
|
+
return self.actor.id
|
553
|
+
|
554
|
+
@property
|
555
|
+
def state(self) -> ConnectionState:
|
556
|
+
"""Get current connection state."""
|
557
|
+
return self.actor.state
|
558
|
+
|
559
|
+
@property
|
560
|
+
def health_score(self) -> float:
|
561
|
+
"""Get current health score."""
|
562
|
+
return self.actor.stats.health_score
|
563
|
+
|
564
|
+
async def stop(self):
|
565
|
+
"""Stop the underlying actor connection."""
|
566
|
+
await self.actor.stop()
|