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.
- polaris/__init__.py +74 -0
- polaris/_version.py +16 -0
- polaris/api/__init__.py +55 -0
- polaris/api/_log_file.py +232 -0
- polaris/api/config.py +145 -0
- polaris/api/consumer.py +240 -0
- polaris/api/log.py +414 -0
- polaris/api/lossless.py +73 -0
- polaris/api/provider.py +152 -0
- polaris/api/skill.py +162 -0
- polaris/pkg/__init__.py +15 -0
- polaris/pkg/common/__init__.py +15 -0
- polaris/pkg/common/plugin.py +33 -0
- polaris/pkg/config/__init__.py +15 -0
- polaris/pkg/config/api.py +1205 -0
- polaris/pkg/config/default.py +92 -0
- polaris/pkg/config/event.py +76 -0
- polaris/pkg/config/loader.py +392 -0
- polaris/pkg/config/stat_reporter.py +100 -0
- polaris/pkg/flow/__init__.py +15 -0
- polaris/pkg/flow/admin_server.py +104 -0
- polaris/pkg/flow/async_flow.py +55 -0
- polaris/pkg/flow/base_flow.py +111 -0
- polaris/pkg/flow/impl.py +1277 -0
- polaris/pkg/flow/lossless.py +605 -0
- polaris/pkg/flow/notify.py +46 -0
- polaris/pkg/flow/register.py +210 -0
- polaris/pkg/flow/skill.py +110 -0
- polaris/pkg/flow/sync_flow.py +20 -0
- polaris/pkg/model/__init__.py +15 -0
- polaris/pkg/model/context.py +53 -0
- polaris/pkg/model/contract.py +89 -0
- polaris/pkg/model/core.py +51 -0
- polaris/pkg/model/engine.py +83 -0
- polaris/pkg/model/error.py +94 -0
- polaris/pkg/model/event.py +159 -0
- polaris/pkg/model/lossless.py +102 -0
- polaris/pkg/model/rule.py +152 -0
- polaris/pkg/model/server.py +288 -0
- polaris/pkg/model/service.py +820 -0
- polaris/pkg/model/skill.py +179 -0
- polaris/pkg/model/stat.py +168 -0
- polaris/pkg/model/util.py +58 -0
- polaris/pkg/version/__init__.py +18 -0
- polaris/plugin/__init__.py +32 -0
- polaris/plugin/circuit_breaker/__init__.py +19 -0
- polaris/plugin/circuit_breaker/base.py +54 -0
- polaris/plugin/circuit_breaker/composite.py +265 -0
- polaris/plugin/circuit_breaker/config.py +36 -0
- polaris/plugin/event/__init__.py +21 -0
- polaris/plugin/event/pushgateway.py +202 -0
- polaris/plugin/load_balancer/__init__.py +24 -0
- polaris/plugin/load_balancer/base.py +30 -0
- polaris/plugin/load_balancer/random.py +39 -0
- polaris/plugin/load_balancer/weighted_random.py +50 -0
- polaris/plugin/local_registry/__init__.py +18 -0
- polaris/plugin/local_registry/base.py +47 -0
- polaris/plugin/local_registry/file_cache.py +335 -0
- polaris/plugin/local_registry/local_cache.py +433 -0
- polaris/plugin/server_connector/__init__.py +20 -0
- polaris/plugin/server_connector/base.py +104 -0
- polaris/plugin/server_connector/convert.py +149 -0
- polaris/plugin/server_connector/grpc_connector/__init__.py +21 -0
- polaris/plugin/server_connector/grpc_connector/connection.py +401 -0
- polaris/plugin/server_connector/grpc_connector/grpc_client.py +384 -0
- polaris/plugin/server_connector/grpc_connector/tls.py +45 -0
- polaris/plugin/service_router/__init__.py +28 -0
- polaris/plugin/service_router/base.py +105 -0
- polaris/plugin/service_router/isolated.py +35 -0
- polaris/plugin/service_router/metadata.py +84 -0
- polaris/plugin/service_router/recover.py +62 -0
- polaris/plugin/service_router/util.py +54 -0
- polaris/plugin/skill/__init__.py +16 -0
- polaris/plugin/skill/connector.py +127 -0
- polaris/plugin/skill/persistence.py +94 -0
- polaris/plugin/stat_reporter/__init__.py +16 -0
- polaris/plugin/stat_reporter/base.py +34 -0
- polaris/plugin/stat_reporter/prometheus.py +375 -0
- polaris/plugin_manager.py +146 -0
- polaris_python-0.0.1.dev3.dist-info/METADATA +213 -0
- polaris_python-0.0.1.dev3.dist-info/RECORD +84 -0
- polaris_python-0.0.1.dev3.dist-info/WHEEL +5 -0
- polaris_python-0.0.1.dev3.dist-info/licenses/LICENSE +18 -0
- 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"]
|