polaris-python 0.0.1.dev3__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 (84) hide show
  1. polaris/__init__.py +74 -0
  2. polaris/_version.py +16 -0
  3. polaris/api/__init__.py +55 -0
  4. polaris/api/_log_file.py +232 -0
  5. polaris/api/config.py +145 -0
  6. polaris/api/consumer.py +240 -0
  7. polaris/api/log.py +414 -0
  8. polaris/api/lossless.py +73 -0
  9. polaris/api/provider.py +152 -0
  10. polaris/api/skill.py +162 -0
  11. polaris/pkg/__init__.py +15 -0
  12. polaris/pkg/common/__init__.py +15 -0
  13. polaris/pkg/common/plugin.py +33 -0
  14. polaris/pkg/config/__init__.py +15 -0
  15. polaris/pkg/config/api.py +1205 -0
  16. polaris/pkg/config/default.py +92 -0
  17. polaris/pkg/config/event.py +76 -0
  18. polaris/pkg/config/loader.py +392 -0
  19. polaris/pkg/config/stat_reporter.py +100 -0
  20. polaris/pkg/flow/__init__.py +15 -0
  21. polaris/pkg/flow/admin_server.py +104 -0
  22. polaris/pkg/flow/async_flow.py +55 -0
  23. polaris/pkg/flow/base_flow.py +111 -0
  24. polaris/pkg/flow/impl.py +1277 -0
  25. polaris/pkg/flow/lossless.py +605 -0
  26. polaris/pkg/flow/notify.py +46 -0
  27. polaris/pkg/flow/register.py +210 -0
  28. polaris/pkg/flow/skill.py +110 -0
  29. polaris/pkg/flow/sync_flow.py +20 -0
  30. polaris/pkg/model/__init__.py +15 -0
  31. polaris/pkg/model/context.py +53 -0
  32. polaris/pkg/model/contract.py +89 -0
  33. polaris/pkg/model/core.py +51 -0
  34. polaris/pkg/model/engine.py +83 -0
  35. polaris/pkg/model/error.py +94 -0
  36. polaris/pkg/model/event.py +159 -0
  37. polaris/pkg/model/lossless.py +102 -0
  38. polaris/pkg/model/rule.py +152 -0
  39. polaris/pkg/model/server.py +288 -0
  40. polaris/pkg/model/service.py +820 -0
  41. polaris/pkg/model/skill.py +179 -0
  42. polaris/pkg/model/stat.py +168 -0
  43. polaris/pkg/model/util.py +58 -0
  44. polaris/pkg/version/__init__.py +18 -0
  45. polaris/plugin/__init__.py +32 -0
  46. polaris/plugin/circuit_breaker/__init__.py +19 -0
  47. polaris/plugin/circuit_breaker/base.py +54 -0
  48. polaris/plugin/circuit_breaker/composite.py +265 -0
  49. polaris/plugin/circuit_breaker/config.py +36 -0
  50. polaris/plugin/event/__init__.py +21 -0
  51. polaris/plugin/event/pushgateway.py +202 -0
  52. polaris/plugin/load_balancer/__init__.py +24 -0
  53. polaris/plugin/load_balancer/base.py +30 -0
  54. polaris/plugin/load_balancer/random.py +39 -0
  55. polaris/plugin/load_balancer/weighted_random.py +50 -0
  56. polaris/plugin/local_registry/__init__.py +18 -0
  57. polaris/plugin/local_registry/base.py +47 -0
  58. polaris/plugin/local_registry/file_cache.py +335 -0
  59. polaris/plugin/local_registry/local_cache.py +433 -0
  60. polaris/plugin/server_connector/__init__.py +20 -0
  61. polaris/plugin/server_connector/base.py +104 -0
  62. polaris/plugin/server_connector/convert.py +149 -0
  63. polaris/plugin/server_connector/grpc_connector/__init__.py +21 -0
  64. polaris/plugin/server_connector/grpc_connector/connection.py +401 -0
  65. polaris/plugin/server_connector/grpc_connector/grpc_client.py +384 -0
  66. polaris/plugin/server_connector/grpc_connector/tls.py +45 -0
  67. polaris/plugin/service_router/__init__.py +28 -0
  68. polaris/plugin/service_router/base.py +105 -0
  69. polaris/plugin/service_router/isolated.py +35 -0
  70. polaris/plugin/service_router/metadata.py +84 -0
  71. polaris/plugin/service_router/recover.py +62 -0
  72. polaris/plugin/service_router/util.py +54 -0
  73. polaris/plugin/skill/__init__.py +16 -0
  74. polaris/plugin/skill/connector.py +127 -0
  75. polaris/plugin/skill/persistence.py +94 -0
  76. polaris/plugin/stat_reporter/__init__.py +16 -0
  77. polaris/plugin/stat_reporter/base.py +34 -0
  78. polaris/plugin/stat_reporter/prometheus.py +375 -0
  79. polaris/plugin_manager.py +146 -0
  80. polaris_python-0.0.1.dev3.dist-info/METADATA +213 -0
  81. polaris_python-0.0.1.dev3.dist-info/RECORD +84 -0
  82. polaris_python-0.0.1.dev3.dist-info/WHEEL +5 -0
  83. polaris_python-0.0.1.dev3.dist-info/licenses/LICENSE +18 -0
  84. polaris_python-0.0.1.dev3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,265 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ import time
17
+
18
+ from .base import BaseCircuitBreaker, CircuitBreakResult
19
+ from ...pkg.model.service import Instance, CBStatusEnum
20
+ from ...pkg.model.stat import Bucket
21
+ from typing import List
22
+ from . import config
23
+ from polaris.plugin_manager import InitContext, EventType, PluginType, register_plugin
24
+ from polaris.pkg.model.stat import SlidingWindow
25
+ from polaris.api.log import get_logger
26
+
27
+ logger = get_logger("circuitbreaker")
28
+
29
+
30
+ class PluginConfig:
31
+ def __init__(self, continuous_err_threshold=config.DefaultOptions.DefaultContinuousErrorThreshold,
32
+ metric_stat_time_window=config.DefaultOptions.DefaultMetricStatWindow,
33
+ metric_num_bucket=config.DefaultOptions.DefaultMetricBucketCount,
34
+ sleep_window=config.DefaultOptions.DefaultSleepTime,
35
+ request_count_after_halfopen=config.DefaultOptions.DefaultRequestCountAfterHalfOpen,
36
+ success_count_after_halfopen=config.DefaultOptions.DefaultSuccessCountAfterHalfOpen,
37
+ err_rate_threshold=config.DefaultOptions.DefaultErrRateThreshold,
38
+ err_rate_request_threshold=config.DefaultOptions.DefaultErrRateRequestThreshold):
39
+ # 连续失败数量
40
+ self.continuous_error_threshold = continuous_err_threshold
41
+ self.metric_stat_time_window = metric_stat_time_window
42
+ # bucket数量
43
+ self.metric_num_bucket = metric_num_bucket
44
+ # 熔断后多久转为半开
45
+ self.sleep_window = sleep_window
46
+ self.request_count_after_halfopen = request_count_after_halfopen
47
+ # 半开后多少请求后可以转为恢复
48
+ self.success_count_after_halfopen = success_count_after_halfopen
49
+
50
+ # 失败率
51
+ self.err_rate_threshold = err_rate_threshold
52
+ self.err_rate_request_threshold = err_rate_request_threshold
53
+
54
+
55
+ class InstanceMetrics:
56
+ """Per-trigger counters, mirroring Java composite TriggerCounter state."""
57
+
58
+ def __init__(self, options: PluginConfig):
59
+ self.window = SlidingWindow(options.metric_num_bucket,
60
+ (options.metric_stat_time_window + options.metric_num_bucket - 1)
61
+ // options.metric_num_bucket)
62
+ self.consecutive_errors = 0
63
+ self.consecutive_suspended = False
64
+ self.consecutive_triggered = False
65
+ self.error_rate_suspended = False
66
+ self.error_rate_check_at = None
67
+
68
+
69
+ @register_plugin(PluginType.CIRCUIT_BREAKER, "composite")
70
+ class CircuitBreaker(BaseCircuitBreaker):
71
+ def __init__(self, ctx: InitContext = None):
72
+ self.ctx = ctx
73
+ self.config = ctx.config.get_consumer().get_circuit_breaker() if ctx is not None else None
74
+ self.plugin_config = PluginConfig()
75
+ self.enabled = self.config is None or (self.config.is_enable() and self.config.is_default_rule_enable())
76
+ if self.config is not None:
77
+ interval_seconds = self.config.get_default_interval() // 1000
78
+ if interval_seconds == 0:
79
+ interval_seconds = config.DefaultOptions.DefaultMetricStatWindow // 1000
80
+ sleep_seconds = self.config.get_sleep_window() // 1000
81
+ self.plugin_config = PluginConfig(
82
+ continuous_err_threshold=self.config.get_default_error_count(),
83
+ metric_stat_time_window=interval_seconds * 1000,
84
+ sleep_window=sleep_seconds * 1000 if sleep_seconds > 0 else self.config.get_sleep_window(),
85
+ request_count_after_halfopen=self.config.get_success_count_after_half_open(),
86
+ success_count_after_halfopen=self.config.get_success_count_after_half_open(),
87
+ err_rate_threshold=self.config.get_default_error_percent() / 100,
88
+ err_rate_request_threshold=self.config.get_default_minimum_request(),
89
+ )
90
+
91
+ if ctx is not None:
92
+ ctx.plugins.register_event_handle(EventType.ON_LOCALVALUE_CREATED, self)
93
+
94
+ @staticmethod
95
+ def now():
96
+ return int(time.time() * 1000)
97
+
98
+ def _metrics(self, local_value):
99
+ if local_value.circuit_breaker_metrics is None:
100
+ metrics = InstanceMetrics(self.plugin_config)
101
+ local_value.circuit_breaker_metrics = metrics
102
+ local_value.set_sliding_window(metrics.window)
103
+ return local_value.circuit_breaker_metrics
104
+
105
+ def realtime_circuit_break(self, instance: Instance, ret: int, cost: int) -> bool:
106
+ if not self.enabled:
107
+ return False
108
+ success = ret != -1 and not 500 <= ret <= 599
109
+ status = instance.get_circuit_breaker_status()
110
+ if status.get_status() == CBStatusEnum.HalfOpen:
111
+ status.report_half_open_result(success)
112
+ return (self.halfopen2open(instance, self.now()) or
113
+ self.halfopen2close(instance, self.now()))
114
+
115
+ metrics = self._metrics(instance.get_local_value())
116
+ now = self.now()
117
+
118
+ if not metrics.error_rate_suspended:
119
+ def callback(bucket: Bucket, value):
120
+ if success:
121
+ bucket.success += value
122
+ else:
123
+ bucket.total_failed += value
124
+
125
+ metrics.window.add(1, callback, now=now)
126
+ if not success and metrics.error_rate_check_at is None:
127
+ metrics.error_rate_check_at = now + self.plugin_config.metric_stat_time_window
128
+
129
+ if not metrics.consecutive_suspended:
130
+ if success:
131
+ metrics.consecutive_errors = 0
132
+ else:
133
+ metrics.consecutive_errors += 1
134
+ if metrics.consecutive_errors == self.plugin_config.continuous_error_threshold:
135
+ metrics.consecutive_suspended = True
136
+ metrics.consecutive_errors = 0
137
+ if status.get_status() == CBStatusEnum.Open:
138
+ # The state change handler ignores close-to-open here.
139
+ return False
140
+ metrics.consecutive_triggered = True
141
+ return True
142
+ return False
143
+
144
+ def timing_circuit_break(self, instance_list: List[Instance]) -> CircuitBreakResult:
145
+ now = self.now()
146
+ result = CircuitBreakResult(ts=now)
147
+ if not self.enabled:
148
+ return result
149
+ for instance in instance_list:
150
+ if self.close2Open(instance, now):
151
+ logger.info("open circuit break for %s:%d", instance.get_host(), instance.get_port())
152
+ result.instance_2_open.append(instance)
153
+ continue
154
+ elif self.open2halfopen(instance, now):
155
+ logger.info("half open circuit break for %s:%d", instance.get_host(), instance.get_port())
156
+ result.instance_2_halfopen.append(instance)
157
+ continue
158
+ elif self.halfopen2open(instance, now):
159
+ logger.info("open circuit break after half open for %s:%d", instance.get_host(), instance.get_port())
160
+ result.instance_2_open.append(instance)
161
+ continue
162
+ elif self.halfopen2close(instance, now):
163
+ logger.info("close circuit break after half open for %s:%d", instance.get_host(),
164
+ instance.get_port())
165
+ result.instance_2_close.append(instance)
166
+ continue
167
+
168
+ return result
169
+
170
+ def close2Open(self, instance: Instance, now: int) -> bool:
171
+ status = instance.get_circuit_breaker_status().status
172
+ if status == CBStatusEnum.Open:
173
+ metrics = instance.get_local_value().circuit_breaker_metrics
174
+ if metrics is not None:
175
+ self._consume_due_error_rate_check(metrics, now)
176
+ return False
177
+ if status != CBStatusEnum.Closed:
178
+ return False
179
+
180
+ metrics = instance.get_local_value().circuit_breaker_metrics
181
+ if metrics is None:
182
+ return False
183
+ if metrics.consecutive_triggered:
184
+ return True
185
+ if metrics.error_rate_check_at is None or now < metrics.error_rate_check_at:
186
+ return False
187
+ metrics.error_rate_check_at = None
188
+ if self._error_rate_reaches_threshold(metrics, now):
189
+ metrics.error_rate_suspended = True
190
+ return True
191
+ return False
192
+
193
+ def _error_rate_reaches_threshold(self, metrics, now: int) -> bool:
194
+ total_success, total_failed, _ = metrics.window.calc_interval_values(
195
+ now - self.plugin_config.metric_stat_time_window, now)
196
+ total_request = total_failed + total_success
197
+ if total_request < self.plugin_config.err_rate_request_threshold:
198
+ return False
199
+ return total_failed / total_request >= self.plugin_config.err_rate_threshold
200
+
201
+ def _consume_due_error_rate_check(self, metrics, now: int):
202
+ if metrics.error_rate_check_at is None or now < metrics.error_rate_check_at:
203
+ return
204
+ metrics.error_rate_check_at = None
205
+ if self._error_rate_reaches_threshold(metrics, now):
206
+ metrics.error_rate_suspended = True
207
+
208
+ def open2halfopen(self, instance: Instance, now: int) -> bool:
209
+ cb_status = instance.get_circuit_breaker_status()
210
+ if cb_status.status != CBStatusEnum.Open:
211
+ return False
212
+
213
+ if now - cb_status.ts < self.plugin_config.sleep_window:
214
+ return False
215
+ metrics = instance.get_local_value().circuit_breaker_metrics
216
+ if metrics is not None:
217
+ self._consume_due_error_rate_check(metrics, now)
218
+ return True
219
+
220
+ def halfopen2open(self, instance: Instance, now: int) -> bool:
221
+ cb_status = instance.get_circuit_breaker_status()
222
+ if cb_status.status != CBStatusEnum.HalfOpen:
223
+ return False
224
+ return cb_status.failed_requests_after_half_open > 0
225
+
226
+ def halfopen2close(self, instance: Instance, now: int) -> bool:
227
+ cb_status = instance.get_circuit_breaker_status()
228
+ if cb_status.status != CBStatusEnum.HalfOpen:
229
+ return False
230
+ return (cb_status.failed_requests_after_half_open == 0 and
231
+ cb_status.successful_requests_after_half_open >= self.plugin_config.success_count_after_halfopen)
232
+
233
+ def next_check_time(self, instance: Instance):
234
+ if not self.enabled:
235
+ return None
236
+ status = instance.get_circuit_breaker_status()
237
+ if status.get_status() == CBStatusEnum.Open:
238
+ due = status.ts + self.plugin_config.sleep_window
239
+ metrics = instance.get_local_value().circuit_breaker_metrics
240
+ if metrics is not None and metrics.error_rate_check_at is not None:
241
+ due = min(due, metrics.error_rate_check_at)
242
+ return due
243
+ metrics = instance.get_local_value().circuit_breaker_metrics
244
+ if status.get_status() == CBStatusEnum.Closed and metrics is not None:
245
+ return metrics.error_rate_check_at
246
+ return None
247
+
248
+ def on_state_change(self, instance: Instance):
249
+ metrics = instance.get_local_value().circuit_breaker_metrics
250
+ if metrics is None:
251
+ return
252
+ status = instance.get_circuit_breaker_status().get_status()
253
+ if status == CBStatusEnum.Open:
254
+ metrics.consecutive_triggered = False
255
+ elif status == CBStatusEnum.Closed:
256
+ metrics.consecutive_suspended = False
257
+ metrics.error_rate_suspended = False
258
+
259
+ def get_half_open_request_count(self):
260
+ return self.plugin_config.request_count_after_halfopen
261
+
262
+ def on_plugin_event(self, event_type, params):
263
+ # Counters are created lazily on the first report, matching Java.
264
+ return
265
+
@@ -0,0 +1,36 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ from polaris.pkg.config.default import DefaultConf
17
+
18
+
19
+ class DefaultOptions:
20
+ # 连续错误数统计周期, 默认1分钟
21
+ DefaultContinuousWindow = 60000
22
+ # 触发熔断的连续错误数,默认10
23
+ DefaultContinuousErrorThreshold = DefaultConf.DefaultErrorCount
24
+ # 窗口时长(ms)
25
+ DefaultMetricStatWindow = DefaultConf.DefaultInterval
26
+ # 滑桶数量
27
+ DefaultMetricBucketCount = 10
28
+ DefaultSleepTime = DefaultConf.DefaultSleepWindow
29
+
30
+ # composite 默认规则按恢复成功数分配半开请求。
31
+ DefaultRequestCountAfterHalfOpen = DefaultConf.DefaultSuccessCountAfterHalfOpen
32
+ # 半开状态后默认3个成功请求则恢复
33
+ DefaultSuccessCountAfterHalfOpen = DefaultConf.DefaultSuccessCountAfterHalfOpen
34
+
35
+ DefaultErrRateThreshold = DefaultConf.DefaultErrorPercent / 100
36
+ DefaultErrRateRequestThreshold = DefaultConf.DefaultMinimumRequest
@@ -0,0 +1,21 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ """Event reporter plugins."""
18
+
19
+ from .pushgateway import PushGatewayEventReporter
20
+
21
+ __all__ = ["PushGatewayEventReporter"]
@@ -0,0 +1,202 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ """Batch SDK events and report them to the Polaris event Pushgateway."""
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ from datetime import datetime
23
+ import json
24
+ import random
25
+ from urllib.parse import urlsplit, urlunsplit
26
+
27
+ from aiohttp import ClientSession, ClientTimeout
28
+
29
+ from polaris.pkg.model.context import ContextKeyEventCenter
30
+ from polaris.pkg.model.event import is_remote_reportable
31
+ from polaris.pkg.model.service import GetOneInstanceRequest
32
+ from polaris.plugin_manager import PluginType, register_plugin
33
+
34
+
35
+ def _event_payload(event):
36
+ """Convert the public event to the flat wire fields used by Java and Go."""
37
+ return {
38
+ "event_type": event.event_type,
39
+ "event_name": event.event_name,
40
+ "event_time": datetime.fromtimestamp(event.timestamp / 1000).isoformat(timespec="milliseconds"),
41
+ "client_id": event.client_id,
42
+ "client_ip": event.client_ip,
43
+ "namespace": event.namespace,
44
+ "service": event.service,
45
+ "instance_id": event.instance_id,
46
+ "host": event.host,
47
+ "port": str(event.port) if event.port else "",
48
+ "previous_status": event.previous_status,
49
+ "current_status": event.current_status,
50
+ "resource_type": "INSTANCE",
51
+ "rule_name": event.rule_name,
52
+ "reason": event.reason,
53
+ "additional_params": event.additional,
54
+ }
55
+
56
+
57
+ @register_plugin(PluginType.EVENT_REPORTER, "pushgateway")
58
+ class PushGatewayEventReporter:
59
+ """Own one context-local event queue, batching task and HTTP session."""
60
+
61
+ def __init__(self, init_ctx):
62
+ reporter = init_ctx.config.get_global().get_event_reporter()
63
+ self.config = reporter.get_pushgateway()
64
+ self.enabled = reporter.is_enable() and "pushgateway" in reporter.get_chain()
65
+ self.value_context = init_ctx.value_ctx
66
+ self.logger = init_ctx.logger.get_logger("event") if init_ctx.logger is not None else None
67
+ self.engine = None
68
+ self.queue = None
69
+ self.task = None
70
+ self.session = None
71
+ self.stopping = None
72
+ self.closed = False
73
+ self.random = random.Random()
74
+
75
+ def on_sdk_event(self, event):
76
+ """Enqueue one Java-reportable event without blocking SDK control flow."""
77
+ if not self.enabled or self.closed or self.queue is None or not is_remote_reportable(event):
78
+ return
79
+ try:
80
+ self.queue.put_nowait(event)
81
+ except asyncio.QueueFull:
82
+ if self.logger is not None:
83
+ self.logger.warning("event Pushgateway queue is full; dropping %s", event.event_name)
84
+
85
+ async def start(self, engine):
86
+ """Create queue resources and subscribe on the context owner loop."""
87
+ if not self.enabled:
88
+ return
89
+ self.engine = engine
90
+ self.queue = asyncio.Queue(maxsize=self.config.event_queue_size)
91
+ self.stopping = asyncio.Event()
92
+ self.session = ClientSession(timeout=ClientTimeout(total=3))
93
+ event_center = self.value_context.core_map.get(ContextKeyEventCenter)
94
+ if event_center is not None:
95
+ event_center.register_listener(self)
96
+ self.task = asyncio.create_task(self._run(), name="polaris-event-pushgateway")
97
+
98
+ async def _run(self):
99
+ interval = self.config.push_interval / 1000
100
+ while not self.closed:
101
+ try:
102
+ await asyncio.wait_for(self.stopping.wait(), interval)
103
+ except TimeoutError:
104
+ pass
105
+ await self._flush_batches()
106
+ if self.stopping.is_set():
107
+ return
108
+
109
+ async def _address(self):
110
+ if self.config.address:
111
+ address = self.random.choice(self.config.address)
112
+ else:
113
+ node = await self.engine._async_get_one_instance(GetOneInstanceRequest(
114
+ namespace=self.config.namespace, service=self.config.service))
115
+ if node is None:
116
+ raise RuntimeError("No event Pushgateway instance available")
117
+ host = node.get_host()
118
+ address = f"[{host}]:{node.get_port()}" if ":" in host else f"{host}:{node.get_port()}"
119
+ url = urlsplit(address if "://" in address else f"http://{address}")
120
+ host = self.config.override_host or url.hostname
121
+ netloc = f"[{host}]" if ":" in host else host
122
+ if url.port:
123
+ netloc += f":{url.port}"
124
+ return urlunsplit((url.scheme or "http", netloc, self.config.report_path, "", ""))
125
+
126
+ def _log_post(self, level, message, *args):
127
+ if self.logger is None:
128
+ return
129
+ getattr(self.logger, level)(message, *args)
130
+
131
+ def _log_post_events(self, level, events, prefix, error=None):
132
+ """Record one line per event, including the same text as the local event log."""
133
+ for event in events:
134
+ detail = event.to_log_message()
135
+ if error is None:
136
+ self._log_post(level, "%s: %s", prefix, detail)
137
+ else:
138
+ self._log_post(level, "%s: %s: %s", prefix, detail, error)
139
+
140
+ async def _post(self, events):
141
+ body = json.dumps({"batch": [_event_payload(event) for event in events]},
142
+ ensure_ascii=False, separators=(",", ":"))
143
+ try:
144
+ target = await self._address()
145
+ except Exception as error:
146
+ self._log_post_events("warning", events, "failed to report event to Pushgateway", error)
147
+ raise
148
+ last_error = None
149
+ for attempt in range(1, self.config.max_retries + 1):
150
+ try:
151
+ async with self.session.post(
152
+ target, data=body.encode("utf-8"), headers={"Content-Type": "application/json"}) as response:
153
+ await response.read()
154
+ if not 200 <= response.status < 300:
155
+ raise RuntimeError(f"Pushgateway returned HTTP {response.status}")
156
+ self._log_post_events("info", events, "reported event to Pushgateway")
157
+ return
158
+ except Exception as error:
159
+ last_error = error
160
+ self._log_post_events(
161
+ "warning", events,
162
+ f"failed to report event to Pushgateway (attempt {attempt}/{self.config.max_retries})",
163
+ error)
164
+ if attempt < self.config.max_retries:
165
+ await asyncio.sleep(self.config.retry_interval / 1000)
166
+ raise last_error
167
+
168
+ async def _flush_batches(self):
169
+ while self.queue is not None and not self.queue.empty():
170
+ events = []
171
+ while len(events) < self.config.max_batch_size:
172
+ try:
173
+ events.append(self.queue.get_nowait())
174
+ except asyncio.QueueEmpty:
175
+ break
176
+ if not events:
177
+ return
178
+ try:
179
+ await self._post(events)
180
+ except Exception:
181
+ pass
182
+
183
+ async def close(self):
184
+ """Stop consuming, flush accepted events, and close the HTTP session."""
185
+ if self.closed:
186
+ return
187
+ self.closed = True
188
+ event_center = self.value_context.core_map.get(ContextKeyEventCenter) if self.value_context else None
189
+ if event_center is not None:
190
+ event_center.unregister_listener(self)
191
+ if self.stopping is not None:
192
+ self.stopping.set()
193
+ if self.task is not None:
194
+ await asyncio.gather(self.task, return_exceptions=True)
195
+ self.task = None
196
+ if self.session is not None:
197
+ await self.session.close()
198
+ self.session = None
199
+
200
+ def destroy(self):
201
+ """Synchronous plugin lifecycle hook; async resources close with the engine."""
202
+ return None
@@ -0,0 +1,24 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ from .base import BaseLoadBalancer
17
+ from .random import RandomLoadBalancer
18
+ from .weighted_random import WeightedRandomLoadBalancer
19
+
20
+ __all__ = [
21
+ "BaseLoadBalancer",
22
+ "RandomLoadBalancer",
23
+ "WeightedRandomLoadBalancer",
24
+ ]
@@ -0,0 +1,30 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ from abc import ABC, abstractmethod
17
+
18
+ from polaris.pkg.model.service import Instance, ServiceInstances
19
+
20
+ __all__ = ["BaseLoadBalancer"]
21
+
22
+
23
+ class BaseLoadBalancer(ABC):
24
+ """
25
+ 负载均衡选择一个实例
26
+ """
27
+
28
+ @abstractmethod
29
+ def choose_instance(self, instances: ServiceInstances) -> Instance:
30
+ pass
@@ -0,0 +1,39 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ from random import randint
17
+ from polaris.plugin_manager import PluginType, register_plugin
18
+ from polaris.pkg.model.service import ServiceInstances, Instance
19
+ from ...pkg.model.error import ErrorCode, SDKError
20
+ from polaris.plugin_manager import InitContext
21
+
22
+ from .base import BaseLoadBalancer
23
+
24
+
25
+ @register_plugin(PluginType.LOAD_BALANCE, 'random')
26
+ class RandomLoadBalancer(BaseLoadBalancer):
27
+ def __init__(self, init_ctx: InitContext = None):
28
+ self.init_ctx = init_ctx
29
+
30
+ def choose_instance(self, instances: ServiceInstances) -> Instance:
31
+ svc_instances = instances.get_instances()
32
+ if len(svc_instances) == 0:
33
+ raise SDKError(ErrorCode.ErrCodeAPIInstanceNotFound, "instances of %s:%s not found for load balance"
34
+ % (instances.get_namespace(), instances.get_service()))
35
+ n = randint(0, len(svc_instances) - 1)
36
+ return svc_instances[n]
37
+
38
+
39
+ __all__ = ["RandomLoadBalancer"]
@@ -0,0 +1,50 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ from .base import BaseLoadBalancer
17
+ from ...pkg.model.service import ServiceInstances, Instance
18
+ import random
19
+ from ...pkg.model.error import SDKError, ErrorCode
20
+ from polaris.plugin_manager import PluginType, register_plugin, InitContext
21
+
22
+
23
+ @register_plugin(PluginType.LOAD_BALANCE, 'weightedRandom')
24
+ class WeightedRandomLoadBalancer(BaseLoadBalancer):
25
+ def __init__(self, init_ctx: InitContext = None):
26
+ self.config = init_ctx.config if init_ctx is not None else None
27
+ self.init_ctx = init_ctx
28
+
29
+ def choose_instance(self, instances: ServiceInstances) -> Instance:
30
+ svc_instances = instances.get_instances()
31
+ if len(svc_instances) == 0:
32
+ raise SDKError(ErrorCode.ErrCodeAPIInstanceNotFound, "instances of %s:%s not found for load balance"
33
+ % (instances.get_namespace(), instances.get_service()))
34
+ total_weight = 0
35
+ curr_sum = 0
36
+ ret_instance = None
37
+ for i in range(len(svc_instances)):
38
+ total_weight += svc_instances[i].get_weight()
39
+ if total_weight == 0:
40
+ return None
41
+ ra = random.randint(1, total_weight)
42
+ for i in range(len(svc_instances)):
43
+ curr_sum += svc_instances[i].get_weight()
44
+ if ra <= curr_sum:
45
+ ret_instance = svc_instances[i]
46
+ break
47
+ return ret_instance
48
+
49
+
50
+ __all__ = ["WeightedRandomLoadBalancer"]