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,84 @@
|
|
|
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.pkg.model.error import ErrorCode, SDKError
|
|
19
|
+
from polaris.plugin.service_router.base import BaseServiceRouter, RouteInfo, RouteResult
|
|
20
|
+
from polaris.plugin_manager import InitContext, PluginType, register_plugin
|
|
21
|
+
|
|
22
|
+
_FAILOVER_KEY = "internal-metadata-failover-type"
|
|
23
|
+
_FAILOVERS = {"none", "all", "others"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _as_dict(metadata) -> dict:
|
|
27
|
+
if not metadata:
|
|
28
|
+
return {}
|
|
29
|
+
return {key: metadata[key] for key in metadata}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _matches(instance: service.Instance, required: dict) -> bool:
|
|
33
|
+
metadata = _as_dict(instance.get_metadata())
|
|
34
|
+
return all(metadata.get(key) == value for key, value in required.items())
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _missing_any_key(instance: service.Instance, required: dict) -> bool:
|
|
38
|
+
metadata = _as_dict(instance.get_metadata())
|
|
39
|
+
return any(key not in metadata for key in required)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@register_plugin(PluginType.SERVICE_ROUTER, DefaultConf.DefaultServiceRouterMetadata)
|
|
43
|
+
class MetadataRouter(BaseServiceRouter):
|
|
44
|
+
"""Keep instances whose metadata contains every requested key and value."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, init_ctx: InitContext = None):
|
|
47
|
+
plugin = {}
|
|
48
|
+
if init_ctx is not None and init_ctx.config is not None:
|
|
49
|
+
plugin = init_ctx.config.get_consumer().get_service_router().plugin.get(
|
|
50
|
+
DefaultConf.DefaultServiceRouterMetadata, {}) or {}
|
|
51
|
+
failover = plugin.get("metadataFailOverType", plugin.get("metadata_fail_over_type", "all"))
|
|
52
|
+
if failover not in _FAILOVERS:
|
|
53
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidArgument,
|
|
54
|
+
"metadataRouter.metadataFailOverType must be one of none, all, others")
|
|
55
|
+
self.failover = failover if failover in _FAILOVERS else "all"
|
|
56
|
+
|
|
57
|
+
def enable(self, route_info: RouteInfo, instances: service.ServiceInstances) -> bool:
|
|
58
|
+
if not route_info.chain_enables.get(DefaultConf.DefaultServiceRouterMetadata, True):
|
|
59
|
+
return False
|
|
60
|
+
return bool(_as_dict(route_info.metadata))
|
|
61
|
+
|
|
62
|
+
def get_filtered_instances(self, route_info: RouteInfo, instances: service.ServiceInstances) -> RouteResult:
|
|
63
|
+
required = _as_dict(route_info.metadata)
|
|
64
|
+
matched = [instance for instance in instances.get_instances() if _matches(instance, required)]
|
|
65
|
+
if matched:
|
|
66
|
+
return RouteResult(matched)
|
|
67
|
+
|
|
68
|
+
failover = self._failover(route_info, instances)
|
|
69
|
+
if failover == "all":
|
|
70
|
+
return RouteResult(list(instances.get_instances()))
|
|
71
|
+
if failover == "others":
|
|
72
|
+
return RouteResult([instance for instance in instances.get_instances()
|
|
73
|
+
if _missing_any_key(instance, required)])
|
|
74
|
+
service_name = route_info.dest_service.get_service() if route_info.dest_service is not None else ""
|
|
75
|
+
raise SDKError(ErrorCode.ErrCodeRouteRuleNotMatch,
|
|
76
|
+
f"can not find any instance by metadata for service {service_name}")
|
|
77
|
+
|
|
78
|
+
def _failover(self, route_info: RouteInfo, instances: service.ServiceInstances) -> str:
|
|
79
|
+
if route_info.metadata_failover_type in _FAILOVERS:
|
|
80
|
+
return route_info.metadata_failover_type
|
|
81
|
+
service_failover = _as_dict(instances.get_metadata()).get(_FAILOVER_KEY)
|
|
82
|
+
if service_failover in _FAILOVERS:
|
|
83
|
+
return service_failover
|
|
84
|
+
return self.failover
|
|
@@ -0,0 +1,62 @@
|
|
|
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.pkg.model.service import CBStatusEnum
|
|
19
|
+
from polaris.plugin.service_router.base import BaseServiceRouter, RouteInfo, RouteResult
|
|
20
|
+
from polaris.plugin_manager import InitContext, PluginType, register_plugin
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _config_bool(plugin: dict, *keys, default: bool) -> bool:
|
|
24
|
+
for key in keys:
|
|
25
|
+
if key in plugin:
|
|
26
|
+
value = plugin[key]
|
|
27
|
+
if isinstance(value, str):
|
|
28
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
29
|
+
return bool(value)
|
|
30
|
+
return default
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@register_plugin(PluginType.SERVICE_ROUTER, DefaultConf.DefaultServiceRouterRecover)
|
|
34
|
+
class RecoverRouter(BaseServiceRouter):
|
|
35
|
+
"""Keep healthy instances and restore the input list when every instance is unavailable."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, init_ctx: InitContext = None):
|
|
38
|
+
plugin = {}
|
|
39
|
+
if init_ctx is not None and init_ctx.config is not None:
|
|
40
|
+
plugin = init_ctx.config.get_consumer().get_service_router().plugin.get(
|
|
41
|
+
DefaultConf.DefaultServiceRouterRecover, {}) or {}
|
|
42
|
+
self.exclude_circuit_break = _config_bool(
|
|
43
|
+
plugin, "excludeCircuitBreakInstances", "exclude_circuit_break_instances", default=True)
|
|
44
|
+
self.all_recover = _config_bool(plugin, "allRecoverEnable", "all_recover_enable", default=True)
|
|
45
|
+
|
|
46
|
+
def enable(self, route_info: RouteInfo, instances: service.ServiceInstances) -> bool:
|
|
47
|
+
return route_info.chain_enables.get(DefaultConf.DefaultServiceRouterRecover, True)
|
|
48
|
+
|
|
49
|
+
def get_filtered_instances(self, route_info: RouteInfo, instances: service.ServiceInstances) -> RouteResult:
|
|
50
|
+
exclude_circuit_break = self.exclude_circuit_break and not route_info.include_circuitbreak_instances
|
|
51
|
+
healthy = []
|
|
52
|
+
for instance in instances.get_instances():
|
|
53
|
+
if not route_info.include_unhealthy_instances and not instance.is_healthy():
|
|
54
|
+
continue
|
|
55
|
+
if exclude_circuit_break:
|
|
56
|
+
status = instance.get_circuit_breaker_status()
|
|
57
|
+
if status is not None and status.get_status() == CBStatusEnum.Open:
|
|
58
|
+
continue
|
|
59
|
+
healthy.append(instance)
|
|
60
|
+
if self.all_recover and not healthy:
|
|
61
|
+
return RouteResult(list(instances.get_instances()))
|
|
62
|
+
return RouteResult(healthy)
|
|
@@ -0,0 +1,54 @@
|
|
|
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.model.service import Instance
|
|
17
|
+
from typing import List
|
|
18
|
+
from polaris.api.log import get_logger
|
|
19
|
+
|
|
20
|
+
from .base import RouteInfo
|
|
21
|
+
|
|
22
|
+
logger = get_logger("route")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# 从subset中选取可用的实例
|
|
26
|
+
def get_available_instances(
|
|
27
|
+
instances: List[Instance], route_info: RouteInfo, percent_of_min_instances: float) -> List[Instance]:
|
|
28
|
+
available_instances = calc_available_instances(
|
|
29
|
+
route_info.include_unhealthy_instances, route_info.include_circuitbreak_instances, instances)
|
|
30
|
+
|
|
31
|
+
min_instances = int(percent_of_min_instances * len(instances))
|
|
32
|
+
if len(instances) > 0 and len(available_instances) <= min_instances:
|
|
33
|
+
logger.warning("all instance is down and return all for %s", route_info.dest_service.get_service())
|
|
34
|
+
available_instances = instances
|
|
35
|
+
return available_instances
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# 计算subset中的可用实例
|
|
39
|
+
def calc_available_instances(
|
|
40
|
+
include_unhealthy_instances: bool, include_circuitbreak_instances: bool, instances: List[Instance]) -> List[
|
|
41
|
+
Instance]:
|
|
42
|
+
available_instances = []
|
|
43
|
+
if len(instances) == 0:
|
|
44
|
+
return available_instances
|
|
45
|
+
for i in range(len(instances)):
|
|
46
|
+
instance = instances[i]
|
|
47
|
+
if not include_unhealthy_instances and not instance.is_healthy():
|
|
48
|
+
continue
|
|
49
|
+
if not include_circuitbreak_instances:
|
|
50
|
+
cb_status = instance.get_circuit_breaker_status()
|
|
51
|
+
if cb_status is not None and not cb_status.is_available():
|
|
52
|
+
continue
|
|
53
|
+
available_instances.append(instance)
|
|
54
|
+
return available_instances
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
"""Read-only Skill transport and persistence plugins."""
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
"""Skill RPCs using a separate specification gRPC connection."""
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
|
|
20
|
+
import grpc
|
|
21
|
+
from polarismesh_specification.api.v1.skill_manage import skill_service_pb2 as pb, skill_service_pb2_grpc
|
|
22
|
+
|
|
23
|
+
from polaris.pkg.model.error import ErrorCode, SDKError, ServerCode, ensure_response
|
|
24
|
+
from polaris.plugin.server_connector.grpc_connector.connection import GrpcConnection, GrpcSettings, _request_id
|
|
25
|
+
from polaris.plugin_manager import PluginType, register_plugin
|
|
26
|
+
from polaris.plugin.server_connector.grpc_connector.tls import channel_credentials
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@register_plugin(PluginType.SKILL_CONNECTOR, "polaris")
|
|
30
|
+
class SkillConnector:
|
|
31
|
+
"""Own the independent Skill channels and validate streamed downloads."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, init_ctx):
|
|
34
|
+
self.config = init_ctx.config.get_ai().get_skill().get_server_connector()
|
|
35
|
+
api = init_ctx.config.get_global().get_api()
|
|
36
|
+
self.connection = GrpcConnection(
|
|
37
|
+
GrpcSettings(self.config.addresses, self.config.message_timeout / 1000,
|
|
38
|
+
self.config.token, api.max_retry_times, api.retry_interval / 1000,
|
|
39
|
+
credentials=channel_credentials(self.config)),
|
|
40
|
+
context_logger=init_ctx.logger, stub_factory=skill_service_pb2_grpc.PolarisSkillStub,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
async def get_skill(self, request):
|
|
44
|
+
"""Fetch one published skill and preserve the server response code."""
|
|
45
|
+
response = ensure_response(await self.connection._unary(
|
|
46
|
+
"GetSkill", pb.GetSkillRequest(namespace=request.namespace, name=request.name, version=request.version),
|
|
47
|
+
timeout_seconds=(request.timeout or self.config.message_timeout) / 1000,
|
|
48
|
+
), (0, ServerCode.ExecuteSuccess, ServerCode.NotFoundResource))
|
|
49
|
+
if response.code in (0, ServerCode.ExecuteSuccess):
|
|
50
|
+
if (response.resource.name != request.name or response.resource.namespace != request.namespace
|
|
51
|
+
or not response.version.version or (request.version and response.version.version != request.version)):
|
|
52
|
+
raise SDKError(ErrorCode.ErrCodeInvalidResponse, "Skill response identity or version mismatch")
|
|
53
|
+
return response
|
|
54
|
+
|
|
55
|
+
async def list_skills(self, request):
|
|
56
|
+
"""Query one remote page of published skills."""
|
|
57
|
+
fields = ("namespace", "name", "tag", "owner", "scope", "keyword", "order_by", "order_type", "offset", "limit")
|
|
58
|
+
return ensure_response(await self.connection._unary(
|
|
59
|
+
"GetSkillList", pb.ListSkillsRequest(**{key: getattr(request, key) for key in fields}),
|
|
60
|
+
timeout_seconds=(request.timeout or self.config.message_timeout) / 1000,
|
|
61
|
+
), (0, ServerCode.ExecuteSuccess, ServerCode.NotFoundResource))
|
|
62
|
+
|
|
63
|
+
async def download_skill(self, request):
|
|
64
|
+
"""Reassemble one whole download; a retry always starts with an empty buffer."""
|
|
65
|
+
connection = self.connection
|
|
66
|
+
outgoing = pb.DownloadSkillRequest(namespace=request.namespace, name=request.name, version=request.version,
|
|
67
|
+
tag=request.tag, format=request.format)
|
|
68
|
+
attempts = connection.settings.retry_attempts + 1
|
|
69
|
+
last_error = None
|
|
70
|
+
for attempt in range(attempts):
|
|
71
|
+
address = connection._addresses[connection._address_index % len(connection._addresses)]
|
|
72
|
+
stream = None
|
|
73
|
+
try:
|
|
74
|
+
stub = await connection._stub(address)
|
|
75
|
+
stream = stub.DownloadSkill(
|
|
76
|
+
outgoing, timeout=(request.timeout or (self.config.download_timeout if request.format == "zip"
|
|
77
|
+
else self.config.message_timeout)) / 1000,
|
|
78
|
+
metadata=_request_id("DownloadSkill") + connection._metadata(),
|
|
79
|
+
)
|
|
80
|
+
result = None
|
|
81
|
+
chunks = []
|
|
82
|
+
async for part in stream:
|
|
83
|
+
ensure_response(part, (0, ServerCode.ExecuteSuccess, ServerCode.NotFoundResource))
|
|
84
|
+
if result is None:
|
|
85
|
+
result = pb.DownloadSkillResponse()
|
|
86
|
+
result.CopyFrom(part)
|
|
87
|
+
result.zip_chunk = b""
|
|
88
|
+
else:
|
|
89
|
+
for key in ("version", "content_type", "filename", "total_size"):
|
|
90
|
+
value = getattr(part, key)
|
|
91
|
+
if value and getattr(result, key) and value != getattr(result, key):
|
|
92
|
+
raise SDKError(ErrorCode.ErrCodeInvalidResponse, f"inconsistent Skill download {key}")
|
|
93
|
+
if value:
|
|
94
|
+
setattr(result, key, value)
|
|
95
|
+
result.content += part.content
|
|
96
|
+
chunks.append(part.zip_chunk)
|
|
97
|
+
if part.code == ServerCode.NotFoundResource:
|
|
98
|
+
return part
|
|
99
|
+
if result is None:
|
|
100
|
+
raise SDKError(ErrorCode.ErrCodeInvalidResponse, "empty Skill download stream")
|
|
101
|
+
result.zip_chunk = b"".join(chunks)
|
|
102
|
+
if request.version and result.version != request.version:
|
|
103
|
+
raise SDKError(ErrorCode.ErrCodeInvalidResponse, "Skill download returned a different version")
|
|
104
|
+
size = len(result.zip_chunk) if request.format == "zip" else len(result.content.encode("utf-8"))
|
|
105
|
+
if result.total_size and result.total_size != size:
|
|
106
|
+
raise SDKError(ErrorCode.ErrCodeInvalidResponse, "Skill download size mismatch")
|
|
107
|
+
return result
|
|
108
|
+
except grpc.aio.AioRpcError as error:
|
|
109
|
+
last_error = error
|
|
110
|
+
if error.code() not in connection._RETRYABLE_STATUS or attempt + 1 >= attempts:
|
|
111
|
+
break
|
|
112
|
+
connection._address_index = (connection._address_index + 1) % len(connection._addresses)
|
|
113
|
+
await asyncio.sleep(connection.settings.retry_backoff_seconds * 2**attempt)
|
|
114
|
+
finally:
|
|
115
|
+
if stream is not None:
|
|
116
|
+
cancel = getattr(stream, "cancel", None)
|
|
117
|
+
if cancel is not None:
|
|
118
|
+
cancel()
|
|
119
|
+
else:
|
|
120
|
+
close = getattr(stream, "aclose", None)
|
|
121
|
+
if close is not None:
|
|
122
|
+
await close()
|
|
123
|
+
raise SDKError(ErrorCode.ErrCodeNetworkError, f"Polaris DownloadSkill failed: {last_error}") from last_error
|
|
124
|
+
|
|
125
|
+
async def close(self):
|
|
126
|
+
"""Close only the Skill connection belonging to this context."""
|
|
127
|
+
await self.connection.close()
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
"""Versioned Skill backups sharing the SDK's atomic writer and retry semantics."""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from types import SimpleNamespace
|
|
21
|
+
from urllib.parse import quote_plus
|
|
22
|
+
|
|
23
|
+
from google.protobuf import json_format
|
|
24
|
+
from polarismesh_specification.api.v1.skill_manage import skill_service_pb2 as pb
|
|
25
|
+
|
|
26
|
+
from polaris.pkg.config.api import LocalCacheConfig
|
|
27
|
+
from polaris.plugin.local_registry.file_cache import LocalFileCache
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class SkillCacheKey:
|
|
32
|
+
"""Identify one persisted payload independently of instance resources."""
|
|
33
|
+
|
|
34
|
+
namespace: str
|
|
35
|
+
name: str
|
|
36
|
+
version: str
|
|
37
|
+
operation: str
|
|
38
|
+
format: str = ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SkillPersistentHandler(LocalFileCache):
|
|
42
|
+
"""Persist complete responses and a last-seen active-version pointer."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, config, logger):
|
|
45
|
+
cache = LocalCacheConfig(persist_dir=config.persist_dir,
|
|
46
|
+
persist_max_read_retry=config.persist_max_read_retry,
|
|
47
|
+
persist_max_write_retry=config.persist_max_write_retry,
|
|
48
|
+
persist_retry_interval=config.persist_retry_interval)
|
|
49
|
+
cache.set_default()
|
|
50
|
+
context = SimpleNamespace(config=SimpleNamespace(get_consumer=lambda: SimpleNamespace(get_local_cache=lambda: cache)),
|
|
51
|
+
logger=logger)
|
|
52
|
+
super().__init__(context)
|
|
53
|
+
self.max_write_retry = config.persist_max_write_retry
|
|
54
|
+
self.max_read_retry = config.persist_max_read_retry
|
|
55
|
+
|
|
56
|
+
def _copy_request(self, key):
|
|
57
|
+
return key
|
|
58
|
+
|
|
59
|
+
def _copy_response(self, data):
|
|
60
|
+
snapshot = type(data)()
|
|
61
|
+
snapshot.CopyFrom(data)
|
|
62
|
+
return snapshot
|
|
63
|
+
|
|
64
|
+
def _parse_response(self, key, buf):
|
|
65
|
+
message = pb.DownloadSkillResponse() if key.operation == "download" else pb.GetSkillResponse()
|
|
66
|
+
return json_format.Parse(buf.decode("utf-8"), message)
|
|
67
|
+
|
|
68
|
+
def get_cache_file_path(self, key):
|
|
69
|
+
parts = [key.namespace, key.name, key.version, key.operation, key.format]
|
|
70
|
+
return os.path.join(self.base_dir, "skill#" + "#".join(quote_plus(part, safe="") for part in parts) + ".json")
|
|
71
|
+
|
|
72
|
+
def save_success(self, request, response, operation):
|
|
73
|
+
"""Queue the resolved payload before publishing its active pointer."""
|
|
74
|
+
version = response.version.version if operation == "get" else response.version
|
|
75
|
+
if not version:
|
|
76
|
+
return
|
|
77
|
+
key = SkillCacheKey(request.namespace, request.name, version, operation, getattr(request, "format", ""))
|
|
78
|
+
self.save(key, response)
|
|
79
|
+
if not request.version:
|
|
80
|
+
pointer = pb.GetSkillResponse(code=200000, version={"version": version})
|
|
81
|
+
self.save(SkillCacheKey(request.namespace, request.name, "", "active"), pointer)
|
|
82
|
+
|
|
83
|
+
async def load(self, request, operation):
|
|
84
|
+
"""Resolve a requested or last active version and load its complete payload."""
|
|
85
|
+
version = request.version
|
|
86
|
+
if not version:
|
|
87
|
+
pointer = await self.async_get(SkillCacheKey(request.namespace, request.name, "", "active"), expire=None)
|
|
88
|
+
if pointer is None:
|
|
89
|
+
return None
|
|
90
|
+
version = pointer.version.version
|
|
91
|
+
if not version:
|
|
92
|
+
return None
|
|
93
|
+
return await self.async_get(SkillCacheKey(request.namespace, request.name, version, operation,
|
|
94
|
+
getattr(request, "format", "")), expire=None)
|
|
@@ -0,0 +1,16 @@
|
|
|
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 .prometheus import PrometheusReporter
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
"""Context-owned invocation metric exporter contract."""
|
|
17
|
+
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseStatReporter(ABC):
|
|
22
|
+
"""Collect invocation results independently of service governance."""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def report(self, result):
|
|
26
|
+
"""Record one invocation."""
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
async def start(self, engine):
|
|
30
|
+
"""Start export on the SDK owner loop."""
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
async def close(self):
|
|
34
|
+
"""Stop export and release HTTP resources."""
|