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