kailash 0.6.0__py3-none-any.whl → 0.6.1__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/access_control/__init__.py +1 -1
- kailash/core/actors/adaptive_pool_controller.py +630 -0
- kailash/core/ml/__init__.py +1 -0
- kailash/core/ml/query_patterns.py +544 -0
- kailash/core/monitoring/__init__.py +19 -0
- kailash/core/monitoring/connection_metrics.py +488 -0
- kailash/core/optimization/__init__.py +1 -0
- kailash/core/resilience/__init__.py +17 -0
- kailash/core/resilience/circuit_breaker.py +382 -0
- kailash/middleware/auth/access_control.py +6 -6
- kailash/middleware/communication/ai_chat.py +7 -7
- kailash/middleware/communication/api_gateway.py +5 -15
- kailash/middleware/gateway/event_store.py +66 -26
- kailash/middleware/mcp/enhanced_server.py +2 -2
- kailash/nodes/data/query_pipeline.py +641 -0
- kailash/nodes/data/query_router.py +895 -0
- kailash/nodes/data/workflow_connection_pool.py +451 -23
- kailash/nodes/monitoring/__init__.py +3 -5
- kailash/nodes/monitoring/connection_dashboard.py +822 -0
- kailash/nodes/rag/__init__.py +1 -3
- {kailash-0.6.0.dist-info → kailash-0.6.1.dist-info}/METADATA +13 -1
- {kailash-0.6.0.dist-info → kailash-0.6.1.dist-info}/RECORD +27 -16
- {kailash-0.6.0.dist-info → kailash-0.6.1.dist-info}/WHEEL +0 -0
- {kailash-0.6.0.dist-info → kailash-0.6.1.dist-info}/entry_points.txt +0 -0
- {kailash-0.6.0.dist-info → kailash-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.6.0.dist-info → kailash-0.6.1.dist-info}/top_level.txt +0 -0
kailash/__init__.py
CHANGED
@@ -42,8 +42,8 @@ set_access_control_manager = _original_module.set_access_control_manager
|
|
42
42
|
|
43
43
|
# Import new composition-based components
|
44
44
|
from kailash.access_control.managers import AccessControlManager # noqa: E402
|
45
|
+
from kailash.access_control.rule_evaluators import ABACRuleEvaluator # noqa: E402
|
45
46
|
from kailash.access_control.rule_evaluators import ( # noqa: E402
|
46
|
-
ABACRuleEvaluator,
|
47
47
|
HybridRuleEvaluator,
|
48
48
|
RBACRuleEvaluator,
|
49
49
|
RuleEvaluator,
|
@@ -0,0 +1,630 @@
|
|
1
|
+
"""Adaptive pool sizing controller for dynamic connection management.
|
2
|
+
|
3
|
+
This module implements intelligent pool size adjustment based on workload
|
4
|
+
patterns and resource constraints.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import asyncio
|
8
|
+
import logging
|
9
|
+
import time
|
10
|
+
from collections import deque
|
11
|
+
from dataclasses import dataclass
|
12
|
+
from datetime import datetime, timedelta
|
13
|
+
from typing import Any, Dict, List, Optional, Tuple
|
14
|
+
|
15
|
+
import psutil
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class PoolMetrics:
|
22
|
+
"""Metrics for pool sizing decisions."""
|
23
|
+
|
24
|
+
current_size: int
|
25
|
+
active_connections: int
|
26
|
+
idle_connections: int
|
27
|
+
queue_depth: int
|
28
|
+
avg_wait_time_ms: float
|
29
|
+
avg_query_time_ms: float
|
30
|
+
queries_per_second: float
|
31
|
+
utilization_rate: float # 0-1
|
32
|
+
health_score: float # 0-100
|
33
|
+
|
34
|
+
|
35
|
+
@dataclass
|
36
|
+
class ResourceConstraints:
|
37
|
+
"""System resource constraints."""
|
38
|
+
|
39
|
+
max_database_connections: int
|
40
|
+
available_memory_mb: float
|
41
|
+
memory_per_connection_mb: float
|
42
|
+
cpu_usage_percent: float
|
43
|
+
network_bandwidth_mbps: float
|
44
|
+
|
45
|
+
|
46
|
+
@dataclass
|
47
|
+
class ScalingDecision:
|
48
|
+
"""Result of scaling decision."""
|
49
|
+
|
50
|
+
action: str # "scale_up", "scale_down", "no_change"
|
51
|
+
current_size: int
|
52
|
+
target_size: int
|
53
|
+
reason: str
|
54
|
+
confidence: float # 0-1
|
55
|
+
|
56
|
+
|
57
|
+
class PoolSizeCalculator:
|
58
|
+
"""Calculates optimal pool size using queueing theory and heuristics."""
|
59
|
+
|
60
|
+
def __init__(
|
61
|
+
self, target_utilization: float = 0.75, max_wait_time_ms: float = 100.0
|
62
|
+
):
|
63
|
+
self.target_utilization = target_utilization
|
64
|
+
self.max_wait_time_ms = max_wait_time_ms
|
65
|
+
|
66
|
+
def calculate_optimal_size(
|
67
|
+
self,
|
68
|
+
metrics: PoolMetrics,
|
69
|
+
constraints: ResourceConstraints,
|
70
|
+
workload_forecast: Optional[Dict[str, Any]] = None,
|
71
|
+
) -> int:
|
72
|
+
"""Calculate optimal pool size based on multiple factors."""
|
73
|
+
|
74
|
+
# Method 1: Little's Law
|
75
|
+
littles_law_size = self._calculate_by_littles_law(metrics)
|
76
|
+
|
77
|
+
# Method 2: Utilization-based
|
78
|
+
utilization_size = self._calculate_by_utilization(metrics)
|
79
|
+
|
80
|
+
# Method 3: Queue depth based
|
81
|
+
queue_size = self._calculate_by_queue_depth(metrics)
|
82
|
+
|
83
|
+
# Method 4: Response time based
|
84
|
+
response_time_size = self._calculate_by_response_time(metrics)
|
85
|
+
|
86
|
+
# Method 5: Forecast-based (if available)
|
87
|
+
forecast_size = metrics.current_size
|
88
|
+
if workload_forecast:
|
89
|
+
forecast_size = self._calculate_by_forecast(workload_forecast)
|
90
|
+
|
91
|
+
# Combine methods with weights
|
92
|
+
weighted_size = (
|
93
|
+
littles_law_size * 0.25
|
94
|
+
+ utilization_size * 0.25
|
95
|
+
+ queue_size * 0.2
|
96
|
+
+ response_time_size * 0.2
|
97
|
+
+ forecast_size * 0.1
|
98
|
+
)
|
99
|
+
|
100
|
+
# Apply constraints
|
101
|
+
optimal_size = self._apply_constraints(
|
102
|
+
int(weighted_size), metrics.current_size, constraints
|
103
|
+
)
|
104
|
+
|
105
|
+
logger.debug(
|
106
|
+
f"Pool size calculation: Little's={littles_law_size}, "
|
107
|
+
f"Utilization={utilization_size}, Queue={queue_size}, "
|
108
|
+
f"Response={response_time_size}, Forecast={forecast_size}, "
|
109
|
+
f"Final={optimal_size}"
|
110
|
+
)
|
111
|
+
|
112
|
+
return optimal_size
|
113
|
+
|
114
|
+
def _calculate_by_littles_law(self, metrics: PoolMetrics) -> int:
|
115
|
+
"""Use Little's Law: L = λW (connections = arrival_rate * service_time)."""
|
116
|
+
if metrics.queries_per_second == 0 or metrics.avg_query_time_ms == 0:
|
117
|
+
return metrics.current_size
|
118
|
+
|
119
|
+
arrival_rate = metrics.queries_per_second
|
120
|
+
service_time_seconds = metrics.avg_query_time_ms / 1000
|
121
|
+
|
122
|
+
# Add buffer for variability
|
123
|
+
required_connections = arrival_rate * service_time_seconds * 1.2
|
124
|
+
|
125
|
+
return max(2, int(required_connections))
|
126
|
+
|
127
|
+
def _calculate_by_utilization(self, metrics: PoolMetrics) -> int:
|
128
|
+
"""Calculate based on target utilization."""
|
129
|
+
if metrics.utilization_rate == 0:
|
130
|
+
return metrics.current_size
|
131
|
+
|
132
|
+
# If utilization is too high, scale up
|
133
|
+
if metrics.utilization_rate > self.target_utilization + 0.1:
|
134
|
+
scale_factor = metrics.utilization_rate / self.target_utilization
|
135
|
+
return int(metrics.current_size * scale_factor)
|
136
|
+
|
137
|
+
# If utilization is too low, scale down
|
138
|
+
elif metrics.utilization_rate < self.target_utilization - 0.2:
|
139
|
+
scale_factor = metrics.utilization_rate / self.target_utilization
|
140
|
+
return max(2, int(metrics.current_size * scale_factor))
|
141
|
+
|
142
|
+
return metrics.current_size
|
143
|
+
|
144
|
+
def _calculate_by_queue_depth(self, metrics: PoolMetrics) -> int:
|
145
|
+
"""Calculate based on queue depth."""
|
146
|
+
if metrics.queue_depth == 0:
|
147
|
+
return metrics.current_size
|
148
|
+
|
149
|
+
# If queue is building up, we need more connections
|
150
|
+
if metrics.queue_depth > metrics.current_size * 0.5:
|
151
|
+
# Add connections proportional to queue depth
|
152
|
+
additional_needed = int(metrics.queue_depth / 2)
|
153
|
+
return metrics.current_size + additional_needed
|
154
|
+
|
155
|
+
# If no queue and low utilization, we might have too many
|
156
|
+
elif metrics.queue_depth == 0 and metrics.utilization_rate < 0.5:
|
157
|
+
return max(2, int(metrics.current_size * 0.8))
|
158
|
+
|
159
|
+
return metrics.current_size
|
160
|
+
|
161
|
+
def _calculate_by_response_time(self, metrics: PoolMetrics) -> int:
|
162
|
+
"""Calculate based on response time targets."""
|
163
|
+
if metrics.avg_wait_time_ms <= self.max_wait_time_ms:
|
164
|
+
# Meeting targets, check if we can reduce
|
165
|
+
if metrics.avg_wait_time_ms < self.max_wait_time_ms * 0.5:
|
166
|
+
return max(2, int(metrics.current_size * 0.9))
|
167
|
+
return metrics.current_size
|
168
|
+
else:
|
169
|
+
# Not meeting targets, scale up
|
170
|
+
scale_factor = metrics.avg_wait_time_ms / self.max_wait_time_ms
|
171
|
+
return int(metrics.current_size * scale_factor)
|
172
|
+
|
173
|
+
def _calculate_by_forecast(self, forecast: Dict[str, Any]) -> int:
|
174
|
+
"""Calculate based on workload forecast."""
|
175
|
+
return forecast.get("recommended_pool_size", 10)
|
176
|
+
|
177
|
+
def _apply_constraints(
|
178
|
+
self, calculated_size: int, current_size: int, constraints: ResourceConstraints
|
179
|
+
) -> int:
|
180
|
+
"""Apply resource constraints to calculated size."""
|
181
|
+
# Database connection limit (use 80% to leave room for other apps)
|
182
|
+
max_by_db = int(constraints.max_database_connections * 0.8)
|
183
|
+
|
184
|
+
# Memory limit
|
185
|
+
max_by_memory = int(
|
186
|
+
constraints.available_memory_mb / constraints.memory_per_connection_mb
|
187
|
+
)
|
188
|
+
|
189
|
+
# CPU constraint (don't scale up if CPU is high)
|
190
|
+
if constraints.cpu_usage_percent > 80 and calculated_size > current_size:
|
191
|
+
calculated_size = current_size
|
192
|
+
|
193
|
+
# Apply all constraints
|
194
|
+
final_size = min(calculated_size, max_by_db, max_by_memory)
|
195
|
+
|
196
|
+
# Ensure minimum size
|
197
|
+
return max(2, final_size)
|
198
|
+
|
199
|
+
|
200
|
+
class ScalingDecisionEngine:
|
201
|
+
"""Makes scaling decisions with hysteresis and dampening."""
|
202
|
+
|
203
|
+
def __init__(
|
204
|
+
self,
|
205
|
+
scale_up_threshold: float = 0.15,
|
206
|
+
scale_down_threshold: float = 0.20,
|
207
|
+
max_adjustment_step: int = 2,
|
208
|
+
cooldown_seconds: int = 60,
|
209
|
+
):
|
210
|
+
self.scale_up_threshold = scale_up_threshold
|
211
|
+
self.scale_down_threshold = scale_down_threshold
|
212
|
+
self.max_adjustment_step = max_adjustment_step
|
213
|
+
self.cooldown_seconds = cooldown_seconds
|
214
|
+
|
215
|
+
# History tracking
|
216
|
+
self.decision_history: deque = deque(maxlen=100)
|
217
|
+
self.last_scaling_time = datetime.min
|
218
|
+
self.size_history: deque = deque(maxlen=20)
|
219
|
+
|
220
|
+
def should_scale(
|
221
|
+
self,
|
222
|
+
current_size: int,
|
223
|
+
optimal_size: int,
|
224
|
+
metrics: PoolMetrics,
|
225
|
+
emergency: bool = False,
|
226
|
+
) -> ScalingDecision:
|
227
|
+
"""Decide whether to scale with hysteresis."""
|
228
|
+
|
229
|
+
# Check cooldown period
|
230
|
+
if not emergency and not self._cooldown_expired():
|
231
|
+
return ScalingDecision(
|
232
|
+
action="no_change",
|
233
|
+
current_size=current_size,
|
234
|
+
target_size=current_size,
|
235
|
+
reason="In cooldown period",
|
236
|
+
confidence=1.0,
|
237
|
+
)
|
238
|
+
|
239
|
+
# Calculate size difference
|
240
|
+
size_diff = optimal_size - current_size
|
241
|
+
size_diff_ratio = abs(size_diff) / current_size if current_size > 0 else 1.0
|
242
|
+
|
243
|
+
# Check for flapping
|
244
|
+
if self._is_flapping():
|
245
|
+
return ScalingDecision(
|
246
|
+
action="no_change",
|
247
|
+
current_size=current_size,
|
248
|
+
target_size=current_size,
|
249
|
+
reason="Flapping detected - stabilizing",
|
250
|
+
confidence=0.9,
|
251
|
+
)
|
252
|
+
|
253
|
+
# Emergency scaling (bypass normal thresholds)
|
254
|
+
if emergency:
|
255
|
+
if metrics.queue_depth > current_size:
|
256
|
+
target = min(current_size + self.max_adjustment_step * 2, optimal_size)
|
257
|
+
return self._create_scaling_decision(
|
258
|
+
"scale_up",
|
259
|
+
current_size,
|
260
|
+
target,
|
261
|
+
"Emergency: High queue depth",
|
262
|
+
0.95,
|
263
|
+
)
|
264
|
+
|
265
|
+
# Normal scaling logic
|
266
|
+
if size_diff > 0 and size_diff_ratio > self.scale_up_threshold:
|
267
|
+
# Scale up
|
268
|
+
target = self._calculate_gradual_target(current_size, optimal_size, "up")
|
269
|
+
reason = self._get_scale_up_reason(metrics)
|
270
|
+
confidence = self._calculate_confidence(metrics, size_diff_ratio)
|
271
|
+
|
272
|
+
return self._create_scaling_decision(
|
273
|
+
"scale_up", current_size, target, reason, confidence
|
274
|
+
)
|
275
|
+
|
276
|
+
elif size_diff < 0 and size_diff_ratio > self.scale_down_threshold:
|
277
|
+
# Scale down
|
278
|
+
target = self._calculate_gradual_target(current_size, optimal_size, "down")
|
279
|
+
reason = self._get_scale_down_reason(metrics)
|
280
|
+
confidence = self._calculate_confidence(metrics, size_diff_ratio)
|
281
|
+
|
282
|
+
return self._create_scaling_decision(
|
283
|
+
"scale_down", current_size, target, reason, confidence
|
284
|
+
)
|
285
|
+
|
286
|
+
else:
|
287
|
+
# No change needed
|
288
|
+
return ScalingDecision(
|
289
|
+
action="no_change",
|
290
|
+
current_size=current_size,
|
291
|
+
target_size=current_size,
|
292
|
+
reason="Within acceptable thresholds",
|
293
|
+
confidence=0.8,
|
294
|
+
)
|
295
|
+
|
296
|
+
def _cooldown_expired(self) -> bool:
|
297
|
+
"""Check if cooldown period has expired."""
|
298
|
+
return (
|
299
|
+
datetime.now() - self.last_scaling_time
|
300
|
+
).total_seconds() > self.cooldown_seconds
|
301
|
+
|
302
|
+
def _is_flapping(self) -> bool:
|
303
|
+
"""Detect if pool size is flapping."""
|
304
|
+
if len(self.decision_history) < 4:
|
305
|
+
return False
|
306
|
+
|
307
|
+
# Check if we've been alternating between scale up/down
|
308
|
+
recent_actions = [d.action for d in list(self.decision_history)[-4:]]
|
309
|
+
alternating = all(
|
310
|
+
recent_actions[i] != recent_actions[i + 1]
|
311
|
+
for i in range(len(recent_actions) - 1)
|
312
|
+
if recent_actions[i] != "no_change"
|
313
|
+
)
|
314
|
+
|
315
|
+
return alternating
|
316
|
+
|
317
|
+
def _calculate_gradual_target(
|
318
|
+
self, current: int, optimal: int, direction: str
|
319
|
+
) -> int:
|
320
|
+
"""Calculate gradual scaling target."""
|
321
|
+
if direction == "up":
|
322
|
+
# Don't scale up more than max_adjustment_step at once
|
323
|
+
max_target = current + self.max_adjustment_step
|
324
|
+
return min(optimal, max_target)
|
325
|
+
else:
|
326
|
+
# Don't scale down more than max_adjustment_step at once
|
327
|
+
min_target = current - self.max_adjustment_step
|
328
|
+
return max(optimal, min_target, 2) # Never go below 2
|
329
|
+
|
330
|
+
def _get_scale_up_reason(self, metrics: PoolMetrics) -> str:
|
331
|
+
"""Generate reason for scaling up."""
|
332
|
+
reasons = []
|
333
|
+
|
334
|
+
if metrics.utilization_rate > 0.85:
|
335
|
+
reasons.append(f"High utilization ({metrics.utilization_rate:.1%})")
|
336
|
+
if metrics.queue_depth > 0:
|
337
|
+
reasons.append(f"Queue depth: {metrics.queue_depth}")
|
338
|
+
if metrics.avg_wait_time_ms > 50:
|
339
|
+
reasons.append(f"Wait time: {metrics.avg_wait_time_ms:.0f}ms")
|
340
|
+
|
341
|
+
return " | ".join(reasons) if reasons else "Optimal size increased"
|
342
|
+
|
343
|
+
def _get_scale_down_reason(self, metrics: PoolMetrics) -> str:
|
344
|
+
"""Generate reason for scaling down."""
|
345
|
+
reasons = []
|
346
|
+
|
347
|
+
if metrics.utilization_rate < 0.5:
|
348
|
+
reasons.append(f"Low utilization ({metrics.utilization_rate:.1%})")
|
349
|
+
if metrics.idle_connections > metrics.active_connections:
|
350
|
+
reasons.append(f"Idle connections: {metrics.idle_connections}")
|
351
|
+
|
352
|
+
return " | ".join(reasons) if reasons else "Optimal size decreased"
|
353
|
+
|
354
|
+
def _calculate_confidence(
|
355
|
+
self, metrics: PoolMetrics, size_diff_ratio: float
|
356
|
+
) -> float:
|
357
|
+
"""Calculate confidence in scaling decision."""
|
358
|
+
confidence = 0.5
|
359
|
+
|
360
|
+
# Higher confidence for extreme situations
|
361
|
+
if metrics.utilization_rate > 0.9 or metrics.utilization_rate < 0.3:
|
362
|
+
confidence += 0.2
|
363
|
+
|
364
|
+
if metrics.queue_depth > 5:
|
365
|
+
confidence += 0.15
|
366
|
+
|
367
|
+
if size_diff_ratio > 0.3:
|
368
|
+
confidence += 0.15
|
369
|
+
|
370
|
+
# Lower confidence if health score is low
|
371
|
+
if metrics.health_score < 70:
|
372
|
+
confidence *= 0.8
|
373
|
+
|
374
|
+
return min(confidence, 0.95)
|
375
|
+
|
376
|
+
def _create_scaling_decision(
|
377
|
+
self,
|
378
|
+
action: str,
|
379
|
+
current_size: int,
|
380
|
+
target_size: int,
|
381
|
+
reason: str,
|
382
|
+
confidence: float,
|
383
|
+
) -> ScalingDecision:
|
384
|
+
"""Create and record scaling decision."""
|
385
|
+
decision = ScalingDecision(
|
386
|
+
action=action,
|
387
|
+
current_size=current_size,
|
388
|
+
target_size=target_size,
|
389
|
+
reason=reason,
|
390
|
+
confidence=confidence,
|
391
|
+
)
|
392
|
+
|
393
|
+
# Record decision
|
394
|
+
self.decision_history.append(decision)
|
395
|
+
self.size_history.append(target_size)
|
396
|
+
|
397
|
+
if action != "no_change":
|
398
|
+
self.last_scaling_time = datetime.now()
|
399
|
+
|
400
|
+
return decision
|
401
|
+
|
402
|
+
|
403
|
+
class ResourceMonitor:
|
404
|
+
"""Monitors system resources for constraint enforcement."""
|
405
|
+
|
406
|
+
def __init__(self):
|
407
|
+
self.process = psutil.Process()
|
408
|
+
self.last_check_time = datetime.min
|
409
|
+
self.check_interval = timedelta(seconds=10)
|
410
|
+
self.cached_constraints: Optional[ResourceConstraints] = None
|
411
|
+
|
412
|
+
async def get_resource_constraints(
|
413
|
+
self, db_connection_info: Dict[str, Any]
|
414
|
+
) -> ResourceConstraints:
|
415
|
+
"""Get current resource constraints."""
|
416
|
+
# Use cache if recent
|
417
|
+
if (
|
418
|
+
self.cached_constraints
|
419
|
+
and datetime.now() - self.last_check_time < self.check_interval
|
420
|
+
):
|
421
|
+
return self.cached_constraints
|
422
|
+
|
423
|
+
# Get system memory
|
424
|
+
memory = psutil.virtual_memory()
|
425
|
+
available_memory_mb = memory.available / (1024 * 1024)
|
426
|
+
|
427
|
+
# Get CPU usage
|
428
|
+
cpu_percent = psutil.cpu_percent(interval=0.1)
|
429
|
+
|
430
|
+
# Estimate network bandwidth (simplified)
|
431
|
+
network_bandwidth_mbps = 100.0 # Default 100 Mbps
|
432
|
+
|
433
|
+
# Get database connection limit
|
434
|
+
max_db_connections = await self._get_database_limit(db_connection_info)
|
435
|
+
|
436
|
+
# Estimate memory per connection
|
437
|
+
memory_per_connection = self._estimate_connection_memory()
|
438
|
+
|
439
|
+
constraints = ResourceConstraints(
|
440
|
+
max_database_connections=max_db_connections,
|
441
|
+
available_memory_mb=available_memory_mb,
|
442
|
+
memory_per_connection_mb=memory_per_connection,
|
443
|
+
cpu_usage_percent=cpu_percent,
|
444
|
+
network_bandwidth_mbps=network_bandwidth_mbps,
|
445
|
+
)
|
446
|
+
|
447
|
+
self.cached_constraints = constraints
|
448
|
+
self.last_check_time = datetime.now()
|
449
|
+
|
450
|
+
return constraints
|
451
|
+
|
452
|
+
async def _get_database_limit(self, db_info: Dict[str, Any]) -> int:
|
453
|
+
"""Get database connection limit."""
|
454
|
+
# This would query the database for max_connections
|
455
|
+
# For now, use reasonable defaults
|
456
|
+
db_type = db_info.get("type", "postgresql")
|
457
|
+
|
458
|
+
defaults = {"postgresql": 100, "mysql": 150, "sqlite": 10}
|
459
|
+
|
460
|
+
return defaults.get(db_type, 50)
|
461
|
+
|
462
|
+
def _estimate_connection_memory(self) -> float:
|
463
|
+
"""Estimate memory usage per connection in MB."""
|
464
|
+
# This is a rough estimate
|
465
|
+
# Real implementation would measure actual usage
|
466
|
+
return 10.0 # 10 MB per connection
|
467
|
+
|
468
|
+
|
469
|
+
class AdaptivePoolController:
|
470
|
+
"""Main controller for adaptive pool sizing."""
|
471
|
+
|
472
|
+
def __init__(
|
473
|
+
self,
|
474
|
+
min_size: int = 2,
|
475
|
+
max_size: int = 50,
|
476
|
+
target_utilization: float = 0.75,
|
477
|
+
adjustment_interval_seconds: int = 30,
|
478
|
+
):
|
479
|
+
self.min_size = min_size
|
480
|
+
self.max_size = max_size
|
481
|
+
self.target_utilization = target_utilization
|
482
|
+
self.adjustment_interval_seconds = adjustment_interval_seconds
|
483
|
+
|
484
|
+
# Components
|
485
|
+
self.calculator = PoolSizeCalculator(target_utilization=target_utilization)
|
486
|
+
self.decision_engine = ScalingDecisionEngine()
|
487
|
+
self.resource_monitor = ResourceMonitor()
|
488
|
+
|
489
|
+
# State
|
490
|
+
self.running = False
|
491
|
+
self.adjustment_task: Optional[asyncio.Task] = None
|
492
|
+
self.metrics_history: deque = deque(maxlen=60) # 30 minutes of history
|
493
|
+
|
494
|
+
async def start(self, pool_ref: Any, pattern_tracker: Optional[Any] = None):
|
495
|
+
"""Start the adaptive controller."""
|
496
|
+
self.pool_ref = pool_ref
|
497
|
+
self.pattern_tracker = pattern_tracker
|
498
|
+
self.running = True
|
499
|
+
|
500
|
+
# Start adjustment loop
|
501
|
+
self.adjustment_task = asyncio.create_task(self._adjustment_loop())
|
502
|
+
|
503
|
+
logger.info("Adaptive pool controller started")
|
504
|
+
|
505
|
+
async def stop(self):
|
506
|
+
"""Stop the adaptive controller."""
|
507
|
+
self.running = False
|
508
|
+
|
509
|
+
if self.adjustment_task:
|
510
|
+
self.adjustment_task.cancel()
|
511
|
+
try:
|
512
|
+
await self.adjustment_task
|
513
|
+
except asyncio.CancelledError:
|
514
|
+
pass
|
515
|
+
|
516
|
+
logger.info("Adaptive pool controller stopped")
|
517
|
+
|
518
|
+
async def _adjustment_loop(self):
|
519
|
+
"""Main loop for pool size adjustments."""
|
520
|
+
while self.running:
|
521
|
+
try:
|
522
|
+
# Collect metrics
|
523
|
+
metrics = await self._collect_metrics()
|
524
|
+
self.metrics_history.append((datetime.now(), metrics))
|
525
|
+
|
526
|
+
# Get resource constraints
|
527
|
+
constraints = await self.resource_monitor.get_resource_constraints(
|
528
|
+
self.pool_ref.db_config
|
529
|
+
)
|
530
|
+
|
531
|
+
# Get workload forecast if available
|
532
|
+
forecast = None
|
533
|
+
if self.pattern_tracker:
|
534
|
+
forecast = self.pattern_tracker.get_workload_forecast(
|
535
|
+
horizon_minutes=self.adjustment_interval_seconds // 60 + 5
|
536
|
+
)
|
537
|
+
|
538
|
+
# Calculate optimal size
|
539
|
+
optimal_size = self.calculator.calculate_optimal_size(
|
540
|
+
metrics, constraints, forecast
|
541
|
+
)
|
542
|
+
|
543
|
+
# Make scaling decision
|
544
|
+
decision = self.decision_engine.should_scale(
|
545
|
+
metrics.current_size,
|
546
|
+
optimal_size,
|
547
|
+
metrics,
|
548
|
+
emergency=self._is_emergency(metrics),
|
549
|
+
)
|
550
|
+
|
551
|
+
# Execute scaling if needed
|
552
|
+
if decision.action != "no_change":
|
553
|
+
await self._execute_scaling(decision)
|
554
|
+
|
555
|
+
# Log metrics
|
556
|
+
if decision.action != "no_change" or metrics.current_size % 10 == 0:
|
557
|
+
logger.info(
|
558
|
+
f"Pool metrics: size={metrics.current_size}, "
|
559
|
+
f"utilization={metrics.utilization_rate:.1%}, "
|
560
|
+
f"queue={metrics.queue_depth}, "
|
561
|
+
f"action={decision.action}, "
|
562
|
+
f"target={decision.target_size}"
|
563
|
+
)
|
564
|
+
|
565
|
+
except Exception as e:
|
566
|
+
logger.error(f"Error in adaptive pool adjustment: {e}")
|
567
|
+
|
568
|
+
# Wait for next adjustment
|
569
|
+
await asyncio.sleep(self.adjustment_interval_seconds)
|
570
|
+
|
571
|
+
async def _collect_metrics(self) -> PoolMetrics:
|
572
|
+
"""Collect current pool metrics."""
|
573
|
+
pool_stats = await self.pool_ref.get_pool_statistics()
|
574
|
+
|
575
|
+
return PoolMetrics(
|
576
|
+
current_size=pool_stats["total_connections"],
|
577
|
+
active_connections=pool_stats["active_connections"],
|
578
|
+
idle_connections=pool_stats["idle_connections"],
|
579
|
+
queue_depth=pool_stats.get("queue_depth", 0),
|
580
|
+
avg_wait_time_ms=pool_stats.get("avg_acquisition_time_ms", 0),
|
581
|
+
avg_query_time_ms=pool_stats.get("avg_query_time_ms", 0),
|
582
|
+
queries_per_second=pool_stats.get("queries_per_second", 0),
|
583
|
+
utilization_rate=pool_stats.get("utilization_rate", 0),
|
584
|
+
health_score=pool_stats.get("avg_health_score", 100),
|
585
|
+
)
|
586
|
+
|
587
|
+
def _is_emergency(self, metrics: PoolMetrics) -> bool:
|
588
|
+
"""Check if emergency scaling is needed."""
|
589
|
+
return (
|
590
|
+
metrics.queue_depth > metrics.current_size * 2
|
591
|
+
or metrics.avg_wait_time_ms > 1000
|
592
|
+
or metrics.utilization_rate > 0.95
|
593
|
+
)
|
594
|
+
|
595
|
+
async def _execute_scaling(self, decision: ScalingDecision):
|
596
|
+
"""Execute the scaling decision."""
|
597
|
+
logger.info(
|
598
|
+
f"Executing pool scaling: {decision.action} from "
|
599
|
+
f"{decision.current_size} to {decision.target_size} - {decision.reason}"
|
600
|
+
)
|
601
|
+
|
602
|
+
try:
|
603
|
+
# Apply bounds
|
604
|
+
target_size = max(self.min_size, min(self.max_size, decision.target_size))
|
605
|
+
|
606
|
+
# Call pool's adjustment method
|
607
|
+
success = await self.pool_ref.adjust_pool_size(target_size)
|
608
|
+
|
609
|
+
if success:
|
610
|
+
logger.info(f"Pool size adjusted to {target_size}")
|
611
|
+
else:
|
612
|
+
logger.warning(f"Failed to adjust pool size to {target_size}")
|
613
|
+
|
614
|
+
except Exception as e:
|
615
|
+
logger.error(f"Error executing pool scaling: {e}")
|
616
|
+
|
617
|
+
def get_adjustment_history(self) -> List[Dict[str, Any]]:
|
618
|
+
"""Get recent adjustment history."""
|
619
|
+
return [
|
620
|
+
{
|
621
|
+
"timestamp": self.decision_engine.last_scaling_time.isoformat(),
|
622
|
+
"action": decision.action,
|
623
|
+
"from_size": decision.current_size,
|
624
|
+
"to_size": decision.target_size,
|
625
|
+
"reason": decision.reason,
|
626
|
+
"confidence": decision.confidence,
|
627
|
+
}
|
628
|
+
for decision in self.decision_engine.decision_history
|
629
|
+
if decision.action != "no_change"
|
630
|
+
]
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Machine learning components for intelligent SDK features."""
|