kailash 0.4.2__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/database/repositories.py +3 -1
- 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/audit_log.py +364 -6
- 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 +1209 -681
- kailash/nodes/api/http.py +95 -71
- kailash/nodes/base.py +281 -164
- kailash/nodes/base_async.py +30 -31
- 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 +12 -25
- 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/utils/resource_manager.py +420 -0
- kailash/workflow/__init__.py +8 -0
- kailash/workflow/async_builder.py +621 -0
- kailash/workflow/async_patterns.py +766 -0
- kailash/workflow/builder.py +93 -10
- kailash/workflow/cyclic_runner.py +111 -41
- kailash/workflow/graph.py +7 -2
- kailash/workflow/resilience.py +11 -1
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/METADATA +12 -7
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/RECORD +64 -28
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,643 @@
|
|
1
|
+
"""Workflow-scoped connection pool for production-grade database management.
|
2
|
+
|
3
|
+
This module implements a connection pool that is scoped to workflow lifecycle,
|
4
|
+
providing better resource management and isolation compared to global pools.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import asyncio
|
8
|
+
import logging
|
9
|
+
import time
|
10
|
+
import uuid
|
11
|
+
from collections import defaultdict
|
12
|
+
from datetime import datetime
|
13
|
+
from typing import Any, Dict, List, Optional, Set
|
14
|
+
|
15
|
+
from kailash.core.actors import (
|
16
|
+
ActorConnection,
|
17
|
+
ActorSupervisor,
|
18
|
+
ConnectionActor,
|
19
|
+
ConnectionState,
|
20
|
+
SupervisionStrategy,
|
21
|
+
)
|
22
|
+
from kailash.nodes.base import NodeParameter, register_node
|
23
|
+
from kailash.nodes.base_async import AsyncNode
|
24
|
+
from kailash.sdk_exceptions import NodeExecutionError
|
25
|
+
|
26
|
+
logger = logging.getLogger(__name__)
|
27
|
+
|
28
|
+
|
29
|
+
class ConnectionPoolMetrics:
|
30
|
+
"""Metrics collector for connection pool monitoring."""
|
31
|
+
|
32
|
+
def __init__(self, pool_name: str):
|
33
|
+
self.pool_name = pool_name
|
34
|
+
self.connections_created = 0
|
35
|
+
self.connections_recycled = 0
|
36
|
+
self.connections_failed = 0
|
37
|
+
self.queries_executed = 0
|
38
|
+
self.query_errors = 0
|
39
|
+
self.acquisition_wait_times: List[float] = []
|
40
|
+
self.health_check_results: List[bool] = []
|
41
|
+
self.start_time = time.time()
|
42
|
+
|
43
|
+
def record_acquisition_time(self, wait_time: float):
|
44
|
+
"""Record time waited to acquire connection."""
|
45
|
+
self.acquisition_wait_times.append(wait_time)
|
46
|
+
# Keep only last 1000 measurements
|
47
|
+
if len(self.acquisition_wait_times) > 1000:
|
48
|
+
self.acquisition_wait_times = self.acquisition_wait_times[-1000:]
|
49
|
+
|
50
|
+
def get_stats(self) -> Dict[str, Any]:
|
51
|
+
"""Get comprehensive pool statistics."""
|
52
|
+
uptime = time.time() - self.start_time
|
53
|
+
|
54
|
+
# Calculate averages
|
55
|
+
avg_wait_time = (
|
56
|
+
sum(self.acquisition_wait_times) / len(self.acquisition_wait_times)
|
57
|
+
if self.acquisition_wait_times
|
58
|
+
else 0.0
|
59
|
+
)
|
60
|
+
|
61
|
+
health_success_rate = (
|
62
|
+
sum(1 for h in self.health_check_results if h)
|
63
|
+
/ len(self.health_check_results)
|
64
|
+
if self.health_check_results
|
65
|
+
else 1.0
|
66
|
+
)
|
67
|
+
|
68
|
+
return {
|
69
|
+
"pool_name": self.pool_name,
|
70
|
+
"uptime_seconds": uptime,
|
71
|
+
"connections": {
|
72
|
+
"created": self.connections_created,
|
73
|
+
"recycled": self.connections_recycled,
|
74
|
+
"failed": self.connections_failed,
|
75
|
+
},
|
76
|
+
"queries": {
|
77
|
+
"executed": self.queries_executed,
|
78
|
+
"errors": self.query_errors,
|
79
|
+
"error_rate": (
|
80
|
+
self.query_errors / self.queries_executed
|
81
|
+
if self.queries_executed > 0
|
82
|
+
else 0
|
83
|
+
),
|
84
|
+
},
|
85
|
+
"performance": {
|
86
|
+
"avg_acquisition_time_ms": avg_wait_time * 1000,
|
87
|
+
"p99_acquisition_time_ms": (
|
88
|
+
sorted(self.acquisition_wait_times)[
|
89
|
+
int(len(self.acquisition_wait_times) * 0.99)
|
90
|
+
]
|
91
|
+
* 1000
|
92
|
+
if self.acquisition_wait_times
|
93
|
+
else 0
|
94
|
+
),
|
95
|
+
},
|
96
|
+
"health": {
|
97
|
+
"success_rate": health_success_rate,
|
98
|
+
"checks_performed": len(self.health_check_results),
|
99
|
+
},
|
100
|
+
}
|
101
|
+
|
102
|
+
|
103
|
+
class WorkflowPatternAnalyzer:
|
104
|
+
"""Analyzes workflow patterns for optimization."""
|
105
|
+
|
106
|
+
def __init__(self):
|
107
|
+
self.workflow_patterns: Dict[str, Dict[str, Any]] = {}
|
108
|
+
self.connection_usage: Dict[str, List[float]] = defaultdict(list)
|
109
|
+
|
110
|
+
def record_workflow_start(self, workflow_id: str, workflow_type: str):
|
111
|
+
"""Record workflow start for pattern analysis."""
|
112
|
+
self.workflow_patterns[workflow_id] = {
|
113
|
+
"type": workflow_type,
|
114
|
+
"start_time": time.time(),
|
115
|
+
"connections_used": 0,
|
116
|
+
"peak_connections": 0,
|
117
|
+
}
|
118
|
+
|
119
|
+
def record_connection_usage(self, workflow_id: str, active_connections: int):
|
120
|
+
"""Record connection usage for workflow."""
|
121
|
+
if workflow_id in self.workflow_patterns:
|
122
|
+
pattern = self.workflow_patterns[workflow_id]
|
123
|
+
pattern["connections_used"] = max(
|
124
|
+
pattern["connections_used"], active_connections
|
125
|
+
)
|
126
|
+
self.connection_usage[workflow_id].append(active_connections)
|
127
|
+
|
128
|
+
def get_expected_connections(self, workflow_type: str) -> int:
|
129
|
+
"""Get expected connection count for workflow type."""
|
130
|
+
# Analyze historical data for this workflow type
|
131
|
+
similar_workflows = [
|
132
|
+
p
|
133
|
+
for p in self.workflow_patterns.values()
|
134
|
+
if p["type"] == workflow_type and "connections_used" in p
|
135
|
+
]
|
136
|
+
|
137
|
+
if not similar_workflows:
|
138
|
+
return 2 # Default
|
139
|
+
|
140
|
+
# Return 90th percentile of historical usage
|
141
|
+
usage_values = sorted([w["connections_used"] for w in similar_workflows])
|
142
|
+
percentile_index = int(len(usage_values) * 0.9)
|
143
|
+
return usage_values[percentile_index] if usage_values else 2
|
144
|
+
|
145
|
+
|
146
|
+
@register_node()
|
147
|
+
class WorkflowConnectionPool(AsyncNode):
|
148
|
+
"""
|
149
|
+
Workflow-scoped connection pool with production-grade features.
|
150
|
+
|
151
|
+
This node provides:
|
152
|
+
- Connections scoped to workflow lifecycle
|
153
|
+
- Actor-based isolation for each connection
|
154
|
+
- Automatic health monitoring and recycling
|
155
|
+
- Pattern-based pre-warming
|
156
|
+
- Comprehensive metrics and monitoring
|
157
|
+
|
158
|
+
Example:
|
159
|
+
>>> pool = WorkflowConnectionPool(
|
160
|
+
... name="workflow_db_pool",
|
161
|
+
... database_type="postgresql",
|
162
|
+
... host="localhost",
|
163
|
+
... database="myapp",
|
164
|
+
... user="dbuser",
|
165
|
+
... password="dbpass",
|
166
|
+
... min_connections=2,
|
167
|
+
... max_connections=10
|
168
|
+
... )
|
169
|
+
>>>
|
170
|
+
>>> # Get connection
|
171
|
+
>>> result = await pool.process({"operation": "acquire"})
|
172
|
+
>>> conn_id = result["connection_id"]
|
173
|
+
>>>
|
174
|
+
>>> # Execute query
|
175
|
+
>>> query_result = await pool.process({
|
176
|
+
... "operation": "execute",
|
177
|
+
... "connection_id": conn_id,
|
178
|
+
... "query": "SELECT * FROM users WHERE active = true",
|
179
|
+
... })
|
180
|
+
"""
|
181
|
+
|
182
|
+
def __init__(self, **config):
|
183
|
+
super().__init__(**config)
|
184
|
+
|
185
|
+
# Pool configuration
|
186
|
+
self.min_connections = config.get("min_connections", 2)
|
187
|
+
self.max_connections = config.get("max_connections", 10)
|
188
|
+
self.health_threshold = config.get("health_threshold", 50)
|
189
|
+
self.pre_warm_enabled = config.get("pre_warm", True)
|
190
|
+
|
191
|
+
# Database configuration
|
192
|
+
self.db_config = {
|
193
|
+
"type": config.get("database_type", "postgresql"),
|
194
|
+
"host": config.get("host"),
|
195
|
+
"port": config.get("port"),
|
196
|
+
"database": config.get("database"),
|
197
|
+
"user": config.get("user"),
|
198
|
+
"password": config.get("password"),
|
199
|
+
"connection_string": config.get("connection_string"),
|
200
|
+
}
|
201
|
+
|
202
|
+
# Actor supervision
|
203
|
+
self.supervisor = ActorSupervisor(
|
204
|
+
name=f"{self.metadata.name}_supervisor",
|
205
|
+
strategy=SupervisionStrategy.ONE_FOR_ONE,
|
206
|
+
max_restarts=3,
|
207
|
+
restart_window=60.0,
|
208
|
+
)
|
209
|
+
|
210
|
+
# Connection tracking
|
211
|
+
self.available_connections: asyncio.Queue = asyncio.Queue()
|
212
|
+
self.active_connections: Dict[str, ConnectionActor] = {}
|
213
|
+
self.all_connections: Dict[str, ConnectionActor] = {}
|
214
|
+
|
215
|
+
# Workflow integration
|
216
|
+
self.workflow_id: Optional[str] = None
|
217
|
+
self.pattern_analyzer = WorkflowPatternAnalyzer()
|
218
|
+
|
219
|
+
# Metrics
|
220
|
+
self.metrics = ConnectionPoolMetrics(self.metadata.name)
|
221
|
+
|
222
|
+
# State
|
223
|
+
self._initialized = False
|
224
|
+
self._closing = False
|
225
|
+
|
226
|
+
def get_parameters(self) -> Dict[str, NodeParameter]:
|
227
|
+
"""Define node parameters."""
|
228
|
+
params = [
|
229
|
+
# Database connection parameters
|
230
|
+
NodeParameter(
|
231
|
+
name="database_type",
|
232
|
+
type=str,
|
233
|
+
required=True,
|
234
|
+
default="postgresql",
|
235
|
+
description="Database type: postgresql, mysql, or sqlite",
|
236
|
+
),
|
237
|
+
NodeParameter(
|
238
|
+
name="connection_string",
|
239
|
+
type=str,
|
240
|
+
required=False,
|
241
|
+
description="Full connection string (overrides individual params)",
|
242
|
+
),
|
243
|
+
NodeParameter(
|
244
|
+
name="host", type=str, required=False, description="Database host"
|
245
|
+
),
|
246
|
+
NodeParameter(
|
247
|
+
name="port", type=int, required=False, description="Database port"
|
248
|
+
),
|
249
|
+
NodeParameter(
|
250
|
+
name="database", type=str, required=False, description="Database name"
|
251
|
+
),
|
252
|
+
NodeParameter(
|
253
|
+
name="user", type=str, required=False, description="Database user"
|
254
|
+
),
|
255
|
+
NodeParameter(
|
256
|
+
name="password",
|
257
|
+
type=str,
|
258
|
+
required=False,
|
259
|
+
description="Database password",
|
260
|
+
),
|
261
|
+
# Pool configuration
|
262
|
+
NodeParameter(
|
263
|
+
name="min_connections",
|
264
|
+
type=int,
|
265
|
+
required=False,
|
266
|
+
default=2,
|
267
|
+
description="Minimum pool connections",
|
268
|
+
),
|
269
|
+
NodeParameter(
|
270
|
+
name="max_connections",
|
271
|
+
type=int,
|
272
|
+
required=False,
|
273
|
+
default=10,
|
274
|
+
description="Maximum pool connections",
|
275
|
+
),
|
276
|
+
NodeParameter(
|
277
|
+
name="health_threshold",
|
278
|
+
type=int,
|
279
|
+
required=False,
|
280
|
+
default=50,
|
281
|
+
description="Minimum health score to keep connection",
|
282
|
+
),
|
283
|
+
NodeParameter(
|
284
|
+
name="pre_warm",
|
285
|
+
type=bool,
|
286
|
+
required=False,
|
287
|
+
default=True,
|
288
|
+
description="Enable pattern-based pre-warming",
|
289
|
+
),
|
290
|
+
# Operation parameters
|
291
|
+
NodeParameter(
|
292
|
+
name="operation",
|
293
|
+
type=str,
|
294
|
+
required=True,
|
295
|
+
description="Operation: initialize, acquire, release, execute, stats",
|
296
|
+
),
|
297
|
+
NodeParameter(
|
298
|
+
name="connection_id",
|
299
|
+
type=str,
|
300
|
+
required=False,
|
301
|
+
description="Connection ID for operations",
|
302
|
+
),
|
303
|
+
NodeParameter(
|
304
|
+
name="query",
|
305
|
+
type=str,
|
306
|
+
required=False,
|
307
|
+
description="SQL query to execute",
|
308
|
+
),
|
309
|
+
NodeParameter(
|
310
|
+
name="params", type=Any, required=False, description="Query parameters"
|
311
|
+
),
|
312
|
+
NodeParameter(
|
313
|
+
name="fetch_mode",
|
314
|
+
type=str,
|
315
|
+
required=False,
|
316
|
+
default="all",
|
317
|
+
description="Fetch mode: one, all, many",
|
318
|
+
),
|
319
|
+
]
|
320
|
+
|
321
|
+
# Convert list to dict as required by base class
|
322
|
+
return {param.name: param for param in params}
|
323
|
+
|
324
|
+
async def on_workflow_start(
|
325
|
+
self, workflow_id: str, workflow_type: Optional[str] = None
|
326
|
+
):
|
327
|
+
"""Called when workflow starts - pre-warm connections."""
|
328
|
+
self.workflow_id = workflow_id
|
329
|
+
self.pattern_analyzer.record_workflow_start(
|
330
|
+
workflow_id, workflow_type or "unknown"
|
331
|
+
)
|
332
|
+
|
333
|
+
if self.pre_warm_enabled and workflow_type:
|
334
|
+
expected_connections = self.pattern_analyzer.get_expected_connections(
|
335
|
+
workflow_type
|
336
|
+
)
|
337
|
+
await self._pre_warm_connections(expected_connections)
|
338
|
+
|
339
|
+
async def on_workflow_complete(self, workflow_id: str):
|
340
|
+
"""Called when workflow completes - clean up resources."""
|
341
|
+
if workflow_id == self.workflow_id:
|
342
|
+
await self._cleanup()
|
343
|
+
|
344
|
+
async def async_run(self, **inputs) -> Dict[str, Any]:
|
345
|
+
"""Process connection pool operations."""
|
346
|
+
operation = inputs.get("operation")
|
347
|
+
|
348
|
+
if operation == "initialize":
|
349
|
+
return await self._initialize()
|
350
|
+
elif operation == "acquire":
|
351
|
+
return await self._acquire_connection()
|
352
|
+
elif operation == "release":
|
353
|
+
return await self._release_connection(inputs.get("connection_id"))
|
354
|
+
elif operation == "execute":
|
355
|
+
return await self._execute_query(inputs)
|
356
|
+
elif operation == "stats":
|
357
|
+
return await self._get_stats()
|
358
|
+
else:
|
359
|
+
raise NodeExecutionError(f"Unknown operation: {operation}")
|
360
|
+
|
361
|
+
async def _initialize(self) -> Dict[str, Any]:
|
362
|
+
"""Initialize the connection pool."""
|
363
|
+
if self._initialized:
|
364
|
+
return {"status": "already_initialized"}
|
365
|
+
|
366
|
+
try:
|
367
|
+
# Start supervisor
|
368
|
+
await self.supervisor.start()
|
369
|
+
|
370
|
+
# Set up callbacks
|
371
|
+
self.supervisor.on_actor_failure = self._on_connection_failure
|
372
|
+
self.supervisor.on_actor_restart = self._on_connection_restart
|
373
|
+
|
374
|
+
# Create minimum connections
|
375
|
+
await self._ensure_min_connections()
|
376
|
+
|
377
|
+
self._initialized = True
|
378
|
+
|
379
|
+
return {
|
380
|
+
"status": "initialized",
|
381
|
+
"min_connections": self.min_connections,
|
382
|
+
"max_connections": self.max_connections,
|
383
|
+
}
|
384
|
+
|
385
|
+
except Exception as e:
|
386
|
+
logger.error(f"Failed to initialize pool: {e}")
|
387
|
+
raise NodeExecutionError(f"Pool initialization failed: {e}")
|
388
|
+
|
389
|
+
async def _acquire_connection(self) -> Dict[str, Any]:
|
390
|
+
"""Acquire a connection from the pool."""
|
391
|
+
if not self._initialized:
|
392
|
+
await self._initialize()
|
393
|
+
|
394
|
+
start_time = time.time()
|
395
|
+
|
396
|
+
try:
|
397
|
+
# Try to get available connection
|
398
|
+
connection = None
|
399
|
+
|
400
|
+
# Fast path: try to get immediately available connection
|
401
|
+
try:
|
402
|
+
connection = await asyncio.wait_for(
|
403
|
+
self.available_connections.get(), timeout=0.1
|
404
|
+
)
|
405
|
+
except asyncio.TimeoutError:
|
406
|
+
# Need to create new connection or wait
|
407
|
+
if len(self.all_connections) < self.max_connections:
|
408
|
+
# Create new connection
|
409
|
+
connection = await self._create_connection()
|
410
|
+
# Don't put it in available queue - we'll use it directly
|
411
|
+
else:
|
412
|
+
# Wait for available connection
|
413
|
+
connection = await self.available_connections.get()
|
414
|
+
|
415
|
+
# Record acquisition time
|
416
|
+
wait_time = time.time() - start_time
|
417
|
+
self.metrics.record_acquisition_time(wait_time)
|
418
|
+
|
419
|
+
# Move to active
|
420
|
+
self.active_connections[connection.id] = connection
|
421
|
+
|
422
|
+
# Update pattern analyzer
|
423
|
+
if self.workflow_id:
|
424
|
+
self.pattern_analyzer.record_connection_usage(
|
425
|
+
self.workflow_id, len(self.active_connections)
|
426
|
+
)
|
427
|
+
|
428
|
+
return {
|
429
|
+
"connection_id": connection.id,
|
430
|
+
"health_score": connection.health_score,
|
431
|
+
"acquisition_time_ms": wait_time * 1000,
|
432
|
+
}
|
433
|
+
|
434
|
+
except Exception as e:
|
435
|
+
logger.error(f"Failed to acquire connection: {e}")
|
436
|
+
raise NodeExecutionError(f"Connection acquisition failed: {e}")
|
437
|
+
|
438
|
+
async def _release_connection(self, connection_id: Optional[str]) -> Dict[str, Any]:
|
439
|
+
"""Release a connection back to the pool."""
|
440
|
+
if not connection_id:
|
441
|
+
raise NodeExecutionError("connection_id required for release")
|
442
|
+
|
443
|
+
if connection_id not in self.active_connections:
|
444
|
+
raise NodeExecutionError(f"Connection {connection_id} not active")
|
445
|
+
|
446
|
+
connection = self.active_connections.pop(connection_id)
|
447
|
+
|
448
|
+
# Check if connection should be recycled
|
449
|
+
if connection.health_score < self.health_threshold:
|
450
|
+
await self._recycle_connection(connection)
|
451
|
+
return {"status": "recycled", "connection_id": connection_id}
|
452
|
+
else:
|
453
|
+
# Return to available pool
|
454
|
+
await self.available_connections.put(connection)
|
455
|
+
return {"status": "released", "connection_id": connection_id}
|
456
|
+
|
457
|
+
async def _execute_query(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
458
|
+
"""Execute a query on a specific connection."""
|
459
|
+
connection_id = inputs.get("connection_id")
|
460
|
+
if not connection_id or connection_id not in self.active_connections:
|
461
|
+
raise NodeExecutionError(f"Invalid connection_id: {connection_id}")
|
462
|
+
|
463
|
+
connection = self.active_connections[connection_id]
|
464
|
+
|
465
|
+
try:
|
466
|
+
# Execute query
|
467
|
+
result = await connection.execute(
|
468
|
+
query=inputs.get("query"),
|
469
|
+
params=inputs.get("params"),
|
470
|
+
fetch_mode=inputs.get("fetch_mode", "all"),
|
471
|
+
)
|
472
|
+
|
473
|
+
# Update metrics
|
474
|
+
self.metrics.queries_executed += 1
|
475
|
+
if not result.success:
|
476
|
+
self.metrics.query_errors += 1
|
477
|
+
|
478
|
+
return {
|
479
|
+
"success": result.success,
|
480
|
+
"data": result.data,
|
481
|
+
"error": result.error,
|
482
|
+
"execution_time_ms": result.execution_time * 1000,
|
483
|
+
"connection_id": connection_id,
|
484
|
+
}
|
485
|
+
|
486
|
+
except Exception as e:
|
487
|
+
self.metrics.query_errors += 1
|
488
|
+
logger.error(f"Query execution failed: {e}")
|
489
|
+
raise NodeExecutionError(f"Query execution failed: {e}")
|
490
|
+
|
491
|
+
async def _get_stats(self) -> Dict[str, Any]:
|
492
|
+
"""Get comprehensive pool statistics."""
|
493
|
+
pool_stats = self.metrics.get_stats()
|
494
|
+
supervisor_stats = self.supervisor.get_stats()
|
495
|
+
|
496
|
+
# Add current pool state
|
497
|
+
pool_stats["current_state"] = {
|
498
|
+
"total_connections": len(self.all_connections),
|
499
|
+
"active_connections": len(self.active_connections),
|
500
|
+
"available_connections": self.available_connections.qsize(),
|
501
|
+
"health_scores": {
|
502
|
+
conn_id: conn.health_score
|
503
|
+
for conn_id, conn in self.all_connections.items()
|
504
|
+
},
|
505
|
+
}
|
506
|
+
|
507
|
+
pool_stats["supervisor"] = supervisor_stats
|
508
|
+
|
509
|
+
return pool_stats
|
510
|
+
|
511
|
+
async def _create_connection(self) -> ConnectionActor:
|
512
|
+
"""Create a new connection actor."""
|
513
|
+
conn_id = f"conn_{uuid.uuid4().hex[:8]}"
|
514
|
+
|
515
|
+
# Create actor connection
|
516
|
+
actor_conn = ActorConnection(
|
517
|
+
connection_id=conn_id,
|
518
|
+
db_config=self.db_config,
|
519
|
+
health_check_interval=30.0,
|
520
|
+
max_lifetime=3600.0,
|
521
|
+
max_idle_time=600.0,
|
522
|
+
)
|
523
|
+
|
524
|
+
# Add to supervisor
|
525
|
+
self.supervisor.add_actor(actor_conn)
|
526
|
+
|
527
|
+
# Create high-level interface
|
528
|
+
connection = ConnectionActor(actor_conn)
|
529
|
+
|
530
|
+
# Track connection
|
531
|
+
self.all_connections[conn_id] = connection
|
532
|
+
self.metrics.connections_created += 1
|
533
|
+
|
534
|
+
logger.info(f"Created connection {conn_id} for pool {self.metadata.name}")
|
535
|
+
|
536
|
+
return connection
|
537
|
+
|
538
|
+
async def _ensure_min_connections(self):
|
539
|
+
"""Ensure minimum connections are available."""
|
540
|
+
current_count = len(self.all_connections)
|
541
|
+
|
542
|
+
for _ in range(self.min_connections - current_count):
|
543
|
+
connection = await self._create_connection()
|
544
|
+
await self.available_connections.put(connection)
|
545
|
+
|
546
|
+
async def _pre_warm_connections(self, target_count: int):
|
547
|
+
"""Pre-warm connections based on expected usage."""
|
548
|
+
current_count = len(self.all_connections)
|
549
|
+
to_create = min(
|
550
|
+
target_count - current_count, self.max_connections - current_count
|
551
|
+
)
|
552
|
+
|
553
|
+
if to_create > 0:
|
554
|
+
logger.info(
|
555
|
+
f"Pre-warming {to_create} connections for pool {self.metadata.name}"
|
556
|
+
)
|
557
|
+
|
558
|
+
# Create connections in parallel
|
559
|
+
tasks = [self._create_connection() for _ in range(to_create)]
|
560
|
+
connections = await asyncio.gather(*tasks)
|
561
|
+
|
562
|
+
# Add to available pool
|
563
|
+
for conn in connections:
|
564
|
+
await self.available_connections.put(conn)
|
565
|
+
|
566
|
+
async def _recycle_connection(self, connection: ConnectionActor):
|
567
|
+
"""Recycle a connection."""
|
568
|
+
logger.info(
|
569
|
+
f"Recycling connection {connection.id} (health: {connection.health_score})"
|
570
|
+
)
|
571
|
+
|
572
|
+
# Remove from all connections
|
573
|
+
if connection.id in self.all_connections:
|
574
|
+
del self.all_connections[connection.id]
|
575
|
+
|
576
|
+
# Request recycling
|
577
|
+
await connection.recycle()
|
578
|
+
|
579
|
+
# Update metrics
|
580
|
+
self.metrics.connections_recycled += 1
|
581
|
+
|
582
|
+
# Ensure minimum connections
|
583
|
+
await self._ensure_min_connections()
|
584
|
+
|
585
|
+
async def _cleanup(self):
|
586
|
+
"""Clean up all connections and resources."""
|
587
|
+
if self._closing:
|
588
|
+
return
|
589
|
+
|
590
|
+
self._closing = True
|
591
|
+
logger.info(f"Cleaning up pool {self.metadata.name}")
|
592
|
+
|
593
|
+
# Stop accepting new connections
|
594
|
+
self._initialized = False
|
595
|
+
|
596
|
+
# Stop all connection actors gracefully
|
597
|
+
actors_to_stop = list(self.all_connections.values())
|
598
|
+
for actor in actors_to_stop:
|
599
|
+
try:
|
600
|
+
await actor.stop()
|
601
|
+
except Exception as e:
|
602
|
+
logger.warning(f"Error stopping actor {actor.id}: {e}")
|
603
|
+
|
604
|
+
# Stop supervisor
|
605
|
+
try:
|
606
|
+
await self.supervisor.stop()
|
607
|
+
except Exception as e:
|
608
|
+
logger.warning(f"Error stopping supervisor: {e}")
|
609
|
+
|
610
|
+
# Clear connection tracking
|
611
|
+
self.available_connections = asyncio.Queue()
|
612
|
+
self.active_connections.clear()
|
613
|
+
self.all_connections.clear()
|
614
|
+
|
615
|
+
logger.info(f"Pool {self.metadata.name} cleaned up")
|
616
|
+
|
617
|
+
def _on_connection_failure(self, actor_id: str, error: Exception):
|
618
|
+
"""Handle connection failure."""
|
619
|
+
logger.error(f"Connection {actor_id} failed: {error}")
|
620
|
+
self.metrics.connections_failed += 1
|
621
|
+
|
622
|
+
# Remove from tracking
|
623
|
+
if actor_id in self.all_connections:
|
624
|
+
del self.all_connections[actor_id]
|
625
|
+
if actor_id in self.active_connections:
|
626
|
+
del self.active_connections[actor_id]
|
627
|
+
|
628
|
+
def _on_connection_restart(self, actor_id: str, restart_count: int):
|
629
|
+
"""Handle connection restart."""
|
630
|
+
logger.info(f"Connection {actor_id} restarted (count: {restart_count})")
|
631
|
+
|
632
|
+
async def process(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
633
|
+
"""Async process method for middleware compatibility."""
|
634
|
+
return await self.async_run(**inputs)
|
635
|
+
|
636
|
+
async def __aenter__(self):
|
637
|
+
"""Context manager entry."""
|
638
|
+
await self._initialize()
|
639
|
+
return self
|
640
|
+
|
641
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
642
|
+
"""Context manager exit."""
|
643
|
+
await self._cleanup()
|
kailash/nodes/rag/__init__.py
CHANGED
@@ -173,10 +173,7 @@ from .query_processing import (
|
|
173
173
|
)
|
174
174
|
|
175
175
|
# Real-time RAG
|
176
|
-
from .realtime import
|
177
|
-
IncrementalIndexNode,
|
178
|
-
RealtimeRAGNode,
|
179
|
-
)
|
176
|
+
from .realtime import IncrementalIndexNode, RealtimeRAGNode
|
180
177
|
from .realtime import (
|
181
178
|
StreamingRAGNode as RealtimeStreamingRAGNode, # Avoid name conflict
|
182
179
|
)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
"""
|
2
|
+
Kailash Resource Management System
|
3
|
+
|
4
|
+
This module provides centralized resource management for the Kailash SDK,
|
5
|
+
solving the fundamental problem of passing non-serializable resources (like
|
6
|
+
database connections, HTTP clients, etc.) through JSON APIs.
|
7
|
+
|
8
|
+
Key Components:
|
9
|
+
- ResourceRegistry: Central registry for all shared resources
|
10
|
+
- ResourceFactory: Abstract factory interface for creating resources
|
11
|
+
- Built-in factories for common resources (databases, HTTP clients, caches)
|
12
|
+
"""
|
13
|
+
|
14
|
+
from .factory import (
|
15
|
+
CacheFactory,
|
16
|
+
DatabasePoolFactory,
|
17
|
+
HttpClientFactory,
|
18
|
+
MessageQueueFactory,
|
19
|
+
ResourceFactory,
|
20
|
+
)
|
21
|
+
from .health import HealthCheck, HealthStatus
|
22
|
+
from .reference import ResourceReference
|
23
|
+
from .registry import ResourceNotFoundError, ResourceRegistry
|
24
|
+
|
25
|
+
__all__ = [
|
26
|
+
# Core
|
27
|
+
"ResourceRegistry",
|
28
|
+
"ResourceNotFoundError",
|
29
|
+
# Factories
|
30
|
+
"ResourceFactory",
|
31
|
+
"DatabasePoolFactory",
|
32
|
+
"HttpClientFactory",
|
33
|
+
"CacheFactory",
|
34
|
+
"MessageQueueFactory",
|
35
|
+
# References
|
36
|
+
"ResourceReference",
|
37
|
+
# Health
|
38
|
+
"HealthCheck",
|
39
|
+
"HealthStatus",
|
40
|
+
]
|