unrealon 1.0.9__py3-none-any.whl → 1.1.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 (302) hide show
  1. unrealon/__init__.py +23 -21
  2. unrealon-1.1.1.dist-info/METADATA +722 -0
  3. unrealon-1.1.1.dist-info/RECORD +82 -0
  4. {unrealon-1.0.9.dist-info → unrealon-1.1.1.dist-info}/WHEEL +1 -1
  5. unrealon-1.1.1.dist-info/entry_points.txt +9 -0
  6. {unrealon-1.0.9.dist-info → unrealon-1.1.1.dist-info/licenses}/LICENSE +1 -1
  7. unrealon_bridge/__init__.py +114 -0
  8. unrealon_bridge/cli.py +316 -0
  9. unrealon_bridge/client/__init__.py +93 -0
  10. unrealon_bridge/client/base.py +78 -0
  11. unrealon_bridge/client/commands.py +89 -0
  12. unrealon_bridge/client/connection.py +90 -0
  13. unrealon_bridge/client/events.py +65 -0
  14. unrealon_bridge/client/health.py +38 -0
  15. unrealon_bridge/client/html_parser.py +146 -0
  16. unrealon_bridge/client/logging.py +139 -0
  17. unrealon_bridge/client/proxy.py +70 -0
  18. unrealon_bridge/client/scheduler.py +450 -0
  19. unrealon_bridge/client/session.py +70 -0
  20. unrealon_bridge/configs/__init__.py +14 -0
  21. unrealon_bridge/configs/bridge_config.py +212 -0
  22. unrealon_bridge/configs/bridge_config.yaml +39 -0
  23. unrealon_bridge/models/__init__.py +138 -0
  24. unrealon_bridge/models/base.py +28 -0
  25. unrealon_bridge/models/command.py +41 -0
  26. unrealon_bridge/models/events.py +40 -0
  27. unrealon_bridge/models/html_parser.py +79 -0
  28. unrealon_bridge/models/logging.py +55 -0
  29. unrealon_bridge/models/parser.py +63 -0
  30. unrealon_bridge/models/proxy.py +41 -0
  31. unrealon_bridge/models/requests.py +95 -0
  32. unrealon_bridge/models/responses.py +88 -0
  33. unrealon_bridge/models/scheduler.py +592 -0
  34. unrealon_bridge/models/session.py +28 -0
  35. unrealon_bridge/server/__init__.py +91 -0
  36. unrealon_bridge/server/base.py +171 -0
  37. unrealon_bridge/server/handlers/__init__.py +23 -0
  38. unrealon_bridge/server/handlers/command.py +110 -0
  39. unrealon_bridge/server/handlers/html_parser.py +139 -0
  40. unrealon_bridge/server/handlers/logging.py +95 -0
  41. unrealon_bridge/server/handlers/parser.py +95 -0
  42. unrealon_bridge/server/handlers/proxy.py +75 -0
  43. unrealon_bridge/server/handlers/scheduler.py +545 -0
  44. unrealon_bridge/server/handlers/session.py +66 -0
  45. unrealon_browser/__init__.py +61 -18
  46. unrealon_browser/{src/cli → cli}/browser_cli.py +6 -13
  47. unrealon_browser/{src/cli → cli}/cookies_cli.py +5 -1
  48. unrealon_browser/{src/core → core}/browser_manager.py +2 -2
  49. unrealon_browser/{src/managers → managers}/captcha.py +1 -1
  50. unrealon_browser/{src/managers → managers}/cookies.py +1 -1
  51. unrealon_browser/managers/logger_bridge.py +231 -0
  52. unrealon_browser/{src/managers → managers}/profile.py +1 -1
  53. unrealon_driver/__init__.py +73 -19
  54. unrealon_driver/browser/__init__.py +8 -0
  55. unrealon_driver/browser/config.py +74 -0
  56. unrealon_driver/browser/manager.py +416 -0
  57. unrealon_driver/exceptions.py +28 -0
  58. unrealon_driver/parser/__init__.py +55 -0
  59. unrealon_driver/parser/cli_manager.py +141 -0
  60. unrealon_driver/parser/daemon_manager.py +227 -0
  61. unrealon_driver/parser/managers/__init__.py +46 -0
  62. unrealon_driver/parser/managers/browser.py +51 -0
  63. unrealon_driver/parser/managers/config.py +281 -0
  64. unrealon_driver/parser/managers/error.py +412 -0
  65. unrealon_driver/parser/managers/html.py +732 -0
  66. unrealon_driver/parser/managers/logging.py +609 -0
  67. unrealon_driver/parser/managers/result.py +321 -0
  68. unrealon_driver/parser/parser_manager.py +628 -0
  69. unrealon/sdk_config.py +0 -88
  70. unrealon-1.0.9.dist-info/METADATA +0 -810
  71. unrealon-1.0.9.dist-info/RECORD +0 -246
  72. unrealon_browser/pyproject.toml +0 -182
  73. unrealon_browser/src/__init__.py +0 -62
  74. unrealon_browser/src/managers/logger_bridge.py +0 -395
  75. unrealon_driver/README.md +0 -204
  76. unrealon_driver/pyproject.toml +0 -187
  77. unrealon_driver/src/__init__.py +0 -90
  78. unrealon_driver/src/cli/__init__.py +0 -10
  79. unrealon_driver/src/cli/main.py +0 -66
  80. unrealon_driver/src/cli/simple.py +0 -510
  81. unrealon_driver/src/config/__init__.py +0 -11
  82. unrealon_driver/src/config/auto_config.py +0 -478
  83. unrealon_driver/src/core/__init__.py +0 -18
  84. unrealon_driver/src/core/exceptions.py +0 -289
  85. unrealon_driver/src/core/parser.py +0 -638
  86. unrealon_driver/src/dto/__init__.py +0 -66
  87. unrealon_driver/src/dto/cli.py +0 -119
  88. unrealon_driver/src/dto/config.py +0 -18
  89. unrealon_driver/src/dto/events.py +0 -237
  90. unrealon_driver/src/dto/execution.py +0 -313
  91. unrealon_driver/src/dto/services.py +0 -311
  92. unrealon_driver/src/execution/__init__.py +0 -23
  93. unrealon_driver/src/execution/daemon_mode.py +0 -317
  94. unrealon_driver/src/execution/interactive_mode.py +0 -88
  95. unrealon_driver/src/execution/modes.py +0 -45
  96. unrealon_driver/src/execution/scheduled_mode.py +0 -209
  97. unrealon_driver/src/execution/test_mode.py +0 -250
  98. unrealon_driver/src/logging/__init__.py +0 -24
  99. unrealon_driver/src/logging/driver_logger.py +0 -512
  100. unrealon_driver/src/services/__init__.py +0 -24
  101. unrealon_driver/src/services/browser_service.py +0 -726
  102. unrealon_driver/src/services/llm/__init__.py +0 -15
  103. unrealon_driver/src/services/llm/browser_llm_service.py +0 -363
  104. unrealon_driver/src/services/llm/llm.py +0 -195
  105. unrealon_driver/src/services/logger_service.py +0 -232
  106. unrealon_driver/src/services/metrics_service.py +0 -185
  107. unrealon_driver/src/services/scheduler_service.py +0 -489
  108. unrealon_driver/src/services/websocket_service.py +0 -362
  109. unrealon_driver/src/utils/__init__.py +0 -16
  110. unrealon_driver/src/utils/service_factory.py +0 -317
  111. unrealon_driver/src/utils/time_formatter.py +0 -338
  112. unrealon_llm/README.md +0 -44
  113. unrealon_llm/__init__.py +0 -26
  114. unrealon_llm/pyproject.toml +0 -154
  115. unrealon_llm/src/__init__.py +0 -228
  116. unrealon_llm/src/cli/__init__.py +0 -0
  117. unrealon_llm/src/core/__init__.py +0 -11
  118. unrealon_llm/src/core/smart_client.py +0 -438
  119. unrealon_llm/src/dto/__init__.py +0 -155
  120. unrealon_llm/src/dto/models/__init__.py +0 -0
  121. unrealon_llm/src/dto/models/config.py +0 -343
  122. unrealon_llm/src/dto/models/core.py +0 -328
  123. unrealon_llm/src/dto/models/enums.py +0 -123
  124. unrealon_llm/src/dto/models/html_analysis.py +0 -345
  125. unrealon_llm/src/dto/models/statistics.py +0 -473
  126. unrealon_llm/src/dto/models/translation.py +0 -383
  127. unrealon_llm/src/dto/models/type_conversion.py +0 -462
  128. unrealon_llm/src/dto/schemas/__init__.py +0 -0
  129. unrealon_llm/src/exceptions.py +0 -392
  130. unrealon_llm/src/llm_config/__init__.py +0 -20
  131. unrealon_llm/src/llm_config/logging_config.py +0 -178
  132. unrealon_llm/src/llm_logging/__init__.py +0 -42
  133. unrealon_llm/src/llm_logging/llm_events.py +0 -107
  134. unrealon_llm/src/llm_logging/llm_logger.py +0 -466
  135. unrealon_llm/src/managers/__init__.py +0 -15
  136. unrealon_llm/src/managers/cache_manager.py +0 -67
  137. unrealon_llm/src/managers/cost_manager.py +0 -107
  138. unrealon_llm/src/managers/request_manager.py +0 -298
  139. unrealon_llm/src/modules/__init__.py +0 -0
  140. unrealon_llm/src/modules/html_processor/__init__.py +0 -25
  141. unrealon_llm/src/modules/html_processor/base_processor.py +0 -415
  142. unrealon_llm/src/modules/html_processor/details_processor.py +0 -85
  143. unrealon_llm/src/modules/html_processor/listing_processor.py +0 -91
  144. unrealon_llm/src/modules/html_processor/models/__init__.py +0 -20
  145. unrealon_llm/src/modules/html_processor/models/processing_models.py +0 -40
  146. unrealon_llm/src/modules/html_processor/models/universal_model.py +0 -56
  147. unrealon_llm/src/modules/html_processor/processor.py +0 -102
  148. unrealon_llm/src/modules/llm/__init__.py +0 -0
  149. unrealon_llm/src/modules/translator/__init__.py +0 -0
  150. unrealon_llm/src/provider.py +0 -116
  151. unrealon_llm/src/utils/__init__.py +0 -95
  152. unrealon_llm/src/utils/common.py +0 -64
  153. unrealon_llm/src/utils/data_extractor.py +0 -188
  154. unrealon_llm/src/utils/html_cleaner.py +0 -767
  155. unrealon_llm/src/utils/language_detector.py +0 -308
  156. unrealon_llm/src/utils/models_cache.py +0 -592
  157. unrealon_llm/src/utils/smart_counter.py +0 -229
  158. unrealon_llm/src/utils/token_counter.py +0 -189
  159. unrealon_sdk/README.md +0 -25
  160. unrealon_sdk/__init__.py +0 -30
  161. unrealon_sdk/pyproject.toml +0 -231
  162. unrealon_sdk/src/__init__.py +0 -150
  163. unrealon_sdk/src/cli/__init__.py +0 -12
  164. unrealon_sdk/src/cli/commands/__init__.py +0 -22
  165. unrealon_sdk/src/cli/commands/benchmark.py +0 -42
  166. unrealon_sdk/src/cli/commands/diagnostics.py +0 -573
  167. unrealon_sdk/src/cli/commands/health.py +0 -46
  168. unrealon_sdk/src/cli/commands/integration.py +0 -498
  169. unrealon_sdk/src/cli/commands/reports.py +0 -43
  170. unrealon_sdk/src/cli/commands/security.py +0 -36
  171. unrealon_sdk/src/cli/commands/server.py +0 -483
  172. unrealon_sdk/src/cli/commands/servers.py +0 -56
  173. unrealon_sdk/src/cli/commands/tests.py +0 -55
  174. unrealon_sdk/src/cli/main.py +0 -126
  175. unrealon_sdk/src/cli/utils/reporter.py +0 -519
  176. unrealon_sdk/src/clients/openapi.yaml +0 -3347
  177. unrealon_sdk/src/clients/python_http/__init__.py +0 -3
  178. unrealon_sdk/src/clients/python_http/api_config.py +0 -228
  179. unrealon_sdk/src/clients/python_http/models/BaseModel.py +0 -12
  180. unrealon_sdk/src/clients/python_http/models/BroadcastDeliveryStats.py +0 -33
  181. unrealon_sdk/src/clients/python_http/models/BroadcastMessage.py +0 -17
  182. unrealon_sdk/src/clients/python_http/models/BroadcastMessageRequest.py +0 -35
  183. unrealon_sdk/src/clients/python_http/models/BroadcastPriority.py +0 -10
  184. unrealon_sdk/src/clients/python_http/models/BroadcastResponse.py +0 -21
  185. unrealon_sdk/src/clients/python_http/models/BroadcastResultResponse.py +0 -33
  186. unrealon_sdk/src/clients/python_http/models/BroadcastTarget.py +0 -11
  187. unrealon_sdk/src/clients/python_http/models/ConnectionStats.py +0 -27
  188. unrealon_sdk/src/clients/python_http/models/ConnectionsResponse.py +0 -21
  189. unrealon_sdk/src/clients/python_http/models/DeveloperMessageResponse.py +0 -23
  190. unrealon_sdk/src/clients/python_http/models/ErrorResponse.py +0 -25
  191. unrealon_sdk/src/clients/python_http/models/HTTPValidationError.py +0 -16
  192. unrealon_sdk/src/clients/python_http/models/HealthResponse.py +0 -23
  193. unrealon_sdk/src/clients/python_http/models/HealthStatus.py +0 -33
  194. unrealon_sdk/src/clients/python_http/models/LogLevel.py +0 -10
  195. unrealon_sdk/src/clients/python_http/models/LoggingRequest.py +0 -27
  196. unrealon_sdk/src/clients/python_http/models/LoggingResponse.py +0 -23
  197. unrealon_sdk/src/clients/python_http/models/MaintenanceMode.py +0 -9
  198. unrealon_sdk/src/clients/python_http/models/MaintenanceModeRequest.py +0 -33
  199. unrealon_sdk/src/clients/python_http/models/MaintenanceStatusResponse.py +0 -39
  200. unrealon_sdk/src/clients/python_http/models/ParserCommandRequest.py +0 -25
  201. unrealon_sdk/src/clients/python_http/models/ParserMessageResponse.py +0 -21
  202. unrealon_sdk/src/clients/python_http/models/ParserRegistrationRequest.py +0 -28
  203. unrealon_sdk/src/clients/python_http/models/ParserRegistrationResponse.py +0 -25
  204. unrealon_sdk/src/clients/python_http/models/ParserType.py +0 -10
  205. unrealon_sdk/src/clients/python_http/models/ProxyBlockRequest.py +0 -19
  206. unrealon_sdk/src/clients/python_http/models/ProxyEndpointResponse.py +0 -20
  207. unrealon_sdk/src/clients/python_http/models/ProxyListResponse.py +0 -19
  208. unrealon_sdk/src/clients/python_http/models/ProxyProvider.py +0 -10
  209. unrealon_sdk/src/clients/python_http/models/ProxyPurchaseRequest.py +0 -25
  210. unrealon_sdk/src/clients/python_http/models/ProxyResponse.py +0 -47
  211. unrealon_sdk/src/clients/python_http/models/ProxyRotationRequest.py +0 -23
  212. unrealon_sdk/src/clients/python_http/models/ProxyStatus.py +0 -10
  213. unrealon_sdk/src/clients/python_http/models/ProxyUsageRequest.py +0 -19
  214. unrealon_sdk/src/clients/python_http/models/ProxyUsageStatsResponse.py +0 -26
  215. unrealon_sdk/src/clients/python_http/models/ServiceRegistrationDto.py +0 -23
  216. unrealon_sdk/src/clients/python_http/models/ServiceStatsResponse.py +0 -31
  217. unrealon_sdk/src/clients/python_http/models/SessionStartRequest.py +0 -23
  218. unrealon_sdk/src/clients/python_http/models/SuccessResponse.py +0 -25
  219. unrealon_sdk/src/clients/python_http/models/SystemNotificationResponse.py +0 -23
  220. unrealon_sdk/src/clients/python_http/models/ValidationError.py +0 -18
  221. unrealon_sdk/src/clients/python_http/models/ValidationErrorResponse.py +0 -21
  222. unrealon_sdk/src/clients/python_http/models/WebSocketMetrics.py +0 -21
  223. unrealon_sdk/src/clients/python_http/models/__init__.py +0 -44
  224. unrealon_sdk/src/clients/python_http/services/None_service.py +0 -35
  225. unrealon_sdk/src/clients/python_http/services/ParserManagement_service.py +0 -190
  226. unrealon_sdk/src/clients/python_http/services/ProxyManagement_service.py +0 -289
  227. unrealon_sdk/src/clients/python_http/services/SocketLogging_service.py +0 -187
  228. unrealon_sdk/src/clients/python_http/services/SystemHealth_service.py +0 -119
  229. unrealon_sdk/src/clients/python_http/services/WebSocketAPI_service.py +0 -198
  230. unrealon_sdk/src/clients/python_http/services/__init__.py +0 -0
  231. unrealon_sdk/src/clients/python_http/services/admin_service.py +0 -125
  232. unrealon_sdk/src/clients/python_http/services/async_None_service.py +0 -35
  233. unrealon_sdk/src/clients/python_http/services/async_ParserManagement_service.py +0 -190
  234. unrealon_sdk/src/clients/python_http/services/async_ProxyManagement_service.py +0 -289
  235. unrealon_sdk/src/clients/python_http/services/async_SocketLogging_service.py +0 -189
  236. unrealon_sdk/src/clients/python_http/services/async_SystemHealth_service.py +0 -123
  237. unrealon_sdk/src/clients/python_http/services/async_WebSocketAPI_service.py +0 -200
  238. unrealon_sdk/src/clients/python_http/services/async_admin_service.py +0 -125
  239. unrealon_sdk/src/clients/python_websocket/__init__.py +0 -28
  240. unrealon_sdk/src/clients/python_websocket/client.py +0 -490
  241. unrealon_sdk/src/clients/python_websocket/events.py +0 -732
  242. unrealon_sdk/src/clients/python_websocket/example.py +0 -136
  243. unrealon_sdk/src/clients/python_websocket/types.py +0 -871
  244. unrealon_sdk/src/core/__init__.py +0 -64
  245. unrealon_sdk/src/core/client.py +0 -556
  246. unrealon_sdk/src/core/config.py +0 -465
  247. unrealon_sdk/src/core/exceptions.py +0 -239
  248. unrealon_sdk/src/core/metadata.py +0 -191
  249. unrealon_sdk/src/core/models.py +0 -142
  250. unrealon_sdk/src/core/types.py +0 -68
  251. unrealon_sdk/src/dto/__init__.py +0 -268
  252. unrealon_sdk/src/dto/authentication.py +0 -108
  253. unrealon_sdk/src/dto/cache.py +0 -208
  254. unrealon_sdk/src/dto/common.py +0 -19
  255. unrealon_sdk/src/dto/concurrency.py +0 -393
  256. unrealon_sdk/src/dto/events.py +0 -108
  257. unrealon_sdk/src/dto/health.py +0 -339
  258. unrealon_sdk/src/dto/load_balancing.py +0 -336
  259. unrealon_sdk/src/dto/logging.py +0 -230
  260. unrealon_sdk/src/dto/performance.py +0 -165
  261. unrealon_sdk/src/dto/rate_limiting.py +0 -295
  262. unrealon_sdk/src/dto/resource_pooling.py +0 -128
  263. unrealon_sdk/src/dto/structured_logging.py +0 -112
  264. unrealon_sdk/src/dto/task_scheduling.py +0 -121
  265. unrealon_sdk/src/dto/websocket.py +0 -55
  266. unrealon_sdk/src/enterprise/__init__.py +0 -59
  267. unrealon_sdk/src/enterprise/authentication.py +0 -401
  268. unrealon_sdk/src/enterprise/cache_manager.py +0 -578
  269. unrealon_sdk/src/enterprise/error_recovery.py +0 -494
  270. unrealon_sdk/src/enterprise/event_system.py +0 -549
  271. unrealon_sdk/src/enterprise/health_monitor.py +0 -747
  272. unrealon_sdk/src/enterprise/load_balancer.py +0 -964
  273. unrealon_sdk/src/enterprise/logging/__init__.py +0 -68
  274. unrealon_sdk/src/enterprise/logging/cleanup.py +0 -156
  275. unrealon_sdk/src/enterprise/logging/development.py +0 -744
  276. unrealon_sdk/src/enterprise/logging/service.py +0 -410
  277. unrealon_sdk/src/enterprise/multithreading_manager.py +0 -853
  278. unrealon_sdk/src/enterprise/performance_monitor.py +0 -539
  279. unrealon_sdk/src/enterprise/proxy_manager.py +0 -696
  280. unrealon_sdk/src/enterprise/rate_limiter.py +0 -652
  281. unrealon_sdk/src/enterprise/resource_pool.py +0 -763
  282. unrealon_sdk/src/enterprise/task_scheduler.py +0 -709
  283. unrealon_sdk/src/internal/__init__.py +0 -10
  284. unrealon_sdk/src/internal/command_router.py +0 -497
  285. unrealon_sdk/src/internal/connection_manager.py +0 -397
  286. unrealon_sdk/src/internal/http_client.py +0 -446
  287. unrealon_sdk/src/internal/websocket_client.py +0 -420
  288. unrealon_sdk/src/provider.py +0 -471
  289. unrealon_sdk/src/utils.py +0 -234
  290. /unrealon_browser/{src/cli → cli}/__init__.py +0 -0
  291. /unrealon_browser/{src/cli → cli}/interactive_mode.py +0 -0
  292. /unrealon_browser/{src/cli → cli}/main.py +0 -0
  293. /unrealon_browser/{src/core → core}/__init__.py +0 -0
  294. /unrealon_browser/{src/dto → dto}/__init__.py +0 -0
  295. /unrealon_browser/{src/dto → dto}/models/config.py +0 -0
  296. /unrealon_browser/{src/dto → dto}/models/core.py +0 -0
  297. /unrealon_browser/{src/dto → dto}/models/dataclasses.py +0 -0
  298. /unrealon_browser/{src/dto → dto}/models/detection.py +0 -0
  299. /unrealon_browser/{src/dto → dto}/models/enums.py +0 -0
  300. /unrealon_browser/{src/dto → dto}/models/statistics.py +0 -0
  301. /unrealon_browser/{src/managers → managers}/__init__.py +0 -0
  302. /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
- ]