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,820 @@
|
|
|
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
|
+
import threading
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from enum import IntEnum
|
|
20
|
+
from polarismesh_specification.api.v1.service_manage import response_pb2, service_pb2
|
|
21
|
+
from polaris.pkg.model import core
|
|
22
|
+
from typing import Dict, List
|
|
23
|
+
from polaris.pkg.model.error import SDKError, ErrorCode
|
|
24
|
+
from polaris.pkg.config.default import DefaultConf
|
|
25
|
+
from .stat import SlidingWindow
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RunMode:
|
|
29
|
+
ModeNoAgent = 0
|
|
30
|
+
ModeWithAgent = 1
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ServiceMetadata 服务元数据信息
|
|
34
|
+
class ServiceMetadata:
|
|
35
|
+
def __init__(self, service: service_pb2.Service):
|
|
36
|
+
self.service = service
|
|
37
|
+
|
|
38
|
+
# 获取服务名
|
|
39
|
+
def get_service(self):
|
|
40
|
+
return self.service.name.value
|
|
41
|
+
|
|
42
|
+
# 获取命名空间
|
|
43
|
+
def get_namespace(self):
|
|
44
|
+
return self.service.namespace.value
|
|
45
|
+
|
|
46
|
+
# 获取元数据信息
|
|
47
|
+
def get_metadata(self):
|
|
48
|
+
return self.service.metadata
|
|
49
|
+
|
|
50
|
+
def get_service_type(self):
|
|
51
|
+
"""Return the registry service category without changing its instance type."""
|
|
52
|
+
return self.service.service_type if self.service is not None else ServiceType.MICROSERVICE
|
|
53
|
+
|
|
54
|
+
def get_service_status(self):
|
|
55
|
+
"""Return the registry lifecycle status of this service."""
|
|
56
|
+
return self.service.service_status if self.service is not None else ServiceStatus.NORMAL
|
|
57
|
+
|
|
58
|
+
def get_extended_metadata(self):
|
|
59
|
+
"""Return typed service metadata, including mounted skills."""
|
|
60
|
+
if self.service is None:
|
|
61
|
+
return []
|
|
62
|
+
return [ExtendedMetadata(
|
|
63
|
+
type=item.type,
|
|
64
|
+
agent_skill=AgentSkill(
|
|
65
|
+
id=item.agent_skill.id, name=item.agent_skill.name,
|
|
66
|
+
description=item.agent_skill.description, version=item.agent_skill.version,
|
|
67
|
+
tags=list(item.agent_skill.tags), examples=list(item.agent_skill.examples),
|
|
68
|
+
input_modes=list(item.agent_skill.input_modes), output_modes=list(item.agent_skill.output_modes),
|
|
69
|
+
) if item.HasField("agent_skill") else None,
|
|
70
|
+
) for item in self.service.extended_metadata]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ServiceType(IntEnum):
|
|
74
|
+
"""Service categories defined by Polaris."""
|
|
75
|
+
|
|
76
|
+
MICROSERVICE = service_pb2.SERVICE_TYPE_MICROSERVICE
|
|
77
|
+
MCP_SERVER = service_pb2.SERVICE_TYPE_MCP_SERVER
|
|
78
|
+
AI_AGENT = service_pb2.SERVICE_TYPE_AI_AGENT
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ServiceStatus(IntEnum):
|
|
82
|
+
"""Service lifecycle states defined by Polaris."""
|
|
83
|
+
|
|
84
|
+
NORMAL = service_pb2.NORMAL
|
|
85
|
+
ABNORMAL = service_pb2.ABNORMAL
|
|
86
|
+
OFFLINE = service_pb2.OFFLINE
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class AgentSkill:
|
|
91
|
+
"""A skill mounted on a registry service."""
|
|
92
|
+
|
|
93
|
+
id: str = ""
|
|
94
|
+
name: str = ""
|
|
95
|
+
description: str = ""
|
|
96
|
+
version: str = ""
|
|
97
|
+
tags: list[str] = field(default_factory=list)
|
|
98
|
+
examples: list[str] = field(default_factory=list)
|
|
99
|
+
input_modes: list[str] = field(default_factory=list)
|
|
100
|
+
output_modes: list[str] = field(default_factory=list)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class ExtendedMetadata:
|
|
105
|
+
"""Typed extended metadata returned with a service snapshot."""
|
|
106
|
+
|
|
107
|
+
SKILL = service_pb2.ExtendedMetadata.EXTENDED_METADATA_SKILL
|
|
108
|
+
type: int = 0
|
|
109
|
+
agent_skill: AgentSkill | None = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# 断路器状态
|
|
113
|
+
class CBStatusEnum:
|
|
114
|
+
# 断路器已打开,代表节点已经被熔断
|
|
115
|
+
Open = 0
|
|
116
|
+
# 断路器半开,节点处于刚熔断恢复,只允许少量请求通过
|
|
117
|
+
HalfOpen = 1
|
|
118
|
+
# 断路器关闭,节点处于正常工作状态
|
|
119
|
+
Closed = 2
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# 熔断器状态管理器
|
|
123
|
+
class CircuitBreakerStatus:
|
|
124
|
+
def __init__(self, status: int = CBStatusEnum.Closed, ts: int = 0, requests_after_half_open: int = 0,
|
|
125
|
+
allowed_requests_after_half_open: int = 0):
|
|
126
|
+
self.lock = threading.RLock()
|
|
127
|
+
self.status = status
|
|
128
|
+
self.ts = ts # start time
|
|
129
|
+
self.requests_after_half_open = requests_after_half_open
|
|
130
|
+
self.allowed_requests_after_half_open = allowed_requests_after_half_open
|
|
131
|
+
self.successful_requests_after_half_open = 0
|
|
132
|
+
self.failed_requests_after_half_open = 0
|
|
133
|
+
|
|
134
|
+
# 熔断状态
|
|
135
|
+
def get_status(self):
|
|
136
|
+
with self.lock:
|
|
137
|
+
return self.status
|
|
138
|
+
|
|
139
|
+
# 状态转换的时间
|
|
140
|
+
def get_start_time(self):
|
|
141
|
+
with self.lock:
|
|
142
|
+
return self.ts
|
|
143
|
+
|
|
144
|
+
# TODO
|
|
145
|
+
# 半开状态后已分配的请求数
|
|
146
|
+
def get_request_count_after_half_open(self):
|
|
147
|
+
with self.lock:
|
|
148
|
+
return self.requests_after_half_open
|
|
149
|
+
|
|
150
|
+
# 尝试分配请求
|
|
151
|
+
def try_allocate_request(self):
|
|
152
|
+
with self.lock:
|
|
153
|
+
if self.status == CBStatusEnum.Closed:
|
|
154
|
+
return True
|
|
155
|
+
if self.status == CBStatusEnum.Open:
|
|
156
|
+
return False
|
|
157
|
+
if self.requests_after_half_open >= self.allowed_requests_after_half_open:
|
|
158
|
+
return False
|
|
159
|
+
self.requests_after_half_open += 1
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
def report_half_open_result(self, success: bool):
|
|
163
|
+
with self.lock:
|
|
164
|
+
if success:
|
|
165
|
+
self.successful_requests_after_half_open += 1
|
|
166
|
+
else:
|
|
167
|
+
self.failed_requests_after_half_open += 1
|
|
168
|
+
|
|
169
|
+
# 是否可以分配请求
|
|
170
|
+
def is_available(self):
|
|
171
|
+
with self.lock:
|
|
172
|
+
if self.status == CBStatusEnum.Closed:
|
|
173
|
+
return True
|
|
174
|
+
if self.status == CBStatusEnum.Open:
|
|
175
|
+
return False
|
|
176
|
+
if self.requests_after_half_open >= self.allowed_requests_after_half_open:
|
|
177
|
+
return False
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class InstanceWeight:
|
|
182
|
+
def __init__(self, instance_id: str, dynamic_weight: int):
|
|
183
|
+
self.instance_id = instance_id
|
|
184
|
+
self.dynamic_weight = dynamic_weight
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class InstanceLocalValue:
|
|
188
|
+
def get_circuit_breaker_status(self) -> CircuitBreakerStatus:
|
|
189
|
+
raise Exception("[InstanceLocalValue] get_circuit_breaker_status not implemented")
|
|
190
|
+
|
|
191
|
+
def set_circuit_breaker_status(self, status: CircuitBreakerStatus):
|
|
192
|
+
raise Exception("[InstanceLocalValue] set_circuit_breaker_status not implemented")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class DefaultInstanceLocalValue(InstanceLocalValue):
|
|
196
|
+
def __init__(self):
|
|
197
|
+
# TODO window是在cb里进行设置的,因为cb知道window的参数
|
|
198
|
+
self.window = None
|
|
199
|
+
self.circuit_breaker_metrics = None
|
|
200
|
+
self.cb_status = CircuitBreakerStatus()
|
|
201
|
+
|
|
202
|
+
# TODO
|
|
203
|
+
def get_sliding_window(self, bucket_count=2, bucket_interval=60000):
|
|
204
|
+
if self.window is None:
|
|
205
|
+
self.window = SlidingWindow(bucket_count, bucket_interval)
|
|
206
|
+
return self.window
|
|
207
|
+
|
|
208
|
+
def set_sliding_window(self, sliding_window):
|
|
209
|
+
self.window = sliding_window
|
|
210
|
+
|
|
211
|
+
def get_circuit_breaker_status(self) -> CircuitBreakerStatus:
|
|
212
|
+
return self.cb_status
|
|
213
|
+
|
|
214
|
+
def set_circuit_breaker_status(self, cb: CircuitBreakerStatus):
|
|
215
|
+
self.cb_status = cb
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def create_default_instance_local_value(inst_id: str) -> InstanceLocalValue:
|
|
219
|
+
return DefaultInstanceLocalValue()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# 服务实例信息
|
|
223
|
+
class Instance:
|
|
224
|
+
def __init__(self, instance: service_pb2.Instance, service_key: core.ServiceKey,
|
|
225
|
+
local_value: InstanceLocalValue = None):
|
|
226
|
+
self.instance = instance
|
|
227
|
+
self.service_key = service_key
|
|
228
|
+
self.local_value: InstanceLocalValue = local_value if local_value is not None else DefaultInstanceLocalValue()
|
|
229
|
+
|
|
230
|
+
# 实例所在命名空间
|
|
231
|
+
def get_namespace(self):
|
|
232
|
+
return self.service_key.namespace
|
|
233
|
+
|
|
234
|
+
# 实例所在服务名
|
|
235
|
+
def get_service(self):
|
|
236
|
+
return self.service_key.service
|
|
237
|
+
|
|
238
|
+
# 服务实例唯一标识
|
|
239
|
+
def get_id(self):
|
|
240
|
+
return self.instance.id.value
|
|
241
|
+
|
|
242
|
+
# 实例的域名/IP信息
|
|
243
|
+
def get_host(self):
|
|
244
|
+
return self.instance.host.value
|
|
245
|
+
|
|
246
|
+
# 实例Revision
|
|
247
|
+
def get_revision(self):
|
|
248
|
+
return self.instance.revision.value
|
|
249
|
+
|
|
250
|
+
# 实例的监听端口
|
|
251
|
+
def get_port(self):
|
|
252
|
+
return self.instance.port.value
|
|
253
|
+
|
|
254
|
+
# 服务实例的协议
|
|
255
|
+
def get_protocol(self):
|
|
256
|
+
return self.instance.protocol.value
|
|
257
|
+
|
|
258
|
+
# 实例版本号
|
|
259
|
+
def get_version(self):
|
|
260
|
+
return self.instance.version.value
|
|
261
|
+
|
|
262
|
+
# 实例静态权重值
|
|
263
|
+
def get_weight(self):
|
|
264
|
+
return self.instance.weight.value if self.instance.HasField("weight") else 100
|
|
265
|
+
|
|
266
|
+
# 实例优先级信息
|
|
267
|
+
def get_priority(self):
|
|
268
|
+
return self.instance.priority.value
|
|
269
|
+
|
|
270
|
+
# 实例逻辑分区
|
|
271
|
+
def get_logicset(self):
|
|
272
|
+
return self.instance.logic_set.value
|
|
273
|
+
|
|
274
|
+
# 实例的断路器状态,包括:
|
|
275
|
+
# 打开(被熔断)、半开(探测恢复)、关闭(正常运行)
|
|
276
|
+
def get_circuit_breaker_status(self) -> CircuitBreakerStatus:
|
|
277
|
+
return self.local_value.get_circuit_breaker_status()
|
|
278
|
+
|
|
279
|
+
# TODO
|
|
280
|
+
def get_sliding_window(self, bucket_count=10, bucket_interval=6000) -> SlidingWindow:
|
|
281
|
+
return self.local_value.get_sliding_window(bucket_count, bucket_interval)
|
|
282
|
+
|
|
283
|
+
# 实例是否健康,基于服务端返回的健康数据
|
|
284
|
+
def is_healthy(self):
|
|
285
|
+
return self.instance.healthy.value
|
|
286
|
+
|
|
287
|
+
# 实例是否已经被手动隔离
|
|
288
|
+
def is_isolated(self):
|
|
289
|
+
return self.instance.isolate.value
|
|
290
|
+
|
|
291
|
+
# 实例是否启动了健康检查
|
|
292
|
+
def is_enable_healthcheck(self):
|
|
293
|
+
return self.instance.HasField("health_check")
|
|
294
|
+
|
|
295
|
+
# 实例所属的大区信息
|
|
296
|
+
def get_region(self):
|
|
297
|
+
if not self.instance.location:
|
|
298
|
+
return ""
|
|
299
|
+
return self.instance.location.region.value
|
|
300
|
+
|
|
301
|
+
# 实例所属的地方信息
|
|
302
|
+
def get_zone(self):
|
|
303
|
+
if not self.instance.location:
|
|
304
|
+
return ""
|
|
305
|
+
return self.instance.location.zone.value
|
|
306
|
+
|
|
307
|
+
# 实例所属的IDC信息
|
|
308
|
+
def get_idc(self):
|
|
309
|
+
if not self.instance.location:
|
|
310
|
+
return ""
|
|
311
|
+
return self.instance.location.campus.value
|
|
312
|
+
|
|
313
|
+
# 实例所属的IDC信息
|
|
314
|
+
def get_campus(self):
|
|
315
|
+
if not self.instance.location:
|
|
316
|
+
return ""
|
|
317
|
+
return self.instance.location.campus.value
|
|
318
|
+
|
|
319
|
+
def get_local_value(self) -> InstanceLocalValue:
|
|
320
|
+
return self.local_value
|
|
321
|
+
|
|
322
|
+
def get_metadata(self):
|
|
323
|
+
return self.instance.metadata
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# 服务路由实例过滤计算
|
|
327
|
+
class RouteFilterCounter:
|
|
328
|
+
def __init__(self, value: int = 0):
|
|
329
|
+
self.value = value
|
|
330
|
+
|
|
331
|
+
# 增加偏移值
|
|
332
|
+
def add_value(self, diff):
|
|
333
|
+
self.value += diff
|
|
334
|
+
|
|
335
|
+
# 获取过滤值
|
|
336
|
+
def get_value(self):
|
|
337
|
+
return self.value
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class FilteredInstanceCounter:
|
|
341
|
+
def __init__(self):
|
|
342
|
+
self.isolated_instances = 0
|
|
343
|
+
self.unhealthy_instances = 0
|
|
344
|
+
self.route_filter_counters = {}
|
|
345
|
+
|
|
346
|
+
def add_isolated_instances(self, n: int):
|
|
347
|
+
self.isolated_instances += n
|
|
348
|
+
|
|
349
|
+
def get_isolated_instances(self):
|
|
350
|
+
return self.isolated_instances
|
|
351
|
+
|
|
352
|
+
def add_unhealthy_instances(self, n: int):
|
|
353
|
+
self.unhealthy_instances += n
|
|
354
|
+
|
|
355
|
+
def get_unhealthy_instances(self):
|
|
356
|
+
return self.unhealthy_instances
|
|
357
|
+
|
|
358
|
+
def add_route_filter_instances(self, router_name: str, n: int):
|
|
359
|
+
if router_name in self.route_filter_counters:
|
|
360
|
+
self.route_filter_counters[router_name].add_value(n)
|
|
361
|
+
self.route_filter_counters[router_name] = RouteFilterCounter(n)
|
|
362
|
+
|
|
363
|
+
def get_route_filter_instances(self, router_name: str):
|
|
364
|
+
if router_name in self.route_filter_counters:
|
|
365
|
+
return self.route_filter_counters[router_name].get_value()
|
|
366
|
+
return 0
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# 服务信息
|
|
370
|
+
class ServiceInstances(ServiceMetadata):
|
|
371
|
+
def __init__(self, service: service_pb2.Service = None, instances: List[Instance] = None,
|
|
372
|
+
filtered_instances: FilteredInstanceCounter = None, instances_map=None,
|
|
373
|
+
initialized: bool = True, total_weight: int = 0, revision: str = "",
|
|
374
|
+
loaded_from_file: bool = False):
|
|
375
|
+
super().__init__(service)
|
|
376
|
+
self.filtered_instances = filtered_instances
|
|
377
|
+
if instances is None:
|
|
378
|
+
self.instances = []
|
|
379
|
+
else:
|
|
380
|
+
self.instances: List[Instance] = instances
|
|
381
|
+
if instances_map is None:
|
|
382
|
+
self.instances_map: Dict[str, Instance] = {}
|
|
383
|
+
else:
|
|
384
|
+
self.instances_map: Dict[str, Instance] = instances_map
|
|
385
|
+
self.total_weight = total_weight
|
|
386
|
+
self.initialized = initialized
|
|
387
|
+
self.revision = revision
|
|
388
|
+
self.loaded_from_file = loaded_from_file
|
|
389
|
+
|
|
390
|
+
@classmethod
|
|
391
|
+
def new_from_discover_rsp(cls, resp: response_pb2.DiscoverResponse, get_local_value_func=None,
|
|
392
|
+
old_instances=None, loaded_from_file=False):
|
|
393
|
+
if resp is None:
|
|
394
|
+
return ServiceInstances()
|
|
395
|
+
|
|
396
|
+
snapshot = response_pb2.DiscoverResponse()
|
|
397
|
+
snapshot.CopyFrom(resp)
|
|
398
|
+
service_instances = ServiceInstances(service=snapshot.service,
|
|
399
|
+
instances=[],
|
|
400
|
+
filtered_instances=FilteredInstanceCounter(),
|
|
401
|
+
instances_map={},
|
|
402
|
+
initialized=True, loaded_from_file=loaded_from_file)
|
|
403
|
+
service_instances.revision = resp.service.revision.value
|
|
404
|
+
|
|
405
|
+
svc_key = core.ServiceKey(resp.service.namespace.value, resp.service.name.value)
|
|
406
|
+
|
|
407
|
+
for ins in snapshot.instances:
|
|
408
|
+
instance = Instance(ins, svc_key)
|
|
409
|
+
local_value = old_instances.get_instance_local_value(instance.get_id()) if old_instances else None
|
|
410
|
+
if local_value is None and get_local_value_func is not None:
|
|
411
|
+
local_value = get_local_value_func(instance)
|
|
412
|
+
if local_value is not None:
|
|
413
|
+
instance.local_value = local_value
|
|
414
|
+
|
|
415
|
+
service_instances.total_weight += instance.get_weight()
|
|
416
|
+
service_instances.instances.append(instance)
|
|
417
|
+
service_instances.instances_map[instance.get_id()] = instance
|
|
418
|
+
|
|
419
|
+
service_instances.instances.sort(key=lambda instance: (instance.get_host(), instance.get_port()))
|
|
420
|
+
return service_instances
|
|
421
|
+
|
|
422
|
+
def is_loaded_from_file(self):
|
|
423
|
+
"""Report whether this snapshot came from a persisted discover response."""
|
|
424
|
+
return self.loaded_from_file
|
|
425
|
+
|
|
426
|
+
# 返回实例的id集合
|
|
427
|
+
def get_svc_id_set(self):
|
|
428
|
+
return self.instances_map.keys()
|
|
429
|
+
|
|
430
|
+
# 获取服务名
|
|
431
|
+
def get_service(self) -> str:
|
|
432
|
+
return self.service.name.value if self.service is not None else ""
|
|
433
|
+
|
|
434
|
+
# 获取名字空间
|
|
435
|
+
def get_namespace(self) -> str:
|
|
436
|
+
return self.service.namespace.value if self.service is not None else ""
|
|
437
|
+
|
|
438
|
+
# 获取元数据信息
|
|
439
|
+
def get_metadata(self):
|
|
440
|
+
return self.service.metadata if self.service is not None else {}
|
|
441
|
+
|
|
442
|
+
# 获取服务实例列表
|
|
443
|
+
def get_instances(self) -> List[Instance]:
|
|
444
|
+
return self.instances
|
|
445
|
+
|
|
446
|
+
def get_revision(self):
|
|
447
|
+
return self.revision
|
|
448
|
+
|
|
449
|
+
def get_total_weight(self):
|
|
450
|
+
return self.total_weight
|
|
451
|
+
|
|
452
|
+
def get_filter_instance_counter(self) -> FilteredInstanceCounter:
|
|
453
|
+
return self.filtered_instances
|
|
454
|
+
|
|
455
|
+
def get_instance_local_value(self, id: str) -> InstanceLocalValue:
|
|
456
|
+
if id not in self.instances_map:
|
|
457
|
+
return None
|
|
458
|
+
return self.instances_map[id].get_local_value()
|
|
459
|
+
|
|
460
|
+
def is_initialized(self):
|
|
461
|
+
return self.initialized
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# 初始化service_instances
|
|
465
|
+
def new_service_instances(resp: response_pb2.DiscoverResponse) -> ServiceInstances:
|
|
466
|
+
return ServiceInstances.new_from_discover_rsp(resp)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# 服务信息
|
|
470
|
+
class ServiceInfo(ServiceMetadata):
|
|
471
|
+
def __init__(self, service: str = "", namespace: str = "", metadata: Dict[str, str] = None):
|
|
472
|
+
super().__init__(service)
|
|
473
|
+
self.namespace = namespace
|
|
474
|
+
if metadata is None:
|
|
475
|
+
self.metadata = {}
|
|
476
|
+
else:
|
|
477
|
+
self.metadata = metadata
|
|
478
|
+
|
|
479
|
+
# 获取服务名
|
|
480
|
+
def get_service(self):
|
|
481
|
+
return self.service
|
|
482
|
+
|
|
483
|
+
# 获取命名空间
|
|
484
|
+
def get_namespace(self):
|
|
485
|
+
return self.namespace
|
|
486
|
+
|
|
487
|
+
# 获取元数据信息
|
|
488
|
+
def get_metadata(self):
|
|
489
|
+
return self.metadata
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# 单个服务实例查询请求
|
|
493
|
+
class GetOneInstanceRequest:
|
|
494
|
+
def __init__(self, flow_id: int = 0, service: str = "", namespace: str = "", timeout: int = 0,
|
|
495
|
+
retry_count: int = 0, source_service: ServiceInfo = None, hash_value: int = 0,
|
|
496
|
+
use_discover_cache: bool = True, canary: str = "", metadata=None,
|
|
497
|
+
metadata_failover_type: str = None, exclude_offline_service: bool = False):
|
|
498
|
+
# 可选,流水号
|
|
499
|
+
if metadata is None:
|
|
500
|
+
metadata = {}
|
|
501
|
+
self.flow_id = flow_id
|
|
502
|
+
# 必选,服务名
|
|
503
|
+
self.service = service
|
|
504
|
+
# 必选,命名空间
|
|
505
|
+
self.namespace = namespace
|
|
506
|
+
# 可选,超时时间
|
|
507
|
+
self.timeout = timeout
|
|
508
|
+
# 可选,超时重试次数
|
|
509
|
+
self.retry_count = retry_count
|
|
510
|
+
# 主调方服务信息
|
|
511
|
+
self.source_service = source_service
|
|
512
|
+
# 用于一致性hash的负载均衡
|
|
513
|
+
self.hash_value = hash_value
|
|
514
|
+
# direct
|
|
515
|
+
self.use_discover_cache = use_discover_cache
|
|
516
|
+
# 金丝雀
|
|
517
|
+
self.canary = canary
|
|
518
|
+
# metadata
|
|
519
|
+
self.metadata = metadata
|
|
520
|
+
# none / all / others;None 表示使用服务或插件配置
|
|
521
|
+
self.metadata_failover_type = metadata_failover_type
|
|
522
|
+
# 可选,显式排除 OFFLINE 服务;默认保持完整发现结果
|
|
523
|
+
self.exclude_offline_service = exclude_offline_service
|
|
524
|
+
|
|
525
|
+
# 设置超时时间
|
|
526
|
+
def set_timeout(self, timeout: int):
|
|
527
|
+
self.timeout = timeout
|
|
528
|
+
|
|
529
|
+
# 设置重试次数
|
|
530
|
+
def set_retrycount(self, retry_count: int):
|
|
531
|
+
self.retry_count = retry_count
|
|
532
|
+
|
|
533
|
+
# 获取服务名
|
|
534
|
+
def get_service(self):
|
|
535
|
+
return self.service
|
|
536
|
+
|
|
537
|
+
# 获取命名空间
|
|
538
|
+
def get_namespace(self):
|
|
539
|
+
return self.namespace
|
|
540
|
+
|
|
541
|
+
def get_metadata(self):
|
|
542
|
+
return self.metadata
|
|
543
|
+
|
|
544
|
+
def get_metadata_failover_type(self):
|
|
545
|
+
return self.metadata_failover_type
|
|
546
|
+
|
|
547
|
+
def validate(self):
|
|
548
|
+
_validate_instance_query(self)
|
|
549
|
+
if self.source_service is not None:
|
|
550
|
+
validate_service_metadata(self.source_service)
|
|
551
|
+
if self.metadata_failover_type not in (None, "none", "all", "others"):
|
|
552
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument,
|
|
553
|
+
"metadata_failover_type must be one of none, all, others")
|
|
554
|
+
if type(self.exclude_offline_service) is not bool:
|
|
555
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "exclude_offline_service must be boolean")
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# 批量服务实例查询请求
|
|
559
|
+
class GetInstancesRequest:
|
|
560
|
+
def __init__(self, flow_id: int = 0, service: str = "", namespace: str = "", timeout: int = 0,
|
|
561
|
+
source_service: ServiceInfo = None, retry_count: int = 0, skip_route_filter: bool = False,
|
|
562
|
+
include_circuit_break_instances=False,
|
|
563
|
+
include_unhealthy_instance=False,
|
|
564
|
+
use_discover_cache: bool = True, metadata=None, metadata_failover_type=None, canary="",
|
|
565
|
+
exclude_offline_service: bool = False):
|
|
566
|
+
# 可选,流水号,用于跟踪用户的请求,默认0
|
|
567
|
+
self.flow_id = flow_id
|
|
568
|
+
# 必选,服务名
|
|
569
|
+
self.service = service
|
|
570
|
+
# 必选,命名空间
|
|
571
|
+
self.namespace = namespace
|
|
572
|
+
# 主调方信息
|
|
573
|
+
self.source_service = source_service
|
|
574
|
+
# 可选,是否包含被熔断的服务实例,默认false
|
|
575
|
+
self.include_circuit_break_instances = include_circuit_break_instances
|
|
576
|
+
# 可选,是否包含不健康的服务实例,默认false
|
|
577
|
+
self.include_unhealthy_instance = include_unhealthy_instance
|
|
578
|
+
# 可选,是否排除 OFFLINE 服务;get_all_instances 始终返回完整快照
|
|
579
|
+
self.exclude_offline_service = exclude_offline_service
|
|
580
|
+
# 可选,是否跳过服务路由筛选,默认false
|
|
581
|
+
self.skip_route_filter = skip_route_filter
|
|
582
|
+
# 可选,单次查询超时时间,默认直接获取全局的超时配置
|
|
583
|
+
# 用户总最大超时时间为(1+RetryCount) * Timeout
|
|
584
|
+
self.timeout = timeout
|
|
585
|
+
# 可选,重试次数
|
|
586
|
+
self.retry_count = retry_count
|
|
587
|
+
# direct
|
|
588
|
+
self.use_discover_cache = use_discover_cache
|
|
589
|
+
self.metadata = {} if metadata is None else metadata
|
|
590
|
+
self.metadata_failover_type = metadata_failover_type
|
|
591
|
+
self.canary = canary
|
|
592
|
+
|
|
593
|
+
def get_metadata(self):
|
|
594
|
+
return self.metadata
|
|
595
|
+
|
|
596
|
+
def get_metadata_failover_type(self):
|
|
597
|
+
return self.metadata_failover_type
|
|
598
|
+
|
|
599
|
+
def set_timeout(self, timeout: int):
|
|
600
|
+
self.timeout = timeout
|
|
601
|
+
|
|
602
|
+
def set_retry_count(self, retry_count: int):
|
|
603
|
+
self.retry_count = retry_count
|
|
604
|
+
|
|
605
|
+
def get_service(self):
|
|
606
|
+
return self.service
|
|
607
|
+
|
|
608
|
+
def get_namespace(self):
|
|
609
|
+
return self.namespace
|
|
610
|
+
|
|
611
|
+
def validate(self):
|
|
612
|
+
_validate_instance_query(self)
|
|
613
|
+
if self.source_service is not None:
|
|
614
|
+
validate_service_metadata(self.source_service)
|
|
615
|
+
for name in ("skip_route_filter", "include_circuit_break_instances", "include_unhealthy_instance",
|
|
616
|
+
"exclude_offline_service"):
|
|
617
|
+
if type(getattr(self, name)) is not bool:
|
|
618
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, f"{name} must be boolean")
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
class GetAllInstancesRequest(GetInstancesRequest):
|
|
622
|
+
"""Fetch the complete instance snapshot without local routing filters."""
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
class GetHealthyInstancesRequest(GetInstancesRequest):
|
|
626
|
+
"""Apply only isolation and recovery filters, including circuit-open nodes by default."""
|
|
627
|
+
|
|
628
|
+
def __init__(self, *args, include_circuit_break_instances=True, **kwargs):
|
|
629
|
+
super().__init__(*args, include_circuit_break_instances=include_circuit_break_instances, **kwargs)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class InstancesResponse(ServiceInstances):
|
|
633
|
+
"""Expose an independent list and service metadata for one discovery query."""
|
|
634
|
+
|
|
635
|
+
def __init__(self, snapshot: ServiceInstances, flow_id=0):
|
|
636
|
+
super().__init__(service=snapshot.service, instances=list(snapshot.instances),
|
|
637
|
+
filtered_instances=snapshot.filtered_instances,
|
|
638
|
+
instances_map={node.get_id(): node for node in snapshot.instances},
|
|
639
|
+
total_weight=sum(node.get_weight() for node in snapshot.instances),
|
|
640
|
+
revision=snapshot.revision, loaded_from_file=snapshot.loaded_from_file)
|
|
641
|
+
self.flow_id = flow_id
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _validate_instance_query(request):
|
|
645
|
+
for name in ("namespace", "service"):
|
|
646
|
+
if not isinstance(getattr(request, name), str) or not getattr(request, name).strip():
|
|
647
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, f"{name} must be nonempty")
|
|
648
|
+
if type(request.timeout) is not int or request.timeout < 0:
|
|
649
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "timeout must be nonnegative milliseconds")
|
|
650
|
+
if type(request.use_discover_cache) is not bool:
|
|
651
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "use_discover_cache must be boolean")
|
|
652
|
+
if request.metadata_failover_type not in (None, "none", "all", "others"):
|
|
653
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "invalid metadata_failover_type")
|
|
654
|
+
if (not isinstance(request.metadata, dict)
|
|
655
|
+
or any(not isinstance(key, str) or not isinstance(value, str)
|
|
656
|
+
for key, value in request.metadata.items())):
|
|
657
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "metadata must contain string keys and values")
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
class InstancesFuture:
|
|
661
|
+
def is_done(self):
|
|
662
|
+
pass
|
|
663
|
+
|
|
664
|
+
def get(self) -> InstancesResponse:
|
|
665
|
+
pass
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
class RetStatus:
|
|
669
|
+
RetUnknown = 0
|
|
670
|
+
RetSuccess = 1
|
|
671
|
+
RetFail = 2
|
|
672
|
+
RetTimeout = 3
|
|
673
|
+
RetReject = 4
|
|
674
|
+
RetFlowControl = 5
|
|
675
|
+
|
|
676
|
+
descriptions = {0: "unknown", 1: "success", 2: "fail", 3: "timeout", 4: "reject", 5: "flow_control"}
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
class InstanceType:
|
|
680
|
+
"""Metric instance categories shared with Java InstanceType."""
|
|
681
|
+
|
|
682
|
+
MICROSERVICE = "microservice"
|
|
683
|
+
MCP = "mcp"
|
|
684
|
+
A2A = "a2a"
|
|
685
|
+
LANGGRAPH = "langgraph"
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
class APICallResult:
|
|
689
|
+
def __init__(self, api_name: str, ret_status: int, ret_code: int, delay: int):
|
|
690
|
+
self.api_name = api_name
|
|
691
|
+
self.ret_status = ret_status
|
|
692
|
+
self.ret_code = ret_code
|
|
693
|
+
self.delay = delay
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
class ServiceCallResult:
|
|
697
|
+
"""Report one invocation using an instance or an explicit service endpoint."""
|
|
698
|
+
|
|
699
|
+
def __init__(self, called_instance: Instance = None, ret_status: int = RetStatus.RetUnknown,
|
|
700
|
+
ret_code: int = None, delay: int = None, *, method: str = None,
|
|
701
|
+
caller_service: ServiceInfo = None, caller_ip: str = None, labels: str = None,
|
|
702
|
+
instance_type: str = InstanceType.MICROSERVICE, namespace: str = "",
|
|
703
|
+
service: str = "", host: str = "", port: int = 0):
|
|
704
|
+
self.called_instance = called_instance
|
|
705
|
+
self.ret_status = ret_status
|
|
706
|
+
self.ret_code = ret_code
|
|
707
|
+
self.delay = delay
|
|
708
|
+
self.method = method
|
|
709
|
+
self.caller_service = caller_service
|
|
710
|
+
self.caller_ip = caller_ip
|
|
711
|
+
self.labels = labels
|
|
712
|
+
self.instance_type = instance_type
|
|
713
|
+
self.namespace, self.service, self.host, self.port = namespace, service, host, port
|
|
714
|
+
|
|
715
|
+
# 设置返回状态
|
|
716
|
+
def set_ret_status(self, ret_status: int):
|
|
717
|
+
self.ret_status = ret_status
|
|
718
|
+
|
|
719
|
+
# 设置实例
|
|
720
|
+
def set_called_instance(self, instance: Instance):
|
|
721
|
+
self.called_instance = instance
|
|
722
|
+
|
|
723
|
+
# 实例所属服务名
|
|
724
|
+
def get_service(self) -> str:
|
|
725
|
+
return self.called_instance.get_service() if self.called_instance is not None else self.service
|
|
726
|
+
|
|
727
|
+
# 实例所属命名空间
|
|
728
|
+
def get_namespace(self) -> str:
|
|
729
|
+
return self.called_instance.get_namespace() if self.called_instance is not None else self.namespace
|
|
730
|
+
|
|
731
|
+
# 实例ID
|
|
732
|
+
def get_id(self):
|
|
733
|
+
return self.called_instance.get_id() if self.called_instance is not None else ""
|
|
734
|
+
|
|
735
|
+
# 实例的节点信息
|
|
736
|
+
def get_host(self):
|
|
737
|
+
return self.called_instance.get_host() if self.called_instance is not None else self.host
|
|
738
|
+
|
|
739
|
+
# 实例的端口信息
|
|
740
|
+
def get_port(self):
|
|
741
|
+
return self.called_instance.get_port() if self.called_instance is not None else self.port
|
|
742
|
+
|
|
743
|
+
# 实例的返回码
|
|
744
|
+
def get_ret_code(self) -> int:
|
|
745
|
+
return self.ret_code
|
|
746
|
+
|
|
747
|
+
# 设置实例返回码
|
|
748
|
+
def set_ret_code(self, value: int):
|
|
749
|
+
self.ret_code = value
|
|
750
|
+
|
|
751
|
+
# 调用时延
|
|
752
|
+
def get_delay(self) -> int:
|
|
753
|
+
return self.delay
|
|
754
|
+
|
|
755
|
+
# 设置时延
|
|
756
|
+
def set_delay(self, delay: int):
|
|
757
|
+
self.delay = delay
|
|
758
|
+
|
|
759
|
+
def get_ret_status(self) -> int:
|
|
760
|
+
return self.ret_status
|
|
761
|
+
|
|
762
|
+
def get_api(self):
|
|
763
|
+
return self.method or ""
|
|
764
|
+
|
|
765
|
+
def get_method(self):
|
|
766
|
+
"""Return the invoked method or HTTP path, not the HTTP verb."""
|
|
767
|
+
return self.method
|
|
768
|
+
|
|
769
|
+
def set_method(self, method):
|
|
770
|
+
"""Assign the invoked method or path."""
|
|
771
|
+
self.method = method
|
|
772
|
+
|
|
773
|
+
def validate(self):
|
|
774
|
+
if self.called_instance is not None and not isinstance(self.called_instance, Instance):
|
|
775
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "called_instance must be an Instance")
|
|
776
|
+
if self.called_instance is None and (not self.namespace or not self.service or not self.host):
|
|
777
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument,
|
|
778
|
+
"ServiceCallResult: provide an instance or namespace/service/host")
|
|
779
|
+
if type(self.ret_status) is not int or self.ret_status not in RetStatus.descriptions:
|
|
780
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument,
|
|
781
|
+
"ServiceCallResult: invalid retStatus")
|
|
782
|
+
if self.instance_type not in (
|
|
783
|
+
InstanceType.MICROSERVICE,
|
|
784
|
+
InstanceType.MCP,
|
|
785
|
+
InstanceType.A2A,
|
|
786
|
+
InstanceType.LANGGRAPH,
|
|
787
|
+
):
|
|
788
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "ServiceCallResult: invalid instanceType")
|
|
789
|
+
if self.delay is not None and (type(self.delay) is not int or self.delay < 0):
|
|
790
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "ServiceCallResult: delay must be nonnegative ms")
|
|
791
|
+
if self.ret_code is not None and type(self.ret_code) is not int:
|
|
792
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "ServiceCallResult: retCode must be integer")
|
|
793
|
+
for key in ("method", "caller_ip", "labels"):
|
|
794
|
+
if getattr(self, key) is not None and not isinstance(getattr(self, key), str):
|
|
795
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, f"ServiceCallResult: {key} must be string")
|
|
796
|
+
if self.caller_service is not None and not isinstance(self.caller_service, ServiceInfo):
|
|
797
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "caller_service must be ServiceInfo")
|
|
798
|
+
if self.called_instance is None and (type(self.port) is not int or not 0 <= self.port <= 65535):
|
|
799
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "port must be 0..65535")
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def validate_service_metadata(svc_meta: ServiceMetadata):
|
|
803
|
+
if len(svc_meta.get_service()) == 0:
|
|
804
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "validate_service_metadata: service is empty")
|
|
805
|
+
if len(svc_meta.get_namespace()) == 0:
|
|
806
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument, "validate_service_metadata: namespace is empty")
|
|
807
|
+
if len(svc_meta.get_metadata()) > 0:
|
|
808
|
+
for k, v in svc_meta.get_metadata().items():
|
|
809
|
+
if len(k) == 0 or len(v) == 0:
|
|
810
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument,
|
|
811
|
+
"validate_service_metadata: metadata has empty key or value")
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def is_instance_available(ins: Instance):
|
|
815
|
+
if not ins.is_healthy():
|
|
816
|
+
return False
|
|
817
|
+
cb_status = ins.get_circuit_breaker_status()
|
|
818
|
+
if cb_status is not None and not cb_status.is_available():
|
|
819
|
+
return False
|
|
820
|
+
return True
|