unrealon 1.0.9__py3-none-any.whl → 1.1.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.
- unrealon/__init__.py +23 -21
- unrealon-1.1.0.dist-info/METADATA +164 -0
- unrealon-1.1.0.dist-info/RECORD +82 -0
- {unrealon-1.0.9.dist-info → unrealon-1.1.0.dist-info}/WHEEL +1 -1
- unrealon-1.1.0.dist-info/entry_points.txt +9 -0
- {unrealon-1.0.9.dist-info → unrealon-1.1.0.dist-info/licenses}/LICENSE +1 -1
- unrealon_bridge/__init__.py +114 -0
- unrealon_bridge/cli.py +316 -0
- unrealon_bridge/client/__init__.py +93 -0
- unrealon_bridge/client/base.py +78 -0
- unrealon_bridge/client/commands.py +89 -0
- unrealon_bridge/client/connection.py +90 -0
- unrealon_bridge/client/events.py +65 -0
- unrealon_bridge/client/health.py +38 -0
- unrealon_bridge/client/html_parser.py +146 -0
- unrealon_bridge/client/logging.py +139 -0
- unrealon_bridge/client/proxy.py +70 -0
- unrealon_bridge/client/scheduler.py +450 -0
- unrealon_bridge/client/session.py +70 -0
- unrealon_bridge/configs/__init__.py +14 -0
- unrealon_bridge/configs/bridge_config.py +212 -0
- unrealon_bridge/configs/bridge_config.yaml +39 -0
- unrealon_bridge/models/__init__.py +138 -0
- unrealon_bridge/models/base.py +28 -0
- unrealon_bridge/models/command.py +41 -0
- unrealon_bridge/models/events.py +40 -0
- unrealon_bridge/models/html_parser.py +79 -0
- unrealon_bridge/models/logging.py +55 -0
- unrealon_bridge/models/parser.py +63 -0
- unrealon_bridge/models/proxy.py +41 -0
- unrealon_bridge/models/requests.py +95 -0
- unrealon_bridge/models/responses.py +88 -0
- unrealon_bridge/models/scheduler.py +592 -0
- unrealon_bridge/models/session.py +28 -0
- unrealon_bridge/server/__init__.py +91 -0
- unrealon_bridge/server/base.py +171 -0
- unrealon_bridge/server/handlers/__init__.py +23 -0
- unrealon_bridge/server/handlers/command.py +110 -0
- unrealon_bridge/server/handlers/html_parser.py +139 -0
- unrealon_bridge/server/handlers/logging.py +95 -0
- unrealon_bridge/server/handlers/parser.py +95 -0
- unrealon_bridge/server/handlers/proxy.py +75 -0
- unrealon_bridge/server/handlers/scheduler.py +545 -0
- unrealon_bridge/server/handlers/session.py +66 -0
- unrealon_browser/__init__.py +61 -18
- unrealon_browser/{src/cli → cli}/browser_cli.py +6 -13
- unrealon_browser/{src/cli → cli}/cookies_cli.py +5 -1
- unrealon_browser/{src/core → core}/browser_manager.py +2 -2
- unrealon_browser/{src/managers → managers}/captcha.py +1 -1
- unrealon_browser/{src/managers → managers}/cookies.py +1 -1
- unrealon_browser/managers/logger_bridge.py +231 -0
- unrealon_browser/{src/managers → managers}/profile.py +1 -1
- unrealon_driver/__init__.py +73 -19
- unrealon_driver/browser/__init__.py +8 -0
- unrealon_driver/browser/config.py +74 -0
- unrealon_driver/browser/manager.py +416 -0
- unrealon_driver/exceptions.py +28 -0
- unrealon_driver/parser/__init__.py +55 -0
- unrealon_driver/parser/cli_manager.py +141 -0
- unrealon_driver/parser/daemon_manager.py +227 -0
- unrealon_driver/parser/managers/__init__.py +46 -0
- unrealon_driver/parser/managers/browser.py +51 -0
- unrealon_driver/parser/managers/config.py +281 -0
- unrealon_driver/parser/managers/error.py +412 -0
- unrealon_driver/parser/managers/html.py +732 -0
- unrealon_driver/parser/managers/logging.py +609 -0
- unrealon_driver/parser/managers/result.py +321 -0
- unrealon_driver/parser/parser_manager.py +628 -0
- unrealon/sdk_config.py +0 -88
- unrealon-1.0.9.dist-info/METADATA +0 -810
- unrealon-1.0.9.dist-info/RECORD +0 -246
- unrealon_browser/pyproject.toml +0 -182
- unrealon_browser/src/__init__.py +0 -62
- unrealon_browser/src/managers/logger_bridge.py +0 -395
- unrealon_driver/README.md +0 -204
- unrealon_driver/pyproject.toml +0 -187
- unrealon_driver/src/__init__.py +0 -90
- unrealon_driver/src/cli/__init__.py +0 -10
- unrealon_driver/src/cli/main.py +0 -66
- unrealon_driver/src/cli/simple.py +0 -510
- unrealon_driver/src/config/__init__.py +0 -11
- unrealon_driver/src/config/auto_config.py +0 -478
- unrealon_driver/src/core/__init__.py +0 -18
- unrealon_driver/src/core/exceptions.py +0 -289
- unrealon_driver/src/core/parser.py +0 -638
- unrealon_driver/src/dto/__init__.py +0 -66
- unrealon_driver/src/dto/cli.py +0 -119
- unrealon_driver/src/dto/config.py +0 -18
- unrealon_driver/src/dto/events.py +0 -237
- unrealon_driver/src/dto/execution.py +0 -313
- unrealon_driver/src/dto/services.py +0 -311
- unrealon_driver/src/execution/__init__.py +0 -23
- unrealon_driver/src/execution/daemon_mode.py +0 -317
- unrealon_driver/src/execution/interactive_mode.py +0 -88
- unrealon_driver/src/execution/modes.py +0 -45
- unrealon_driver/src/execution/scheduled_mode.py +0 -209
- unrealon_driver/src/execution/test_mode.py +0 -250
- unrealon_driver/src/logging/__init__.py +0 -24
- unrealon_driver/src/logging/driver_logger.py +0 -512
- unrealon_driver/src/services/__init__.py +0 -24
- unrealon_driver/src/services/browser_service.py +0 -726
- unrealon_driver/src/services/llm/__init__.py +0 -15
- unrealon_driver/src/services/llm/browser_llm_service.py +0 -363
- unrealon_driver/src/services/llm/llm.py +0 -195
- unrealon_driver/src/services/logger_service.py +0 -232
- unrealon_driver/src/services/metrics_service.py +0 -185
- unrealon_driver/src/services/scheduler_service.py +0 -489
- unrealon_driver/src/services/websocket_service.py +0 -362
- unrealon_driver/src/utils/__init__.py +0 -16
- unrealon_driver/src/utils/service_factory.py +0 -317
- unrealon_driver/src/utils/time_formatter.py +0 -338
- unrealon_llm/README.md +0 -44
- unrealon_llm/__init__.py +0 -26
- unrealon_llm/pyproject.toml +0 -154
- unrealon_llm/src/__init__.py +0 -228
- unrealon_llm/src/cli/__init__.py +0 -0
- unrealon_llm/src/core/__init__.py +0 -11
- unrealon_llm/src/core/smart_client.py +0 -438
- unrealon_llm/src/dto/__init__.py +0 -155
- unrealon_llm/src/dto/models/__init__.py +0 -0
- unrealon_llm/src/dto/models/config.py +0 -343
- unrealon_llm/src/dto/models/core.py +0 -328
- unrealon_llm/src/dto/models/enums.py +0 -123
- unrealon_llm/src/dto/models/html_analysis.py +0 -345
- unrealon_llm/src/dto/models/statistics.py +0 -473
- unrealon_llm/src/dto/models/translation.py +0 -383
- unrealon_llm/src/dto/models/type_conversion.py +0 -462
- unrealon_llm/src/dto/schemas/__init__.py +0 -0
- unrealon_llm/src/exceptions.py +0 -392
- unrealon_llm/src/llm_config/__init__.py +0 -20
- unrealon_llm/src/llm_config/logging_config.py +0 -178
- unrealon_llm/src/llm_logging/__init__.py +0 -42
- unrealon_llm/src/llm_logging/llm_events.py +0 -107
- unrealon_llm/src/llm_logging/llm_logger.py +0 -466
- unrealon_llm/src/managers/__init__.py +0 -15
- unrealon_llm/src/managers/cache_manager.py +0 -67
- unrealon_llm/src/managers/cost_manager.py +0 -107
- unrealon_llm/src/managers/request_manager.py +0 -298
- unrealon_llm/src/modules/__init__.py +0 -0
- unrealon_llm/src/modules/html_processor/__init__.py +0 -25
- unrealon_llm/src/modules/html_processor/base_processor.py +0 -415
- unrealon_llm/src/modules/html_processor/details_processor.py +0 -85
- unrealon_llm/src/modules/html_processor/listing_processor.py +0 -91
- unrealon_llm/src/modules/html_processor/models/__init__.py +0 -20
- unrealon_llm/src/modules/html_processor/models/processing_models.py +0 -40
- unrealon_llm/src/modules/html_processor/models/universal_model.py +0 -56
- unrealon_llm/src/modules/html_processor/processor.py +0 -102
- unrealon_llm/src/modules/llm/__init__.py +0 -0
- unrealon_llm/src/modules/translator/__init__.py +0 -0
- unrealon_llm/src/provider.py +0 -116
- unrealon_llm/src/utils/__init__.py +0 -95
- unrealon_llm/src/utils/common.py +0 -64
- unrealon_llm/src/utils/data_extractor.py +0 -188
- unrealon_llm/src/utils/html_cleaner.py +0 -767
- unrealon_llm/src/utils/language_detector.py +0 -308
- unrealon_llm/src/utils/models_cache.py +0 -592
- unrealon_llm/src/utils/smart_counter.py +0 -229
- unrealon_llm/src/utils/token_counter.py +0 -189
- unrealon_sdk/README.md +0 -25
- unrealon_sdk/__init__.py +0 -30
- unrealon_sdk/pyproject.toml +0 -231
- unrealon_sdk/src/__init__.py +0 -150
- unrealon_sdk/src/cli/__init__.py +0 -12
- unrealon_sdk/src/cli/commands/__init__.py +0 -22
- unrealon_sdk/src/cli/commands/benchmark.py +0 -42
- unrealon_sdk/src/cli/commands/diagnostics.py +0 -573
- unrealon_sdk/src/cli/commands/health.py +0 -46
- unrealon_sdk/src/cli/commands/integration.py +0 -498
- unrealon_sdk/src/cli/commands/reports.py +0 -43
- unrealon_sdk/src/cli/commands/security.py +0 -36
- unrealon_sdk/src/cli/commands/server.py +0 -483
- unrealon_sdk/src/cli/commands/servers.py +0 -56
- unrealon_sdk/src/cli/commands/tests.py +0 -55
- unrealon_sdk/src/cli/main.py +0 -126
- unrealon_sdk/src/cli/utils/reporter.py +0 -519
- unrealon_sdk/src/clients/openapi.yaml +0 -3347
- unrealon_sdk/src/clients/python_http/__init__.py +0 -3
- unrealon_sdk/src/clients/python_http/api_config.py +0 -228
- unrealon_sdk/src/clients/python_http/models/BaseModel.py +0 -12
- unrealon_sdk/src/clients/python_http/models/BroadcastDeliveryStats.py +0 -33
- unrealon_sdk/src/clients/python_http/models/BroadcastMessage.py +0 -17
- unrealon_sdk/src/clients/python_http/models/BroadcastMessageRequest.py +0 -35
- unrealon_sdk/src/clients/python_http/models/BroadcastPriority.py +0 -10
- unrealon_sdk/src/clients/python_http/models/BroadcastResponse.py +0 -21
- unrealon_sdk/src/clients/python_http/models/BroadcastResultResponse.py +0 -33
- unrealon_sdk/src/clients/python_http/models/BroadcastTarget.py +0 -11
- unrealon_sdk/src/clients/python_http/models/ConnectionStats.py +0 -27
- unrealon_sdk/src/clients/python_http/models/ConnectionsResponse.py +0 -21
- unrealon_sdk/src/clients/python_http/models/DeveloperMessageResponse.py +0 -23
- unrealon_sdk/src/clients/python_http/models/ErrorResponse.py +0 -25
- unrealon_sdk/src/clients/python_http/models/HTTPValidationError.py +0 -16
- unrealon_sdk/src/clients/python_http/models/HealthResponse.py +0 -23
- unrealon_sdk/src/clients/python_http/models/HealthStatus.py +0 -33
- unrealon_sdk/src/clients/python_http/models/LogLevel.py +0 -10
- unrealon_sdk/src/clients/python_http/models/LoggingRequest.py +0 -27
- unrealon_sdk/src/clients/python_http/models/LoggingResponse.py +0 -23
- unrealon_sdk/src/clients/python_http/models/MaintenanceMode.py +0 -9
- unrealon_sdk/src/clients/python_http/models/MaintenanceModeRequest.py +0 -33
- unrealon_sdk/src/clients/python_http/models/MaintenanceStatusResponse.py +0 -39
- unrealon_sdk/src/clients/python_http/models/ParserCommandRequest.py +0 -25
- unrealon_sdk/src/clients/python_http/models/ParserMessageResponse.py +0 -21
- unrealon_sdk/src/clients/python_http/models/ParserRegistrationRequest.py +0 -28
- unrealon_sdk/src/clients/python_http/models/ParserRegistrationResponse.py +0 -25
- unrealon_sdk/src/clients/python_http/models/ParserType.py +0 -10
- unrealon_sdk/src/clients/python_http/models/ProxyBlockRequest.py +0 -19
- unrealon_sdk/src/clients/python_http/models/ProxyEndpointResponse.py +0 -20
- unrealon_sdk/src/clients/python_http/models/ProxyListResponse.py +0 -19
- unrealon_sdk/src/clients/python_http/models/ProxyProvider.py +0 -10
- unrealon_sdk/src/clients/python_http/models/ProxyPurchaseRequest.py +0 -25
- unrealon_sdk/src/clients/python_http/models/ProxyResponse.py +0 -47
- unrealon_sdk/src/clients/python_http/models/ProxyRotationRequest.py +0 -23
- unrealon_sdk/src/clients/python_http/models/ProxyStatus.py +0 -10
- unrealon_sdk/src/clients/python_http/models/ProxyUsageRequest.py +0 -19
- unrealon_sdk/src/clients/python_http/models/ProxyUsageStatsResponse.py +0 -26
- unrealon_sdk/src/clients/python_http/models/ServiceRegistrationDto.py +0 -23
- unrealon_sdk/src/clients/python_http/models/ServiceStatsResponse.py +0 -31
- unrealon_sdk/src/clients/python_http/models/SessionStartRequest.py +0 -23
- unrealon_sdk/src/clients/python_http/models/SuccessResponse.py +0 -25
- unrealon_sdk/src/clients/python_http/models/SystemNotificationResponse.py +0 -23
- unrealon_sdk/src/clients/python_http/models/ValidationError.py +0 -18
- unrealon_sdk/src/clients/python_http/models/ValidationErrorResponse.py +0 -21
- unrealon_sdk/src/clients/python_http/models/WebSocketMetrics.py +0 -21
- unrealon_sdk/src/clients/python_http/models/__init__.py +0 -44
- unrealon_sdk/src/clients/python_http/services/None_service.py +0 -35
- unrealon_sdk/src/clients/python_http/services/ParserManagement_service.py +0 -190
- unrealon_sdk/src/clients/python_http/services/ProxyManagement_service.py +0 -289
- unrealon_sdk/src/clients/python_http/services/SocketLogging_service.py +0 -187
- unrealon_sdk/src/clients/python_http/services/SystemHealth_service.py +0 -119
- unrealon_sdk/src/clients/python_http/services/WebSocketAPI_service.py +0 -198
- unrealon_sdk/src/clients/python_http/services/__init__.py +0 -0
- unrealon_sdk/src/clients/python_http/services/admin_service.py +0 -125
- unrealon_sdk/src/clients/python_http/services/async_None_service.py +0 -35
- unrealon_sdk/src/clients/python_http/services/async_ParserManagement_service.py +0 -190
- unrealon_sdk/src/clients/python_http/services/async_ProxyManagement_service.py +0 -289
- unrealon_sdk/src/clients/python_http/services/async_SocketLogging_service.py +0 -189
- unrealon_sdk/src/clients/python_http/services/async_SystemHealth_service.py +0 -123
- unrealon_sdk/src/clients/python_http/services/async_WebSocketAPI_service.py +0 -200
- unrealon_sdk/src/clients/python_http/services/async_admin_service.py +0 -125
- unrealon_sdk/src/clients/python_websocket/__init__.py +0 -28
- unrealon_sdk/src/clients/python_websocket/client.py +0 -490
- unrealon_sdk/src/clients/python_websocket/events.py +0 -732
- unrealon_sdk/src/clients/python_websocket/example.py +0 -136
- unrealon_sdk/src/clients/python_websocket/types.py +0 -871
- unrealon_sdk/src/core/__init__.py +0 -64
- unrealon_sdk/src/core/client.py +0 -556
- unrealon_sdk/src/core/config.py +0 -465
- unrealon_sdk/src/core/exceptions.py +0 -239
- unrealon_sdk/src/core/metadata.py +0 -191
- unrealon_sdk/src/core/models.py +0 -142
- unrealon_sdk/src/core/types.py +0 -68
- unrealon_sdk/src/dto/__init__.py +0 -268
- unrealon_sdk/src/dto/authentication.py +0 -108
- unrealon_sdk/src/dto/cache.py +0 -208
- unrealon_sdk/src/dto/common.py +0 -19
- unrealon_sdk/src/dto/concurrency.py +0 -393
- unrealon_sdk/src/dto/events.py +0 -108
- unrealon_sdk/src/dto/health.py +0 -339
- unrealon_sdk/src/dto/load_balancing.py +0 -336
- unrealon_sdk/src/dto/logging.py +0 -230
- unrealon_sdk/src/dto/performance.py +0 -165
- unrealon_sdk/src/dto/rate_limiting.py +0 -295
- unrealon_sdk/src/dto/resource_pooling.py +0 -128
- unrealon_sdk/src/dto/structured_logging.py +0 -112
- unrealon_sdk/src/dto/task_scheduling.py +0 -121
- unrealon_sdk/src/dto/websocket.py +0 -55
- unrealon_sdk/src/enterprise/__init__.py +0 -59
- unrealon_sdk/src/enterprise/authentication.py +0 -401
- unrealon_sdk/src/enterprise/cache_manager.py +0 -578
- unrealon_sdk/src/enterprise/error_recovery.py +0 -494
- unrealon_sdk/src/enterprise/event_system.py +0 -549
- unrealon_sdk/src/enterprise/health_monitor.py +0 -747
- unrealon_sdk/src/enterprise/load_balancer.py +0 -964
- unrealon_sdk/src/enterprise/logging/__init__.py +0 -68
- unrealon_sdk/src/enterprise/logging/cleanup.py +0 -156
- unrealon_sdk/src/enterprise/logging/development.py +0 -744
- unrealon_sdk/src/enterprise/logging/service.py +0 -410
- unrealon_sdk/src/enterprise/multithreading_manager.py +0 -853
- unrealon_sdk/src/enterprise/performance_monitor.py +0 -539
- unrealon_sdk/src/enterprise/proxy_manager.py +0 -696
- unrealon_sdk/src/enterprise/rate_limiter.py +0 -652
- unrealon_sdk/src/enterprise/resource_pool.py +0 -763
- unrealon_sdk/src/enterprise/task_scheduler.py +0 -709
- unrealon_sdk/src/internal/__init__.py +0 -10
- unrealon_sdk/src/internal/command_router.py +0 -497
- unrealon_sdk/src/internal/connection_manager.py +0 -397
- unrealon_sdk/src/internal/http_client.py +0 -446
- unrealon_sdk/src/internal/websocket_client.py +0 -420
- unrealon_sdk/src/provider.py +0 -471
- unrealon_sdk/src/utils.py +0 -234
- /unrealon_browser/{src/cli → cli}/__init__.py +0 -0
- /unrealon_browser/{src/cli → cli}/interactive_mode.py +0 -0
- /unrealon_browser/{src/cli → cli}/main.py +0 -0
- /unrealon_browser/{src/core → core}/__init__.py +0 -0
- /unrealon_browser/{src/dto → dto}/__init__.py +0 -0
- /unrealon_browser/{src/dto → dto}/models/config.py +0 -0
- /unrealon_browser/{src/dto → dto}/models/core.py +0 -0
- /unrealon_browser/{src/dto → dto}/models/dataclasses.py +0 -0
- /unrealon_browser/{src/dto → dto}/models/detection.py +0 -0
- /unrealon_browser/{src/dto → dto}/models/enums.py +0 -0
- /unrealon_browser/{src/dto → dto}/models/statistics.py +0 -0
- /unrealon_browser/{src/managers → managers}/__init__.py +0 -0
- /unrealon_browser/{src/managers → managers}/stealth.py +0 -0
|
@@ -1,964 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Load Balancer - Layer 4 Concurrency Service
|
|
3
|
-
|
|
4
|
-
Enterprise-grade load balancing system with intelligent traffic distribution,
|
|
5
|
-
health monitoring, and adaptive algorithms. Provides optimal resource utilization
|
|
6
|
-
for parsing operations with geographic awareness and performance optimization.
|
|
7
|
-
|
|
8
|
-
Features:
|
|
9
|
-
- Multiple load balancing algorithms (round-robin, least connections, performance-based)
|
|
10
|
-
- Dynamic node health monitoring with circuit breaker patterns
|
|
11
|
-
- Geographic and proximity-based routing for parsing operations
|
|
12
|
-
- Session affinity and sticky sessions support
|
|
13
|
-
- Real-time performance metrics and adaptive optimization
|
|
14
|
-
- Failover strategies with graceful degradation
|
|
15
|
-
- Integration with proxy management and resource pooling
|
|
16
|
-
- Configurable traffic routing rules and policies
|
|
17
|
-
- Connection draining and maintenance mode support
|
|
18
|
-
- Advanced analytics and decision tracking
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
import asyncio
|
|
22
|
-
import logging
|
|
23
|
-
import time
|
|
24
|
-
import threading
|
|
25
|
-
import hashlib
|
|
26
|
-
import random
|
|
27
|
-
from typing import Dict, List, Optional, Any, Set, Callable, Union
|
|
28
|
-
from datetime import datetime, timezone, timedelta
|
|
29
|
-
from collections import defaultdict, deque
|
|
30
|
-
from dataclasses import dataclass, field
|
|
31
|
-
import weakref
|
|
32
|
-
|
|
33
|
-
# Core SDK components
|
|
34
|
-
from unrealon_sdk.src.core.config import AdapterConfig
|
|
35
|
-
from unrealon_sdk.src.utils import generate_correlation_id
|
|
36
|
-
|
|
37
|
-
# DTO models
|
|
38
|
-
from unrealon_sdk.src.dto.logging import SDKEventType, SDKSeverity
|
|
39
|
-
from unrealon_sdk.src.dto.concurrency import ConcurrencyEventType, ConcurrencyMetrics
|
|
40
|
-
from unrealon_sdk.src.dto.load_balancing import (
|
|
41
|
-
LoadBalancingAlgorithm,
|
|
42
|
-
NodeHealthStatus,
|
|
43
|
-
TrafficDirection,
|
|
44
|
-
FailoverStrategy,
|
|
45
|
-
CircuitBreakerState,
|
|
46
|
-
LoadBalancerNode,
|
|
47
|
-
LoadBalancingRule,
|
|
48
|
-
LoadBalancingDecisionRequest,
|
|
49
|
-
LoadBalancingDecisionResult,
|
|
50
|
-
LoadBalancerStatistics,
|
|
51
|
-
HealthCheckConfig,
|
|
52
|
-
LoadBalancingSession,
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
# Development logging
|
|
56
|
-
from typing import TYPE_CHECKING
|
|
57
|
-
|
|
58
|
-
if TYPE_CHECKING:
|
|
59
|
-
from unrealon_sdk.src.enterprise.logging import DevelopmentLogger
|
|
60
|
-
|
|
61
|
-
logger = logging.getLogger(__name__)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@dataclass
|
|
65
|
-
class NodePerformanceTracker:
|
|
66
|
-
"""Track node performance metrics."""
|
|
67
|
-
|
|
68
|
-
node_id: str
|
|
69
|
-
response_times: deque[float] = field(default_factory=lambda: deque(maxlen=100))
|
|
70
|
-
success_count: int = 0
|
|
71
|
-
failure_count: int = 0
|
|
72
|
-
last_request: Optional[datetime] = None
|
|
73
|
-
avg_response_time: float = 0.0
|
|
74
|
-
success_rate: float = 1.0
|
|
75
|
-
load_score: float = 0.0
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class LoadBalancer:
|
|
79
|
-
"""
|
|
80
|
-
Enterprise-grade load balancer.
|
|
81
|
-
|
|
82
|
-
Provides intelligent traffic distribution with health monitoring,
|
|
83
|
-
performance optimization, and adaptive algorithms for parsing operations.
|
|
84
|
-
"""
|
|
85
|
-
|
|
86
|
-
def __init__(
|
|
87
|
-
self,
|
|
88
|
-
config: AdapterConfig,
|
|
89
|
-
health_check_config: Optional[HealthCheckConfig] = None,
|
|
90
|
-
dev_logger: Optional["DevelopmentLogger"] = None,
|
|
91
|
-
):
|
|
92
|
-
"""Initialize load balancer."""
|
|
93
|
-
self.config = config
|
|
94
|
-
self.health_check_config = health_check_config or HealthCheckConfig()
|
|
95
|
-
self.dev_logger = dev_logger
|
|
96
|
-
|
|
97
|
-
# Node management
|
|
98
|
-
self._nodes: Dict[str, LoadBalancerNode] = {}
|
|
99
|
-
self._node_performance: Dict[str, NodePerformanceTracker] = {}
|
|
100
|
-
self._healthy_nodes: Set[str] = set()
|
|
101
|
-
self._unhealthy_nodes: Set[str] = set()
|
|
102
|
-
|
|
103
|
-
# Load balancing rules
|
|
104
|
-
self._rules: Dict[str, LoadBalancingRule] = {}
|
|
105
|
-
self._default_rule: Optional[LoadBalancingRule] = None
|
|
106
|
-
|
|
107
|
-
# Algorithm implementations
|
|
108
|
-
self._algorithms: Dict[LoadBalancingAlgorithm, Callable] = {
|
|
109
|
-
LoadBalancingAlgorithm.ROUND_ROBIN: self._round_robin_selection,
|
|
110
|
-
LoadBalancingAlgorithm.WEIGHTED_ROUND_ROBIN: self._weighted_round_robin_selection,
|
|
111
|
-
LoadBalancingAlgorithm.LEAST_CONNECTIONS: self._least_connections_selection,
|
|
112
|
-
LoadBalancingAlgorithm.WEIGHTED_LEAST_CONNECTIONS: self._weighted_least_connections_selection,
|
|
113
|
-
LoadBalancingAlgorithm.LEAST_RESPONSE_TIME: self._least_response_time_selection,
|
|
114
|
-
LoadBalancingAlgorithm.RESOURCE_BASED: self._resource_based_selection,
|
|
115
|
-
LoadBalancingAlgorithm.GEOGRAPHIC: self._geographic_selection,
|
|
116
|
-
LoadBalancingAlgorithm.HASH_BASED: self._hash_based_selection,
|
|
117
|
-
LoadBalancingAlgorithm.RANDOM: self._random_selection,
|
|
118
|
-
LoadBalancingAlgorithm.ADAPTIVE: self._adaptive_selection,
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
# Session management
|
|
122
|
-
self._sessions: Dict[str, LoadBalancingSession] = {}
|
|
123
|
-
self._session_cleanup_threshold = timedelta(hours=24)
|
|
124
|
-
|
|
125
|
-
# State tracking
|
|
126
|
-
self._round_robin_index: Dict[str, int] = defaultdict(int)
|
|
127
|
-
self._decision_cache: Dict[str, LoadBalancingDecisionResult] = {}
|
|
128
|
-
self._cache_ttl_seconds = 60.0
|
|
129
|
-
|
|
130
|
-
# Statistics
|
|
131
|
-
self._statistics = LoadBalancerStatistics()
|
|
132
|
-
self._decision_times: deque[float] = deque(maxlen=1000)
|
|
133
|
-
|
|
134
|
-
# Background tasks
|
|
135
|
-
self._health_check_task: Optional[asyncio.Task[None]] = None
|
|
136
|
-
self._cleanup_task: Optional[asyncio.Task[None]] = None
|
|
137
|
-
self._metrics_task: Optional[asyncio.Task[None]] = None
|
|
138
|
-
self._shutdown = False
|
|
139
|
-
|
|
140
|
-
# Thread safety
|
|
141
|
-
self._lock = threading.RLock()
|
|
142
|
-
|
|
143
|
-
self._log_info("Load balancer initialized")
|
|
144
|
-
|
|
145
|
-
async def start(self) -> None:
|
|
146
|
-
"""Start load balancer."""
|
|
147
|
-
# Start background tasks
|
|
148
|
-
if self._health_check_task is None:
|
|
149
|
-
self._health_check_task = asyncio.create_task(self._health_check_loop())
|
|
150
|
-
|
|
151
|
-
if self._cleanup_task is None:
|
|
152
|
-
self._cleanup_task = asyncio.create_task(self._cleanup_loop())
|
|
153
|
-
|
|
154
|
-
if self._metrics_task is None:
|
|
155
|
-
self._metrics_task = asyncio.create_task(self._metrics_loop())
|
|
156
|
-
|
|
157
|
-
self._log_info("Load balancer started")
|
|
158
|
-
|
|
159
|
-
async def stop(self) -> None:
|
|
160
|
-
"""Stop load balancer."""
|
|
161
|
-
self._shutdown = True
|
|
162
|
-
|
|
163
|
-
# Cancel background tasks
|
|
164
|
-
for task in [self._health_check_task, self._cleanup_task, self._metrics_task]:
|
|
165
|
-
if task:
|
|
166
|
-
task.cancel()
|
|
167
|
-
try:
|
|
168
|
-
await task
|
|
169
|
-
except asyncio.CancelledError:
|
|
170
|
-
pass
|
|
171
|
-
|
|
172
|
-
self._log_info("Load balancer stopped")
|
|
173
|
-
|
|
174
|
-
def add_node(self, node: LoadBalancerNode) -> None:
|
|
175
|
-
"""Add node to load balancer."""
|
|
176
|
-
with self._lock:
|
|
177
|
-
self._nodes[node.node_id] = node
|
|
178
|
-
self._node_performance[node.node_id] = NodePerformanceTracker(node_id=node.node_id)
|
|
179
|
-
|
|
180
|
-
if node.is_enabled and node.health_status == NodeHealthStatus.HEALTHY:
|
|
181
|
-
self._healthy_nodes.add(node.node_id)
|
|
182
|
-
else:
|
|
183
|
-
self._unhealthy_nodes.add(node.node_id)
|
|
184
|
-
|
|
185
|
-
self._log_info(f"Added node '{node.node_name}' ({node.host}:{node.port})")
|
|
186
|
-
|
|
187
|
-
def remove_node(self, node_id: str) -> bool:
|
|
188
|
-
"""Remove node from load balancer."""
|
|
189
|
-
with self._lock:
|
|
190
|
-
if node_id not in self._nodes:
|
|
191
|
-
return False
|
|
192
|
-
|
|
193
|
-
del self._nodes[node_id]
|
|
194
|
-
del self._node_performance[node_id]
|
|
195
|
-
self._healthy_nodes.discard(node_id)
|
|
196
|
-
self._unhealthy_nodes.discard(node_id)
|
|
197
|
-
|
|
198
|
-
self._log_info(f"Removed node '{node_id}'")
|
|
199
|
-
return True
|
|
200
|
-
|
|
201
|
-
def add_rule(self, rule: LoadBalancingRule) -> None:
|
|
202
|
-
"""Add load balancing rule."""
|
|
203
|
-
with self._lock:
|
|
204
|
-
self._rules[rule.rule_id] = rule
|
|
205
|
-
|
|
206
|
-
self._log_info(f"Added load balancing rule '{rule.rule_name}'")
|
|
207
|
-
|
|
208
|
-
def set_default_rule(self, rule: LoadBalancingRule) -> None:
|
|
209
|
-
"""Set default load balancing rule."""
|
|
210
|
-
self._default_rule = rule
|
|
211
|
-
self._log_info(f"Set default rule to '{rule.rule_name}'")
|
|
212
|
-
|
|
213
|
-
async def select_node(
|
|
214
|
-
self, request: LoadBalancingDecisionRequest
|
|
215
|
-
) -> LoadBalancingDecisionResult:
|
|
216
|
-
"""Select node for request using load balancing."""
|
|
217
|
-
start_time = time.time()
|
|
218
|
-
|
|
219
|
-
try:
|
|
220
|
-
# Check cache first
|
|
221
|
-
cache_key = self._generate_cache_key(request)
|
|
222
|
-
if cache_key in self._decision_cache:
|
|
223
|
-
cached_result = self._decision_cache[cache_key]
|
|
224
|
-
if self._is_cache_valid(cached_result):
|
|
225
|
-
return cached_result
|
|
226
|
-
|
|
227
|
-
# Find applicable rule
|
|
228
|
-
rule = self._find_applicable_rule(request)
|
|
229
|
-
if not rule:
|
|
230
|
-
rule = self._default_rule
|
|
231
|
-
|
|
232
|
-
if not rule:
|
|
233
|
-
raise RuntimeError("No load balancing rule available")
|
|
234
|
-
|
|
235
|
-
# Check session affinity
|
|
236
|
-
if rule.session_affinity and request.session_id:
|
|
237
|
-
session_result = self._handle_session_affinity(request, rule)
|
|
238
|
-
if session_result:
|
|
239
|
-
return session_result
|
|
240
|
-
|
|
241
|
-
# Get candidate nodes
|
|
242
|
-
candidate_nodes = self._get_candidate_nodes(request, rule)
|
|
243
|
-
if not candidate_nodes:
|
|
244
|
-
raise RuntimeError("No healthy nodes available")
|
|
245
|
-
|
|
246
|
-
# Apply load balancing algorithm
|
|
247
|
-
selected_node = self._apply_algorithm(rule.algorithm, candidate_nodes, request)
|
|
248
|
-
if not selected_node:
|
|
249
|
-
raise RuntimeError("Load balancing algorithm failed to select node")
|
|
250
|
-
|
|
251
|
-
# Get backup nodes
|
|
252
|
-
backup_nodes = self._get_backup_nodes(selected_node, candidate_nodes, rule)
|
|
253
|
-
|
|
254
|
-
# Create decision result
|
|
255
|
-
decision_time = (time.time() - start_time) * 1000
|
|
256
|
-
result = LoadBalancingDecisionResult(
|
|
257
|
-
selected_node=selected_node,
|
|
258
|
-
backup_nodes=backup_nodes,
|
|
259
|
-
algorithm_used=rule.algorithm,
|
|
260
|
-
rule_applied=rule.rule_id,
|
|
261
|
-
decision_time_ms=decision_time,
|
|
262
|
-
selection_factors=self._get_selection_factors(selected_node, rule.algorithm),
|
|
263
|
-
timestamp=datetime.now(timezone.utc),
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
# Handle session creation
|
|
267
|
-
if rule.session_affinity and request.session_id:
|
|
268
|
-
self._create_session(request, selected_node)
|
|
269
|
-
result.new_session_created = True
|
|
270
|
-
|
|
271
|
-
# Cache decision
|
|
272
|
-
self._decision_cache[cache_key] = result
|
|
273
|
-
|
|
274
|
-
# Update statistics
|
|
275
|
-
self._update_statistics(result)
|
|
276
|
-
|
|
277
|
-
return result
|
|
278
|
-
|
|
279
|
-
except Exception as e:
|
|
280
|
-
decision_time = (time.time() - start_time) * 1000
|
|
281
|
-
self._statistics.failed_requests += 1
|
|
282
|
-
|
|
283
|
-
error_result = LoadBalancingDecisionResult(
|
|
284
|
-
selected_node=None,
|
|
285
|
-
algorithm_used=(
|
|
286
|
-
rule.algorithm if "rule" in locals() else LoadBalancingAlgorithm.ROUND_ROBIN
|
|
287
|
-
),
|
|
288
|
-
decision_time_ms=decision_time,
|
|
289
|
-
rejection_reasons={"error": str(e)},
|
|
290
|
-
timestamp=datetime.now(timezone.utc),
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
self._log_error(f"Load balancing failed: {e}")
|
|
294
|
-
return error_result
|
|
295
|
-
|
|
296
|
-
def report_request_result(
|
|
297
|
-
self,
|
|
298
|
-
node_id: str,
|
|
299
|
-
success: bool,
|
|
300
|
-
response_time_ms: float,
|
|
301
|
-
error_message: Optional[str] = None,
|
|
302
|
-
) -> None:
|
|
303
|
-
"""Report request result for node performance tracking."""
|
|
304
|
-
with self._lock:
|
|
305
|
-
if node_id not in self._nodes:
|
|
306
|
-
return
|
|
307
|
-
|
|
308
|
-
node = self._nodes[node_id]
|
|
309
|
-
performance = self._node_performance[node_id]
|
|
310
|
-
|
|
311
|
-
# Update performance metrics
|
|
312
|
-
performance.response_times.append(response_time_ms)
|
|
313
|
-
performance.last_request = datetime.now(timezone.utc)
|
|
314
|
-
|
|
315
|
-
if success:
|
|
316
|
-
performance.success_count += 1
|
|
317
|
-
node.total_requests += 1
|
|
318
|
-
else:
|
|
319
|
-
performance.failure_count += 1
|
|
320
|
-
node.failed_requests += 1
|
|
321
|
-
node.failure_count += 1
|
|
322
|
-
|
|
323
|
-
# Calculate averages
|
|
324
|
-
if performance.response_times:
|
|
325
|
-
performance.avg_response_time = sum(performance.response_times) / len(
|
|
326
|
-
performance.response_times
|
|
327
|
-
)
|
|
328
|
-
node.avg_response_time_ms = performance.avg_response_time
|
|
329
|
-
|
|
330
|
-
total_requests = performance.success_count + performance.failure_count
|
|
331
|
-
if total_requests > 0:
|
|
332
|
-
performance.success_rate = performance.success_count / total_requests
|
|
333
|
-
|
|
334
|
-
# Update circuit breaker state
|
|
335
|
-
self._update_circuit_breaker(node, success, error_message)
|
|
336
|
-
|
|
337
|
-
# Update node health
|
|
338
|
-
self._update_node_health(node_id, success)
|
|
339
|
-
|
|
340
|
-
def _find_applicable_rule(
|
|
341
|
-
self, request: LoadBalancingDecisionRequest
|
|
342
|
-
) -> Optional[LoadBalancingRule]:
|
|
343
|
-
"""Find applicable load balancing rule for request."""
|
|
344
|
-
for rule in sorted(self._rules.values(), key=lambda r: r.priority):
|
|
345
|
-
if self._rule_matches(rule, request):
|
|
346
|
-
return rule
|
|
347
|
-
return None
|
|
348
|
-
|
|
349
|
-
def _rule_matches(self, rule: LoadBalancingRule, request: LoadBalancingDecisionRequest) -> bool:
|
|
350
|
-
"""Check if rule matches request."""
|
|
351
|
-
# Check traffic direction
|
|
352
|
-
if rule.traffic_direction != TrafficDirection.BIDIRECTIONAL:
|
|
353
|
-
if rule.traffic_direction != request.traffic_direction:
|
|
354
|
-
return False
|
|
355
|
-
|
|
356
|
-
# Check source patterns
|
|
357
|
-
if rule.source_patterns:
|
|
358
|
-
if not any(
|
|
359
|
-
self._pattern_matches(pattern, request.source_ip)
|
|
360
|
-
for pattern in rule.source_patterns
|
|
361
|
-
):
|
|
362
|
-
return False
|
|
363
|
-
|
|
364
|
-
# Check path patterns (if HTTP)
|
|
365
|
-
if rule.path_patterns and request.path:
|
|
366
|
-
if not any(
|
|
367
|
-
self._pattern_matches(pattern, request.path) for pattern in rule.path_patterns
|
|
368
|
-
):
|
|
369
|
-
return False
|
|
370
|
-
|
|
371
|
-
return True
|
|
372
|
-
|
|
373
|
-
def _pattern_matches(self, pattern: str, value: str) -> bool:
|
|
374
|
-
"""Check if pattern matches value (simple wildcard support)."""
|
|
375
|
-
if "*" not in pattern:
|
|
376
|
-
return pattern == value
|
|
377
|
-
|
|
378
|
-
# Simple wildcard matching
|
|
379
|
-
parts = pattern.split("*")
|
|
380
|
-
if not value.startswith(parts[0]):
|
|
381
|
-
return False
|
|
382
|
-
if not value.endswith(parts[-1]):
|
|
383
|
-
return False
|
|
384
|
-
|
|
385
|
-
return True
|
|
386
|
-
|
|
387
|
-
def _get_candidate_nodes(
|
|
388
|
-
self, request: LoadBalancingDecisionRequest, rule: LoadBalancingRule
|
|
389
|
-
) -> List[LoadBalancerNode]:
|
|
390
|
-
"""Get candidate nodes for request."""
|
|
391
|
-
candidates = []
|
|
392
|
-
|
|
393
|
-
# Start with rule target nodes or all healthy nodes
|
|
394
|
-
target_node_ids = rule.target_nodes if rule.target_nodes else list(self._healthy_nodes)
|
|
395
|
-
|
|
396
|
-
for node_id in target_node_ids:
|
|
397
|
-
if node_id not in self._nodes:
|
|
398
|
-
continue
|
|
399
|
-
|
|
400
|
-
node = self._nodes[node_id]
|
|
401
|
-
|
|
402
|
-
# Check if node is enabled and healthy
|
|
403
|
-
if not node.is_enabled or node.health_status not in [
|
|
404
|
-
NodeHealthStatus.HEALTHY,
|
|
405
|
-
NodeHealthStatus.DEGRADED,
|
|
406
|
-
]:
|
|
407
|
-
continue
|
|
408
|
-
|
|
409
|
-
# Check circuit breaker
|
|
410
|
-
if node.circuit_breaker_state == CircuitBreakerState.OPEN:
|
|
411
|
-
continue
|
|
412
|
-
|
|
413
|
-
# Check required tags
|
|
414
|
-
if request.required_tags:
|
|
415
|
-
if not all(node.tags.get(k) == v for k, v in request.required_tags.items()):
|
|
416
|
-
continue
|
|
417
|
-
|
|
418
|
-
# Check excluded nodes
|
|
419
|
-
if node.node_id in request.excluded_nodes:
|
|
420
|
-
continue
|
|
421
|
-
|
|
422
|
-
# Check connection limits
|
|
423
|
-
if node.max_connections and node.current_connections >= node.max_connections:
|
|
424
|
-
continue
|
|
425
|
-
|
|
426
|
-
candidates.append(node)
|
|
427
|
-
|
|
428
|
-
return candidates
|
|
429
|
-
|
|
430
|
-
def _apply_algorithm(
|
|
431
|
-
self,
|
|
432
|
-
algorithm: LoadBalancingAlgorithm,
|
|
433
|
-
candidates: List[LoadBalancerNode],
|
|
434
|
-
request: LoadBalancingDecisionRequest,
|
|
435
|
-
) -> Optional[LoadBalancerNode]:
|
|
436
|
-
"""Apply load balancing algorithm to select node."""
|
|
437
|
-
if not candidates:
|
|
438
|
-
return None
|
|
439
|
-
|
|
440
|
-
algorithm_func = self._algorithms.get(algorithm)
|
|
441
|
-
if not algorithm_func:
|
|
442
|
-
# Fallback to round robin
|
|
443
|
-
algorithm_func = self._algorithms[LoadBalancingAlgorithm.ROUND_ROBIN]
|
|
444
|
-
|
|
445
|
-
return algorithm_func(candidates, request)
|
|
446
|
-
|
|
447
|
-
def _round_robin_selection(
|
|
448
|
-
self, candidates: List[LoadBalancerNode], request: LoadBalancingDecisionRequest
|
|
449
|
-
) -> Optional[LoadBalancerNode]:
|
|
450
|
-
"""Round-robin selection algorithm."""
|
|
451
|
-
if not candidates:
|
|
452
|
-
return None
|
|
453
|
-
|
|
454
|
-
rule_key = request.request_id[:8] # Use part of request ID as key
|
|
455
|
-
index = self._round_robin_index[rule_key] % len(candidates)
|
|
456
|
-
self._round_robin_index[rule_key] = (index + 1) % len(candidates)
|
|
457
|
-
|
|
458
|
-
return candidates[index]
|
|
459
|
-
|
|
460
|
-
def _weighted_round_robin_selection(
|
|
461
|
-
self, candidates: List[LoadBalancerNode], request: LoadBalancingDecisionRequest
|
|
462
|
-
) -> Optional[LoadBalancerNode]:
|
|
463
|
-
"""Weighted round-robin selection algorithm."""
|
|
464
|
-
if not candidates:
|
|
465
|
-
return None
|
|
466
|
-
|
|
467
|
-
# Create weighted list
|
|
468
|
-
weighted_candidates = []
|
|
469
|
-
for node in candidates:
|
|
470
|
-
weighted_candidates.extend([node] * node.weight)
|
|
471
|
-
|
|
472
|
-
if not weighted_candidates:
|
|
473
|
-
return candidates[0]
|
|
474
|
-
|
|
475
|
-
rule_key = f"weighted_{request.request_id[:8]}"
|
|
476
|
-
index = self._round_robin_index[rule_key] % len(weighted_candidates)
|
|
477
|
-
self._round_robin_index[rule_key] = (index + 1) % len(weighted_candidates)
|
|
478
|
-
|
|
479
|
-
return weighted_candidates[index]
|
|
480
|
-
|
|
481
|
-
def _least_connections_selection(
|
|
482
|
-
self, candidates: List[LoadBalancerNode], request: LoadBalancingDecisionRequest
|
|
483
|
-
) -> Optional[LoadBalancerNode]:
|
|
484
|
-
"""Least connections selection algorithm."""
|
|
485
|
-
if not candidates:
|
|
486
|
-
return None
|
|
487
|
-
|
|
488
|
-
return min(candidates, key=lambda node: node.current_connections)
|
|
489
|
-
|
|
490
|
-
def _weighted_least_connections_selection(
|
|
491
|
-
self, candidates: List[LoadBalancerNode], request: LoadBalancingDecisionRequest
|
|
492
|
-
) -> Optional[LoadBalancerNode]:
|
|
493
|
-
"""Weighted least connections selection algorithm."""
|
|
494
|
-
if not candidates:
|
|
495
|
-
return None
|
|
496
|
-
|
|
497
|
-
# Calculate weighted connection ratio
|
|
498
|
-
def weighted_ratio(node: LoadBalancerNode) -> float:
|
|
499
|
-
if node.weight <= 0:
|
|
500
|
-
return float("inf")
|
|
501
|
-
return node.current_connections / node.weight
|
|
502
|
-
|
|
503
|
-
return min(candidates, key=weighted_ratio)
|
|
504
|
-
|
|
505
|
-
def _least_response_time_selection(
|
|
506
|
-
self, candidates: List[LoadBalancerNode], request: LoadBalancingDecisionRequest
|
|
507
|
-
) -> Optional[LoadBalancerNode]:
|
|
508
|
-
"""Least response time selection algorithm."""
|
|
509
|
-
if not candidates:
|
|
510
|
-
return None
|
|
511
|
-
|
|
512
|
-
return min(candidates, key=lambda node: node.avg_response_time_ms)
|
|
513
|
-
|
|
514
|
-
def _resource_based_selection(
|
|
515
|
-
self, candidates: List[LoadBalancerNode], request: LoadBalancingDecisionRequest
|
|
516
|
-
) -> Optional[LoadBalancerNode]:
|
|
517
|
-
"""Resource-based selection algorithm."""
|
|
518
|
-
if not candidates:
|
|
519
|
-
return None
|
|
520
|
-
|
|
521
|
-
# Calculate resource utilization score
|
|
522
|
-
def resource_score(node: LoadBalancerNode) -> float:
|
|
523
|
-
connection_ratio = 0.0
|
|
524
|
-
if node.max_connections:
|
|
525
|
-
connection_ratio = node.current_connections / node.max_connections
|
|
526
|
-
|
|
527
|
-
# Consider response time and failure rate
|
|
528
|
-
performance = self._node_performance.get(node.node_id)
|
|
529
|
-
if performance:
|
|
530
|
-
response_factor = min(
|
|
531
|
-
performance.avg_response_time / 1000.0, 1.0
|
|
532
|
-
) # Normalize to 0-1
|
|
533
|
-
failure_factor = 1.0 - performance.success_rate
|
|
534
|
-
return connection_ratio * 0.4 + response_factor * 0.3 + failure_factor * 0.3
|
|
535
|
-
|
|
536
|
-
return connection_ratio
|
|
537
|
-
|
|
538
|
-
return min(candidates, key=resource_score)
|
|
539
|
-
|
|
540
|
-
def _geographic_selection(
|
|
541
|
-
self, candidates: List[LoadBalancerNode], request: LoadBalancingDecisionRequest
|
|
542
|
-
) -> Optional[LoadBalancerNode]:
|
|
543
|
-
"""Geographic selection algorithm."""
|
|
544
|
-
if not candidates:
|
|
545
|
-
return None
|
|
546
|
-
|
|
547
|
-
# If preferred region specified, try to use it
|
|
548
|
-
if request.preferred_region:
|
|
549
|
-
regional_candidates = [
|
|
550
|
-
node for node in candidates if node.region == request.preferred_region
|
|
551
|
-
]
|
|
552
|
-
if regional_candidates:
|
|
553
|
-
# Use least connections within preferred region
|
|
554
|
-
return self._least_connections_selection(regional_candidates, request)
|
|
555
|
-
|
|
556
|
-
# Fallback to least connections
|
|
557
|
-
return self._least_connections_selection(candidates, request)
|
|
558
|
-
|
|
559
|
-
def _hash_based_selection(
|
|
560
|
-
self, candidates: List[LoadBalancerNode], request: LoadBalancingDecisionRequest
|
|
561
|
-
) -> Optional[LoadBalancerNode]:
|
|
562
|
-
"""Hash-based selection algorithm (consistent hashing)."""
|
|
563
|
-
if not candidates:
|
|
564
|
-
return None
|
|
565
|
-
|
|
566
|
-
# Use session ID or source IP for hashing
|
|
567
|
-
hash_key = request.session_id or request.source_ip
|
|
568
|
-
hash_value = int(hashlib.md5(hash_key.encode(), usedforsecurity=False).hexdigest(), 16)
|
|
569
|
-
|
|
570
|
-
return candidates[hash_value % len(candidates)]
|
|
571
|
-
|
|
572
|
-
def _random_selection(
|
|
573
|
-
self, candidates: List[LoadBalancerNode], request: LoadBalancingDecisionRequest
|
|
574
|
-
) -> Optional[LoadBalancerNode]:
|
|
575
|
-
"""Random selection algorithm."""
|
|
576
|
-
if not candidates:
|
|
577
|
-
return None
|
|
578
|
-
|
|
579
|
-
return random.choice(candidates)
|
|
580
|
-
|
|
581
|
-
def _adaptive_selection(
|
|
582
|
-
self, candidates: List[LoadBalancerNode], request: LoadBalancingDecisionRequest
|
|
583
|
-
) -> Optional[LoadBalancerNode]:
|
|
584
|
-
"""Adaptive selection algorithm based on current performance."""
|
|
585
|
-
if not candidates:
|
|
586
|
-
return None
|
|
587
|
-
|
|
588
|
-
# Calculate adaptive score based on multiple factors
|
|
589
|
-
def adaptive_score(node: LoadBalancerNode) -> float:
|
|
590
|
-
performance = self._node_performance.get(node.node_id)
|
|
591
|
-
if not performance:
|
|
592
|
-
return 0.5 # Neutral score for new nodes
|
|
593
|
-
|
|
594
|
-
# Combine multiple metrics
|
|
595
|
-
response_factor = min(performance.avg_response_time / 1000.0, 1.0)
|
|
596
|
-
success_factor = performance.success_rate
|
|
597
|
-
connection_factor = 0.0
|
|
598
|
-
|
|
599
|
-
if node.max_connections:
|
|
600
|
-
connection_factor = 1.0 - (node.current_connections / node.max_connections)
|
|
601
|
-
|
|
602
|
-
# Weight factors: success rate is most important, then response time, then connections
|
|
603
|
-
score = success_factor * 0.5 + (1.0 - response_factor) * 0.3 + connection_factor * 0.2
|
|
604
|
-
return score
|
|
605
|
-
|
|
606
|
-
# Select node with highest adaptive score
|
|
607
|
-
return max(candidates, key=adaptive_score)
|
|
608
|
-
|
|
609
|
-
def _get_backup_nodes(
|
|
610
|
-
self,
|
|
611
|
-
selected_node: LoadBalancerNode,
|
|
612
|
-
candidates: List[LoadBalancerNode],
|
|
613
|
-
rule: LoadBalancingRule,
|
|
614
|
-
) -> List[LoadBalancerNode]:
|
|
615
|
-
"""Get backup nodes for failover."""
|
|
616
|
-
backup_nodes = []
|
|
617
|
-
|
|
618
|
-
# Add explicitly configured backup nodes
|
|
619
|
-
for backup_id in rule.backup_nodes:
|
|
620
|
-
if backup_id in self._nodes and backup_id != selected_node.node_id:
|
|
621
|
-
backup_node = self._nodes[backup_id]
|
|
622
|
-
if backup_node.is_enabled and backup_node.health_status in [
|
|
623
|
-
NodeHealthStatus.HEALTHY,
|
|
624
|
-
NodeHealthStatus.DEGRADED,
|
|
625
|
-
]:
|
|
626
|
-
backup_nodes.append(backup_node)
|
|
627
|
-
|
|
628
|
-
# Add other healthy candidates as additional backups
|
|
629
|
-
for candidate in candidates:
|
|
630
|
-
if candidate.node_id != selected_node.node_id and candidate not in backup_nodes:
|
|
631
|
-
backup_nodes.append(candidate)
|
|
632
|
-
if len(backup_nodes) >= 3: # Limit backup nodes
|
|
633
|
-
break
|
|
634
|
-
|
|
635
|
-
return backup_nodes
|
|
636
|
-
|
|
637
|
-
def _handle_session_affinity(
|
|
638
|
-
self, request: LoadBalancingDecisionRequest, rule: LoadBalancingRule
|
|
639
|
-
) -> Optional[LoadBalancingDecisionResult]:
|
|
640
|
-
"""Handle session affinity for sticky sessions."""
|
|
641
|
-
if not request.session_id:
|
|
642
|
-
return None
|
|
643
|
-
|
|
644
|
-
session = self._sessions.get(request.session_id)
|
|
645
|
-
if not session or not session.is_active:
|
|
646
|
-
return None
|
|
647
|
-
|
|
648
|
-
# Check if assigned node is still healthy
|
|
649
|
-
if session.assigned_node_id in self._healthy_nodes:
|
|
650
|
-
assigned_node = self._nodes[session.assigned_node_id]
|
|
651
|
-
|
|
652
|
-
# Update session
|
|
653
|
-
session.last_accessed = datetime.now(timezone.utc)
|
|
654
|
-
session.request_count += 1
|
|
655
|
-
|
|
656
|
-
return LoadBalancingDecisionResult(
|
|
657
|
-
selected_node=assigned_node,
|
|
658
|
-
algorithm_used=rule.algorithm,
|
|
659
|
-
rule_applied=rule.rule_id,
|
|
660
|
-
decision_time_ms=1.0, # Cached decision
|
|
661
|
-
session_affinity_used=True,
|
|
662
|
-
timestamp=datetime.now(timezone.utc),
|
|
663
|
-
)
|
|
664
|
-
|
|
665
|
-
# Session node is unhealthy, remove session
|
|
666
|
-
del self._sessions[request.session_id]
|
|
667
|
-
return None
|
|
668
|
-
|
|
669
|
-
def _create_session(
|
|
670
|
-
self, request: LoadBalancingDecisionRequest, selected_node: LoadBalancerNode
|
|
671
|
-
) -> None:
|
|
672
|
-
"""Create new session for sticky sessions."""
|
|
673
|
-
if not request.session_id:
|
|
674
|
-
return
|
|
675
|
-
|
|
676
|
-
session = LoadBalancingSession(
|
|
677
|
-
session_id=request.session_id,
|
|
678
|
-
client_ip=request.source_ip,
|
|
679
|
-
assigned_node_id=selected_node.node_id,
|
|
680
|
-
created_at=datetime.now(timezone.utc),
|
|
681
|
-
last_accessed=datetime.now(timezone.utc),
|
|
682
|
-
)
|
|
683
|
-
|
|
684
|
-
self._sessions[request.session_id] = session
|
|
685
|
-
|
|
686
|
-
def _update_circuit_breaker(
|
|
687
|
-
self, node: LoadBalancerNode, success: bool, error_message: Optional[str]
|
|
688
|
-
) -> None:
|
|
689
|
-
"""Update circuit breaker state for node."""
|
|
690
|
-
if not success:
|
|
691
|
-
node.failure_count += 1
|
|
692
|
-
node.last_failure = datetime.now(timezone.utc)
|
|
693
|
-
|
|
694
|
-
# Trip circuit breaker if failure threshold reached
|
|
695
|
-
if node.failure_count >= 5: # Configurable threshold
|
|
696
|
-
node.circuit_breaker_state = CircuitBreakerState.OPEN
|
|
697
|
-
self._log_info(f"Circuit breaker opened for node {node.node_id}")
|
|
698
|
-
else:
|
|
699
|
-
# Reset failure count on success
|
|
700
|
-
if node.circuit_breaker_state == CircuitBreakerState.HALF_OPEN:
|
|
701
|
-
node.circuit_breaker_state = CircuitBreakerState.CLOSED
|
|
702
|
-
node.failure_count = 0
|
|
703
|
-
self._log_info(f"Circuit breaker closed for node {node.node_id}")
|
|
704
|
-
|
|
705
|
-
def _update_node_health(self, node_id: str, success: bool) -> None:
|
|
706
|
-
"""Update node health status."""
|
|
707
|
-
with self._lock:
|
|
708
|
-
if node_id not in self._nodes:
|
|
709
|
-
return
|
|
710
|
-
|
|
711
|
-
node = self._nodes[node_id]
|
|
712
|
-
performance = self._node_performance[node_id]
|
|
713
|
-
|
|
714
|
-
# Update health based on success rate
|
|
715
|
-
if performance.success_rate < 0.5: # Less than 50% success
|
|
716
|
-
if node.health_status == NodeHealthStatus.HEALTHY:
|
|
717
|
-
node.health_status = NodeHealthStatus.DEGRADED
|
|
718
|
-
self._log_info(f"Node {node_id} health degraded")
|
|
719
|
-
elif performance.success_rate > 0.8: # More than 80% success
|
|
720
|
-
if node.health_status == NodeHealthStatus.DEGRADED:
|
|
721
|
-
node.health_status = NodeHealthStatus.HEALTHY
|
|
722
|
-
self._log_info(f"Node {node_id} health recovered")
|
|
723
|
-
|
|
724
|
-
# Update healthy/unhealthy sets
|
|
725
|
-
if (
|
|
726
|
-
node.health_status in [NodeHealthStatus.HEALTHY, NodeHealthStatus.DEGRADED]
|
|
727
|
-
and node.is_enabled
|
|
728
|
-
):
|
|
729
|
-
self._healthy_nodes.add(node_id)
|
|
730
|
-
self._unhealthy_nodes.discard(node_id)
|
|
731
|
-
else:
|
|
732
|
-
self._unhealthy_nodes.add(node_id)
|
|
733
|
-
self._healthy_nodes.discard(node_id)
|
|
734
|
-
|
|
735
|
-
async def _health_check_loop(self) -> None:
|
|
736
|
-
"""Background health check loop."""
|
|
737
|
-
while not self._shutdown:
|
|
738
|
-
try:
|
|
739
|
-
await asyncio.sleep(self.health_check_config.interval_seconds)
|
|
740
|
-
await self._perform_health_checks()
|
|
741
|
-
except asyncio.CancelledError:
|
|
742
|
-
break
|
|
743
|
-
except Exception as e:
|
|
744
|
-
logger.error(f"Error in health check loop: {e}")
|
|
745
|
-
|
|
746
|
-
async def _perform_health_checks(self) -> None:
|
|
747
|
-
"""Perform health checks on all nodes."""
|
|
748
|
-
if not self.health_check_config.enabled:
|
|
749
|
-
return
|
|
750
|
-
|
|
751
|
-
for node_id, node in list(self._nodes.items()):
|
|
752
|
-
try:
|
|
753
|
-
is_healthy = await self._check_node_health(node)
|
|
754
|
-
self._update_node_health_status(node_id, is_healthy)
|
|
755
|
-
except Exception as e:
|
|
756
|
-
logger.error(f"Health check failed for node {node_id}: {e}")
|
|
757
|
-
self._update_node_health_status(node_id, False)
|
|
758
|
-
|
|
759
|
-
async def _check_node_health(self, node: LoadBalancerNode) -> bool:
|
|
760
|
-
"""Check individual node health."""
|
|
761
|
-
# Simple TCP connection check
|
|
762
|
-
try:
|
|
763
|
-
reader, writer = await asyncio.wait_for(
|
|
764
|
-
asyncio.open_connection(node.host, node.port),
|
|
765
|
-
timeout=self.health_check_config.timeout_seconds,
|
|
766
|
-
)
|
|
767
|
-
writer.close()
|
|
768
|
-
await writer.wait_closed()
|
|
769
|
-
return True
|
|
770
|
-
except Exception:
|
|
771
|
-
return False
|
|
772
|
-
|
|
773
|
-
def _update_node_health_status(self, node_id: str, is_healthy: bool) -> None:
|
|
774
|
-
"""Update node health status based on health check."""
|
|
775
|
-
with self._lock:
|
|
776
|
-
if node_id not in self._nodes:
|
|
777
|
-
return
|
|
778
|
-
|
|
779
|
-
node = self._nodes[node_id]
|
|
780
|
-
node.last_health_check = datetime.now(timezone.utc)
|
|
781
|
-
|
|
782
|
-
if is_healthy:
|
|
783
|
-
if node.health_status == NodeHealthStatus.UNHEALTHY:
|
|
784
|
-
node.health_status = NodeHealthStatus.HEALTHY
|
|
785
|
-
self._log_info(f"Node {node_id} health recovered")
|
|
786
|
-
else:
|
|
787
|
-
if node.health_status in [NodeHealthStatus.HEALTHY, NodeHealthStatus.DEGRADED]:
|
|
788
|
-
node.health_status = NodeHealthStatus.UNHEALTHY
|
|
789
|
-
self._log_info(f"Node {node_id} marked unhealthy")
|
|
790
|
-
|
|
791
|
-
# Update sets
|
|
792
|
-
if (
|
|
793
|
-
node.health_status in [NodeHealthStatus.HEALTHY, NodeHealthStatus.DEGRADED]
|
|
794
|
-
and node.is_enabled
|
|
795
|
-
):
|
|
796
|
-
self._healthy_nodes.add(node_id)
|
|
797
|
-
self._unhealthy_nodes.discard(node_id)
|
|
798
|
-
else:
|
|
799
|
-
self._unhealthy_nodes.add(node_id)
|
|
800
|
-
self._healthy_nodes.discard(node_id)
|
|
801
|
-
|
|
802
|
-
async def _cleanup_loop(self) -> None:
|
|
803
|
-
"""Background cleanup loop."""
|
|
804
|
-
while not self._shutdown:
|
|
805
|
-
try:
|
|
806
|
-
await asyncio.sleep(300) # Cleanup every 5 minutes
|
|
807
|
-
await self._cleanup_sessions()
|
|
808
|
-
await self._cleanup_cache()
|
|
809
|
-
except asyncio.CancelledError:
|
|
810
|
-
break
|
|
811
|
-
except Exception as e:
|
|
812
|
-
logger.error(f"Error in cleanup loop: {e}")
|
|
813
|
-
|
|
814
|
-
async def _cleanup_sessions(self) -> None:
|
|
815
|
-
"""Clean up expired sessions."""
|
|
816
|
-
current_time = datetime.now(timezone.utc)
|
|
817
|
-
expired_sessions = []
|
|
818
|
-
|
|
819
|
-
for session_id, session in self._sessions.items():
|
|
820
|
-
if current_time - session.last_accessed > self._session_cleanup_threshold:
|
|
821
|
-
expired_sessions.append(session_id)
|
|
822
|
-
|
|
823
|
-
for session_id in expired_sessions:
|
|
824
|
-
del self._sessions[session_id]
|
|
825
|
-
|
|
826
|
-
if expired_sessions:
|
|
827
|
-
self._log_info(f"Cleaned up {len(expired_sessions)} expired sessions")
|
|
828
|
-
|
|
829
|
-
async def _cleanup_cache(self) -> None:
|
|
830
|
-
"""Clean up expired cache entries."""
|
|
831
|
-
current_time = time.time()
|
|
832
|
-
expired_keys = []
|
|
833
|
-
|
|
834
|
-
for cache_key, result in self._decision_cache.items():
|
|
835
|
-
if current_time - result.timestamp.timestamp() > self._cache_ttl_seconds:
|
|
836
|
-
expired_keys.append(cache_key)
|
|
837
|
-
|
|
838
|
-
for cache_key in expired_keys:
|
|
839
|
-
del self._decision_cache[cache_key]
|
|
840
|
-
|
|
841
|
-
async def _metrics_loop(self) -> None:
|
|
842
|
-
"""Background metrics collection loop."""
|
|
843
|
-
while not self._shutdown:
|
|
844
|
-
try:
|
|
845
|
-
await asyncio.sleep(60) # Collect every minute
|
|
846
|
-
await self._collect_metrics()
|
|
847
|
-
except asyncio.CancelledError:
|
|
848
|
-
break
|
|
849
|
-
except Exception as e:
|
|
850
|
-
logger.error(f"Error in metrics loop: {e}")
|
|
851
|
-
|
|
852
|
-
async def _collect_metrics(self) -> None:
|
|
853
|
-
"""Collect load balancer metrics."""
|
|
854
|
-
with self._lock:
|
|
855
|
-
self._statistics.total_nodes = len(self._nodes)
|
|
856
|
-
self._statistics.healthy_nodes = len(self._healthy_nodes)
|
|
857
|
-
self._statistics.unhealthy_nodes = len(self._unhealthy_nodes)
|
|
858
|
-
|
|
859
|
-
# Calculate average decision time
|
|
860
|
-
if self._decision_times:
|
|
861
|
-
self._statistics.avg_decision_time_ms = sum(self._decision_times) / len(
|
|
862
|
-
self._decision_times
|
|
863
|
-
)
|
|
864
|
-
|
|
865
|
-
# Update algorithm usage
|
|
866
|
-
for node_id, node in self._nodes.items():
|
|
867
|
-
self._statistics.node_selection_count[node_id] = node.total_requests
|
|
868
|
-
|
|
869
|
-
def get_statistics(self) -> LoadBalancerStatistics:
|
|
870
|
-
"""Get load balancer statistics."""
|
|
871
|
-
return self._statistics.model_copy()
|
|
872
|
-
|
|
873
|
-
def get_node_status(self, node_id: str) -> Optional[LoadBalancerNode]:
|
|
874
|
-
"""Get node status."""
|
|
875
|
-
return self._nodes.get(node_id)
|
|
876
|
-
|
|
877
|
-
def get_all_nodes(self) -> List[LoadBalancerNode]:
|
|
878
|
-
"""Get all nodes."""
|
|
879
|
-
return list(self._nodes.values())
|
|
880
|
-
|
|
881
|
-
def get_healthy_nodes(self) -> List[LoadBalancerNode]:
|
|
882
|
-
"""Get healthy nodes."""
|
|
883
|
-
return [self._nodes[node_id] for node_id in self._healthy_nodes]
|
|
884
|
-
|
|
885
|
-
def _generate_cache_key(self, request: LoadBalancingDecisionRequest) -> str:
|
|
886
|
-
"""Generate cache key for request."""
|
|
887
|
-
key_parts = [
|
|
888
|
-
request.source_ip,
|
|
889
|
-
request.destination_ip or "",
|
|
890
|
-
str(request.destination_port or ""),
|
|
891
|
-
request.path or "",
|
|
892
|
-
str(sorted(request.required_tags.items())),
|
|
893
|
-
str(sorted(request.excluded_nodes)),
|
|
894
|
-
]
|
|
895
|
-
return hashlib.md5("|".join(key_parts).encode(), usedforsecurity=False).hexdigest()
|
|
896
|
-
|
|
897
|
-
def _is_cache_valid(self, result: LoadBalancingDecisionResult) -> bool:
|
|
898
|
-
"""Check if cached result is still valid."""
|
|
899
|
-
age = time.time() - result.timestamp.timestamp()
|
|
900
|
-
return age < self._cache_ttl_seconds
|
|
901
|
-
|
|
902
|
-
def _get_selection_factors(
|
|
903
|
-
self, node: LoadBalancerNode, algorithm: LoadBalancingAlgorithm
|
|
904
|
-
) -> Dict[str, Any]:
|
|
905
|
-
"""Get factors that influenced node selection."""
|
|
906
|
-
factors = {
|
|
907
|
-
"node_id": node.node_id,
|
|
908
|
-
"algorithm": algorithm.value,
|
|
909
|
-
"current_connections": node.current_connections,
|
|
910
|
-
"avg_response_time": node.avg_response_time_ms,
|
|
911
|
-
"health_status": node.health_status.value,
|
|
912
|
-
"weight": node.weight,
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
performance = self._node_performance.get(node.node_id)
|
|
916
|
-
if performance:
|
|
917
|
-
factors.update(
|
|
918
|
-
{
|
|
919
|
-
"success_rate": performance.success_rate,
|
|
920
|
-
"load_score": performance.load_score,
|
|
921
|
-
}
|
|
922
|
-
)
|
|
923
|
-
|
|
924
|
-
return factors
|
|
925
|
-
|
|
926
|
-
def _update_statistics(self, result: LoadBalancingDecisionResult) -> None:
|
|
927
|
-
"""Update statistics with decision result."""
|
|
928
|
-
self._statistics.total_requests += 1
|
|
929
|
-
if result.selected_node:
|
|
930
|
-
self._statistics.successful_requests += 1
|
|
931
|
-
|
|
932
|
-
self._decision_times.append(result.decision_time_ms)
|
|
933
|
-
|
|
934
|
-
# Update algorithm usage
|
|
935
|
-
algorithm_key = result.algorithm_used.value
|
|
936
|
-
self._statistics.algorithm_usage[algorithm_key] = (
|
|
937
|
-
self._statistics.algorithm_usage.get(algorithm_key, 0) + 1
|
|
938
|
-
)
|
|
939
|
-
|
|
940
|
-
def _log_info(self, message: str, **kwargs: Any) -> None:
|
|
941
|
-
"""Log info message."""
|
|
942
|
-
if self.dev_logger:
|
|
943
|
-
self.dev_logger.log_info(
|
|
944
|
-
SDKEventType.PERFORMANCE_OPTIMIZATION_APPLIED, message, **kwargs
|
|
945
|
-
)
|
|
946
|
-
else:
|
|
947
|
-
logger.info(message)
|
|
948
|
-
|
|
949
|
-
def _log_error(self, message: str, **kwargs: Any) -> None:
|
|
950
|
-
"""Log error message."""
|
|
951
|
-
if self.dev_logger:
|
|
952
|
-
self.dev_logger.log_error(SDKEventType.CRITICAL_ERROR, message, **kwargs)
|
|
953
|
-
else:
|
|
954
|
-
logger.error(message)
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
__all__ = [
|
|
958
|
-
# Main class
|
|
959
|
-
"LoadBalancer",
|
|
960
|
-
# Utility classes
|
|
961
|
-
"NodePerformanceTracker",
|
|
962
|
-
# Note: Load balancing models are available via DTO imports:
|
|
963
|
-
# from unrealon_sdk.src.dto.load_balancing import ...
|
|
964
|
-
]
|