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,384 @@
|
|
|
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
|
+
"""Server connector plugin backed by the Polaris gRPC API."""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
from dataclasses import replace
|
|
22
|
+
from collections.abc import AsyncIterator
|
|
23
|
+
from contextlib import asynccontextmanager
|
|
24
|
+
import time
|
|
25
|
+
|
|
26
|
+
from polarismesh_specification.api.v1.service_manage import response_pb2
|
|
27
|
+
|
|
28
|
+
from polaris.pkg.model.error import ErrorCode, SDKError, ServerCode, ensure_response
|
|
29
|
+
from polaris.pkg.model.server import (
|
|
30
|
+
InstanceDeRegisterRequest,
|
|
31
|
+
InstanceDiscoverRequest,
|
|
32
|
+
InstanceHeartbeatRequest,
|
|
33
|
+
InstanceRegisterRequest,
|
|
34
|
+
InstanceRegisterResponse,
|
|
35
|
+
ReportClientRequest,
|
|
36
|
+
ReportClientResponse,
|
|
37
|
+
)
|
|
38
|
+
from polaris.pkg.model.service import GetOneInstanceRequest, InstancesResponse
|
|
39
|
+
from ..base import BaseServerConnector
|
|
40
|
+
from ..convert import (
|
|
41
|
+
contract_request_to_proto,
|
|
42
|
+
contract_response_from_proto,
|
|
43
|
+
deregister_request_to_proto,
|
|
44
|
+
discover_request_to_proto,
|
|
45
|
+
heartbeat_request_to_proto,
|
|
46
|
+
register_request_to_proto,
|
|
47
|
+
report_client_to_proto,
|
|
48
|
+
)
|
|
49
|
+
from polaris.plugin_manager import InitContext, PluginType, register_plugin
|
|
50
|
+
|
|
51
|
+
from .connection import GrpcConnection, GrpcSettings
|
|
52
|
+
from .tls import channel_credentials
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@register_plugin(PluginType.SERVER_CONNECTOR, "grpc")
|
|
56
|
+
class GrpcConnector(BaseServerConnector):
|
|
57
|
+
"""Adapt internal SDK request objects to specification gRPC calls."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, init_ctx: InitContext) -> None:
|
|
60
|
+
self.init_ctx = init_ctx
|
|
61
|
+
self.config = init_ctx.config
|
|
62
|
+
connector_config = self.config.get_global().get_server_connector()
|
|
63
|
+
api_config = self.config.get_global().get_api()
|
|
64
|
+
cache_config = self.config.get_consumer().get_local_cache()
|
|
65
|
+
self.connection = GrpcConnection(
|
|
66
|
+
GrpcSettings(
|
|
67
|
+
addresses=connector_config.get_addresses(),
|
|
68
|
+
timeout_seconds=max(connector_config.get_message_timeout(), 1) / 1000,
|
|
69
|
+
token=connector_config.get_token(),
|
|
70
|
+
retry_attempts=api_config.get_max_retry_times(),
|
|
71
|
+
retry_backoff_seconds=max(api_config.get_retry_interval(), 0) / 1000,
|
|
72
|
+
refresh_interval_seconds=max(cache_config.get_service_refresh_interval(), 1) / 1000,
|
|
73
|
+
switch_interval_seconds=max(connector_config.get_server_switch_interval(), 1) / 1000,
|
|
74
|
+
idle_timeout_seconds=max(connector_config.get_connect_idle_timeout(), 1) / 1000,
|
|
75
|
+
credentials=channel_credentials(connector_config),
|
|
76
|
+
),
|
|
77
|
+
context_logger=init_ctx.logger,
|
|
78
|
+
)
|
|
79
|
+
self._streams: dict[str, AsyncIterator[response_pb2.DiscoverResponse]] = {}
|
|
80
|
+
self._stream_locks: dict[str, asyncio.Lock] = {}
|
|
81
|
+
self._closed = False
|
|
82
|
+
self.engine = None
|
|
83
|
+
self._cluster_connections = {}
|
|
84
|
+
self._stream_connections = {}
|
|
85
|
+
self._stream_last_activity = {}
|
|
86
|
+
self._idle_task = None
|
|
87
|
+
self._request_slots = asyncio.Semaphore(connector_config.get_request_queue_size() + 1)
|
|
88
|
+
self.logger = init_ctx.logger.get_logger("network") if init_ctx.logger is not None else None
|
|
89
|
+
self.base_logger = init_ctx.logger.get_logger("base") if init_ctx.logger is not None else None
|
|
90
|
+
|
|
91
|
+
@asynccontextmanager
|
|
92
|
+
async def _request_slot(self):
|
|
93
|
+
"""Bound concurrent requests; extra callers wait like Go's buffered task channel."""
|
|
94
|
+
await self._request_slots.acquire()
|
|
95
|
+
try:
|
|
96
|
+
yield
|
|
97
|
+
finally:
|
|
98
|
+
self._request_slots.release()
|
|
99
|
+
|
|
100
|
+
def start_address_switching(self):
|
|
101
|
+
"""Start periodic address rotation and idle stream cleanup."""
|
|
102
|
+
self._start_address_switching(self.connection, self.engine.loop if self.engine is not None else None)
|
|
103
|
+
self._start_idle_cleanup(self.engine.loop if self.engine is not None else None)
|
|
104
|
+
|
|
105
|
+
def _start_address_switching(self, connection, loop=None):
|
|
106
|
+
connection.on_switch = lambda: self._on_address_switched(connection)
|
|
107
|
+
connection.start_switching(loop)
|
|
108
|
+
|
|
109
|
+
def _start_idle_cleanup(self, loop=None):
|
|
110
|
+
if self._closed or self._idle_task is not None:
|
|
111
|
+
return
|
|
112
|
+
loop = loop or asyncio.get_running_loop()
|
|
113
|
+
self._idle_task = loop.create_task(self._idle_cleanup_loop(), name="polaris-idle-stream-cleanup")
|
|
114
|
+
|
|
115
|
+
async def _idle_cleanup_loop(self):
|
|
116
|
+
interval = self.connection.settings.idle_timeout_seconds
|
|
117
|
+
while not self._closed:
|
|
118
|
+
await asyncio.sleep(interval)
|
|
119
|
+
if self._closed:
|
|
120
|
+
return
|
|
121
|
+
await self.close_idle_streams()
|
|
122
|
+
|
|
123
|
+
async def close_idle_streams(self):
|
|
124
|
+
"""Close idle Discover streams while retaining shared gRPC channels."""
|
|
125
|
+
timeout = self.connection.settings.idle_timeout_seconds
|
|
126
|
+
now = time.monotonic()
|
|
127
|
+
for key, last_activity in list(self._stream_last_activity.items()):
|
|
128
|
+
if now - last_activity < timeout:
|
|
129
|
+
continue
|
|
130
|
+
lock = self._stream_locks.get(key)
|
|
131
|
+
if lock is None or lock.locked():
|
|
132
|
+
continue
|
|
133
|
+
async with lock:
|
|
134
|
+
current_activity = self._stream_last_activity.get(key)
|
|
135
|
+
if current_activity != last_activity or now - current_activity < timeout:
|
|
136
|
+
continue
|
|
137
|
+
stream = self._streams.pop(key, None)
|
|
138
|
+
self._stream_connections.pop(key, None)
|
|
139
|
+
self._stream_last_activity.pop(key, None)
|
|
140
|
+
self._stream_locks.pop(key, None)
|
|
141
|
+
if stream is not None:
|
|
142
|
+
await stream.aclose()
|
|
143
|
+
if self.logger is not None:
|
|
144
|
+
self.logger.debug("closed idle Discover stream %s", key)
|
|
145
|
+
|
|
146
|
+
async def _stop_idle_cleanup(self):
|
|
147
|
+
if self._idle_task is not None:
|
|
148
|
+
self._idle_task.cancel()
|
|
149
|
+
await asyncio.gather(self._idle_task, return_exceptions=True)
|
|
150
|
+
self._idle_task = None
|
|
151
|
+
|
|
152
|
+
async def _on_address_switched(self, connection):
|
|
153
|
+
"""Close streams pinned to the old endpoint so the next read reconnects."""
|
|
154
|
+
keys = [key for key, owner in self._stream_connections.items() if owner is connection]
|
|
155
|
+
for key in keys:
|
|
156
|
+
stream = self._streams.get(key)
|
|
157
|
+
if stream is None:
|
|
158
|
+
self._stream_connections.pop(key, None)
|
|
159
|
+
continue
|
|
160
|
+
lock = self._stream_locks.get(key)
|
|
161
|
+
if lock is not None and not lock.locked():
|
|
162
|
+
async with lock:
|
|
163
|
+
if self._streams.get(key) is stream:
|
|
164
|
+
self._streams.pop(key, None)
|
|
165
|
+
self._stream_connections.pop(key, None)
|
|
166
|
+
self._stream_last_activity.pop(key, None)
|
|
167
|
+
await stream.aclose()
|
|
168
|
+
else:
|
|
169
|
+
await stream.aclose()
|
|
170
|
+
self._streams.pop(key, None)
|
|
171
|
+
self._stream_connections.pop(key, None)
|
|
172
|
+
self._stream_last_activity.pop(key, None)
|
|
173
|
+
|
|
174
|
+
async def _connection(self, heartbeat=False):
|
|
175
|
+
system = self.config.get_global().get_system()
|
|
176
|
+
cluster = system.get_health_check_cluster() if heartbeat else system.get_discover_cluster()
|
|
177
|
+
if not cluster.namespace or not cluster.service or self.engine is None:
|
|
178
|
+
return self.connection
|
|
179
|
+
key = (cluster.namespace, cluster.service)
|
|
180
|
+
from polaris.pkg.model.service import GetInstancesRequest
|
|
181
|
+
snapshot = await self.engine._query_instances(GetInstancesRequest(
|
|
182
|
+
namespace=cluster.namespace, service=cluster.service), "healthy")
|
|
183
|
+
if snapshot is None or not snapshot.instances:
|
|
184
|
+
raise SDKError(ErrorCode.ErrCodeAPIInstanceNotFound, f"No system service instance for {key}")
|
|
185
|
+
addresses = []
|
|
186
|
+
for node in snapshot.instances:
|
|
187
|
+
host = node.get_host()
|
|
188
|
+
addresses.append(f"[{host}]:{node.get_port()}" if ":" in host else f"{host}:{node.get_port()}")
|
|
189
|
+
if key not in self._cluster_connections:
|
|
190
|
+
connection = GrpcConnection(replace(self.connection.settings, addresses=addresses),
|
|
191
|
+
context_logger=self.init_ctx.logger)
|
|
192
|
+
connection.target_service = key
|
|
193
|
+
connection.stat_reporter = self.connection.stat_reporter
|
|
194
|
+
connection.address_resolver = lambda: self._connection(heartbeat=heartbeat)
|
|
195
|
+
self._cluster_connections[key] = connection
|
|
196
|
+
connection = self._cluster_connections[key]
|
|
197
|
+
self._start_address_switching(connection)
|
|
198
|
+
connection._addresses = addresses
|
|
199
|
+
return connection
|
|
200
|
+
|
|
201
|
+
async def register_instance(self, request: InstanceRegisterRequest) -> InstanceRegisterResponse:
|
|
202
|
+
connection = await self._connection()
|
|
203
|
+
async with self._request_slot():
|
|
204
|
+
response = await connection.register_instance(
|
|
205
|
+
register_request_to_proto(request), auto_heartbeat=request.auto_heartbeat,
|
|
206
|
+
)
|
|
207
|
+
ensure_response(response, (ServerCode.ExecuteSuccess, ServerCode.ExistedResource))
|
|
208
|
+
instance_id = response.instance.id.value if response.HasField("instance") else ""
|
|
209
|
+
if self.logger is not None:
|
|
210
|
+
self.logger.info(
|
|
211
|
+
"instance registered namespace=%s service=%s host=%s port=%s instanceId=%s autoHeartbeat=%s",
|
|
212
|
+
request.namespace, request.service, request.host, request.port, instance_id,
|
|
213
|
+
request.auto_heartbeat,
|
|
214
|
+
)
|
|
215
|
+
if self.base_logger is not None:
|
|
216
|
+
metadata = dict(getattr(request, "metadata", {}) or {})
|
|
217
|
+
self.base_logger.info(
|
|
218
|
+
"registerInstance success namespace=%s service=%s host=%s port=%s "
|
|
219
|
+
"instanceId=%s metadataSize=%d metadataKeys=%s",
|
|
220
|
+
request.namespace, request.service, request.host, request.port, instance_id,
|
|
221
|
+
len(metadata), sorted(metadata),
|
|
222
|
+
)
|
|
223
|
+
return InstanceRegisterResponse(instance_id)
|
|
224
|
+
|
|
225
|
+
async def deregister_instance(self, request: InstanceDeRegisterRequest) -> None:
|
|
226
|
+
connection = await self._connection()
|
|
227
|
+
async with self._request_slot():
|
|
228
|
+
response = await connection.deregister_instance(deregister_request_to_proto(request))
|
|
229
|
+
ensure_response(response, (ServerCode.ExecuteSuccess, ServerCode.NotFoundResource))
|
|
230
|
+
if self.logger is not None:
|
|
231
|
+
self.logger.info(
|
|
232
|
+
"instance deregistered namespace=%s service=%s host=%s port=%s instanceId=%s",
|
|
233
|
+
request.namespace, request.service, request.host, request.port, request.instance_id,
|
|
234
|
+
)
|
|
235
|
+
if self.base_logger is not None:
|
|
236
|
+
self.base_logger.info(
|
|
237
|
+
"deregisterInstance success namespace=%s service=%s host=%s port=%s instanceId=%s",
|
|
238
|
+
request.namespace, request.service, request.host, request.port, request.instance_id,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def _stream_key(request: InstanceDiscoverRequest) -> str:
|
|
243
|
+
return f"{request.namespace}\\x00{request.name}\\x00{request.type}"
|
|
244
|
+
|
|
245
|
+
async def discover(
|
|
246
|
+
self,
|
|
247
|
+
request: InstanceDiscoverRequest,
|
|
248
|
+
use_discover_cache: bool = True,
|
|
249
|
+
) -> response_pb2.DiscoverResponse:
|
|
250
|
+
del use_discover_cache
|
|
251
|
+
if self._closed:
|
|
252
|
+
raise SDKError(ErrorCode.ErrCodeInvalidStateError, "gRPC connector has been closed")
|
|
253
|
+
key = self._stream_key(request)
|
|
254
|
+
lock = self._stream_locks.setdefault(key, asyncio.Lock())
|
|
255
|
+
async with lock:
|
|
256
|
+
stream = self._streams.get(key)
|
|
257
|
+
if stream is None:
|
|
258
|
+
system = self.config.get_global().get_system()
|
|
259
|
+
system_keys = {(system.get_discover_cluster().namespace, system.get_discover_cluster().service),
|
|
260
|
+
(system.get_health_check_cluster().namespace, system.get_health_check_cluster().service)}
|
|
261
|
+
connection = (self.connection if (request.namespace, request.name) in system_keys
|
|
262
|
+
else await self._connection())
|
|
263
|
+
stream = connection.discover(discover_request_to_proto(request))
|
|
264
|
+
self._streams[key] = stream
|
|
265
|
+
self._stream_connections[key] = connection
|
|
266
|
+
self._stream_last_activity[key] = time.monotonic()
|
|
267
|
+
if self.logger is not None:
|
|
268
|
+
self.logger.info(
|
|
269
|
+
"Discover stream opened namespace=%s service=%s type=%s revision=%s",
|
|
270
|
+
request.namespace, request.name, request.type, request.revision,
|
|
271
|
+
)
|
|
272
|
+
try:
|
|
273
|
+
async with self._request_slot():
|
|
274
|
+
response = await anext(stream)
|
|
275
|
+
self._stream_last_activity[key] = time.monotonic()
|
|
276
|
+
except StopAsyncIteration:
|
|
277
|
+
self._streams.pop(key, None)
|
|
278
|
+
self._stream_connections.pop(key, None)
|
|
279
|
+
self._stream_last_activity.pop(key, None)
|
|
280
|
+
if self.logger is not None:
|
|
281
|
+
self.logger.warning(
|
|
282
|
+
"Discover stream closed namespace=%s service=%s type=%s",
|
|
283
|
+
request.namespace, request.name, request.type,
|
|
284
|
+
)
|
|
285
|
+
raise SDKError(ErrorCode.ErrCodeNetworkError, f"Discover stream closed for {request.name}")
|
|
286
|
+
except (asyncio.CancelledError, Exception):
|
|
287
|
+
self._streams.pop(key, None)
|
|
288
|
+
self._stream_connections.pop(key, None)
|
|
289
|
+
self._stream_last_activity.pop(key, None)
|
|
290
|
+
await stream.aclose()
|
|
291
|
+
raise
|
|
292
|
+
ensure_response(response)
|
|
293
|
+
if self.logger is not None:
|
|
294
|
+
self.logger.debug(
|
|
295
|
+
"Discover response namespace=%s service=%s type=%s revision=%s instances=%s",
|
|
296
|
+
request.namespace, request.name, request.type, response.service.revision.value,
|
|
297
|
+
len(response.instances),
|
|
298
|
+
)
|
|
299
|
+
return response
|
|
300
|
+
|
|
301
|
+
async def stop_discover(self, request: InstanceDiscoverRequest) -> None:
|
|
302
|
+
"""Close one idle resource stream without closing shared gRPC channels."""
|
|
303
|
+
key = self._stream_key(request)
|
|
304
|
+
lock = self._stream_locks.get(key)
|
|
305
|
+
if lock is None:
|
|
306
|
+
return
|
|
307
|
+
async with lock:
|
|
308
|
+
stream = self._streams.pop(key, None)
|
|
309
|
+
self._stream_connections.pop(key, None)
|
|
310
|
+
self._stream_last_activity.pop(key, None)
|
|
311
|
+
try:
|
|
312
|
+
if stream is not None:
|
|
313
|
+
await stream.aclose()
|
|
314
|
+
finally:
|
|
315
|
+
self._stream_locks.pop(key, None)
|
|
316
|
+
if self.logger is not None:
|
|
317
|
+
self.logger.info(
|
|
318
|
+
"Discover stream stopped namespace=%s service=%s type=%s",
|
|
319
|
+
request.namespace, request.name, request.type,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
async def heartbeat(self, request: InstanceHeartbeatRequest) -> None:
|
|
323
|
+
connection = await self._connection(heartbeat=True)
|
|
324
|
+
async with self._request_slot():
|
|
325
|
+
response = await connection.heartbeat(heartbeat_request_to_proto(request))
|
|
326
|
+
ensure_response(response)
|
|
327
|
+
|
|
328
|
+
async def report_client(self, request: ReportClientRequest) -> ReportClientResponse:
|
|
329
|
+
connection = await self._connection()
|
|
330
|
+
async with self._request_slot():
|
|
331
|
+
response = await connection.report_client(report_client_to_proto(request))
|
|
332
|
+
ensure_response(response)
|
|
333
|
+
if not response.HasField("client") or not response.client.HasField("location"):
|
|
334
|
+
return ReportClientResponse("", "", "")
|
|
335
|
+
location = response.client.location
|
|
336
|
+
return ReportClientResponse(location.region.value, location.zone.value, location.campus.value)
|
|
337
|
+
|
|
338
|
+
async def report_service_contract(self, request):
|
|
339
|
+
"""Report the contract over the discovery connection's channel."""
|
|
340
|
+
connection = await self._connection()
|
|
341
|
+
async with self._request_slot():
|
|
342
|
+
response = await connection._unary(
|
|
343
|
+
"ReportServiceContract", contract_request_to_proto(request),
|
|
344
|
+
timeout_seconds=request.timeout / 1000 if request.timeout else None,
|
|
345
|
+
)
|
|
346
|
+
ensure_response(response)
|
|
347
|
+
|
|
348
|
+
async def get_service_contract(self, request):
|
|
349
|
+
"""Retrieve a contract without adding another discovery cache type."""
|
|
350
|
+
connection = await self._connection()
|
|
351
|
+
async with self._request_slot():
|
|
352
|
+
response = await connection._unary(
|
|
353
|
+
"GetServiceContract", contract_request_to_proto(request),
|
|
354
|
+
timeout_seconds=request.timeout / 1000 if request.timeout else None,
|
|
355
|
+
)
|
|
356
|
+
ensure_response(response)
|
|
357
|
+
if not response.HasField("serviceContract"):
|
|
358
|
+
raise SDKError(ErrorCode.ErrCodeInvalidResponse, "missing service contract response")
|
|
359
|
+
return contract_response_from_proto(response.serviceContract)
|
|
360
|
+
|
|
361
|
+
def get_one_instance(self, request: GetOneInstanceRequest) -> InstancesResponse:
|
|
362
|
+
del request
|
|
363
|
+
raise SDKError(ErrorCode.ErrCodePluginError, "single-instance server RPC is not implemented")
|
|
364
|
+
|
|
365
|
+
async def close(self) -> None:
|
|
366
|
+
"""Close discovery streams and their gRPC channels."""
|
|
367
|
+
if self._closed:
|
|
368
|
+
return
|
|
369
|
+
self._closed = True
|
|
370
|
+
await self._stop_idle_cleanup()
|
|
371
|
+
streams = list(self._streams.values())
|
|
372
|
+
self._streams.clear()
|
|
373
|
+
self._stream_locks.clear()
|
|
374
|
+
self._stream_connections.clear()
|
|
375
|
+
self._stream_last_activity.clear()
|
|
376
|
+
for stream in streams:
|
|
377
|
+
try:
|
|
378
|
+
await stream.aclose()
|
|
379
|
+
except RuntimeError:
|
|
380
|
+
pass
|
|
381
|
+
await self.connection.close()
|
|
382
|
+
for connection in self._cluster_connections.values():
|
|
383
|
+
await connection.close()
|
|
384
|
+
self._cluster_connections.clear()
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
"""Load configured TLS material once, refusing incomplete or invalid credentials."""
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
import ssl
|
|
20
|
+
|
|
21
|
+
import grpc
|
|
22
|
+
|
|
23
|
+
from polaris.pkg.model.error import ErrorCode, SDKError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def channel_credentials(config):
|
|
27
|
+
"""Build validated gRPC credentials, or None only when no TLS files are configured."""
|
|
28
|
+
ca, cert, key = config.trusted_ca_file, config.cert_file, config.key_file
|
|
29
|
+
if not any((ca, cert, key)):
|
|
30
|
+
return None
|
|
31
|
+
if bool(cert) != bool(key):
|
|
32
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, "TLS certFile and keyFile must be configured together")
|
|
33
|
+
try:
|
|
34
|
+
roots = Path(ca).read_bytes() if ca else None
|
|
35
|
+
chain = Path(cert).read_bytes() if cert else None
|
|
36
|
+
private_key = Path(key).read_bytes() if key else None
|
|
37
|
+
validator = ssl.create_default_context()
|
|
38
|
+
if roots is not None:
|
|
39
|
+
validator.load_verify_locations(cadata=roots.decode("ascii"))
|
|
40
|
+
if cert:
|
|
41
|
+
validator.load_cert_chain(cert, key, password=lambda: "")
|
|
42
|
+
return grpc.ssl_channel_credentials(roots, private_key, chain)
|
|
43
|
+
except (OSError, ValueError, UnicodeError, grpc.RpcError) as error:
|
|
44
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig,
|
|
45
|
+
f"Invalid TLS certificate configuration: {error}") from error
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Tencent is pleased to support the open source community by making polaris-python available.
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2021 Tencent. All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the BSD 3-Clause License (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# https://opensource.org/licenses/BSD-3-Clause
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software distributed
|
|
12
|
+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
13
|
+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
14
|
+
# specific language governing permissions and limitations under the License.
|
|
15
|
+
|
|
16
|
+
from .base import BaseServiceRouter, RouteInfo, RouteResult
|
|
17
|
+
from .isolated import IsolatedRouter
|
|
18
|
+
from .metadata import MetadataRouter
|
|
19
|
+
from .recover import RecoverRouter
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"BaseServiceRouter",
|
|
23
|
+
"IsolatedRouter",
|
|
24
|
+
"MetadataRouter",
|
|
25
|
+
"RecoverRouter",
|
|
26
|
+
"RouteInfo",
|
|
27
|
+
"RouteResult",
|
|
28
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Tencent is pleased to support the open source community by making polaris-python available.
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2021 Tencent. All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the BSD 3-Clause License (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# https://opensource.org/licenses/BSD-3-Clause
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software distributed
|
|
12
|
+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
13
|
+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
14
|
+
# specific language governing permissions and limitations under the License.
|
|
15
|
+
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
from typing import Dict, List
|
|
18
|
+
|
|
19
|
+
from polaris.pkg.model import service
|
|
20
|
+
from polaris.pkg.model.rule import ServiceRule
|
|
21
|
+
from polaris.pkg.model.service import ServiceInfo
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"BaseServiceRouter",
|
|
25
|
+
"CanaryMetaKey",
|
|
26
|
+
"CanaryMetadataEnable",
|
|
27
|
+
"MetadataFailoverKey",
|
|
28
|
+
"NearbyKeyAreaWithCampus",
|
|
29
|
+
"NearbyKeyAreaWithRegion",
|
|
30
|
+
"NearbyKeyAreaWithZone",
|
|
31
|
+
"NearbyMetadataEnable",
|
|
32
|
+
"RouteInfo",
|
|
33
|
+
"RouteResult",
|
|
34
|
+
"SetEnableKey",
|
|
35
|
+
"SetEnableValue",
|
|
36
|
+
"SetNameKey",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
# 保存客户端地域信息 idc key
|
|
40
|
+
NearbyKeyAreaWithCampus = "client_area_campus"
|
|
41
|
+
# 保存客户端地域信息 zone key
|
|
42
|
+
NearbyKeyAreaWithZone = "client_area_zone"
|
|
43
|
+
# 保存客户端地域信息 region key
|
|
44
|
+
NearbyKeyAreaWithRegion = "client_area_region"
|
|
45
|
+
|
|
46
|
+
NearbyMetadataEnable = "internal-enable-nearby"
|
|
47
|
+
CanaryMetadataEnable = "internal-canary"
|
|
48
|
+
CanaryMetaKey = "canary"
|
|
49
|
+
|
|
50
|
+
SetEnableKey = "internal-enable-set"
|
|
51
|
+
SetEnableValue = "Y"
|
|
52
|
+
SetNameKey = "internal-set-name"
|
|
53
|
+
MetadataFailoverKey = "internal-metadata-failover-type"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class RouteInfo:
|
|
57
|
+
def __init__(self, source_service: ServiceInfo = None, source_routerule: ServiceRule = None,
|
|
58
|
+
dest_service: ServiceInfo = None, dest_routerule: ServiceRule = None,
|
|
59
|
+
include_unhealthy_instances: bool = False, include_circuitbreak_instances: bool = False,
|
|
60
|
+
canary: str = "", metadata: Dict[str, str] = None,
|
|
61
|
+
metadata_failover_type: str = None):
|
|
62
|
+
# 源服务信息
|
|
63
|
+
self.source_service = source_service
|
|
64
|
+
# 源路由规则
|
|
65
|
+
self.source_routerule = source_routerule
|
|
66
|
+
# 目标服务信息
|
|
67
|
+
self.dest_service = dest_service
|
|
68
|
+
# 目标路由规则
|
|
69
|
+
self.dest_routerule = dest_routerule
|
|
70
|
+
# 是否包含不健康的服务实例,默认false
|
|
71
|
+
self.include_unhealthy_instances = include_unhealthy_instances
|
|
72
|
+
# 是否包含被熔断的服务实例,默认false
|
|
73
|
+
self.include_circuitbreak_instances = include_circuitbreak_instances
|
|
74
|
+
# 标识是否不需要执行全死全活兜底
|
|
75
|
+
# 对于全死全活插件,以及就近路由插件等,已经做了最小实例数检查,则可以设置该属性为true
|
|
76
|
+
# 需要插件内部进行设置
|
|
77
|
+
self.ignore_filter_only_on_end_chain = False
|
|
78
|
+
# 可动态调整路由插件是否启用,不存在或者为true代表启用
|
|
79
|
+
# key为路由插件的id
|
|
80
|
+
self.chain_enables = {}
|
|
81
|
+
# 金丝雀
|
|
82
|
+
self.canary = canary
|
|
83
|
+
# 请求 metadata,用于 metadataRouter
|
|
84
|
+
self.metadata = {} if metadata is None else metadata
|
|
85
|
+
# none / all / others;None 表示使用服务或插件配置
|
|
86
|
+
self.metadata_failover_type = metadata_failover_type
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class RouteResult:
|
|
90
|
+
def __init__(self, instances: List[service.Instance]):
|
|
91
|
+
# 根据路由规则过滤后的目标服务实例列表,假如重定向则列表为空
|
|
92
|
+
self.instances = instances
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class BaseServiceRouter(ABC):
|
|
96
|
+
"""
|
|
97
|
+
获取规则过滤后的服务实例列表
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
def get_filtered_instances(self, route_info: RouteInfo, instances: service.ServiceInstances) -> RouteResult:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
def enable(self, route_info: RouteInfo, instances: service.ServiceInstances) -> bool:
|
|
105
|
+
return True
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Tencent is pleased to support the open source community by making polaris-python available.
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2021 Tencent. All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the BSD 3-Clause License (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# https://opensource.org/licenses/BSD-3-Clause
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software distributed
|
|
12
|
+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
13
|
+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
14
|
+
# specific language governing permissions and limitations under the License.
|
|
15
|
+
|
|
16
|
+
from polaris.pkg.config.default import DefaultConf
|
|
17
|
+
from polaris.pkg.model import service
|
|
18
|
+
from polaris.plugin.service_router.base import BaseServiceRouter, RouteInfo, RouteResult
|
|
19
|
+
from polaris.plugin_manager import InitContext, PluginType, register_plugin
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@register_plugin(PluginType.SERVICE_ROUTER, DefaultConf.DefaultServiceRouterIsolated)
|
|
23
|
+
class IsolatedRouter(BaseServiceRouter):
|
|
24
|
+
"""Drop isolated instances and instances whose weight is zero."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, init_ctx: InitContext = None):
|
|
27
|
+
self.init_ctx = init_ctx
|
|
28
|
+
|
|
29
|
+
def enable(self, route_info: RouteInfo, instances: service.ServiceInstances) -> bool:
|
|
30
|
+
return route_info.chain_enables.get(DefaultConf.DefaultServiceRouterIsolated, True)
|
|
31
|
+
|
|
32
|
+
def get_filtered_instances(self, route_info: RouteInfo, instances: service.ServiceInstances) -> RouteResult:
|
|
33
|
+
selected = [instance for instance in instances.get_instances()
|
|
34
|
+
if instance.get_weight() != 0 and not instance.is_isolated()]
|
|
35
|
+
return RouteResult(selected)
|