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,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