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,149 @@
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 google.protobuf import wrappers_pb2 as wrappers
17
+ from polarismesh_specification.api.v1.service_manage.client_pb2 import Client
18
+ from polarismesh_specification.api.v1.service_manage.request_pb2 import DiscoverRequest
19
+ from polarismesh_specification.api.v1.service_manage.service_pb2 import (
20
+ HealthCheck,
21
+ HeartbeatHealthCheck,
22
+ Instance,
23
+ Service,
24
+ )
25
+
26
+ from polaris.pkg.model.server import *
27
+ from dataclasses import asdict
28
+ from polarismesh_specification.api.v1.service_manage import contract_pb2
29
+ from polaris.pkg.model.contract import InterfaceDescriptor, ServiceContract, ServiceFeature
30
+
31
+
32
+ # 注册服务请求转成proto
33
+ def register_request_to_proto(request: InstanceRegisterRequest):
34
+ pb_register_instance = Instance()
35
+ pb_register_instance.service.CopyFrom(wrappers.StringValue(value=request.service))
36
+ pb_register_instance.namespace.CopyFrom(wrappers.StringValue(value=request.namespace))
37
+ pb_register_instance.host.CopyFrom(wrappers.StringValue(value=request.host))
38
+ pb_register_instance.port.CopyFrom(wrappers.UInt32Value(value=request.port))
39
+ pb_register_instance.service_token.CopyFrom(wrappers.StringValue(value=request.service_token))
40
+ if request.protocol != '':
41
+ pb_register_instance.protocol.CopyFrom(wrappers.StringValue(value=request.protocol))
42
+ if request.weight is not None:
43
+ pb_register_instance.weight.CopyFrom(wrappers.UInt32Value(value=request.weight))
44
+ if request.priority is not None:
45
+ pb_register_instance.priority.CopyFrom(wrappers.UInt32Value(value=request.priority))
46
+ if request.version != '':
47
+ pb_register_instance.version.CopyFrom(wrappers.StringValue(value=request.version))
48
+ if len(request.metadata) > 0:
49
+ for k, v in request.metadata.items():
50
+ pb_register_instance.metadata[k] = v
51
+ if request.healthy is not None:
52
+ pb_register_instance.healthy.CopyFrom(wrappers.BoolValue(value=request.healthy))
53
+ if request.ttl is not None:
54
+ heartbeat = HeartbeatHealthCheck(ttl=wrappers.UInt32Value(value=request.ttl))
55
+ health_check = HealthCheck(type=HealthCheck.HEARTBEAT, heartbeat=heartbeat)
56
+ pb_register_instance.health_check.CopyFrom(health_check)
57
+ return pb_register_instance
58
+
59
+
60
+ # 反注册服务请求转成proto
61
+ def deregister_request_to_proto(request: InstanceDeRegisterRequest):
62
+ pb_deregister_instance = Instance()
63
+ pb_deregister_instance.service.CopyFrom(wrappers.StringValue(value=request.service))
64
+ pb_deregister_instance.namespace.CopyFrom(wrappers.StringValue(value=request.namespace))
65
+ pb_deregister_instance.host.CopyFrom(wrappers.StringValue(value=request.host))
66
+ pb_deregister_instance.port.CopyFrom(wrappers.UInt32Value(value=request.port))
67
+ pb_deregister_instance.service_token.CopyFrom(wrappers.StringValue(value=request.service_token))
68
+ if request.instance_id != '':
69
+ pb_deregister_instance.id.CopyFrom(wrappers.StringValue(value=request.instance_id))
70
+ return pb_deregister_instance
71
+
72
+
73
+ # service转成proto
74
+ def service_to_proto(name, namespace, revision=""):
75
+ pb_service_instance = Service()
76
+ pb_service_instance.name.CopyFrom(wrappers.StringValue(value=name))
77
+ pb_service_instance.namespace.CopyFrom(wrappers.StringValue(value=namespace))
78
+ pb_service_instance.revision.CopyFrom(wrappers.StringValue(value=revision))
79
+ return pb_service_instance
80
+
81
+
82
+ # discover请求转成proto
83
+ def discover_request_to_proto(request: InstanceDiscoverRequest):
84
+ pb_discover_instance = DiscoverRequest()
85
+ pb_discover_instance.type = request.type
86
+ pb_discover_instance.service.CopyFrom(service_to_proto(request.name, request.namespace, request.revision))
87
+ return pb_discover_instance
88
+
89
+
90
+ # # location转成proto
91
+ # def location_to_proto(region, zone, campus):
92
+ # pb_location = Location()
93
+ # pb_location.region.CopyFrom(wrappers.StringValue(value=region))
94
+ # pb_location.zoneCopyFrom(wrappers.StringValue(value=zone))
95
+ # pb_location.campus.CopyFrom(wrappers.StringValue(value=campus))
96
+ # return pb_location
97
+
98
+
99
+ # report_client请求转成proto
100
+ def report_client_to_proto(request: ReportClientRequest):
101
+ pb_client_instance = Client()
102
+ pb_client_instance.host.CopyFrom(wrappers.StringValue(value=request.host))
103
+ pb_client_instance.id.CopyFrom(wrappers.StringValue(value=request.client_id))
104
+ pb_client_instance.type = Client.SDK
105
+ pb_client_instance.version.CopyFrom(wrappers.StringValue(value=request.version))
106
+ for info in request.reporter_meta_infos:
107
+ stat = pb_client_instance.stat.add()
108
+ stat.target.value = info["target"]
109
+ stat.port.value = info["port"]
110
+ stat.path.value = info["path"]
111
+ stat.protocol.value = info["protocol"]
112
+ # pb_client_instance.location = location_to_proto(request.region, request.zone, request.campus)
113
+ return pb_client_instance
114
+
115
+
116
+ # hearbeat请求转成proto
117
+ def heartbeat_request_to_proto(request: InstanceHeartbeatRequest):
118
+ pb_heartbeat = Instance()
119
+ pb_heartbeat.service.CopyFrom(wrappers.StringValue(value=request.service))
120
+ pb_heartbeat.namespace.CopyFrom(wrappers.StringValue(value=request.namespace))
121
+ pb_heartbeat.host.CopyFrom(wrappers.StringValue(value=request.host))
122
+ pb_heartbeat.port.CopyFrom(wrappers.UInt32Value(value=request.port))
123
+ pb_heartbeat.service_token.CopyFrom(wrappers.StringValue(value=request.service_token))
124
+ if request.instance_id != '':
125
+ pb_heartbeat.id.CopyFrom(wrappers.StringValue(value=request.instance_id))
126
+
127
+ return pb_heartbeat
128
+
129
+
130
+ def contract_request_to_proto(request):
131
+ """Convert public contract models into the specification request."""
132
+ values = {key: getattr(request, key) for key in ("namespace", "service", "protocol", "version")}
133
+ values["name"] = getattr(request, "name", "") or request.service
134
+ if isinstance(request, ServiceContract):
135
+ values.update(name=request.name or request.service, revision=request.revision, content=request.content,
136
+ service_features=[asdict(feature) for feature in request.service_features],
137
+ interfaces=[asdict(interface) for interface in request.interfaces])
138
+ return contract_pb2.ServiceContract(**values)
139
+
140
+
141
+ def contract_response_from_proto(response):
142
+ """Expose a contract without protobuf objects in the public result."""
143
+ return ServiceContract(
144
+ namespace=response.namespace, service=response.service, protocol=response.protocol,
145
+ version=response.version, name=response.name, revision=response.revision, content=response.content,
146
+ service_features=[ServiceFeature(item.type, item.name, item.description, item.content, item.status)
147
+ for item in response.service_features],
148
+ interfaces=[InterfaceDescriptor(item.name, item.method, item.path, item.content) for item in response.interfaces],
149
+ )
@@ -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 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
+ """Polaris gRPC server connector plugin."""
17
+
18
+ from .connection import GrpcConnection, GrpcSettings
19
+ from .grpc_client import GrpcConnector
20
+
21
+ __all__ = ["GrpcConnection", "GrpcConnector", "GrpcSettings"]
@@ -0,0 +1,401 @@
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
+ """Low-level asynchronous gRPC connection management."""
17
+
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ import itertools
22
+ import time
23
+ import traceback
24
+ from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
25
+ from dataclasses import dataclass
26
+
27
+ import grpc
28
+ from google.protobuf.message import Message
29
+ from polarismesh_specification.api.v1.service_manage import (
30
+ client_pb2,
31
+ grpcapi_pb2_grpc,
32
+ request_pb2,
33
+ response_pb2,
34
+ service_pb2,
35
+ )
36
+
37
+ from polaris.pkg.model.error import ErrorCode, SDKError, response_code, ServerCode
38
+ from polaris.api.log import ContextLogger, get_logger
39
+
40
+ logger = get_logger("network")
41
+ base_logger = get_logger("base")
42
+
43
+ _REQUEST_ID_PREFIX = {
44
+ "RegisterInstance": "1",
45
+ "DeregisterInstance": "2",
46
+ "Heartbeat": "3",
47
+ "ReportClient": "3",
48
+ "Discover": "4",
49
+ "ReportServiceContract": "5",
50
+ "GetServiceContract": "6",
51
+ "GetSkill": "1",
52
+ "GetSkillList": "1",
53
+ "DownloadSkill": "1",
54
+ }
55
+ _REQUEST_ID_SEEDS = {prefix: itertools.count(1) for prefix in set(_REQUEST_ID_PREFIX.values())}
56
+
57
+
58
+ def _request_id(method: str) -> tuple[tuple[str, str], ...]:
59
+ prefix = _REQUEST_ID_PREFIX.get(method)
60
+ if prefix is None:
61
+ return ()
62
+ return (("request-id", f"{prefix}{next(_REQUEST_ID_SEEDS[prefix])}"),)
63
+
64
+
65
+ def _response_location(response) -> str:
66
+ try:
67
+ if response.HasField("client") and response.client.HasField("location"):
68
+ location = response.client.location
69
+ return f"{location.region.value}/{location.zone.value}/{location.campus.value}"
70
+ except (AttributeError, ValueError):
71
+ pass
72
+ return "-"
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class GrpcSettings:
77
+ """Values required by the gRPC transport."""
78
+
79
+ addresses: Sequence[str]
80
+ timeout_seconds: float
81
+ token: str = ""
82
+ retry_attempts: int = 2
83
+ retry_backoff_seconds: float = 0.1
84
+ refresh_interval_seconds: float = 2.0
85
+ switch_interval_seconds: float | None = None
86
+ idle_timeout_seconds: float | None = None
87
+ credentials: grpc.ChannelCredentials | None = None
88
+
89
+
90
+ class GrpcConnection:
91
+ """Manage channels, endpoint rotation, retries and discovery streams."""
92
+
93
+ _RETRYABLE_STATUS = {
94
+ grpc.StatusCode.UNAVAILABLE,
95
+ grpc.StatusCode.DEADLINE_EXCEEDED,
96
+ grpc.StatusCode.RESOURCE_EXHAUSTED,
97
+ }
98
+
99
+ def __init__(self, settings: GrpcSettings, context_logger=None, stub_factory=None) -> None:
100
+ self.settings = settings
101
+ self.logger = (context_logger.get_logger("network") if isinstance(context_logger, ContextLogger)
102
+ else context_logger if context_logger is not None else logger)
103
+ self.base_logger = (context_logger.get_logger("base") if isinstance(context_logger, ContextLogger)
104
+ else base_logger)
105
+ self._addresses = [address.strip() for address in settings.addresses if address.strip()]
106
+ self._channels: dict[str, grpc.aio.Channel] = {}
107
+ self._stubs: dict[str, grpcapi_pb2_grpc.PolarisGRPCStub] = {}
108
+ self._stub_factory = stub_factory or grpcapi_pb2_grpc.PolarisGRPCStub
109
+ self._contract_stubs = {}
110
+ self._address_index = 0
111
+ self._lock = asyncio.Lock()
112
+ self._closed = False
113
+ self.stat_reporter = None
114
+ self.target_service = ("Polaris", "polaris.discover")
115
+ self.address_resolver = None
116
+ self.on_switch = None
117
+ self._switch_task = None
118
+
119
+ def _metadata(self) -> tuple[tuple[str, str], ...]:
120
+ if not self.settings.token:
121
+ return ()
122
+ return (("x-polaris-token", self.settings.token),)
123
+
124
+ async def _stub(self, address: str) -> grpcapi_pb2_grpc.PolarisGRPCStub:
125
+ async with self._lock:
126
+ if self._closed:
127
+ raise SDKError(ErrorCode.ErrCodeInvalidStateError, "gRPC connector has been closed")
128
+ if address not in self._stubs:
129
+ secure = self.settings.credentials is not None
130
+ channel = (grpc.aio.secure_channel(address, self.settings.credentials)
131
+ if self.settings.credentials is not None else grpc.aio.insecure_channel(address))
132
+ self._channels[address] = channel
133
+ self._stubs[address] = self._stub_factory(channel)
134
+ self.logger.info(
135
+ "grpc channel opened address=%s security=%s",
136
+ address, "mtls" if secure else "plaintext",
137
+ )
138
+ return self._stubs[address]
139
+
140
+ async def _rpc_stub(self, method, address):
141
+ stub = await self._stub(address)
142
+ if method not in ("ReportServiceContract", "GetServiceContract") or hasattr(stub, method):
143
+ return stub
144
+ if address not in self._contract_stubs:
145
+ self._contract_stubs[address] = grpcapi_pb2_grpc.PolarisServiceContractGRPCStub(self._channels[address])
146
+ return self._contract_stubs[address]
147
+
148
+ async def _unary(
149
+ self,
150
+ method: str,
151
+ request: Message,
152
+ extra_metadata: tuple[tuple[str, str], ...] = (),
153
+ timeout_seconds: float | None = None,
154
+ ) -> response_pb2.Response:
155
+ if not self._addresses:
156
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, "no Polaris server address configured")
157
+ last_error: Exception | None = None
158
+ attempts = self.settings.retry_attempts + 1
159
+ for attempt in range(attempts):
160
+ address = self._addresses[self._address_index % len(self._addresses)]
161
+ started = time.monotonic()
162
+ response = None
163
+ try:
164
+ stub = await self._rpc_stub(method, address)
165
+ rpc: Callable[..., Awaitable[response_pb2.Response]] = getattr(stub, method)
166
+ response = await rpc(
167
+ request,
168
+ timeout=self.settings.timeout_seconds if timeout_seconds is None else timeout_seconds,
169
+ metadata=_request_id(method) + extra_metadata + self._metadata(),
170
+ )
171
+ code = response_code(response)
172
+ cost = (time.monotonic() - started) * 1000
173
+ if method == "ReportClient":
174
+ if code == ServerCode.ExecuteSuccess:
175
+ self.base_logger.info(
176
+ "reportClient success address=%s serverCode=%s cost=%.3fms location=%s",
177
+ address, code, cost, _response_location(response),
178
+ )
179
+ else:
180
+ self.base_logger.warning(
181
+ "reportClient failed address=%s serverCode=%s cost=%.3fms location=%s",
182
+ address, code, cost, _response_location(response),
183
+ )
184
+ self.logger.debug("Polaris ReportClient success address=%s code=%s cost=%.3fms",
185
+ address, code, cost)
186
+ else:
187
+ # Heartbeats repeat on every TTL; keep them out of the INFO stream like polaris-go.
188
+ quiet_methods = {"Heartbeat", "GetServiceContract"}
189
+ log_success = self.logger.debug if method in quiet_methods else self.logger.info
190
+ log_success(
191
+ "Polaris %s success address=%s code=%s cost=%.3fms",
192
+ method, address, code, cost,
193
+ )
194
+ return response
195
+ except grpc.aio.AioRpcError as exc:
196
+ last_error = exc
197
+ cost = (time.monotonic() - started) * 1000
198
+ if method == "ReportClient":
199
+ self.base_logger.warning(
200
+ "reportClient failed address=%s code=%s cost=%.3fms: %s",
201
+ address, exc.code().name, cost, exc.details(),
202
+ )
203
+ self.logger.debug("Polaris ReportClient failed address=%s code=%s cost=%.3fms: %s",
204
+ address, exc.code().name, cost, exc.details())
205
+ else:
206
+ self.logger.error(
207
+ "Polaris %s failed address=%s code=%s cost=%.3fms: %s",
208
+ method, address, exc.code().name, cost, exc.details(),
209
+ )
210
+ if exc.code() not in self._RETRYABLE_STATUS or attempt + 1 >= attempts:
211
+ break
212
+ self._address_index = (self._address_index + 1) % len(self._addresses)
213
+ self.logger.warning(
214
+ "Polaris %s on %s failed with %s; retrying on %s",
215
+ method,
216
+ address,
217
+ exc.code().name,
218
+ self._addresses[self._address_index],
219
+ )
220
+ await asyncio.sleep(self.settings.retry_backoff_seconds * (2**attempt))
221
+ finally:
222
+ if method in {"RegisterInstance", "DeregisterInstance", "Heartbeat",
223
+ "ReportServiceContract", "GetServiceContract"}:
224
+ self._report_call(method, address, started, response)
225
+ raise SDKError(ErrorCode.ErrCodeNetworkError, f"Polaris {method} failed: {last_error}") from last_error
226
+
227
+ async def switch_address(self):
228
+ """Move subsequent calls and streams to the next configured endpoint."""
229
+ if len(self._addresses) < 2:
230
+ return
231
+ previous = self._addresses[self._address_index % len(self._addresses)]
232
+ self._address_index = (self._address_index + 1) % len(self._addresses)
233
+ current = self._addresses[self._address_index]
234
+ self.logger.info("server connection switched from %s to %s", previous, current)
235
+ if self.on_switch is not None:
236
+ try:
237
+ await self.on_switch()
238
+ except Exception:
239
+ self.logger.warning("server switch callback failed: %s", traceback.format_exc())
240
+
241
+ def start_switching(self, loop=None):
242
+ """Start periodic endpoint rotation on the context's event loop."""
243
+ if (self._closed or self._switch_task is not None
244
+ or self.settings.switch_interval_seconds is None or len(self._addresses) < 2):
245
+ return
246
+ loop = loop or asyncio.get_running_loop()
247
+ self._switch_task = loop.create_task(self._switch_loop(), name="polaris-server-switch")
248
+
249
+ async def _switch_loop(self):
250
+ interval = self.settings.switch_interval_seconds
251
+ while not self._closed:
252
+ await asyncio.sleep(interval)
253
+ if self._closed:
254
+ return
255
+ await self.switch_address()
256
+
257
+ async def stop_switching(self):
258
+ """Stop endpoint rotation before the connection closes."""
259
+ if self._switch_task is not None:
260
+ self._switch_task.cancel()
261
+ await asyncio.gather(self._switch_task, return_exceptions=True)
262
+ self._switch_task = None
263
+
264
+ def _report_call(self, method, address, started, response):
265
+ if self.stat_reporter is None:
266
+ return
267
+ from polaris.pkg.model.service import ServiceCallResult, RetStatus
268
+ from polaris.pkg.model.util import get_ip_from_int_with_port
269
+ host, port = get_ip_from_int_with_port(address)
270
+ code = response_code(response) if response is not None else ErrorCode.ErrCodeNetworkError
271
+ accepted = {ServerCode.ExecuteSuccess}
272
+ if method == "RegisterInstance":
273
+ accepted.add(ServerCode.ExistedResource)
274
+ if method == "DeregisterInstance":
275
+ accepted.add(ServerCode.NotFoundResource)
276
+ success = code in accepted
277
+ names = {"RegisterInstance": "register", "DeregisterInstance": "deRegister", "Heartbeat": "heartbeat",
278
+ "ReportServiceContract": "reportServiceContract", "GetServiceContract": "getServiceContract"}
279
+ self.stat_reporter.report(ServiceCallResult(
280
+ namespace=self.target_service[0], service=self.target_service[1], host=host, port=port,
281
+ method=names[method], ret_code=0 if success else ErrorCode.ErrCodeServerError if response is not None else code,
282
+ ret_status=RetStatus.RetSuccess if success else RetStatus.RetFail,
283
+ delay=int((time.monotonic() - started) * 1000),
284
+ ))
285
+
286
+ async def register_instance(self, instance: service_pb2.Instance,
287
+ auto_heartbeat: bool = False) -> response_pb2.Response:
288
+ """Register an instance using the headers sent by polaris-java."""
289
+ metadata = (("async-regis", "true"),) if auto_heartbeat else ()
290
+ return await self._unary("RegisterInstance", instance, metadata)
291
+
292
+ async def deregister_instance(self, instance: service_pb2.Instance) -> response_pb2.Response:
293
+ """Deregister an instance."""
294
+ return await self._unary("DeregisterInstance", instance)
295
+
296
+ async def heartbeat(self, instance: service_pb2.Instance) -> response_pb2.Response:
297
+ """Report an instance heartbeat."""
298
+ return await self._unary("Heartbeat", instance)
299
+
300
+ async def report_client(self, client: client_pb2.Client) -> response_pb2.Response:
301
+ """Report this SDK client."""
302
+ return await self._unary("ReportClient", client)
303
+
304
+ async def _request_once(
305
+ self,
306
+ address: str,
307
+ request: request_pb2.DiscoverRequest,
308
+ ) -> AsyncIterator[response_pb2.DiscoverResponse]:
309
+ stub = await self._stub(address)
310
+ pending = request_pb2.DiscoverRequest()
311
+ pending.CopyFrom(request)
312
+ request_reader = None
313
+
314
+ async def requests() -> AsyncIterator[request_pb2.DiscoverRequest]:
315
+ nonlocal request_reader
316
+ request_reader = asyncio.current_task()
317
+ while not self._closed:
318
+ outgoing = request_pb2.DiscoverRequest()
319
+ outgoing.CopyFrom(pending)
320
+ yield outgoing
321
+ await asyncio.sleep(self.settings.refresh_interval_seconds)
322
+
323
+ request_stream = requests()
324
+ stream = stub.Discover(request_stream, timeout=None, metadata=_request_id("Discover") + self._metadata())
325
+ try:
326
+ async for response in stream:
327
+ revision = response.service.revision.value
328
+ if revision:
329
+ pending.service.revision.value = revision
330
+ yield response
331
+ finally:
332
+ cancel = getattr(stream, "cancel", None)
333
+ if cancel is not None:
334
+ cancel()
335
+ else:
336
+ close = getattr(stream, "aclose", None)
337
+ if close is not None:
338
+ await close()
339
+ # gRPC consumes requests on its own task; wait for cancellation before closing the generator.
340
+ if request_reader is not None and request_reader is not asyncio.current_task():
341
+ if not request_reader.done():
342
+ request_reader.cancel()
343
+ await asyncio.gather(request_reader, return_exceptions=True)
344
+ await request_stream.aclose()
345
+
346
+ async def discover(
347
+ self,
348
+ request: request_pb2.DiscoverRequest,
349
+ ) -> AsyncIterator[response_pb2.DiscoverResponse]:
350
+ """Open a reconnecting discovery stream."""
351
+ if not self._addresses:
352
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, "no Polaris server address configured")
353
+ service = request.service
354
+ self.logger.debug(
355
+ "Discover stream starting namespace=%s service=%s type=%s revision=%s",
356
+ service.namespace.value, service.name.value, request.type, service.revision.value,
357
+ )
358
+ failing = False
359
+ while not self._closed:
360
+ if self.address_resolver is not None:
361
+ await self.address_resolver()
362
+ address = self._addresses[self._address_index % len(self._addresses)]
363
+ try:
364
+ session = self._request_once(address, request)
365
+ try:
366
+ async for response in session:
367
+ if failing:
368
+ self.logger.info("Discover reconnected to %s", address)
369
+ failing = False
370
+ yield response
371
+ finally:
372
+ await session.aclose()
373
+ if not self._closed:
374
+ self.logger.warning(
375
+ "Discover stream ended namespace=%s service=%s type=%s; reconnecting",
376
+ service.namespace.value, service.name.value, request.type,
377
+ )
378
+ await asyncio.sleep(self.settings.retry_backoff_seconds)
379
+ except grpc.aio.AioRpcError as exc:
380
+ if exc.code() not in self._RETRYABLE_STATUS:
381
+ raise SDKError(ErrorCode.ErrCodeNetworkError, f"Polaris Discover failed: {exc}") from exc
382
+ if not failing:
383
+ self.logger.warning("Discover on %s failed with %s; reconnecting", address, exc.code().name)
384
+ failing = True
385
+ self._address_index = (self._address_index + 1) % len(self._addresses)
386
+ await asyncio.sleep(self.settings.retry_backoff_seconds)
387
+
388
+ async def close(self) -> None:
389
+ """Close every channel owned by this connection."""
390
+ self._closed = True
391
+ if self._switch_task is not None:
392
+ self._switch_task.cancel()
393
+ switch_task = self._switch_task
394
+ self._switch_task = None
395
+ await asyncio.gather(switch_task, return_exceptions=True)
396
+ channels = list(self._channels.values())
397
+ self._channels.clear()
398
+ self._stubs.clear()
399
+ self._contract_stubs.clear()
400
+ for channel in channels:
401
+ await channel.close()