kailash 0.5.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.
Files changed (74) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/access_control/__init__.py +1 -1
  3. kailash/client/__init__.py +12 -0
  4. kailash/client/enhanced_client.py +306 -0
  5. kailash/core/actors/__init__.py +16 -0
  6. kailash/core/actors/adaptive_pool_controller.py +630 -0
  7. kailash/core/actors/connection_actor.py +566 -0
  8. kailash/core/actors/supervisor.py +364 -0
  9. kailash/core/ml/__init__.py +1 -0
  10. kailash/core/ml/query_patterns.py +544 -0
  11. kailash/core/monitoring/__init__.py +19 -0
  12. kailash/core/monitoring/connection_metrics.py +488 -0
  13. kailash/core/optimization/__init__.py +1 -0
  14. kailash/core/resilience/__init__.py +17 -0
  15. kailash/core/resilience/circuit_breaker.py +382 -0
  16. kailash/edge/__init__.py +16 -0
  17. kailash/edge/compliance.py +834 -0
  18. kailash/edge/discovery.py +659 -0
  19. kailash/edge/location.py +582 -0
  20. kailash/gateway/__init__.py +33 -0
  21. kailash/gateway/api.py +289 -0
  22. kailash/gateway/enhanced_gateway.py +357 -0
  23. kailash/gateway/resource_resolver.py +217 -0
  24. kailash/gateway/security.py +227 -0
  25. kailash/middleware/auth/access_control.py +6 -6
  26. kailash/middleware/auth/models.py +2 -2
  27. kailash/middleware/communication/ai_chat.py +7 -7
  28. kailash/middleware/communication/api_gateway.py +5 -15
  29. kailash/middleware/database/base_models.py +1 -7
  30. kailash/middleware/gateway/__init__.py +22 -0
  31. kailash/middleware/gateway/checkpoint_manager.py +398 -0
  32. kailash/middleware/gateway/deduplicator.py +382 -0
  33. kailash/middleware/gateway/durable_gateway.py +417 -0
  34. kailash/middleware/gateway/durable_request.py +498 -0
  35. kailash/middleware/gateway/event_store.py +499 -0
  36. kailash/middleware/mcp/enhanced_server.py +2 -2
  37. kailash/nodes/admin/permission_check.py +817 -33
  38. kailash/nodes/admin/role_management.py +1242 -108
  39. kailash/nodes/admin/schema_manager.py +438 -0
  40. kailash/nodes/admin/user_management.py +1124 -1582
  41. kailash/nodes/code/__init__.py +8 -1
  42. kailash/nodes/code/async_python.py +1035 -0
  43. kailash/nodes/code/python.py +1 -0
  44. kailash/nodes/data/async_sql.py +9 -3
  45. kailash/nodes/data/query_pipeline.py +641 -0
  46. kailash/nodes/data/query_router.py +895 -0
  47. kailash/nodes/data/sql.py +20 -11
  48. kailash/nodes/data/workflow_connection_pool.py +1071 -0
  49. kailash/nodes/monitoring/__init__.py +3 -5
  50. kailash/nodes/monitoring/connection_dashboard.py +822 -0
  51. kailash/nodes/rag/__init__.py +2 -7
  52. kailash/resources/__init__.py +40 -0
  53. kailash/resources/factory.py +533 -0
  54. kailash/resources/health.py +319 -0
  55. kailash/resources/reference.py +288 -0
  56. kailash/resources/registry.py +392 -0
  57. kailash/runtime/async_local.py +711 -302
  58. kailash/testing/__init__.py +34 -0
  59. kailash/testing/async_test_case.py +353 -0
  60. kailash/testing/async_utils.py +345 -0
  61. kailash/testing/fixtures.py +458 -0
  62. kailash/testing/mock_registry.py +495 -0
  63. kailash/workflow/__init__.py +8 -0
  64. kailash/workflow/async_builder.py +621 -0
  65. kailash/workflow/async_patterns.py +766 -0
  66. kailash/workflow/cyclic_runner.py +107 -16
  67. kailash/workflow/graph.py +7 -2
  68. kailash/workflow/resilience.py +11 -1
  69. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/METADATA +19 -4
  70. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/RECORD +74 -28
  71. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/WHEEL +0 -0
  72. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/entry_points.txt +0 -0
  73. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/licenses/LICENSE +0 -0
  74. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,659 @@
1
+ """Edge discovery and selection system for optimal edge placement."""
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import random
7
+ import time
8
+ from dataclasses import dataclass
9
+ from datetime import UTC, datetime, timedelta
10
+ from enum import Enum
11
+ from typing import Any, Callable, Dict, List, Optional, Set, Tuple
12
+
13
+ from .location import ComplianceZone, EdgeLocation, EdgeRegion, GeographicCoordinates
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class EdgeSelectionStrategy(Enum):
19
+ """Strategies for selecting optimal edge locations."""
20
+
21
+ LATENCY_OPTIMAL = "latency_optimal" # Minimize latency
22
+ COST_OPTIMAL = "cost_optimal" # Minimize cost
23
+ BALANCED = "balanced" # Balance latency and cost
24
+ CAPACITY_OPTIMAL = "capacity_optimal" # Maximize available capacity
25
+ COMPLIANCE_FIRST = "compliance_first" # Prioritize compliance requirements
26
+ LOAD_BALANCED = "load_balanced" # Distribute load evenly
27
+ PERFORMANCE_OPTIMAL = "performance_optimal" # Maximize performance metrics
28
+
29
+
30
+ class HealthCheckResult(Enum):
31
+ """Health check result status."""
32
+
33
+ HEALTHY = "healthy"
34
+ DEGRADED = "degraded"
35
+ UNHEALTHY = "unhealthy"
36
+ UNREACHABLE = "unreachable"
37
+
38
+
39
+ @dataclass
40
+ class EdgeDiscoveryRequest:
41
+ """Request for discovering optimal edge locations."""
42
+
43
+ # Geographic preferences
44
+ user_coordinates: Optional[GeographicCoordinates] = None
45
+ preferred_regions: List[EdgeRegion] = None
46
+ excluded_regions: List[EdgeRegion] = None
47
+
48
+ # Resource requirements
49
+ min_cpu_cores: int = 1
50
+ min_memory_gb: float = 1.0
51
+ min_storage_gb: float = 10.0
52
+ gpu_required: bool = False
53
+ bandwidth_requirements: float = 1.0 # Gbps
54
+
55
+ # Service requirements
56
+ database_support: List[str] = None
57
+ ai_models_required: List[str] = None
58
+
59
+ # Compliance requirements
60
+ compliance_zones: List[ComplianceZone] = None
61
+ data_residency_required: bool = False
62
+
63
+ # Performance requirements
64
+ max_latency_ms: float = 100.0
65
+ min_uptime_percentage: float = 99.0
66
+ max_error_rate: float = 0.01
67
+
68
+ # Selection preferences
69
+ selection_strategy: EdgeSelectionStrategy = EdgeSelectionStrategy.BALANCED
70
+ max_results: int = 5
71
+
72
+ # Cost constraints
73
+ max_cost_per_hour: Optional[float] = None
74
+
75
+ def __post_init__(self):
76
+ if self.preferred_regions is None:
77
+ self.preferred_regions = []
78
+ if self.excluded_regions is None:
79
+ self.excluded_regions = []
80
+ if self.database_support is None:
81
+ self.database_support = []
82
+ if self.ai_models_required is None:
83
+ self.ai_models_required = []
84
+ if self.compliance_zones is None:
85
+ self.compliance_zones = [ComplianceZone.PUBLIC]
86
+
87
+
88
+ @dataclass
89
+ class EdgeScore:
90
+ """Scoring result for an edge location."""
91
+
92
+ location: EdgeLocation
93
+ total_score: float
94
+
95
+ # Individual scoring components
96
+ latency_score: float = 0.0
97
+ cost_score: float = 0.0
98
+ capacity_score: float = 0.0
99
+ performance_score: float = 0.0
100
+ compliance_score: float = 0.0
101
+
102
+ # Calculated metrics
103
+ estimated_latency_ms: float = 0.0
104
+ estimated_cost_per_hour: float = 0.0
105
+ available_capacity_percentage: float = 0.0
106
+
107
+ # Reasoning
108
+ selection_reasons: List[str] = None
109
+ warnings: List[str] = None
110
+
111
+ def __post_init__(self):
112
+ if self.selection_reasons is None:
113
+ self.selection_reasons = []
114
+ if self.warnings is None:
115
+ self.warnings = []
116
+
117
+
118
+ class EdgeDiscovery:
119
+ """Edge discovery service for finding optimal edge locations.
120
+
121
+ Provides intelligent edge selection based on latency, cost, compliance,
122
+ and performance requirements.
123
+ """
124
+
125
+ def __init__(
126
+ self,
127
+ locations: List[EdgeLocation] = None,
128
+ health_check_interval_seconds: int = 60,
129
+ cost_model_enabled: bool = True,
130
+ performance_tracking_enabled: bool = True,
131
+ ):
132
+ """Initialize edge discovery service.
133
+
134
+ Args:
135
+ locations: Available edge locations
136
+ health_check_interval_seconds: How often to health check locations
137
+ cost_model_enabled: Enable cost optimization features
138
+ performance_tracking_enabled: Track performance metrics
139
+ """
140
+ self.locations: Dict[str, EdgeLocation] = {}
141
+ self.health_check_interval = health_check_interval_seconds
142
+ self.cost_model_enabled = cost_model_enabled
143
+ self.performance_tracking_enabled = performance_tracking_enabled
144
+
145
+ # Performance tracking
146
+ self._performance_history: Dict[str, List[Dict]] = {}
147
+ self._cost_history: Dict[str, List[Dict]] = {}
148
+
149
+ # Health monitoring
150
+ self._health_check_task: Optional[asyncio.Task] = None
151
+ self._health_results: Dict[str, HealthCheckResult] = {}
152
+ self._last_health_check: Dict[str, datetime] = {}
153
+
154
+ # Add provided locations
155
+ if locations:
156
+ for location in locations:
157
+ self.add_location(location)
158
+
159
+ # Selection algorithm weights (can be tuned)
160
+ self.scoring_weights = {
161
+ EdgeSelectionStrategy.LATENCY_OPTIMAL: {
162
+ "latency": 0.7,
163
+ "cost": 0.1,
164
+ "capacity": 0.1,
165
+ "performance": 0.1,
166
+ },
167
+ EdgeSelectionStrategy.COST_OPTIMAL: {
168
+ "latency": 0.1,
169
+ "cost": 0.7,
170
+ "capacity": 0.1,
171
+ "performance": 0.1,
172
+ },
173
+ EdgeSelectionStrategy.BALANCED: {
174
+ "latency": 0.3,
175
+ "cost": 0.3,
176
+ "capacity": 0.2,
177
+ "performance": 0.2,
178
+ },
179
+ EdgeSelectionStrategy.CAPACITY_OPTIMAL: {
180
+ "latency": 0.2,
181
+ "cost": 0.1,
182
+ "capacity": 0.5,
183
+ "performance": 0.2,
184
+ },
185
+ EdgeSelectionStrategy.PERFORMANCE_OPTIMAL: {
186
+ "latency": 0.2,
187
+ "cost": 0.1,
188
+ "capacity": 0.2,
189
+ "performance": 0.5,
190
+ },
191
+ }
192
+
193
+ logger.info(f"Initialized EdgeDiscovery with {len(self.locations)} locations")
194
+
195
+ def add_location(self, location: EdgeLocation):
196
+ """Add an edge location to the discovery pool."""
197
+ self.locations[location.location_id] = location
198
+ self._health_results[location.location_id] = HealthCheckResult.HEALTHY
199
+ self._last_health_check[location.location_id] = datetime.now(UTC)
200
+ logger.info(f"Added edge location: {location.name}")
201
+
202
+ def remove_location(self, location_id: str):
203
+ """Remove an edge location from the discovery pool."""
204
+ if location_id in self.locations:
205
+ location = self.locations[location_id]
206
+ del self.locations[location_id]
207
+ del self._health_results[location_id]
208
+ del self._last_health_check[location_id]
209
+ logger.info(f"Removed edge location: {location.name}")
210
+
211
+ def get_location(self, location_id: str) -> Optional[EdgeLocation]:
212
+ """Get edge location by ID."""
213
+ return self.locations.get(location_id)
214
+
215
+ def list_locations(
216
+ self,
217
+ regions: List[EdgeRegion] = None,
218
+ compliance_zones: List[ComplianceZone] = None,
219
+ healthy_only: bool = True,
220
+ ) -> List[EdgeLocation]:
221
+ """List edge locations with optional filtering."""
222
+ locations = list(self.locations.values())
223
+
224
+ # Filter by region
225
+ if regions:
226
+ locations = [loc for loc in locations if loc.region in regions]
227
+
228
+ # Filter by compliance
229
+ if compliance_zones:
230
+ locations = [
231
+ loc
232
+ for loc in locations
233
+ if any(zone in loc.compliance_zones for zone in compliance_zones)
234
+ ]
235
+
236
+ # Filter by health
237
+ if healthy_only:
238
+ locations = [
239
+ loc
240
+ for loc in locations
241
+ if self._health_results.get(loc.location_id)
242
+ == HealthCheckResult.HEALTHY
243
+ ]
244
+
245
+ return locations
246
+
247
+ async def discover_optimal_edges(
248
+ self, request: EdgeDiscoveryRequest
249
+ ) -> List[EdgeScore]:
250
+ """Discover optimal edge locations based on requirements.
251
+
252
+ Args:
253
+ request: Discovery request with requirements and preferences
254
+
255
+ Returns:
256
+ List of scored edge locations sorted by total score (best first)
257
+ """
258
+ logger.info(
259
+ f"Discovering optimal edges with strategy: {request.selection_strategy.value}"
260
+ )
261
+
262
+ # Get candidate locations
263
+ candidates = await self._get_candidate_locations(request)
264
+
265
+ if not candidates:
266
+ logger.warning("No candidate locations found matching requirements")
267
+ return []
268
+
269
+ # Score each candidate
270
+ scored_locations = []
271
+ for location in candidates:
272
+ score = await self._score_location(location, request)
273
+ if score:
274
+ scored_locations.append(score)
275
+
276
+ # Sort by total score (highest first)
277
+ scored_locations.sort(key=lambda x: x.total_score, reverse=True)
278
+
279
+ # Apply result limit
280
+ results = scored_locations[: request.max_results]
281
+
282
+ logger.info(f"Found {len(results)} optimal edge locations")
283
+ return results
284
+
285
+ async def _get_candidate_locations(
286
+ self, request: EdgeDiscoveryRequest
287
+ ) -> List[EdgeLocation]:
288
+ """Get candidate locations that meet basic requirements."""
289
+ candidates = []
290
+
291
+ for location in self.locations.values():
292
+ # Skip unhealthy locations
293
+ if not location.is_healthy:
294
+ continue
295
+
296
+ # Skip if not available for workloads
297
+ if not location.is_available_for_workload:
298
+ continue
299
+
300
+ # Check region preferences
301
+ if request.excluded_regions and location.region in request.excluded_regions:
302
+ continue
303
+
304
+ # Check resource requirements
305
+ if not location.supports_capabilities(
306
+ {
307
+ "cpu_cores": request.min_cpu_cores,
308
+ "memory_gb": request.min_memory_gb,
309
+ "gpu_required": request.gpu_required,
310
+ "database_support": request.database_support,
311
+ "ai_models": request.ai_models_required,
312
+ }
313
+ ):
314
+ continue
315
+
316
+ # Check compliance requirements
317
+ if not location.supports_compliance(request.compliance_zones):
318
+ continue
319
+
320
+ # Check performance requirements
321
+ if (
322
+ location.metrics.uptime_percentage < request.min_uptime_percentage
323
+ or location.metrics.error_rate > request.max_error_rate
324
+ ):
325
+ continue
326
+
327
+ # Check latency requirements if user coordinates provided
328
+ if request.user_coordinates:
329
+ estimated_latency = location.calculate_latency_to(
330
+ request.user_coordinates
331
+ )
332
+ if estimated_latency > request.max_latency_ms:
333
+ continue
334
+
335
+ # Check cost constraints
336
+ if request.max_cost_per_hour:
337
+ estimated_cost = location.calculate_cost_for_workload()
338
+ if estimated_cost > request.max_cost_per_hour:
339
+ continue
340
+
341
+ candidates.append(location)
342
+
343
+ return candidates
344
+
345
+ async def _score_location(
346
+ self, location: EdgeLocation, request: EdgeDiscoveryRequest
347
+ ) -> Optional[EdgeScore]:
348
+ """Score a location based on request requirements."""
349
+ try:
350
+ # Calculate individual scores
351
+ latency_score = self._calculate_latency_score(location, request)
352
+ cost_score = self._calculate_cost_score(location, request)
353
+ capacity_score = self._calculate_capacity_score(location, request)
354
+ performance_score = self._calculate_performance_score(location, request)
355
+ compliance_score = self._calculate_compliance_score(location, request)
356
+
357
+ # Get weights for scoring strategy
358
+ weights = self.scoring_weights.get(
359
+ request.selection_strategy,
360
+ self.scoring_weights[EdgeSelectionStrategy.BALANCED],
361
+ )
362
+
363
+ # Calculate weighted total score
364
+ total_score = (
365
+ latency_score * weights.get("latency", 0.25)
366
+ + cost_score * weights.get("cost", 0.25)
367
+ + capacity_score * weights.get("capacity", 0.25)
368
+ + performance_score * weights.get("performance", 0.25)
369
+ )
370
+
371
+ # Create score object
372
+ score = EdgeScore(
373
+ location=location,
374
+ total_score=total_score,
375
+ latency_score=latency_score,
376
+ cost_score=cost_score,
377
+ capacity_score=capacity_score,
378
+ performance_score=performance_score,
379
+ compliance_score=compliance_score,
380
+ estimated_latency_ms=(
381
+ location.calculate_latency_to(request.user_coordinates)
382
+ if request.user_coordinates
383
+ else 0.0
384
+ ),
385
+ estimated_cost_per_hour=location.calculate_cost_for_workload(),
386
+ available_capacity_percentage=(1.0 - location.get_load_factor()) * 100,
387
+ )
388
+
389
+ # Add selection reasoning
390
+ self._add_selection_reasoning(score, request)
391
+
392
+ return score
393
+
394
+ except Exception as e:
395
+ logger.error(f"Error scoring location {location.location_id}: {e}")
396
+ return None
397
+
398
+ def _calculate_latency_score(
399
+ self, location: EdgeLocation, request: EdgeDiscoveryRequest
400
+ ) -> float:
401
+ """Calculate latency score (0.0 to 1.0, higher is better)."""
402
+ if not request.user_coordinates:
403
+ return 0.8 # Neutral score if no user location
404
+
405
+ estimated_latency = location.calculate_latency_to(request.user_coordinates)
406
+
407
+ # Score based on latency: 0ms = 1.0, 100ms = 0.5, 200ms+ = 0.0
408
+ if estimated_latency <= 10:
409
+ return 1.0
410
+ elif estimated_latency <= 50:
411
+ return 0.9 - (estimated_latency - 10) * 0.01 # Linear decay
412
+ elif estimated_latency <= 100:
413
+ return 0.5 - (estimated_latency - 50) * 0.008
414
+ else:
415
+ return max(0.0, 0.1 - (estimated_latency - 100) * 0.001)
416
+
417
+ def _calculate_cost_score(
418
+ self, location: EdgeLocation, request: EdgeDiscoveryRequest
419
+ ) -> float:
420
+ """Calculate cost score (0.0 to 1.0, higher is better - lower cost)."""
421
+ estimated_cost = location.calculate_cost_for_workload()
422
+
423
+ # Cost scoring: $0.01/hour = 1.0, $0.10/hour = 0.5, $0.20/hour+ = 0.0
424
+ if estimated_cost <= 0.01:
425
+ return 1.0
426
+ elif estimated_cost <= 0.05:
427
+ return 0.9 - (estimated_cost - 0.01) * 20 # Linear decay
428
+ elif estimated_cost <= 0.10:
429
+ return 0.5 - (estimated_cost - 0.05) * 10
430
+ else:
431
+ return max(0.0, 0.1 - (estimated_cost - 0.10) * 1)
432
+
433
+ def _calculate_capacity_score(
434
+ self, location: EdgeLocation, request: EdgeDiscoveryRequest
435
+ ) -> float:
436
+ """Calculate capacity score (0.0 to 1.0, higher is better)."""
437
+ load_factor = location.get_load_factor()
438
+
439
+ # Capacity score: 0% load = 1.0, 50% load = 0.75, 90% load = 0.25
440
+ if load_factor <= 0.5:
441
+ return 1.0 - load_factor * 0.5 # Slow decay up to 50%
442
+ elif load_factor <= 0.8:
443
+ return 0.75 - (load_factor - 0.5) * 1.67 # Faster decay
444
+ else:
445
+ return max(0.0, 0.25 - (load_factor - 0.8) * 1.25) # Steep decay
446
+
447
+ def _calculate_performance_score(
448
+ self, location: EdgeLocation, request: EdgeDiscoveryRequest
449
+ ) -> float:
450
+ """Calculate performance score (0.0 to 1.0, higher is better)."""
451
+ metrics = location.metrics
452
+
453
+ # Combine multiple performance factors
454
+ uptime_score = metrics.uptime_percentage / 100.0
455
+ error_score = max(0.0, 1.0 - metrics.error_rate * 10) # Error rate penalty
456
+ success_score = metrics.success_rate
457
+
458
+ # Weighted combination
459
+ performance_score = uptime_score * 0.4 + error_score * 0.3 + success_score * 0.3
460
+
461
+ return max(0.0, min(1.0, performance_score))
462
+
463
+ def _calculate_compliance_score(
464
+ self, location: EdgeLocation, request: EdgeDiscoveryRequest
465
+ ) -> float:
466
+ """Calculate compliance score (0.0 to 1.0, higher is better)."""
467
+ required_zones = set(request.compliance_zones)
468
+ available_zones = set(location.compliance_zones)
469
+
470
+ # Perfect match gets full score
471
+ if required_zones.issubset(available_zones):
472
+ # Bonus for additional compliance zones
473
+ bonus = min(0.2, len(available_zones - required_zones) * 0.05)
474
+ return 1.0 + bonus
475
+ else:
476
+ # Partial compliance scoring
477
+ overlap = len(required_zones.intersection(available_zones))
478
+ return overlap / len(required_zones) if required_zones else 1.0
479
+
480
+ def _add_selection_reasoning(self, score: EdgeScore, request: EdgeDiscoveryRequest):
481
+ """Add human-readable reasoning for edge selection."""
482
+ reasons = []
483
+ warnings = []
484
+
485
+ # Latency reasoning
486
+ if score.estimated_latency_ms <= 10:
487
+ reasons.append(f"Excellent latency: {score.estimated_latency_ms:.1f}ms")
488
+ elif score.estimated_latency_ms <= 50:
489
+ reasons.append(f"Good latency: {score.estimated_latency_ms:.1f}ms")
490
+ elif score.estimated_latency_ms > 100:
491
+ warnings.append(f"High latency: {score.estimated_latency_ms:.1f}ms")
492
+
493
+ # Cost reasoning
494
+ if score.estimated_cost_per_hour <= 0.02:
495
+ reasons.append(f"Low cost: ${score.estimated_cost_per_hour:.3f}/hour")
496
+ elif score.estimated_cost_per_hour > 0.10:
497
+ warnings.append(f"High cost: ${score.estimated_cost_per_hour:.3f}/hour")
498
+
499
+ # Capacity reasoning
500
+ if score.available_capacity_percentage > 70:
501
+ reasons.append(
502
+ f"High capacity: {score.available_capacity_percentage:.0f}% available"
503
+ )
504
+ elif score.available_capacity_percentage < 30:
505
+ warnings.append(
506
+ f"Limited capacity: {score.available_capacity_percentage:.0f}% available"
507
+ )
508
+
509
+ # Performance reasoning
510
+ if score.location.metrics.uptime_percentage > 99.5:
511
+ reasons.append(
512
+ f"Excellent uptime: {score.location.metrics.uptime_percentage:.1f}%"
513
+ )
514
+ elif score.location.metrics.uptime_percentage < 99.0:
515
+ warnings.append(
516
+ f"Lower uptime: {score.location.metrics.uptime_percentage:.1f}%"
517
+ )
518
+
519
+ # Compliance reasoning
520
+ compliance_match = set(request.compliance_zones).issubset(
521
+ set(score.location.compliance_zones)
522
+ )
523
+ if compliance_match:
524
+ reasons.append("Full compliance requirements met")
525
+ else:
526
+ warnings.append("Partial compliance match only")
527
+
528
+ score.selection_reasons = reasons
529
+ score.warnings = warnings
530
+
531
+ async def start_health_monitoring(self):
532
+ """Start continuous health monitoring of edge locations."""
533
+ if self._health_check_task:
534
+ return # Already running
535
+
536
+ self._health_check_task = asyncio.create_task(self._health_check_loop())
537
+ logger.info("Started edge health monitoring")
538
+
539
+ async def stop_health_monitoring(self):
540
+ """Stop health monitoring."""
541
+ if self._health_check_task:
542
+ self._health_check_task.cancel()
543
+ try:
544
+ await self._health_check_task
545
+ except asyncio.CancelledError:
546
+ pass
547
+ self._health_check_task = None
548
+ logger.info("Stopped edge health monitoring")
549
+
550
+ async def _health_check_loop(self):
551
+ """Continuous health checking loop."""
552
+ while True:
553
+ try:
554
+ await self._perform_health_checks()
555
+ await asyncio.sleep(self.health_check_interval)
556
+ except asyncio.CancelledError:
557
+ break
558
+ except Exception as e:
559
+ logger.error(f"Health check loop error: {e}")
560
+ await asyncio.sleep(5) # Brief pause on error
561
+
562
+ async def _perform_health_checks(self):
563
+ """Perform health checks on all locations."""
564
+ health_check_tasks = []
565
+
566
+ for location_id, location in self.locations.items():
567
+ task = asyncio.create_task(
568
+ self._check_location_health(location_id, location)
569
+ )
570
+ health_check_tasks.append(task)
571
+
572
+ if health_check_tasks:
573
+ await asyncio.gather(*health_check_tasks, return_exceptions=True)
574
+
575
+ async def _check_location_health(self, location_id: str, location: EdgeLocation):
576
+ """Check health of a specific location."""
577
+ try:
578
+ is_healthy = await location.health_check()
579
+
580
+ if is_healthy:
581
+ self._health_results[location_id] = HealthCheckResult.HEALTHY
582
+ else:
583
+ # Determine degraded vs unhealthy based on failure count
584
+ if location.health_check_failures > 5:
585
+ self._health_results[location_id] = HealthCheckResult.UNHEALTHY
586
+ else:
587
+ self._health_results[location_id] = HealthCheckResult.DEGRADED
588
+
589
+ self._last_health_check[location_id] = datetime.now(UTC)
590
+
591
+ except Exception as e:
592
+ logger.error(f"Health check failed for {location.name}: {e}")
593
+ self._health_results[location_id] = HealthCheckResult.UNREACHABLE
594
+
595
+ def get_health_status(self) -> Dict[str, Any]:
596
+ """Get overall health status of edge infrastructure."""
597
+ total_locations = len(self.locations)
598
+ healthy_count = sum(
599
+ 1
600
+ for result in self._health_results.values()
601
+ if result == HealthCheckResult.HEALTHY
602
+ )
603
+
604
+ return {
605
+ "total_locations": total_locations,
606
+ "healthy_locations": healthy_count,
607
+ "health_percentage": (
608
+ (healthy_count / total_locations * 100) if total_locations > 0 else 0
609
+ ),
610
+ "locations": {
611
+ location_id: {
612
+ "name": location.name,
613
+ "region": location.region.value,
614
+ "status": location.status.value,
615
+ "health": self._health_results.get(
616
+ location_id, HealthCheckResult.UNREACHABLE
617
+ ).value,
618
+ "last_check": self._last_health_check.get(
619
+ location_id, datetime.now(UTC)
620
+ ).isoformat(),
621
+ "load_factor": location.get_load_factor(),
622
+ "active_workloads": len(location.active_workloads),
623
+ }
624
+ for location_id, location in self.locations.items()
625
+ },
626
+ }
627
+
628
+ async def find_nearest_edge(
629
+ self, user_coordinates: GeographicCoordinates, max_results: int = 1
630
+ ) -> List[EdgeScore]:
631
+ """Find nearest edge location(s) to user coordinates."""
632
+ request = EdgeDiscoveryRequest(
633
+ user_coordinates=user_coordinates,
634
+ selection_strategy=EdgeSelectionStrategy.LATENCY_OPTIMAL,
635
+ max_results=max_results,
636
+ )
637
+
638
+ return await self.discover_optimal_edges(request)
639
+
640
+ async def find_cheapest_edge(
641
+ self, requirements: Dict[str, Any] = None, max_results: int = 1
642
+ ) -> List[EdgeScore]:
643
+ """Find cheapest edge location(s) meeting requirements."""
644
+ request = EdgeDiscoveryRequest(
645
+ selection_strategy=EdgeSelectionStrategy.COST_OPTIMAL,
646
+ max_results=max_results,
647
+ **(requirements or {}),
648
+ )
649
+
650
+ return await self.discover_optimal_edges(request)
651
+
652
+ def __len__(self) -> int:
653
+ return len(self.locations)
654
+
655
+ def __contains__(self, location_id: str) -> bool:
656
+ return location_id in self.locations
657
+
658
+ def __iter__(self):
659
+ return iter(self.locations.values())