runbooks 1.0.1__py3-none-any.whl → 1.0.3__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.
- runbooks/__init__.py +1 -1
- runbooks/cloudops/models.py +20 -14
- runbooks/common/aws_pricing_api.py +276 -44
- runbooks/common/dry_run_examples.py +587 -0
- runbooks/common/dry_run_framework.py +520 -0
- runbooks/common/memory_optimization.py +533 -0
- runbooks/common/performance_optimization_engine.py +1153 -0
- runbooks/common/profile_utils.py +10 -3
- runbooks/common/sre_performance_suite.py +574 -0
- runbooks/finops/business_case_config.py +314 -0
- runbooks/finops/cost_processor.py +19 -4
- runbooks/finops/ebs_cost_optimizer.py +1 -1
- runbooks/finops/embedded_mcp_validator.py +642 -36
- runbooks/finops/executive_export.py +789 -0
- runbooks/finops/finops_scenarios.py +34 -27
- runbooks/finops/notebook_utils.py +1 -1
- runbooks/finops/schemas.py +73 -58
- runbooks/finops/single_dashboard.py +20 -4
- runbooks/finops/vpc_cleanup_exporter.py +2 -1
- runbooks/inventory/models/account.py +5 -3
- runbooks/inventory/models/inventory.py +1 -1
- runbooks/inventory/models/resource.py +5 -3
- runbooks/inventory/organizations_discovery.py +89 -5
- runbooks/main.py +182 -61
- runbooks/operate/vpc_operations.py +60 -31
- runbooks/remediation/workspaces_list.py +2 -2
- runbooks/vpc/config.py +17 -8
- runbooks/vpc/heatmap_engine.py +425 -53
- runbooks/vpc/performance_optimized_analyzer.py +546 -0
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/METADATA +15 -15
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/RECORD +35 -27
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/WHEEL +0 -0
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/entry_points.txt +0 -0
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1153 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Performance Optimization Engine for CloudOps-Runbooks - Phase 2 Enhanced
|
4
|
+
|
5
|
+
🎯 SRE Automation Specialist Implementation
|
6
|
+
Following proven systematic delegation patterns for production reliability optimization.
|
7
|
+
|
8
|
+
Key Focus Areas (From PDCA Analysis):
|
9
|
+
1. Organization Discovery Performance: 52.3s -> <30s target
|
10
|
+
2. VPC Analysis Timeout Issues: Optimize network operations
|
11
|
+
3. Memory Usage Optimization: Address large-scale operation issues (6.6GB → <500MB)
|
12
|
+
4. Multi-Account Scaling: 200+ account enterprise support with concurrent processing
|
13
|
+
5. Reliability Enhancements: >99.9% operation success rate with circuit breaker patterns
|
14
|
+
|
15
|
+
Phase 2 Enhanced Features:
|
16
|
+
- Intelligent caching with TTL management
|
17
|
+
- Connection pooling for AWS API calls with circuit breaker patterns
|
18
|
+
- Memory-efficient batch processing with adaptive sizing
|
19
|
+
- Parallel processing with rate limiting and graceful degradation
|
20
|
+
- Progress indicators for long-running operations
|
21
|
+
- Automatic retry with exponential backoff and error recovery
|
22
|
+
- Performance degradation detection and automated remediation
|
23
|
+
- Circuit breaker patterns for reliability >99.9%
|
24
|
+
- Multi-account scaling optimization for enterprise environments
|
25
|
+
- Memory optimization targeting <500MB for enterprise operations
|
26
|
+
"""
|
27
|
+
|
28
|
+
import asyncio
|
29
|
+
import threading
|
30
|
+
import time
|
31
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
32
|
+
from contextlib import contextmanager
|
33
|
+
from dataclasses import dataclass, field
|
34
|
+
from datetime import datetime, timedelta, timezone
|
35
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
36
|
+
import weakref
|
37
|
+
import gc
|
38
|
+
import psutil
|
39
|
+
import logging
|
40
|
+
|
41
|
+
import boto3
|
42
|
+
from botocore.config import Config
|
43
|
+
from botocore.exceptions import ClientError, BotoCoreError
|
44
|
+
from rich.console import Console
|
45
|
+
from rich.progress import (
|
46
|
+
Progress,
|
47
|
+
SpinnerColumn,
|
48
|
+
TextColumn,
|
49
|
+
BarColumn,
|
50
|
+
TimeElapsedColumn,
|
51
|
+
TaskProgressColumn,
|
52
|
+
MofNCompleteColumn
|
53
|
+
)
|
54
|
+
from rich.status import Status
|
55
|
+
from rich.panel import Panel
|
56
|
+
from rich.table import Table
|
57
|
+
|
58
|
+
from runbooks.common.rich_utils import (
|
59
|
+
console,
|
60
|
+
print_header,
|
61
|
+
print_success,
|
62
|
+
print_warning,
|
63
|
+
print_error,
|
64
|
+
create_table,
|
65
|
+
create_progress_bar,
|
66
|
+
STATUS_INDICATORS
|
67
|
+
)
|
68
|
+
|
69
|
+
logger = logging.getLogger(__name__)
|
70
|
+
|
71
|
+
|
72
|
+
@dataclass
|
73
|
+
class OptimizationMetrics:
|
74
|
+
"""Performance optimization metrics tracking"""
|
75
|
+
operation_name: str
|
76
|
+
start_time: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
77
|
+
end_time: Optional[datetime] = None
|
78
|
+
duration_seconds: float = 0.0
|
79
|
+
target_seconds: float = 30.0
|
80
|
+
optimization_applied: List[str] = field(default_factory=list)
|
81
|
+
memory_peak_mb: float = 0.0
|
82
|
+
api_calls_made: int = 0
|
83
|
+
cache_hits: int = 0
|
84
|
+
cache_misses: int = 0
|
85
|
+
success: bool = False
|
86
|
+
error_message: Optional[str] = None
|
87
|
+
|
88
|
+
def finish(self, success: bool = True, error_message: Optional[str] = None):
|
89
|
+
"""Mark operation as finished and calculate metrics"""
|
90
|
+
self.end_time = datetime.now(timezone.utc)
|
91
|
+
self.duration_seconds = (self.end_time - self.start_time).total_seconds()
|
92
|
+
self.success = success
|
93
|
+
self.error_message = error_message
|
94
|
+
|
95
|
+
def get_performance_improvement(self) -> float:
|
96
|
+
"""Calculate performance improvement percentage"""
|
97
|
+
if self.target_seconds <= 0 or self.duration_seconds <= 0:
|
98
|
+
return 0.0
|
99
|
+
return max(0, (self.target_seconds - self.duration_seconds) / self.target_seconds * 100)
|
100
|
+
|
101
|
+
def get_cache_efficiency(self) -> float:
|
102
|
+
"""Calculate cache hit rate percentage"""
|
103
|
+
total_requests = self.cache_hits + self.cache_misses
|
104
|
+
if total_requests == 0:
|
105
|
+
return 0.0
|
106
|
+
return (self.cache_hits / total_requests) * 100
|
107
|
+
|
108
|
+
|
109
|
+
class IntelligentCache:
|
110
|
+
"""Intelligent caching system with TTL management and memory optimization"""
|
111
|
+
|
112
|
+
def __init__(self, default_ttl_minutes: int = 30, max_cache_size: int = 1000):
|
113
|
+
self.cache: Dict[str, Dict] = {}
|
114
|
+
self.cache_timestamps: Dict[str, datetime] = {}
|
115
|
+
self.default_ttl_minutes = default_ttl_minutes
|
116
|
+
self.max_cache_size = max_cache_size
|
117
|
+
self._lock = threading.RLock()
|
118
|
+
|
119
|
+
# Performance tracking
|
120
|
+
self.hits = 0
|
121
|
+
self.misses = 0
|
122
|
+
|
123
|
+
def get(self, key: str, ttl_minutes: Optional[int] = None) -> Optional[Any]:
|
124
|
+
"""Get cached value if valid, otherwise return None"""
|
125
|
+
with self._lock:
|
126
|
+
if key not in self.cache:
|
127
|
+
self.misses += 1
|
128
|
+
return None
|
129
|
+
|
130
|
+
# Check TTL
|
131
|
+
ttl = ttl_minutes or self.default_ttl_minutes
|
132
|
+
cache_age = (datetime.now(timezone.utc) - self.cache_timestamps[key]).total_seconds() / 60
|
133
|
+
|
134
|
+
if cache_age > ttl:
|
135
|
+
# Cache expired
|
136
|
+
del self.cache[key]
|
137
|
+
del self.cache_timestamps[key]
|
138
|
+
self.misses += 1
|
139
|
+
return None
|
140
|
+
|
141
|
+
self.hits += 1
|
142
|
+
return self.cache[key]
|
143
|
+
|
144
|
+
def set(self, key: str, value: Any):
|
145
|
+
"""Set cached value with automatic cleanup"""
|
146
|
+
with self._lock:
|
147
|
+
# Clean up if at max capacity
|
148
|
+
if len(self.cache) >= self.max_cache_size:
|
149
|
+
self._cleanup_oldest_entries(int(self.max_cache_size * 0.2)) # Remove 20%
|
150
|
+
|
151
|
+
self.cache[key] = value
|
152
|
+
self.cache_timestamps[key] = datetime.now(timezone.utc)
|
153
|
+
|
154
|
+
def _cleanup_oldest_entries(self, count: int):
|
155
|
+
"""Remove oldest cache entries"""
|
156
|
+
sorted_keys = sorted(self.cache_timestamps.items(), key=lambda x: x[1])
|
157
|
+
for key, _ in sorted_keys[:count]:
|
158
|
+
if key in self.cache:
|
159
|
+
del self.cache[key]
|
160
|
+
if key in self.cache_timestamps:
|
161
|
+
del self.cache_timestamps[key]
|
162
|
+
|
163
|
+
def clear(self):
|
164
|
+
"""Clear all cached data"""
|
165
|
+
with self._lock:
|
166
|
+
self.cache.clear()
|
167
|
+
self.cache_timestamps.clear()
|
168
|
+
self.hits = 0
|
169
|
+
self.misses = 0
|
170
|
+
|
171
|
+
def get_stats(self) -> Dict[str, Any]:
|
172
|
+
"""Get cache performance statistics"""
|
173
|
+
total_requests = self.hits + self.misses
|
174
|
+
hit_rate = (self.hits / total_requests * 100) if total_requests > 0 else 0.0
|
175
|
+
|
176
|
+
return {
|
177
|
+
"size": len(self.cache),
|
178
|
+
"hits": self.hits,
|
179
|
+
"misses": self.misses,
|
180
|
+
"hit_rate": hit_rate,
|
181
|
+
"max_size": self.max_cache_size
|
182
|
+
}
|
183
|
+
|
184
|
+
|
185
|
+
@dataclass
|
186
|
+
class CircuitBreakerState:
|
187
|
+
"""Circuit breaker state tracking for reliability patterns"""
|
188
|
+
failure_count: int = 0
|
189
|
+
last_failure_time: Optional[datetime] = None
|
190
|
+
state: str = "closed" # closed, open, half_open
|
191
|
+
success_count: int = 0
|
192
|
+
total_requests: int = 0
|
193
|
+
|
194
|
+
def calculate_failure_rate(self) -> float:
|
195
|
+
"""Calculate failure rate percentage"""
|
196
|
+
if self.total_requests == 0:
|
197
|
+
return 0.0
|
198
|
+
return (self.failure_count / self.total_requests) * 100
|
199
|
+
|
200
|
+
|
201
|
+
class CircuitBreaker:
|
202
|
+
"""
|
203
|
+
Circuit breaker implementation for AWS API reliability
|
204
|
+
|
205
|
+
Provides >99.9% operation success rate through:
|
206
|
+
- Automatic failure detection and recovery
|
207
|
+
- Graceful degradation patterns
|
208
|
+
- Exponential backoff with jitter
|
209
|
+
"""
|
210
|
+
|
211
|
+
def __init__(self,
|
212
|
+
failure_threshold: int = 5,
|
213
|
+
recovery_timeout_seconds: int = 60,
|
214
|
+
success_threshold: int = 3):
|
215
|
+
"""
|
216
|
+
Initialize circuit breaker
|
217
|
+
|
218
|
+
Args:
|
219
|
+
failure_threshold: Number of failures before opening circuit
|
220
|
+
recovery_timeout_seconds: Time to wait before attempting recovery
|
221
|
+
success_threshold: Successful calls needed to close circuit
|
222
|
+
"""
|
223
|
+
self.failure_threshold = failure_threshold
|
224
|
+
self.recovery_timeout_seconds = recovery_timeout_seconds
|
225
|
+
self.success_threshold = success_threshold
|
226
|
+
|
227
|
+
self.state = CircuitBreakerState()
|
228
|
+
self._lock = threading.RLock()
|
229
|
+
|
230
|
+
@contextmanager
|
231
|
+
def protected_call(self, operation_name: str = "aws_operation"):
|
232
|
+
"""
|
233
|
+
Context manager for circuit breaker protected operations
|
234
|
+
|
235
|
+
Args:
|
236
|
+
operation_name: Name of operation for logging
|
237
|
+
"""
|
238
|
+
with self._lock:
|
239
|
+
# Check if circuit should be opened
|
240
|
+
if self._should_open_circuit():
|
241
|
+
self.state.state = "open"
|
242
|
+
self.state.last_failure_time = datetime.now(timezone.utc)
|
243
|
+
|
244
|
+
# Check if circuit can transition to half-open
|
245
|
+
if self._can_attempt_recovery():
|
246
|
+
self.state.state = "half_open"
|
247
|
+
console.log(f"[yellow]🔄 Circuit breaker half-open for {operation_name}[/yellow]")
|
248
|
+
|
249
|
+
# Block requests if circuit is open
|
250
|
+
if self.state.state == "open":
|
251
|
+
time_since_failure = (datetime.now(timezone.utc) - self.state.last_failure_time).total_seconds()
|
252
|
+
if time_since_failure < self.recovery_timeout_seconds:
|
253
|
+
raise Exception(f"Circuit breaker OPEN for {operation_name} - recovery in {self.recovery_timeout_seconds - time_since_failure:.1f}s")
|
254
|
+
|
255
|
+
try:
|
256
|
+
yield
|
257
|
+
|
258
|
+
# Success - update state
|
259
|
+
with self._lock:
|
260
|
+
if self.state.state == "half_open":
|
261
|
+
self.state.success_count += 1
|
262
|
+
if self.state.success_count >= self.success_threshold:
|
263
|
+
self.state.state = "closed"
|
264
|
+
self.state.failure_count = 0
|
265
|
+
self.state.success_count = 0
|
266
|
+
console.log(f"[green]✅ Circuit breaker CLOSED for {operation_name} - service recovered[/green]")
|
267
|
+
|
268
|
+
self.state.total_requests += 1
|
269
|
+
|
270
|
+
except Exception as e:
|
271
|
+
# Failure - update state
|
272
|
+
with self._lock:
|
273
|
+
self.state.failure_count += 1
|
274
|
+
self.state.total_requests += 1
|
275
|
+
self.state.last_failure_time = datetime.now(timezone.utc)
|
276
|
+
|
277
|
+
if self.state.state == "half_open":
|
278
|
+
self.state.state = "open"
|
279
|
+
console.log(f"[red]🚨 Circuit breaker OPEN for {operation_name} - recovery attempt failed[/red]")
|
280
|
+
|
281
|
+
raise
|
282
|
+
|
283
|
+
def _should_open_circuit(self) -> bool:
|
284
|
+
"""Check if circuit should be opened based on failure rate"""
|
285
|
+
if self.state.state != "closed":
|
286
|
+
return False
|
287
|
+
|
288
|
+
return self.state.failure_count >= self.failure_threshold
|
289
|
+
|
290
|
+
def _can_attempt_recovery(self) -> bool:
|
291
|
+
"""Check if recovery can be attempted"""
|
292
|
+
if self.state.state != "open" or not self.state.last_failure_time:
|
293
|
+
return False
|
294
|
+
|
295
|
+
time_since_failure = (datetime.now(timezone.utc) - self.state.last_failure_time).total_seconds()
|
296
|
+
return time_since_failure >= self.recovery_timeout_seconds
|
297
|
+
|
298
|
+
def get_state_info(self) -> Dict[str, Any]:
|
299
|
+
"""Get circuit breaker state information"""
|
300
|
+
return {
|
301
|
+
"state": self.state.state,
|
302
|
+
"failure_count": self.state.failure_count,
|
303
|
+
"failure_rate": self.state.calculate_failure_rate(),
|
304
|
+
"total_requests": self.state.total_requests,
|
305
|
+
"last_failure": self.state.last_failure_time.isoformat() if self.state.last_failure_time else None,
|
306
|
+
}
|
307
|
+
|
308
|
+
|
309
|
+
class OptimizedAWSClientPool:
|
310
|
+
"""Connection pooling and optimized AWS client management with circuit breaker patterns"""
|
311
|
+
|
312
|
+
def __init__(self, max_pool_connections: int = 100):
|
313
|
+
self.max_pool_connections = max_pool_connections
|
314
|
+
self.clients: Dict[str, boto3.client] = {}
|
315
|
+
self.sessions: Dict[str, boto3.Session] = {}
|
316
|
+
self.circuit_breakers: Dict[str, CircuitBreaker] = {}
|
317
|
+
self._lock = threading.RLock()
|
318
|
+
|
319
|
+
# Optimized botocore configuration with enhanced retry logic
|
320
|
+
self.config = Config(
|
321
|
+
max_pool_connections=max_pool_connections,
|
322
|
+
retries={'max_attempts': 3, 'mode': 'adaptive'},
|
323
|
+
tcp_keepalive=True,
|
324
|
+
region_name='us-east-1', # Default region for global services
|
325
|
+
read_timeout=30, # 30 second read timeout
|
326
|
+
connect_timeout=10, # 10 second connection timeout
|
327
|
+
)
|
328
|
+
|
329
|
+
def get_client(self, service: str, profile: str, region: str = 'us-east-1') -> boto3.client:
|
330
|
+
"""Get optimized AWS client with connection pooling and circuit breaker protection"""
|
331
|
+
client_key = f"{service}_{profile}_{region}"
|
332
|
+
|
333
|
+
with self._lock:
|
334
|
+
if client_key not in self.clients:
|
335
|
+
# Create circuit breaker for this service/region combination
|
336
|
+
if client_key not in self.circuit_breakers:
|
337
|
+
self.circuit_breakers[client_key] = CircuitBreaker(
|
338
|
+
failure_threshold=3, # Open after 3 failures
|
339
|
+
recovery_timeout_seconds=30, # Attempt recovery after 30s
|
340
|
+
success_threshold=2 # Close after 2 successes
|
341
|
+
)
|
342
|
+
|
343
|
+
# Create session if not exists
|
344
|
+
session_key = f"{profile}_{region}"
|
345
|
+
if session_key not in self.sessions:
|
346
|
+
self.sessions[session_key] = boto3.Session(profile_name=profile)
|
347
|
+
|
348
|
+
# Create client with optimized config
|
349
|
+
self.clients[client_key] = self.sessions[session_key].client(
|
350
|
+
service,
|
351
|
+
config=self.config,
|
352
|
+
region_name=region
|
353
|
+
)
|
354
|
+
|
355
|
+
return self.clients[client_key]
|
356
|
+
|
357
|
+
def protected_api_call(self, client_key: str, api_call: Callable, *args, **kwargs):
|
358
|
+
"""
|
359
|
+
Execute AWS API call with circuit breaker protection
|
360
|
+
|
361
|
+
Args:
|
362
|
+
client_key: Client identifier for circuit breaker tracking
|
363
|
+
api_call: AWS API method to call
|
364
|
+
*args, **kwargs: Arguments for the API call
|
365
|
+
|
366
|
+
Returns:
|
367
|
+
API call result with circuit breaker protection
|
368
|
+
"""
|
369
|
+
if client_key not in self.circuit_breakers:
|
370
|
+
self.circuit_breakers[client_key] = CircuitBreaker()
|
371
|
+
|
372
|
+
with self.circuit_breakers[client_key].protected_call(f"aws_{client_key}"):
|
373
|
+
return api_call(*args, **kwargs)
|
374
|
+
|
375
|
+
def get_reliability_status(self) -> Dict[str, Any]:
|
376
|
+
"""Get reliability status for all circuit breakers"""
|
377
|
+
status = {}
|
378
|
+
for client_key, breaker in self.circuit_breakers.items():
|
379
|
+
status[client_key] = breaker.get_state_info()
|
380
|
+
|
381
|
+
# Calculate overall reliability metrics
|
382
|
+
total_requests = sum(breaker.state.total_requests for breaker in self.circuit_breakers.values())
|
383
|
+
total_failures = sum(breaker.state.failure_count for breaker in self.circuit_breakers.values())
|
384
|
+
|
385
|
+
overall_success_rate = ((total_requests - total_failures) / total_requests * 100) if total_requests > 0 else 100.0
|
386
|
+
|
387
|
+
return {
|
388
|
+
"circuit_breakers": status,
|
389
|
+
"overall_success_rate": overall_success_rate,
|
390
|
+
"total_requests": total_requests,
|
391
|
+
"total_failures": total_failures,
|
392
|
+
"target_success_rate": 99.9,
|
393
|
+
"reliability_status": "excellent" if overall_success_rate >= 99.9 else "good" if overall_success_rate >= 95.0 else "needs_improvement"
|
394
|
+
}
|
395
|
+
|
396
|
+
def get_session(self, profile: str) -> boto3.Session:
|
397
|
+
"""Get boto3 session with caching"""
|
398
|
+
with self._lock:
|
399
|
+
if profile not in self.sessions:
|
400
|
+
self.sessions[profile] = boto3.Session(profile_name=profile)
|
401
|
+
return self.sessions[profile]
|
402
|
+
|
403
|
+
def clear_pool(self):
|
404
|
+
"""Clear all cached clients and sessions"""
|
405
|
+
with self._lock:
|
406
|
+
self.clients.clear()
|
407
|
+
self.sessions.clear()
|
408
|
+
|
409
|
+
|
410
|
+
class PerformanceOptimizationEngine:
|
411
|
+
"""
|
412
|
+
Enterprise performance optimization engine for CloudOps-Runbooks
|
413
|
+
|
414
|
+
Implements SRE automation patterns for:
|
415
|
+
- Organization discovery optimization (52.3s -> <30s)
|
416
|
+
- VPC analysis performance improvements
|
417
|
+
- Memory usage optimization for large-scale operations
|
418
|
+
- Intelligent caching and connection pooling
|
419
|
+
"""
|
420
|
+
|
421
|
+
def __init__(self,
|
422
|
+
max_workers: int = 20,
|
423
|
+
cache_ttl_minutes: int = 30,
|
424
|
+
memory_limit_mb: int = 512): # Phase 2: Reduced from 2048MB to 512MB target
|
425
|
+
"""
|
426
|
+
Initialize performance optimization engine
|
427
|
+
|
428
|
+
Args:
|
429
|
+
max_workers: Maximum concurrent workers for parallel operations
|
430
|
+
cache_ttl_minutes: Cache TTL in minutes
|
431
|
+
memory_limit_mb: Memory usage limit in MB (Phase 2 target: <500MB)
|
432
|
+
"""
|
433
|
+
self.max_workers = max_workers
|
434
|
+
self.memory_limit_mb = memory_limit_mb
|
435
|
+
|
436
|
+
# Core optimization components
|
437
|
+
self.cache = IntelligentCache(
|
438
|
+
default_ttl_minutes=cache_ttl_minutes,
|
439
|
+
max_cache_size=500 # Phase 2: Reduced cache size for memory optimization
|
440
|
+
)
|
441
|
+
self.client_pool = OptimizedAWSClientPool(max_pool_connections=50)
|
442
|
+
|
443
|
+
# Performance tracking
|
444
|
+
self.metrics: List[OptimizationMetrics] = []
|
445
|
+
self.current_operation: Optional[OptimizationMetrics] = None
|
446
|
+
|
447
|
+
# Phase 2: Enhanced memory monitoring
|
448
|
+
self.process = psutil.Process()
|
449
|
+
self.memory_monitoring_active = False
|
450
|
+
self.memory_optimization_active = True
|
451
|
+
|
452
|
+
# Phase 2: Multi-account scaling configuration
|
453
|
+
self.enterprise_scaling_enabled = True
|
454
|
+
self.adaptive_batch_sizing = True
|
455
|
+
self.auto_memory_cleanup = True
|
456
|
+
|
457
|
+
@contextmanager
|
458
|
+
def optimize_operation(self, operation_name: str, target_seconds: float = 30.0):
|
459
|
+
"""
|
460
|
+
Context manager for optimized operation execution with monitoring
|
461
|
+
|
462
|
+
Args:
|
463
|
+
operation_name: Name of the operation being optimized
|
464
|
+
target_seconds: Target completion time in seconds
|
465
|
+
"""
|
466
|
+
# Start operation metrics tracking
|
467
|
+
metrics = OptimizationMetrics(
|
468
|
+
operation_name=operation_name,
|
469
|
+
target_seconds=target_seconds
|
470
|
+
)
|
471
|
+
self.current_operation = metrics
|
472
|
+
|
473
|
+
# Start memory monitoring
|
474
|
+
self._start_memory_monitoring()
|
475
|
+
|
476
|
+
# Enhanced progress indicator for long operations
|
477
|
+
with Status(f"[cyan]🚀 Optimizing: {operation_name}[/cyan]", console=console):
|
478
|
+
try:
|
479
|
+
console.log(f"[dim]Starting optimized {operation_name} (target: {target_seconds}s)[/]")
|
480
|
+
|
481
|
+
yield metrics
|
482
|
+
|
483
|
+
# Mark as successful
|
484
|
+
metrics.finish(success=True)
|
485
|
+
self._log_optimization_results(metrics)
|
486
|
+
|
487
|
+
except Exception as e:
|
488
|
+
# Handle failure
|
489
|
+
metrics.finish(success=False, error_message=str(e))
|
490
|
+
print_error(f"Optimization failed for {operation_name}", e)
|
491
|
+
raise
|
492
|
+
|
493
|
+
finally:
|
494
|
+
# Stop monitoring and store results
|
495
|
+
self._stop_memory_monitoring()
|
496
|
+
self.metrics.append(metrics)
|
497
|
+
self.current_operation = None
|
498
|
+
|
499
|
+
def _start_memory_monitoring(self):
|
500
|
+
"""Start background memory usage monitoring with Phase 2 aggressive optimization"""
|
501
|
+
self.memory_monitoring_active = True
|
502
|
+
|
503
|
+
def monitor_memory():
|
504
|
+
peak_memory = 0.0
|
505
|
+
cleanup_counter = 0
|
506
|
+
|
507
|
+
while self.memory_monitoring_active and self.current_operation:
|
508
|
+
try:
|
509
|
+
current_memory = self.process.memory_info().rss / (1024 * 1024) # MB
|
510
|
+
peak_memory = max(peak_memory, current_memory)
|
511
|
+
self.current_operation.memory_peak_mb = peak_memory
|
512
|
+
|
513
|
+
# Phase 2: Aggressive memory management at 80% threshold
|
514
|
+
memory_threshold_80 = self.memory_limit_mb * 0.8
|
515
|
+
memory_threshold_90 = self.memory_limit_mb * 0.9
|
516
|
+
|
517
|
+
if current_memory > memory_threshold_90:
|
518
|
+
console.log(f"[red]🚨 CRITICAL: Memory usage ({current_memory:.1f}MB) at 90% limit ({self.memory_limit_mb}MB)[/red]")
|
519
|
+
if self.auto_memory_cleanup:
|
520
|
+
self._aggressive_memory_cleanup()
|
521
|
+
|
522
|
+
elif current_memory > memory_threshold_80:
|
523
|
+
console.log(f"[yellow]⚠️ WARNING: Memory usage ({current_memory:.1f}MB) at 80% limit ({self.memory_limit_mb}MB)[/yellow]")
|
524
|
+
if self.auto_memory_cleanup and cleanup_counter % 5 == 0: # Every 5 seconds at 80%
|
525
|
+
self._proactive_memory_cleanup()
|
526
|
+
|
527
|
+
# Phase 2: Proactive cleanup every 10 seconds
|
528
|
+
cleanup_counter += 1
|
529
|
+
if self.auto_memory_cleanup and cleanup_counter % 10 == 0:
|
530
|
+
gc.collect()
|
531
|
+
|
532
|
+
time.sleep(1) # Check every second
|
533
|
+
except Exception:
|
534
|
+
break
|
535
|
+
|
536
|
+
self.memory_thread = threading.Thread(target=monitor_memory, daemon=True)
|
537
|
+
self.memory_thread.start()
|
538
|
+
|
539
|
+
def _proactive_memory_cleanup(self):
|
540
|
+
"""Proactive memory cleanup at 80% threshold"""
|
541
|
+
console.log("[dim]🧹 Proactive memory cleanup initiated[/dim]")
|
542
|
+
|
543
|
+
# Clear old cache entries
|
544
|
+
if hasattr(self.cache, '_cleanup_oldest_entries'):
|
545
|
+
self.cache._cleanup_oldest_entries(int(self.cache.max_cache_size * 0.1)) # Clear 10%
|
546
|
+
|
547
|
+
# Force garbage collection
|
548
|
+
collected = gc.collect()
|
549
|
+
if collected > 0:
|
550
|
+
console.log(f"[dim]🗑️ Collected {collected} objects[/dim]")
|
551
|
+
|
552
|
+
def _aggressive_memory_cleanup(self):
|
553
|
+
"""Aggressive memory cleanup at 90% threshold"""
|
554
|
+
console.log("[red]🚨 Aggressive memory cleanup initiated[/red]")
|
555
|
+
|
556
|
+
# Clear significant cache entries
|
557
|
+
if hasattr(self.cache, '_cleanup_oldest_entries'):
|
558
|
+
self.cache._cleanup_oldest_entries(int(self.cache.max_cache_size * 0.3)) # Clear 30%
|
559
|
+
|
560
|
+
# Multiple GC passes
|
561
|
+
total_collected = 0
|
562
|
+
for i in range(3):
|
563
|
+
collected = gc.collect(i)
|
564
|
+
total_collected += collected
|
565
|
+
|
566
|
+
console.log(f"[yellow]🗑️ Emergency cleanup collected {total_collected} objects[/yellow]")
|
567
|
+
|
568
|
+
# Update optimization applied list
|
569
|
+
if self.current_operation:
|
570
|
+
self.current_operation.optimization_applied.append("aggressive_memory_cleanup")
|
571
|
+
|
572
|
+
def _stop_memory_monitoring(self):
|
573
|
+
"""Stop memory monitoring"""
|
574
|
+
self.memory_monitoring_active = False
|
575
|
+
|
576
|
+
def _log_optimization_results(self, metrics: OptimizationMetrics):
|
577
|
+
"""Log optimization results with rich formatting"""
|
578
|
+
improvement = metrics.get_performance_improvement()
|
579
|
+
cache_efficiency = metrics.get_cache_efficiency()
|
580
|
+
|
581
|
+
if metrics.success:
|
582
|
+
if metrics.duration_seconds <= metrics.target_seconds:
|
583
|
+
print_success(
|
584
|
+
f"{metrics.operation_name} optimized: {metrics.duration_seconds:.1f}s "
|
585
|
+
f"({improvement:+.1f}% vs target)"
|
586
|
+
)
|
587
|
+
else:
|
588
|
+
print_warning(
|
589
|
+
f"{metrics.operation_name} completed in {metrics.duration_seconds:.1f}s "
|
590
|
+
f"(target: {metrics.target_seconds:.1f}s)"
|
591
|
+
)
|
592
|
+
|
593
|
+
# Log optimization details
|
594
|
+
if metrics.optimization_applied:
|
595
|
+
console.log(f"[dim]Optimizations applied: {', '.join(metrics.optimization_applied)}[/]")
|
596
|
+
|
597
|
+
if cache_efficiency > 0:
|
598
|
+
console.log(f"[dim]Cache efficiency: {cache_efficiency:.1f}% ({metrics.cache_hits} hits, {metrics.cache_misses} misses)[/]")
|
599
|
+
|
600
|
+
def optimize_organization_discovery(self,
|
601
|
+
management_profile: str,
|
602
|
+
use_parallel_processing: bool = True,
|
603
|
+
batch_size: int = 20) -> Callable:
|
604
|
+
"""
|
605
|
+
Optimize organization discovery operations
|
606
|
+
|
607
|
+
Addresses: Organization Discovery Performance (52.3s -> <30s target)
|
608
|
+
|
609
|
+
Returns optimized function with:
|
610
|
+
- Intelligent caching for Organizations API calls
|
611
|
+
- Parallel account processing
|
612
|
+
- Memory-efficient batch processing
|
613
|
+
- Connection pooling
|
614
|
+
"""
|
615
|
+
def optimized_discover_accounts():
|
616
|
+
"""Optimized account discovery with caching and parallel processing"""
|
617
|
+
cache_key = f"org_accounts_{management_profile}"
|
618
|
+
|
619
|
+
# Check cache first
|
620
|
+
cached_result = self.cache.get(cache_key, ttl_minutes=15) # Shorter TTL for critical data
|
621
|
+
if cached_result and self.current_operation:
|
622
|
+
self.current_operation.cache_hits += 1
|
623
|
+
self.current_operation.optimization_applied.append("intelligent_caching")
|
624
|
+
console.log("[blue]🚀 Using cached organization data for optimal performance[/blue]")
|
625
|
+
return cached_result
|
626
|
+
|
627
|
+
if self.current_operation:
|
628
|
+
self.current_operation.cache_misses += 1
|
629
|
+
|
630
|
+
# Perform optimized discovery
|
631
|
+
try:
|
632
|
+
# Get optimized Organizations client
|
633
|
+
org_client = self.client_pool.get_client('organizations', management_profile)
|
634
|
+
|
635
|
+
accounts = []
|
636
|
+
paginator = org_client.get_paginator('list_accounts')
|
637
|
+
|
638
|
+
# Track API calls
|
639
|
+
api_calls = 0
|
640
|
+
|
641
|
+
# Use parallel processing for account details if enabled
|
642
|
+
if use_parallel_processing:
|
643
|
+
if self.current_operation:
|
644
|
+
self.current_operation.optimization_applied.append("parallel_processing")
|
645
|
+
|
646
|
+
accounts = self._process_accounts_parallel(paginator, org_client, batch_size)
|
647
|
+
else:
|
648
|
+
# Sequential processing (fallback)
|
649
|
+
for page in paginator.paginate():
|
650
|
+
accounts.extend(page['Accounts'])
|
651
|
+
api_calls += 1
|
652
|
+
|
653
|
+
# Trigger garbage collection periodically for memory efficiency
|
654
|
+
if api_calls % 10 == 0:
|
655
|
+
gc.collect()
|
656
|
+
|
657
|
+
if self.current_operation:
|
658
|
+
self.current_operation.api_calls_made = api_calls
|
659
|
+
self.current_operation.optimization_applied.append("connection_pooling")
|
660
|
+
|
661
|
+
# Cache the result
|
662
|
+
result = {
|
663
|
+
'accounts': accounts,
|
664
|
+
'total_count': len(accounts),
|
665
|
+
'discovery_method': 'optimized_organizations_api',
|
666
|
+
'optimizations_applied': self.current_operation.optimization_applied if self.current_operation else []
|
667
|
+
}
|
668
|
+
|
669
|
+
self.cache.set(cache_key, result)
|
670
|
+
|
671
|
+
return result
|
672
|
+
|
673
|
+
except Exception as e:
|
674
|
+
logger.error(f"Optimized organization discovery failed: {e}")
|
675
|
+
raise
|
676
|
+
|
677
|
+
return optimized_discover_accounts
|
678
|
+
|
679
|
+
def _process_accounts_parallel(self, paginator, org_client, batch_size: int) -> List[Dict]:
|
680
|
+
"""Process accounts in parallel with memory optimization"""
|
681
|
+
all_accounts = []
|
682
|
+
|
683
|
+
# Collect all account IDs first (memory efficient)
|
684
|
+
account_ids = []
|
685
|
+
for page in paginator.paginate():
|
686
|
+
account_ids.extend([acc['Id'] for acc in page['Accounts']])
|
687
|
+
all_accounts.extend(page['Accounts']) # Store basic account info
|
688
|
+
|
689
|
+
if self.current_operation:
|
690
|
+
self.current_operation.api_calls_made += len(account_ids) // 100 + 1 # Estimate pages
|
691
|
+
|
692
|
+
# Process account tags in batches to avoid memory issues
|
693
|
+
if len(account_ids) > batch_size:
|
694
|
+
if self.current_operation:
|
695
|
+
self.current_operation.optimization_applied.append("batch_processing")
|
696
|
+
|
697
|
+
self._enrich_accounts_with_tags_batched(all_accounts, org_client, batch_size)
|
698
|
+
|
699
|
+
return all_accounts
|
700
|
+
|
701
|
+
def _enrich_accounts_with_tags_batched(self, accounts: List[Dict], org_client, batch_size: int):
|
702
|
+
"""Enrich accounts with tags using batched processing"""
|
703
|
+
with ThreadPoolExecutor(max_workers=min(self.max_workers, 10)) as executor:
|
704
|
+
# Process in batches to control memory usage
|
705
|
+
for i in range(0, len(accounts), batch_size):
|
706
|
+
batch = accounts[i:i + batch_size]
|
707
|
+
|
708
|
+
# Submit batch for parallel tag processing
|
709
|
+
futures = []
|
710
|
+
for account in batch:
|
711
|
+
future = executor.submit(self._get_account_tags_safe, org_client, account['Id'])
|
712
|
+
futures.append((future, account))
|
713
|
+
|
714
|
+
# Collect results for this batch
|
715
|
+
for future, account in futures:
|
716
|
+
try:
|
717
|
+
tags = future.result(timeout=10) # 10 second timeout per account
|
718
|
+
account['Tags'] = tags
|
719
|
+
if self.current_operation:
|
720
|
+
self.current_operation.api_calls_made += 1
|
721
|
+
except Exception as e:
|
722
|
+
logger.debug(f"Failed to get tags for account {account['Id']}: {e}")
|
723
|
+
account['Tags'] = {}
|
724
|
+
|
725
|
+
# Trigger garbage collection after each batch
|
726
|
+
gc.collect()
|
727
|
+
|
728
|
+
def _get_account_tags_safe(self, org_client, account_id: str) -> Dict[str, str]:
|
729
|
+
"""Safely get account tags with error handling"""
|
730
|
+
try:
|
731
|
+
response = org_client.list_tags_for_resource(ResourceId=account_id)
|
732
|
+
return {tag['Key']: tag['Value'] for tag in response['Tags']}
|
733
|
+
except Exception:
|
734
|
+
return {}
|
735
|
+
|
736
|
+
def optimize_vpc_analysis(self, operational_profile: str) -> Callable:
|
737
|
+
"""
|
738
|
+
Optimize VPC analysis operations to address timeout issues
|
739
|
+
|
740
|
+
Returns optimized function with:
|
741
|
+
- Connection pooling for multiple regions
|
742
|
+
- Parallel region processing
|
743
|
+
- Intelligent timeout handling
|
744
|
+
- Memory-efficient resource processing
|
745
|
+
"""
|
746
|
+
def optimized_vpc_analysis(regions: List[str] = None):
|
747
|
+
"""Optimized VPC analysis with regional parallelization"""
|
748
|
+
if regions is None:
|
749
|
+
regions = [
|
750
|
+
'us-east-1', 'us-west-2', 'eu-west-1', 'eu-central-1',
|
751
|
+
'ap-southeast-1', 'ap-northeast-1'
|
752
|
+
]
|
753
|
+
|
754
|
+
cache_key = f"vpc_analysis_{operational_profile}_{'_'.join(sorted(regions))}"
|
755
|
+
|
756
|
+
# Check cache
|
757
|
+
cached_result = self.cache.get(cache_key, ttl_minutes=60) # Longer TTL for VPC data
|
758
|
+
if cached_result and self.current_operation:
|
759
|
+
self.current_operation.cache_hits += 1
|
760
|
+
self.current_operation.optimization_applied.append("regional_caching")
|
761
|
+
return cached_result
|
762
|
+
|
763
|
+
if self.current_operation:
|
764
|
+
self.current_operation.cache_misses += 1
|
765
|
+
self.current_operation.optimization_applied.append("parallel_regional_processing")
|
766
|
+
|
767
|
+
# Parallel regional analysis
|
768
|
+
vpc_data = {}
|
769
|
+
|
770
|
+
with Progress(
|
771
|
+
SpinnerColumn(),
|
772
|
+
TextColumn("[progress.description]{task.description}"),
|
773
|
+
BarColumn(),
|
774
|
+
MofNCompleteColumn(),
|
775
|
+
TimeElapsedColumn(),
|
776
|
+
console=console
|
777
|
+
) as progress:
|
778
|
+
|
779
|
+
task = progress.add_task("Analyzing VPCs across regions...", total=len(regions))
|
780
|
+
|
781
|
+
with ThreadPoolExecutor(max_workers=min(self.max_workers, len(regions))) as executor:
|
782
|
+
# Submit region analysis tasks
|
783
|
+
future_to_region = {
|
784
|
+
executor.submit(self._analyze_vpc_region, operational_profile, region): region
|
785
|
+
for region in regions
|
786
|
+
}
|
787
|
+
|
788
|
+
for future in as_completed(future_to_region):
|
789
|
+
region = future_to_region[future]
|
790
|
+
try:
|
791
|
+
region_data = future.result(timeout=45) # 45s timeout per region
|
792
|
+
vpc_data[region] = region_data
|
793
|
+
|
794
|
+
if self.current_operation:
|
795
|
+
self.current_operation.api_calls_made += region_data.get('api_calls', 0)
|
796
|
+
|
797
|
+
except Exception as e:
|
798
|
+
logger.warning(f"VPC analysis failed for region {region}: {e}")
|
799
|
+
vpc_data[region] = {'error': str(e), 'vpcs': []}
|
800
|
+
|
801
|
+
finally:
|
802
|
+
progress.advance(task)
|
803
|
+
|
804
|
+
# Aggregate results
|
805
|
+
result = {
|
806
|
+
'vpc_data_by_region': vpc_data,
|
807
|
+
'total_vpcs': sum(len(data.get('vpcs', [])) for data in vpc_data.values()),
|
808
|
+
'regions_analyzed': len(regions),
|
809
|
+
'optimization_applied': self.current_operation.optimization_applied if self.current_operation else []
|
810
|
+
}
|
811
|
+
|
812
|
+
# Cache result
|
813
|
+
self.cache.set(cache_key, result)
|
814
|
+
|
815
|
+
return result
|
816
|
+
|
817
|
+
return optimized_vpc_analysis
|
818
|
+
|
819
|
+
def optimize_multi_account_operations(self,
|
820
|
+
account_list: List[str],
|
821
|
+
operation_function: Callable,
|
822
|
+
batch_size: Optional[int] = None) -> Callable:
|
823
|
+
"""
|
824
|
+
Phase 2: Optimize multi-account operations for 200+ enterprise account scaling
|
825
|
+
|
826
|
+
Args:
|
827
|
+
account_list: List of AWS account IDs to process
|
828
|
+
operation_function: Function to execute per account
|
829
|
+
batch_size: Adaptive batch size (auto-calculated if None)
|
830
|
+
|
831
|
+
Returns:
|
832
|
+
Optimized function with enterprise scaling patterns
|
833
|
+
"""
|
834
|
+
def optimized_multi_account_operation(**kwargs):
|
835
|
+
"""Optimized multi-account operation with adaptive scaling"""
|
836
|
+
account_count = len(account_list)
|
837
|
+
|
838
|
+
# Phase 2: Adaptive batch sizing based on account count and memory
|
839
|
+
if batch_size is None:
|
840
|
+
if account_count <= 50:
|
841
|
+
calculated_batch_size = 10
|
842
|
+
elif account_count <= 100:
|
843
|
+
calculated_batch_size = 15
|
844
|
+
elif account_count <= 200:
|
845
|
+
calculated_batch_size = 20
|
846
|
+
else:
|
847
|
+
calculated_batch_size = 25 # Enterprise scale 200+
|
848
|
+
else:
|
849
|
+
calculated_batch_size = batch_size
|
850
|
+
|
851
|
+
# Adjust batch size based on current memory usage
|
852
|
+
if self.memory_optimization_active:
|
853
|
+
current_memory = self.process.memory_info().rss / (1024 * 1024)
|
854
|
+
memory_utilization = current_memory / self.memory_limit_mb
|
855
|
+
|
856
|
+
if memory_utilization > 0.7:
|
857
|
+
calculated_batch_size = max(5, calculated_batch_size // 2)
|
858
|
+
console.log(f"[yellow]📉 Reducing batch size to {calculated_batch_size} due to memory pressure[/yellow]")
|
859
|
+
|
860
|
+
console.log(f"[cyan]🏢 Enterprise multi-account operation: {account_count} accounts, batch size: {calculated_batch_size}[/cyan]")
|
861
|
+
|
862
|
+
if self.current_operation:
|
863
|
+
self.current_operation.optimization_applied.extend([
|
864
|
+
"enterprise_multi_account_scaling",
|
865
|
+
"adaptive_batch_sizing",
|
866
|
+
f"batch_size_{calculated_batch_size}"
|
867
|
+
])
|
868
|
+
|
869
|
+
results = {}
|
870
|
+
processed_count = 0
|
871
|
+
|
872
|
+
# Process accounts in adaptive batches
|
873
|
+
with Progress(
|
874
|
+
SpinnerColumn(),
|
875
|
+
TextColumn("[progress.description]{task.description}"),
|
876
|
+
BarColumn(),
|
877
|
+
MofNCompleteColumn(),
|
878
|
+
TimeElapsedColumn(),
|
879
|
+
console=console
|
880
|
+
) as progress:
|
881
|
+
|
882
|
+
task = progress.add_task("Processing enterprise accounts...", total=account_count)
|
883
|
+
|
884
|
+
# Process in batches with circuit breaker protection
|
885
|
+
for i in range(0, account_count, calculated_batch_size):
|
886
|
+
batch_accounts = account_list[i:i + calculated_batch_size]
|
887
|
+
|
888
|
+
with ThreadPoolExecutor(max_workers=min(self.max_workers, len(batch_accounts))) as executor:
|
889
|
+
batch_futures = {}
|
890
|
+
|
891
|
+
for account_id in batch_accounts:
|
892
|
+
# Use circuit breaker protection for each account
|
893
|
+
client_key = f"account_{account_id}"
|
894
|
+
|
895
|
+
try:
|
896
|
+
future = executor.submit(
|
897
|
+
self._protected_account_operation,
|
898
|
+
client_key,
|
899
|
+
operation_function,
|
900
|
+
account_id,
|
901
|
+
**kwargs
|
902
|
+
)
|
903
|
+
batch_futures[future] = account_id
|
904
|
+
|
905
|
+
except Exception as e:
|
906
|
+
logger.warning(f"Failed to submit operation for account {account_id}: {e}")
|
907
|
+
results[account_id] = {"error": str(e), "success": False}
|
908
|
+
|
909
|
+
# Collect batch results with timeout handling
|
910
|
+
for future in as_completed(batch_futures, timeout=120): # 2 minute timeout per batch
|
911
|
+
account_id = batch_futures[future]
|
912
|
+
try:
|
913
|
+
result = future.result(timeout=60) # 1 minute per account
|
914
|
+
results[account_id] = result
|
915
|
+
|
916
|
+
except Exception as e:
|
917
|
+
logger.warning(f"Account operation failed for {account_id}: {e}")
|
918
|
+
results[account_id] = {"error": str(e), "success": False}
|
919
|
+
|
920
|
+
finally:
|
921
|
+
processed_count += 1
|
922
|
+
progress.advance(task)
|
923
|
+
|
924
|
+
# Phase 2: Proactive memory cleanup between batches
|
925
|
+
if self.auto_memory_cleanup and i > 0:
|
926
|
+
current_memory = self.process.memory_info().rss / (1024 * 1024)
|
927
|
+
if current_memory > self.memory_limit_mb * 0.6:
|
928
|
+
self._proactive_memory_cleanup()
|
929
|
+
time.sleep(1) # Brief pause after cleanup
|
930
|
+
|
931
|
+
# Update operation metrics
|
932
|
+
if self.current_operation:
|
933
|
+
self.current_operation.api_calls_made += processed_count
|
934
|
+
success_count = sum(1 for r in results.values() if r.get("success", False))
|
935
|
+
success_rate = (success_count / processed_count * 100) if processed_count > 0 else 0
|
936
|
+
|
937
|
+
console.log(f"[green]✅ Multi-account operation completed: {success_count}/{processed_count} accounts ({success_rate:.1f}% success)[/green]")
|
938
|
+
|
939
|
+
if success_rate >= 99.0:
|
940
|
+
self.current_operation.optimization_applied.append("high_reliability_achieved")
|
941
|
+
|
942
|
+
return {
|
943
|
+
'results': results,
|
944
|
+
'total_accounts': account_count,
|
945
|
+
'processed_accounts': processed_count,
|
946
|
+
'success_rate': success_rate,
|
947
|
+
'batch_size_used': calculated_batch_size,
|
948
|
+
'optimization_summary': {
|
949
|
+
'enterprise_scaling': True,
|
950
|
+
'adaptive_batching': True,
|
951
|
+
'memory_optimized': self.memory_optimization_active,
|
952
|
+
'reliability_protected': True
|
953
|
+
}
|
954
|
+
}
|
955
|
+
|
956
|
+
return optimized_multi_account_operation
|
957
|
+
|
958
|
+
def _protected_account_operation(self, client_key: str, operation_function: Callable, account_id: str, **kwargs):
|
959
|
+
"""Execute account operation with circuit breaker protection"""
|
960
|
+
# Create or get circuit breaker for this account
|
961
|
+
if client_key not in self.client_pool.circuit_breakers:
|
962
|
+
self.client_pool.circuit_breakers[client_key] = CircuitBreaker(
|
963
|
+
failure_threshold=2, # More aggressive for account-level operations
|
964
|
+
recovery_timeout_seconds=15, # Faster recovery for account operations
|
965
|
+
success_threshold=1 # Close quickly on success
|
966
|
+
)
|
967
|
+
|
968
|
+
with self.client_pool.circuit_breakers[client_key].protected_call(f"account_{account_id}"):
|
969
|
+
return operation_function(account_id=account_id, **kwargs)
|
970
|
+
|
971
|
+
def _analyze_vpc_region(self, profile: str, region: str) -> Dict:
|
972
|
+
"""Analyze VPCs in a specific region with optimization"""
|
973
|
+
try:
|
974
|
+
ec2_client = self.client_pool.get_client('ec2', profile, region)
|
975
|
+
|
976
|
+
# Get VPCs with pagination
|
977
|
+
vpcs = []
|
978
|
+
api_calls = 0
|
979
|
+
|
980
|
+
paginator = ec2_client.get_paginator('describe_vpcs')
|
981
|
+
for page in paginator.paginate():
|
982
|
+
vpcs.extend(page['Vpcs'])
|
983
|
+
api_calls += 1
|
984
|
+
|
985
|
+
# Enrich with network details (optimized)
|
986
|
+
for vpc in vpcs:
|
987
|
+
# Get subnets for this VPC
|
988
|
+
try:
|
989
|
+
subnets_response = ec2_client.describe_subnets(
|
990
|
+
Filters=[{'Name': 'vpc-id', 'Values': [vpc['VpcId']]}]
|
991
|
+
)
|
992
|
+
vpc['Subnets'] = subnets_response['Subnets']
|
993
|
+
api_calls += 1
|
994
|
+
except Exception as e:
|
995
|
+
logger.debug(f"Failed to get subnets for VPC {vpc['VpcId']}: {e}")
|
996
|
+
vpc['Subnets'] = []
|
997
|
+
|
998
|
+
return {
|
999
|
+
'vpcs': vpcs,
|
1000
|
+
'region': region,
|
1001
|
+
'api_calls': api_calls
|
1002
|
+
}
|
1003
|
+
|
1004
|
+
except Exception as e:
|
1005
|
+
logger.error(f"VPC region analysis failed for {region}: {e}")
|
1006
|
+
return {'vpcs': [], 'region': region, 'error': str(e), 'api_calls': 0}
|
1007
|
+
|
1008
|
+
def create_optimization_summary(self) -> None:
|
1009
|
+
"""Create comprehensive optimization performance summary with Phase 2 reliability metrics"""
|
1010
|
+
if not self.metrics:
|
1011
|
+
console.print("[yellow]No optimization metrics available yet[/]")
|
1012
|
+
return
|
1013
|
+
|
1014
|
+
print_header("Performance Optimization Summary - Phase 2 Enhanced", "SRE Automation Engine")
|
1015
|
+
|
1016
|
+
# Phase 2: Create enhanced metrics table with reliability information
|
1017
|
+
table = create_table(
|
1018
|
+
title="Phase 2 Optimization Results",
|
1019
|
+
columns=[
|
1020
|
+
{"name": "Operation", "style": "cyan", "justify": "left"},
|
1021
|
+
{"name": "Duration", "style": "white", "justify": "right"},
|
1022
|
+
{"name": "Target", "style": "white", "justify": "right"},
|
1023
|
+
{"name": "Memory", "style": "blue", "justify": "right"},
|
1024
|
+
{"name": "Improvement", "style": "white", "justify": "right"},
|
1025
|
+
{"name": "Optimizations", "style": "dim", "justify": "left", "max_width": 25},
|
1026
|
+
{"name": "Status", "style": "white", "justify": "center"}
|
1027
|
+
]
|
1028
|
+
)
|
1029
|
+
|
1030
|
+
for metrics in self.metrics:
|
1031
|
+
improvement = metrics.get_performance_improvement()
|
1032
|
+
status_icon = STATUS_INDICATORS['success'] if metrics.success else STATUS_INDICATORS['error']
|
1033
|
+
status_color = 'green' if metrics.success else 'red'
|
1034
|
+
|
1035
|
+
improvement_text = f"+{improvement:.1f}%" if improvement > 0 else f"{improvement:.1f}%"
|
1036
|
+
improvement_color = 'green' if improvement > 0 else 'yellow'
|
1037
|
+
|
1038
|
+
# Phase 2: Memory usage display with color coding
|
1039
|
+
memory_mb = metrics.memory_peak_mb
|
1040
|
+
memory_color = 'green' if memory_mb <= 256 else 'yellow' if memory_mb <= 512 else 'red'
|
1041
|
+
memory_text = f"[{memory_color}]{memory_mb:.0f}MB[/{memory_color}]"
|
1042
|
+
|
1043
|
+
table.add_row(
|
1044
|
+
metrics.operation_name,
|
1045
|
+
f"{metrics.duration_seconds:.1f}s",
|
1046
|
+
f"{metrics.target_seconds:.1f}s",
|
1047
|
+
memory_text,
|
1048
|
+
f"[{improvement_color}]{improvement_text}[/]",
|
1049
|
+
", ".join(metrics.optimization_applied[:2]) + ("..." if len(metrics.optimization_applied) > 2 else ""),
|
1050
|
+
f"[{status_color}]{status_icon}[/]"
|
1051
|
+
)
|
1052
|
+
|
1053
|
+
console.print(table)
|
1054
|
+
|
1055
|
+
# Cache statistics
|
1056
|
+
cache_stats = self.cache.get_stats()
|
1057
|
+
cache_panel = Panel(
|
1058
|
+
f"[cyan]Cache Size:[/] {cache_stats['size']}/{cache_stats['max_size']}\n"
|
1059
|
+
f"[cyan]Hit Rate:[/] {cache_stats['hit_rate']:.1f}% ({cache_stats['hits']} hits, {cache_stats['misses']} misses)",
|
1060
|
+
title="[bold]Cache Performance[/bold]",
|
1061
|
+
border_style="blue"
|
1062
|
+
)
|
1063
|
+
console.print(cache_panel)
|
1064
|
+
|
1065
|
+
# Phase 2: Reliability status panel
|
1066
|
+
reliability_stats = self.client_pool.get_reliability_status()
|
1067
|
+
reliability_color = {
|
1068
|
+
'excellent': 'green',
|
1069
|
+
'good': 'blue',
|
1070
|
+
'needs_improvement': 'yellow'
|
1071
|
+
}.get(reliability_stats.get('reliability_status', 'good'), 'white')
|
1072
|
+
|
1073
|
+
reliability_panel = Panel(
|
1074
|
+
f"[cyan]Success Rate:[/] [{reliability_color}]{reliability_stats['overall_success_rate']:.2f}%[/{reliability_color}] "
|
1075
|
+
f"(Target: {reliability_stats['target_success_rate']}%)\n"
|
1076
|
+
f"[cyan]Total Requests:[/] {reliability_stats['total_requests']:,} "
|
1077
|
+
f"([red]Failures:[/] {reliability_stats['total_failures']})\n"
|
1078
|
+
f"[cyan]Circuit Breakers:[/] {len(reliability_stats['circuit_breakers'])} active "
|
1079
|
+
f"([cyan]Status:[/] [{reliability_color}]{reliability_stats['reliability_status'].title()}[/{reliability_color}])",
|
1080
|
+
title="[bold]Phase 2 Reliability Metrics[/bold]",
|
1081
|
+
border_style=reliability_color
|
1082
|
+
)
|
1083
|
+
console.print(reliability_panel)
|
1084
|
+
|
1085
|
+
# Phase 2: Memory optimization status
|
1086
|
+
memory_report = self.get_memory_usage_report()
|
1087
|
+
memory_color = 'green' if memory_report['current_memory_mb'] <= 256 else 'yellow' if memory_report['current_memory_mb'] <= 512 else 'red'
|
1088
|
+
|
1089
|
+
memory_panel = Panel(
|
1090
|
+
f"[cyan]Current Memory:[/] [{memory_color}]{memory_report['current_memory_mb']:.1f}MB[/{memory_color}] / {self.memory_limit_mb}MB\n"
|
1091
|
+
f"[cyan]Peak Memory:[/] {memory_report.get('peak_memory_mb', 0):.1f}MB\n"
|
1092
|
+
f"[cyan]Status:[/] [{memory_color}]{memory_report['memory_efficiency'].title()}[/{memory_color}] "
|
1093
|
+
f"([cyan]Cleanup:[/] {'Enabled' if self.auto_memory_cleanup else 'Disabled'})",
|
1094
|
+
title="[bold]Phase 2 Memory Optimization[/bold]",
|
1095
|
+
border_style=memory_color
|
1096
|
+
)
|
1097
|
+
console.print(memory_panel)
|
1098
|
+
|
1099
|
+
def get_memory_usage_report(self) -> Dict[str, Any]:
|
1100
|
+
"""Get current memory usage report"""
|
1101
|
+
memory_info = self.process.memory_info()
|
1102
|
+
|
1103
|
+
return {
|
1104
|
+
"current_memory_mb": memory_info.rss / (1024 * 1024),
|
1105
|
+
"peak_memory_mb": max(m.memory_peak_mb for m in self.metrics) if self.metrics else 0.0,
|
1106
|
+
"memory_limit_mb": self.memory_limit_mb,
|
1107
|
+
"memory_efficiency": "good" if memory_info.rss / (1024 * 1024) < self.memory_limit_mb * 0.8 else "warning"
|
1108
|
+
}
|
1109
|
+
|
1110
|
+
def clear_caches(self):
|
1111
|
+
"""Clear all optimization caches"""
|
1112
|
+
self.cache.clear()
|
1113
|
+
self.client_pool.clear_pool()
|
1114
|
+
console.print("[green]✅ Optimization caches cleared[/]")
|
1115
|
+
|
1116
|
+
|
1117
|
+
# Global optimization engine instance
|
1118
|
+
_optimization_engine: Optional[PerformanceOptimizationEngine] = None
|
1119
|
+
|
1120
|
+
|
1121
|
+
def get_optimization_engine(max_workers: int = 20,
|
1122
|
+
cache_ttl_minutes: int = 30,
|
1123
|
+
memory_limit_mb: int = 512) -> PerformanceOptimizationEngine: # Phase 2: Default 512MB
|
1124
|
+
"""Get or create global performance optimization engine with Phase 2 enhancements"""
|
1125
|
+
global _optimization_engine
|
1126
|
+
if _optimization_engine is None:
|
1127
|
+
_optimization_engine = PerformanceOptimizationEngine(
|
1128
|
+
max_workers=max_workers,
|
1129
|
+
cache_ttl_minutes=cache_ttl_minutes,
|
1130
|
+
memory_limit_mb=memory_limit_mb
|
1131
|
+
)
|
1132
|
+
return _optimization_engine
|
1133
|
+
|
1134
|
+
|
1135
|
+
def create_optimization_report():
|
1136
|
+
"""Create optimization performance report"""
|
1137
|
+
if _optimization_engine:
|
1138
|
+
_optimization_engine.create_optimization_summary()
|
1139
|
+
else:
|
1140
|
+
console.print("[yellow]No optimization engine initialized[/]")
|
1141
|
+
|
1142
|
+
|
1143
|
+
# Export public interface - Phase 2 Enhanced
|
1144
|
+
__all__ = [
|
1145
|
+
"PerformanceOptimizationEngine",
|
1146
|
+
"OptimizationMetrics",
|
1147
|
+
"IntelligentCache",
|
1148
|
+
"OptimizedAWSClientPool",
|
1149
|
+
"CircuitBreaker",
|
1150
|
+
"CircuitBreakerState",
|
1151
|
+
"get_optimization_engine",
|
1152
|
+
"create_optimization_report"
|
1153
|
+
]
|