kailash 0.4.2__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kailash/__init__.py +1 -1
- kailash/client/__init__.py +12 -0
- kailash/client/enhanced_client.py +306 -0
- kailash/core/actors/__init__.py +16 -0
- kailash/core/actors/connection_actor.py +566 -0
- kailash/core/actors/supervisor.py +364 -0
- kailash/edge/__init__.py +16 -0
- kailash/edge/compliance.py +834 -0
- kailash/edge/discovery.py +659 -0
- kailash/edge/location.py +582 -0
- kailash/gateway/__init__.py +33 -0
- kailash/gateway/api.py +289 -0
- kailash/gateway/enhanced_gateway.py +357 -0
- kailash/gateway/resource_resolver.py +217 -0
- kailash/gateway/security.py +227 -0
- kailash/middleware/auth/models.py +2 -2
- kailash/middleware/database/base_models.py +1 -7
- kailash/middleware/database/repositories.py +3 -1
- kailash/middleware/gateway/__init__.py +22 -0
- kailash/middleware/gateway/checkpoint_manager.py +398 -0
- kailash/middleware/gateway/deduplicator.py +382 -0
- kailash/middleware/gateway/durable_gateway.py +417 -0
- kailash/middleware/gateway/durable_request.py +498 -0
- kailash/middleware/gateway/event_store.py +459 -0
- kailash/nodes/admin/audit_log.py +364 -6
- kailash/nodes/admin/permission_check.py +817 -33
- kailash/nodes/admin/role_management.py +1242 -108
- kailash/nodes/admin/schema_manager.py +438 -0
- kailash/nodes/admin/user_management.py +1209 -681
- kailash/nodes/api/http.py +95 -71
- kailash/nodes/base.py +281 -164
- kailash/nodes/base_async.py +30 -31
- kailash/nodes/code/__init__.py +8 -1
- kailash/nodes/code/async_python.py +1035 -0
- kailash/nodes/code/python.py +1 -0
- kailash/nodes/data/async_sql.py +12 -25
- kailash/nodes/data/sql.py +20 -11
- kailash/nodes/data/workflow_connection_pool.py +643 -0
- kailash/nodes/rag/__init__.py +1 -4
- kailash/resources/__init__.py +40 -0
- kailash/resources/factory.py +533 -0
- kailash/resources/health.py +319 -0
- kailash/resources/reference.py +288 -0
- kailash/resources/registry.py +392 -0
- kailash/runtime/async_local.py +711 -302
- kailash/testing/__init__.py +34 -0
- kailash/testing/async_test_case.py +353 -0
- kailash/testing/async_utils.py +345 -0
- kailash/testing/fixtures.py +458 -0
- kailash/testing/mock_registry.py +495 -0
- kailash/utils/resource_manager.py +420 -0
- kailash/workflow/__init__.py +8 -0
- kailash/workflow/async_builder.py +621 -0
- kailash/workflow/async_patterns.py +766 -0
- kailash/workflow/builder.py +93 -10
- kailash/workflow/cyclic_runner.py +111 -41
- kailash/workflow/graph.py +7 -2
- kailash/workflow/resilience.py +11 -1
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/METADATA +12 -7
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/RECORD +64 -28
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,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())
|