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,18 @@
|
|
|
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 BaseLocalRegistry, InstanceFilter
|
|
17
|
+
|
|
18
|
+
__all__ = ["BaseLocalRegistry", "InstanceFilter"]
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
|
18
|
+
|
|
19
|
+
from polaris.pkg.model.core import ServiceEventKey
|
|
20
|
+
from polaris.pkg.model.service import ServiceInstances
|
|
21
|
+
|
|
22
|
+
__all__ = ["BaseLocalRegistry", "InstanceFilter"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InstanceFilter:
|
|
26
|
+
def __init__(self, namespace: str, service: str, is_internal_request: bool, include_cache=False):
|
|
27
|
+
self.namespace = namespace
|
|
28
|
+
self.service = service
|
|
29
|
+
self.is_internal_request = is_internal_request
|
|
30
|
+
self.include_cache = include_cache
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BaseLocalRegistry(ABC):
|
|
34
|
+
"""
|
|
35
|
+
获取服务实例列表
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def get_instances(self, instance_filter: InstanceFilter) -> ServiceInstances:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def get_services(self) -> Dict[ServiceEventKey, bool]:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
async def expire_services(self):
|
|
46
|
+
"""Release idle resources owned by this registry."""
|
|
47
|
+
return []
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# Tencent is pleased to support the open source community by making polaris-python available.
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2021 Tencent. All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the BSD 3-Clause License (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# https://opensource.org/licenses/BSD-3-Clause
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software distributed
|
|
12
|
+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
13
|
+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
14
|
+
# specific language governing permissions and limitations under the License.
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import os
|
|
18
|
+
import tempfile
|
|
19
|
+
import time
|
|
20
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
21
|
+
from queue import Queue
|
|
22
|
+
from threading import Event, Lock, Thread
|
|
23
|
+
from urllib.parse import quote_plus
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import fcntl
|
|
27
|
+
except ImportError: # pragma: no cover - exercised on Windows CI
|
|
28
|
+
fcntl = None
|
|
29
|
+
|
|
30
|
+
from google.protobuf import json_format
|
|
31
|
+
from google.protobuf.message import DecodeError
|
|
32
|
+
from polarismesh_specification.api.v1.service_manage import response_pb2
|
|
33
|
+
|
|
34
|
+
from polaris.api.log import ContextLogger, get_logger
|
|
35
|
+
from polaris.pkg.config.api import LocalCacheConfig
|
|
36
|
+
from polaris.pkg.config.default import DefaultConf
|
|
37
|
+
from polaris.pkg.model.server import InstanceDiscoverRequest
|
|
38
|
+
from polaris.pkg.model.error import ErrorCode, SDKError
|
|
39
|
+
from polaris.plugin_manager import PluginType
|
|
40
|
+
from polaris.plugin_manager import register_plugin
|
|
41
|
+
|
|
42
|
+
LOCK_FILE = "file_cache.lock"
|
|
43
|
+
CACHE_FILE_SUFFIX = ".json"
|
|
44
|
+
CACHE_READ_ERRORS = (OSError, DecodeError, json_format.ParseError, UnicodeDecodeError)
|
|
45
|
+
DISCOVER_TYPE_FILE_NAMES = {
|
|
46
|
+
1: "instance",
|
|
47
|
+
}
|
|
48
|
+
logger = get_logger("cache")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _resource_type(service) -> object:
|
|
52
|
+
"""Return a discover type or the key class used by Skill persistence."""
|
|
53
|
+
return getattr(service, "type", type(service).__name__)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BackupTaskMsg:
|
|
57
|
+
def __init__(self, service: InstanceDiscoverRequest, data: response_pb2.DiscoverResponse | None,
|
|
58
|
+
completion: Future | None = None):
|
|
59
|
+
self.service = service
|
|
60
|
+
self.data = data
|
|
61
|
+
self.completion = completion
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@register_plugin(PluginType.LOCAL_CACHE, 'file')
|
|
65
|
+
class LocalFileCache:
|
|
66
|
+
"""Own a disk backup queue, lock and writer thread for one context."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, init_ctx=None, base_dir=None):
|
|
69
|
+
cache_config = LocalCacheConfig()
|
|
70
|
+
cache_config.set_default()
|
|
71
|
+
if init_ctx is not None:
|
|
72
|
+
cache_config = init_ctx.config.get_consumer().get_local_cache()
|
|
73
|
+
if base_dir is None:
|
|
74
|
+
base_dir = cache_config.get_persist_dir()
|
|
75
|
+
self.max_write_retry = cache_config.get_persist_max_write_retry()
|
|
76
|
+
self.max_read_retry = cache_config.get_persist_max_read_retry()
|
|
77
|
+
self.retry_interval = cache_config.get_persist_retry_interval() / 1000
|
|
78
|
+
context_logger = getattr(init_ctx, "logger", None)
|
|
79
|
+
self.logger = (context_logger.get_logger("cache") if isinstance(context_logger, ContextLogger)
|
|
80
|
+
else context_logger or logger)
|
|
81
|
+
self.base_dir = os.path.expandvars(
|
|
82
|
+
os.path.expanduser(base_dir or DefaultConf.DefaultCachePersistDir)
|
|
83
|
+
)
|
|
84
|
+
os.makedirs(self.base_dir, exist_ok=True)
|
|
85
|
+
self.logger.info(
|
|
86
|
+
"cache backup initialized dir=%s writeRetry=%d readRetry=%d retryInterval=%.3fs",
|
|
87
|
+
self.base_dir, self.max_write_retry, self.max_read_retry, self.retry_interval,
|
|
88
|
+
)
|
|
89
|
+
lock_file_path = os.path.join(self.base_dir, LOCK_FILE)
|
|
90
|
+
self.lock_file = open(lock_file_path, 'a+')
|
|
91
|
+
self.thread_lock = Lock()
|
|
92
|
+
self._state_lock = Lock()
|
|
93
|
+
self._close_lock = Lock()
|
|
94
|
+
self._closed = False
|
|
95
|
+
self._closing = Event()
|
|
96
|
+
self._read_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="polaris-cache-read")
|
|
97
|
+
|
|
98
|
+
self.queue = Queue()
|
|
99
|
+
self.write_thread = Thread(target=self.thread_loop, daemon=True)
|
|
100
|
+
self.write_thread.start()
|
|
101
|
+
|
|
102
|
+
def _lock(self):
|
|
103
|
+
"""
|
|
104
|
+
write lock (contention is retried outside the critical section)
|
|
105
|
+
:return:
|
|
106
|
+
"""
|
|
107
|
+
if not self.thread_lock.acquire(blocking=False):
|
|
108
|
+
raise BlockingIOError("cache backup is busy")
|
|
109
|
+
try:
|
|
110
|
+
if fcntl is not None:
|
|
111
|
+
fcntl.flock(self.lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
112
|
+
except OSError:
|
|
113
|
+
self.thread_lock.release()
|
|
114
|
+
raise
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
def thread_loop(self):
|
|
118
|
+
self.logger.info("cache backup writer started")
|
|
119
|
+
while True:
|
|
120
|
+
msg: BackupTaskMsg = self.queue.get()
|
|
121
|
+
try:
|
|
122
|
+
if msg is None:
|
|
123
|
+
return
|
|
124
|
+
if msg.completion is not None:
|
|
125
|
+
msg.completion.set_running_or_notify_cancel()
|
|
126
|
+
if msg.data is None:
|
|
127
|
+
operation = lambda: self._delete(msg.service)
|
|
128
|
+
else:
|
|
129
|
+
operation = lambda: self._save(msg.service, msg.data)
|
|
130
|
+
self._retry(operation, self.max_write_retry)
|
|
131
|
+
if msg.completion is not None and not msg.completion.done():
|
|
132
|
+
msg.completion.set_result(None)
|
|
133
|
+
except Exception as error:
|
|
134
|
+
operation = "delete" if msg.data is None else "write"
|
|
135
|
+
self.logger.error("failed to %s cache backup: %s", operation, error)
|
|
136
|
+
if msg.completion is not None and not msg.completion.done():
|
|
137
|
+
msg.completion.set_exception(error)
|
|
138
|
+
finally:
|
|
139
|
+
self.queue.task_done()
|
|
140
|
+
|
|
141
|
+
def _share_lock(self):
|
|
142
|
+
"""
|
|
143
|
+
read lock
|
|
144
|
+
:return:
|
|
145
|
+
"""
|
|
146
|
+
if not self.thread_lock.acquire(blocking=False):
|
|
147
|
+
return False
|
|
148
|
+
if self._closed:
|
|
149
|
+
self.thread_lock.release()
|
|
150
|
+
return False
|
|
151
|
+
if fcntl is not None:
|
|
152
|
+
try:
|
|
153
|
+
fcntl.flock(self.lock_file, fcntl.LOCK_SH | fcntl.LOCK_NB)
|
|
154
|
+
except OSError:
|
|
155
|
+
self.thread_lock.release()
|
|
156
|
+
return False
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
def _unlock(self):
|
|
160
|
+
try:
|
|
161
|
+
if fcntl is not None:
|
|
162
|
+
fcntl.flock(self.lock_file, fcntl.LOCK_UN)
|
|
163
|
+
finally:
|
|
164
|
+
self.thread_lock.release()
|
|
165
|
+
|
|
166
|
+
def _retry(self, operation, max_retry, reading=False):
|
|
167
|
+
for attempt in range(max_retry + 1):
|
|
168
|
+
if reading and self._closed:
|
|
169
|
+
return None
|
|
170
|
+
try:
|
|
171
|
+
return operation()
|
|
172
|
+
except CACHE_READ_ERRORS as error:
|
|
173
|
+
if attempt == max_retry:
|
|
174
|
+
raise
|
|
175
|
+
self.logger.warning("cache backup failed; retry %d/%d: %s", attempt + 1, max_retry, error)
|
|
176
|
+
if reading:
|
|
177
|
+
if self._closing.wait(self.retry_interval):
|
|
178
|
+
return None
|
|
179
|
+
else:
|
|
180
|
+
time.sleep(self.retry_interval)
|
|
181
|
+
|
|
182
|
+
def destroy(self):
|
|
183
|
+
"""Drain accepted writes, stop the writer, then release its file descriptor."""
|
|
184
|
+
with self._close_lock:
|
|
185
|
+
with self._state_lock:
|
|
186
|
+
if not self._closed:
|
|
187
|
+
self._closed = True
|
|
188
|
+
self._closing.set()
|
|
189
|
+
self.queue.put(None)
|
|
190
|
+
# Accepted writes retain their retry budget, which can exceed five seconds.
|
|
191
|
+
self.write_thread.join()
|
|
192
|
+
self._read_executor.shutdown(wait=True, cancel_futures=True)
|
|
193
|
+
with self.thread_lock:
|
|
194
|
+
self.lock_file.close()
|
|
195
|
+
self.logger.info("cache backup writer stopped dir=%s", self.base_dir)
|
|
196
|
+
|
|
197
|
+
def save(self, service: InstanceDiscoverRequest, data: response_pb2.DiscoverResponse):
|
|
198
|
+
"""Queue a private snapshot while this context's backup writer is open."""
|
|
199
|
+
with self._state_lock:
|
|
200
|
+
if self._closed:
|
|
201
|
+
raise SDKError(ErrorCode.ErrCodeInvalidStateError, "File cache has been destroyed")
|
|
202
|
+
snapshot = self._copy_response(data)
|
|
203
|
+
request = self._copy_request(service)
|
|
204
|
+
self.queue.put(BackupTaskMsg(request, snapshot))
|
|
205
|
+
self.logger.debug(
|
|
206
|
+
"cache backup queued namespace=%s resource=%s type=%s",
|
|
207
|
+
service.namespace, service.name, _resource_type(service),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def _copy_request(self, service):
|
|
211
|
+
return InstanceDiscoverRequest(service.type, service.name, service.namespace)
|
|
212
|
+
|
|
213
|
+
def _copy_response(self, data):
|
|
214
|
+
snapshot = response_pb2.DiscoverResponse()
|
|
215
|
+
snapshot.CopyFrom(data)
|
|
216
|
+
return snapshot
|
|
217
|
+
|
|
218
|
+
def _parse_response(self, service, buf):
|
|
219
|
+
return json_format.Parse(buf.decode("utf-8"), response_pb2.DiscoverResponse())
|
|
220
|
+
|
|
221
|
+
async def delete(self, service: InstanceDiscoverRequest):
|
|
222
|
+
"""Delete a backup after earlier queued writes without blocking the owner loop."""
|
|
223
|
+
completion = Future()
|
|
224
|
+
with self._state_lock:
|
|
225
|
+
if self._closed:
|
|
226
|
+
raise SDKError(ErrorCode.ErrCodeInvalidStateError, "File cache has been destroyed")
|
|
227
|
+
request = InstanceDiscoverRequest(service.type, service.name, service.namespace)
|
|
228
|
+
self.queue.put(BackupTaskMsg(request, None, completion))
|
|
229
|
+
await asyncio.wrap_future(completion)
|
|
230
|
+
|
|
231
|
+
def _delete(self, service: InstanceDiscoverRequest):
|
|
232
|
+
self._lock()
|
|
233
|
+
try:
|
|
234
|
+
try:
|
|
235
|
+
os.remove(self.get_cache_file_path(service))
|
|
236
|
+
except FileNotFoundError:
|
|
237
|
+
pass
|
|
238
|
+
else:
|
|
239
|
+
self.logger.info("cache backup deleted namespace=%s resource=%s", service.namespace, service.name)
|
|
240
|
+
finally:
|
|
241
|
+
self._unlock()
|
|
242
|
+
|
|
243
|
+
def _save(self, service: InstanceDiscoverRequest, data: response_pb2.DiscoverResponse):
|
|
244
|
+
temporary_path = None
|
|
245
|
+
file_path = self.get_cache_file_path(service)
|
|
246
|
+
self._lock()
|
|
247
|
+
try:
|
|
248
|
+
with tempfile.NamedTemporaryFile(mode="wb", dir=self.base_dir,
|
|
249
|
+
prefix=os.path.basename(file_path) + ".", suffix=".tmp",
|
|
250
|
+
delete=False) as data_file:
|
|
251
|
+
temporary_path = data_file.name
|
|
252
|
+
data_file.write(
|
|
253
|
+
json_format.MessageToJson(
|
|
254
|
+
data,
|
|
255
|
+
indent=2,
|
|
256
|
+
sort_keys=True,
|
|
257
|
+
).encode("utf-8")
|
|
258
|
+
)
|
|
259
|
+
data_file.flush()
|
|
260
|
+
os.fsync(data_file.fileno())
|
|
261
|
+
os.replace(temporary_path, file_path)
|
|
262
|
+
temporary_path = None
|
|
263
|
+
self.logger.info(
|
|
264
|
+
"cache backup written namespace=%s resource=%s type=%s file=%s",
|
|
265
|
+
service.namespace, service.name, _resource_type(service), file_path,
|
|
266
|
+
)
|
|
267
|
+
finally:
|
|
268
|
+
try:
|
|
269
|
+
if temporary_path is not None:
|
|
270
|
+
os.unlink(temporary_path)
|
|
271
|
+
finally:
|
|
272
|
+
self._unlock()
|
|
273
|
+
|
|
274
|
+
def get(self, service: InstanceDiscoverRequest, expire: int | None = 60) -> response_pb2.DiscoverResponse | None:
|
|
275
|
+
"""Read a snapshot with configured retries. ``expire=None`` ignores file modification time."""
|
|
276
|
+
if self._closed:
|
|
277
|
+
return None
|
|
278
|
+
try:
|
|
279
|
+
result = self._retry(lambda: self._read(service, expire), self.max_read_retry, reading=True)
|
|
280
|
+
if result is not None:
|
|
281
|
+
self.logger.info(
|
|
282
|
+
"cache backup loaded namespace=%s resource=%s type=%s",
|
|
283
|
+
service.namespace, service.name, _resource_type(service),
|
|
284
|
+
)
|
|
285
|
+
return result
|
|
286
|
+
except CACHE_READ_ERRORS as error:
|
|
287
|
+
self.logger.error("failed to read cache backup for %s: %s", service.name, error)
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
async def async_get(self, service: InstanceDiscoverRequest, expire: int | None = 60):
|
|
291
|
+
"""Read a backup on this context's reader without blocking its owner loop."""
|
|
292
|
+
with self._state_lock:
|
|
293
|
+
if self._closed:
|
|
294
|
+
return None
|
|
295
|
+
future = self._read_executor.submit(self.get, service, expire)
|
|
296
|
+
return await asyncio.wrap_future(future)
|
|
297
|
+
|
|
298
|
+
def _read(self, service, expire):
|
|
299
|
+
file_path = self.get_cache_file_path(service)
|
|
300
|
+
if not self._share_lock():
|
|
301
|
+
raise BlockingIOError("cache backup is busy")
|
|
302
|
+
try:
|
|
303
|
+
if self._closed:
|
|
304
|
+
return None
|
|
305
|
+
try:
|
|
306
|
+
with open(file_path, "rb") as data_file:
|
|
307
|
+
if expire is not None and time.time() - os.fstat(data_file.fileno()).st_mtime > expire:
|
|
308
|
+
return None
|
|
309
|
+
buf = data_file.read()
|
|
310
|
+
except FileNotFoundError:
|
|
311
|
+
return None
|
|
312
|
+
if not buf:
|
|
313
|
+
raise DecodeError("empty cache backup")
|
|
314
|
+
return self._parse_response(service, buf)
|
|
315
|
+
finally:
|
|
316
|
+
self._unlock()
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def _get_cache_file_name(service: InstanceDiscoverRequest) -> str:
|
|
320
|
+
type_name = DISCOVER_TYPE_FILE_NAMES.get(service.type)
|
|
321
|
+
if type_name is None:
|
|
322
|
+
raise SDKError(
|
|
323
|
+
ErrorCode.ErrCodeAPIInvalidArgument,
|
|
324
|
+
"unsupported discover request type: %s" % service.type,
|
|
325
|
+
)
|
|
326
|
+
return "svc#%s#%s#%s%s" % (
|
|
327
|
+
quote_plus(service.namespace, safe=""),
|
|
328
|
+
quote_plus(service.name, safe=""),
|
|
329
|
+
type_name,
|
|
330
|
+
CACHE_FILE_SUFFIX,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def get_cache_file_path(self, service: InstanceDiscoverRequest) -> str:
|
|
334
|
+
file_name = self._get_cache_file_name(service)
|
|
335
|
+
return os.path.join(self.base_dir, file_name)
|