polaris-python 0.0.1.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. polaris/__init__.py +74 -0
  2. polaris/_version.py +16 -0
  3. polaris/api/__init__.py +55 -0
  4. polaris/api/_log_file.py +232 -0
  5. polaris/api/config.py +145 -0
  6. polaris/api/consumer.py +240 -0
  7. polaris/api/log.py +414 -0
  8. polaris/api/lossless.py +73 -0
  9. polaris/api/provider.py +152 -0
  10. polaris/api/skill.py +162 -0
  11. polaris/pkg/__init__.py +15 -0
  12. polaris/pkg/common/__init__.py +15 -0
  13. polaris/pkg/common/plugin.py +33 -0
  14. polaris/pkg/config/__init__.py +15 -0
  15. polaris/pkg/config/api.py +1205 -0
  16. polaris/pkg/config/default.py +92 -0
  17. polaris/pkg/config/event.py +76 -0
  18. polaris/pkg/config/loader.py +392 -0
  19. polaris/pkg/config/stat_reporter.py +100 -0
  20. polaris/pkg/flow/__init__.py +15 -0
  21. polaris/pkg/flow/admin_server.py +104 -0
  22. polaris/pkg/flow/async_flow.py +55 -0
  23. polaris/pkg/flow/base_flow.py +111 -0
  24. polaris/pkg/flow/impl.py +1277 -0
  25. polaris/pkg/flow/lossless.py +605 -0
  26. polaris/pkg/flow/notify.py +46 -0
  27. polaris/pkg/flow/register.py +210 -0
  28. polaris/pkg/flow/skill.py +110 -0
  29. polaris/pkg/flow/sync_flow.py +20 -0
  30. polaris/pkg/model/__init__.py +15 -0
  31. polaris/pkg/model/context.py +53 -0
  32. polaris/pkg/model/contract.py +89 -0
  33. polaris/pkg/model/core.py +51 -0
  34. polaris/pkg/model/engine.py +83 -0
  35. polaris/pkg/model/error.py +94 -0
  36. polaris/pkg/model/event.py +159 -0
  37. polaris/pkg/model/lossless.py +102 -0
  38. polaris/pkg/model/rule.py +152 -0
  39. polaris/pkg/model/server.py +288 -0
  40. polaris/pkg/model/service.py +820 -0
  41. polaris/pkg/model/skill.py +179 -0
  42. polaris/pkg/model/stat.py +168 -0
  43. polaris/pkg/model/util.py +58 -0
  44. polaris/pkg/version/__init__.py +18 -0
  45. polaris/plugin/__init__.py +32 -0
  46. polaris/plugin/circuit_breaker/__init__.py +19 -0
  47. polaris/plugin/circuit_breaker/base.py +54 -0
  48. polaris/plugin/circuit_breaker/composite.py +265 -0
  49. polaris/plugin/circuit_breaker/config.py +36 -0
  50. polaris/plugin/event/__init__.py +21 -0
  51. polaris/plugin/event/pushgateway.py +202 -0
  52. polaris/plugin/load_balancer/__init__.py +24 -0
  53. polaris/plugin/load_balancer/base.py +30 -0
  54. polaris/plugin/load_balancer/random.py +39 -0
  55. polaris/plugin/load_balancer/weighted_random.py +50 -0
  56. polaris/plugin/local_registry/__init__.py +18 -0
  57. polaris/plugin/local_registry/base.py +47 -0
  58. polaris/plugin/local_registry/file_cache.py +335 -0
  59. polaris/plugin/local_registry/local_cache.py +433 -0
  60. polaris/plugin/server_connector/__init__.py +20 -0
  61. polaris/plugin/server_connector/base.py +104 -0
  62. polaris/plugin/server_connector/convert.py +149 -0
  63. polaris/plugin/server_connector/grpc_connector/__init__.py +21 -0
  64. polaris/plugin/server_connector/grpc_connector/connection.py +401 -0
  65. polaris/plugin/server_connector/grpc_connector/grpc_client.py +384 -0
  66. polaris/plugin/server_connector/grpc_connector/tls.py +45 -0
  67. polaris/plugin/service_router/__init__.py +28 -0
  68. polaris/plugin/service_router/base.py +105 -0
  69. polaris/plugin/service_router/isolated.py +35 -0
  70. polaris/plugin/service_router/metadata.py +84 -0
  71. polaris/plugin/service_router/recover.py +62 -0
  72. polaris/plugin/service_router/util.py +54 -0
  73. polaris/plugin/skill/__init__.py +16 -0
  74. polaris/plugin/skill/connector.py +127 -0
  75. polaris/plugin/skill/persistence.py +94 -0
  76. polaris/plugin/stat_reporter/__init__.py +16 -0
  77. polaris/plugin/stat_reporter/base.py +34 -0
  78. polaris/plugin/stat_reporter/prometheus.py +375 -0
  79. polaris/plugin_manager.py +146 -0
  80. polaris_python-0.0.1.dev3.dist-info/METADATA +213 -0
  81. polaris_python-0.0.1.dev3.dist-info/RECORD +84 -0
  82. polaris_python-0.0.1.dev3.dist-info/WHEEL +5 -0
  83. polaris_python-0.0.1.dev3.dist-info/licenses/LICENSE +18 -0
  84. polaris_python-0.0.1.dev3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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)