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.
Files changed (57) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/client/__init__.py +12 -0
  3. kailash/client/enhanced_client.py +306 -0
  4. kailash/core/actors/__init__.py +16 -0
  5. kailash/core/actors/connection_actor.py +566 -0
  6. kailash/core/actors/supervisor.py +364 -0
  7. kailash/edge/__init__.py +16 -0
  8. kailash/edge/compliance.py +834 -0
  9. kailash/edge/discovery.py +659 -0
  10. kailash/edge/location.py +582 -0
  11. kailash/gateway/__init__.py +33 -0
  12. kailash/gateway/api.py +289 -0
  13. kailash/gateway/enhanced_gateway.py +357 -0
  14. kailash/gateway/resource_resolver.py +217 -0
  15. kailash/gateway/security.py +227 -0
  16. kailash/middleware/auth/models.py +2 -2
  17. kailash/middleware/database/base_models.py +1 -7
  18. kailash/middleware/gateway/__init__.py +22 -0
  19. kailash/middleware/gateway/checkpoint_manager.py +398 -0
  20. kailash/middleware/gateway/deduplicator.py +382 -0
  21. kailash/middleware/gateway/durable_gateway.py +417 -0
  22. kailash/middleware/gateway/durable_request.py +498 -0
  23. kailash/middleware/gateway/event_store.py +459 -0
  24. kailash/nodes/admin/permission_check.py +817 -33
  25. kailash/nodes/admin/role_management.py +1242 -108
  26. kailash/nodes/admin/schema_manager.py +438 -0
  27. kailash/nodes/admin/user_management.py +1124 -1582
  28. kailash/nodes/code/__init__.py +8 -1
  29. kailash/nodes/code/async_python.py +1035 -0
  30. kailash/nodes/code/python.py +1 -0
  31. kailash/nodes/data/async_sql.py +9 -3
  32. kailash/nodes/data/sql.py +20 -11
  33. kailash/nodes/data/workflow_connection_pool.py +643 -0
  34. kailash/nodes/rag/__init__.py +1 -4
  35. kailash/resources/__init__.py +40 -0
  36. kailash/resources/factory.py +533 -0
  37. kailash/resources/health.py +319 -0
  38. kailash/resources/reference.py +288 -0
  39. kailash/resources/registry.py +392 -0
  40. kailash/runtime/async_local.py +711 -302
  41. kailash/testing/__init__.py +34 -0
  42. kailash/testing/async_test_case.py +353 -0
  43. kailash/testing/async_utils.py +345 -0
  44. kailash/testing/fixtures.py +458 -0
  45. kailash/testing/mock_registry.py +495 -0
  46. kailash/workflow/__init__.py +8 -0
  47. kailash/workflow/async_builder.py +621 -0
  48. kailash/workflow/async_patterns.py +766 -0
  49. kailash/workflow/cyclic_runner.py +107 -16
  50. kailash/workflow/graph.py +7 -2
  51. kailash/workflow/resilience.py +11 -1
  52. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/METADATA +7 -4
  53. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/RECORD +57 -22
  54. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
  55. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
  56. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
  57. {kailash-0.5.0.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()
@@ -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
+ ]