opengris-scaler 1.12.37__cp38-cp38-musllinux_1_2_x86_64.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 (196) hide show
  1. opengris_scaler-1.12.37.dist-info/METADATA +730 -0
  2. opengris_scaler-1.12.37.dist-info/RECORD +196 -0
  3. opengris_scaler-1.12.37.dist-info/WHEEL +5 -0
  4. opengris_scaler-1.12.37.dist-info/entry_points.txt +10 -0
  5. opengris_scaler-1.12.37.dist-info/licenses/LICENSE +201 -0
  6. opengris_scaler-1.12.37.dist-info/licenses/LICENSE.spdx +7 -0
  7. opengris_scaler-1.12.37.dist-info/licenses/NOTICE +8 -0
  8. opengris_scaler.libs/libcapnp-1-e88d5415.0.1.so +0 -0
  9. opengris_scaler.libs/libgcc_s-2298274a.so.1 +0 -0
  10. opengris_scaler.libs/libkj-1-9bebd8ac.0.1.so +0 -0
  11. opengris_scaler.libs/libstdc++-08d5c7eb.so.6.0.33 +0 -0
  12. scaler/__init__.py +14 -0
  13. scaler/about.py +5 -0
  14. scaler/client/__init__.py +0 -0
  15. scaler/client/agent/__init__.py +0 -0
  16. scaler/client/agent/client_agent.py +218 -0
  17. scaler/client/agent/disconnect_manager.py +27 -0
  18. scaler/client/agent/future_manager.py +112 -0
  19. scaler/client/agent/heartbeat_manager.py +74 -0
  20. scaler/client/agent/mixins.py +89 -0
  21. scaler/client/agent/object_manager.py +98 -0
  22. scaler/client/agent/task_manager.py +64 -0
  23. scaler/client/client.py +672 -0
  24. scaler/client/future.py +252 -0
  25. scaler/client/object_buffer.py +129 -0
  26. scaler/client/object_reference.py +25 -0
  27. scaler/client/serializer/__init__.py +0 -0
  28. scaler/client/serializer/default.py +16 -0
  29. scaler/client/serializer/mixins.py +38 -0
  30. scaler/cluster/__init__.py +0 -0
  31. scaler/cluster/cluster.py +95 -0
  32. scaler/cluster/combo.py +157 -0
  33. scaler/cluster/object_storage_server.py +45 -0
  34. scaler/cluster/scheduler.py +86 -0
  35. scaler/config/__init__.py +0 -0
  36. scaler/config/common/__init__.py +0 -0
  37. scaler/config/common/logging.py +41 -0
  38. scaler/config/common/web.py +18 -0
  39. scaler/config/common/worker.py +65 -0
  40. scaler/config/common/worker_adapter.py +28 -0
  41. scaler/config/config_class.py +317 -0
  42. scaler/config/defaults.py +94 -0
  43. scaler/config/mixins.py +20 -0
  44. scaler/config/section/__init__.py +0 -0
  45. scaler/config/section/cluster.py +66 -0
  46. scaler/config/section/ecs_worker_adapter.py +78 -0
  47. scaler/config/section/native_worker_adapter.py +30 -0
  48. scaler/config/section/object_storage_server.py +13 -0
  49. scaler/config/section/scheduler.py +126 -0
  50. scaler/config/section/symphony_worker_adapter.py +35 -0
  51. scaler/config/section/top.py +16 -0
  52. scaler/config/section/webui.py +16 -0
  53. scaler/config/types/__init__.py +0 -0
  54. scaler/config/types/network_backend.py +12 -0
  55. scaler/config/types/object_storage_server.py +45 -0
  56. scaler/config/types/worker.py +67 -0
  57. scaler/config/types/zmq.py +83 -0
  58. scaler/entry_points/__init__.py +0 -0
  59. scaler/entry_points/cluster.py +10 -0
  60. scaler/entry_points/object_storage_server.py +26 -0
  61. scaler/entry_points/scheduler.py +51 -0
  62. scaler/entry_points/top.py +272 -0
  63. scaler/entry_points/webui.py +6 -0
  64. scaler/entry_points/worker_adapter_ecs.py +22 -0
  65. scaler/entry_points/worker_adapter_native.py +31 -0
  66. scaler/entry_points/worker_adapter_symphony.py +26 -0
  67. scaler/io/__init__.py +0 -0
  68. scaler/io/async_binder.py +89 -0
  69. scaler/io/async_connector.py +95 -0
  70. scaler/io/async_object_storage_connector.py +225 -0
  71. scaler/io/mixins.py +154 -0
  72. scaler/io/sync_connector.py +68 -0
  73. scaler/io/sync_object_storage_connector.py +249 -0
  74. scaler/io/sync_subscriber.py +83 -0
  75. scaler/io/utility.py +80 -0
  76. scaler/io/ymq/__init__.py +0 -0
  77. scaler/io/ymq/_ymq.pyi +95 -0
  78. scaler/io/ymq/_ymq.so +0 -0
  79. scaler/io/ymq/ymq.py +138 -0
  80. scaler/io/ymq_async_object_storage_connector.py +184 -0
  81. scaler/io/ymq_sync_object_storage_connector.py +184 -0
  82. scaler/object_storage/__init__.py +0 -0
  83. scaler/object_storage/object_storage_server.so +0 -0
  84. scaler/protocol/__init__.py +0 -0
  85. scaler/protocol/capnp/__init__.py +0 -0
  86. scaler/protocol/capnp/_python.py +6 -0
  87. scaler/protocol/capnp/common.capnp +68 -0
  88. scaler/protocol/capnp/message.capnp +218 -0
  89. scaler/protocol/capnp/object_storage.capnp +57 -0
  90. scaler/protocol/capnp/status.capnp +73 -0
  91. scaler/protocol/introduction.md +105 -0
  92. scaler/protocol/python/__init__.py +0 -0
  93. scaler/protocol/python/common.py +140 -0
  94. scaler/protocol/python/message.py +751 -0
  95. scaler/protocol/python/mixins.py +13 -0
  96. scaler/protocol/python/object_storage.py +118 -0
  97. scaler/protocol/python/status.py +279 -0
  98. scaler/protocol/worker.md +228 -0
  99. scaler/scheduler/__init__.py +0 -0
  100. scaler/scheduler/allocate_policy/__init__.py +0 -0
  101. scaler/scheduler/allocate_policy/allocate_policy.py +9 -0
  102. scaler/scheduler/allocate_policy/capability_allocate_policy.py +280 -0
  103. scaler/scheduler/allocate_policy/even_load_allocate_policy.py +159 -0
  104. scaler/scheduler/allocate_policy/mixins.py +55 -0
  105. scaler/scheduler/controllers/__init__.py +0 -0
  106. scaler/scheduler/controllers/balance_controller.py +65 -0
  107. scaler/scheduler/controllers/client_controller.py +131 -0
  108. scaler/scheduler/controllers/config_controller.py +31 -0
  109. scaler/scheduler/controllers/graph_controller.py +424 -0
  110. scaler/scheduler/controllers/information_controller.py +81 -0
  111. scaler/scheduler/controllers/mixins.py +194 -0
  112. scaler/scheduler/controllers/object_controller.py +147 -0
  113. scaler/scheduler/controllers/scaling_policies/__init__.py +0 -0
  114. scaler/scheduler/controllers/scaling_policies/fixed_elastic.py +145 -0
  115. scaler/scheduler/controllers/scaling_policies/mixins.py +10 -0
  116. scaler/scheduler/controllers/scaling_policies/null.py +14 -0
  117. scaler/scheduler/controllers/scaling_policies/types.py +9 -0
  118. scaler/scheduler/controllers/scaling_policies/utility.py +20 -0
  119. scaler/scheduler/controllers/scaling_policies/vanilla.py +95 -0
  120. scaler/scheduler/controllers/task_controller.py +376 -0
  121. scaler/scheduler/controllers/worker_controller.py +169 -0
  122. scaler/scheduler/object_usage/__init__.py +0 -0
  123. scaler/scheduler/object_usage/object_tracker.py +131 -0
  124. scaler/scheduler/scheduler.py +251 -0
  125. scaler/scheduler/task/__init__.py +0 -0
  126. scaler/scheduler/task/task_state_machine.py +92 -0
  127. scaler/scheduler/task/task_state_manager.py +61 -0
  128. scaler/ui/__init__.py +0 -0
  129. scaler/ui/common/__init__.py +0 -0
  130. scaler/ui/common/constants.py +9 -0
  131. scaler/ui/common/live_display.py +147 -0
  132. scaler/ui/common/memory_window.py +146 -0
  133. scaler/ui/common/setting_page.py +40 -0
  134. scaler/ui/common/task_graph.py +840 -0
  135. scaler/ui/common/task_log.py +111 -0
  136. scaler/ui/common/utility.py +66 -0
  137. scaler/ui/common/webui.py +80 -0
  138. scaler/ui/common/worker_processors.py +104 -0
  139. scaler/ui/v1.py +76 -0
  140. scaler/ui/v2.py +102 -0
  141. scaler/ui/webui.py +21 -0
  142. scaler/utility/__init__.py +0 -0
  143. scaler/utility/debug.py +19 -0
  144. scaler/utility/event_list.py +63 -0
  145. scaler/utility/event_loop.py +58 -0
  146. scaler/utility/exceptions.py +42 -0
  147. scaler/utility/formatter.py +44 -0
  148. scaler/utility/graph/__init__.py +0 -0
  149. scaler/utility/graph/optimization.py +27 -0
  150. scaler/utility/graph/topological_sorter.py +11 -0
  151. scaler/utility/graph/topological_sorter_graphblas.py +174 -0
  152. scaler/utility/identifiers.py +107 -0
  153. scaler/utility/logging/__init__.py +0 -0
  154. scaler/utility/logging/decorators.py +25 -0
  155. scaler/utility/logging/scoped_logger.py +33 -0
  156. scaler/utility/logging/utility.py +183 -0
  157. scaler/utility/many_to_many_dict.py +123 -0
  158. scaler/utility/metadata/__init__.py +0 -0
  159. scaler/utility/metadata/profile_result.py +31 -0
  160. scaler/utility/metadata/task_flags.py +30 -0
  161. scaler/utility/mixins.py +13 -0
  162. scaler/utility/network_util.py +7 -0
  163. scaler/utility/one_to_many_dict.py +72 -0
  164. scaler/utility/queues/__init__.py +0 -0
  165. scaler/utility/queues/async_indexed_queue.py +37 -0
  166. scaler/utility/queues/async_priority_queue.py +70 -0
  167. scaler/utility/queues/async_sorted_priority_queue.py +45 -0
  168. scaler/utility/queues/indexed_queue.py +114 -0
  169. scaler/utility/serialization.py +9 -0
  170. scaler/version.txt +1 -0
  171. scaler/worker/__init__.py +0 -0
  172. scaler/worker/agent/__init__.py +0 -0
  173. scaler/worker/agent/heartbeat_manager.py +110 -0
  174. scaler/worker/agent/mixins.py +137 -0
  175. scaler/worker/agent/processor/__init__.py +0 -0
  176. scaler/worker/agent/processor/object_cache.py +107 -0
  177. scaler/worker/agent/processor/processor.py +285 -0
  178. scaler/worker/agent/processor/streaming_buffer.py +28 -0
  179. scaler/worker/agent/processor_holder.py +147 -0
  180. scaler/worker/agent/processor_manager.py +369 -0
  181. scaler/worker/agent/profiling_manager.py +109 -0
  182. scaler/worker/agent/task_manager.py +150 -0
  183. scaler/worker/agent/timeout_manager.py +19 -0
  184. scaler/worker/preload.py +84 -0
  185. scaler/worker/worker.py +265 -0
  186. scaler/worker_adapter/__init__.py +0 -0
  187. scaler/worker_adapter/common.py +26 -0
  188. scaler/worker_adapter/ecs.py +241 -0
  189. scaler/worker_adapter/native.py +138 -0
  190. scaler/worker_adapter/symphony/__init__.py +0 -0
  191. scaler/worker_adapter/symphony/callback.py +45 -0
  192. scaler/worker_adapter/symphony/heartbeat_manager.py +82 -0
  193. scaler/worker_adapter/symphony/message.py +24 -0
  194. scaler/worker_adapter/symphony/task_manager.py +289 -0
  195. scaler/worker_adapter/symphony/worker.py +204 -0
  196. scaler/worker_adapter/symphony/worker_adapter.py +123 -0
@@ -0,0 +1,83 @@
1
+ import logging
2
+ import threading
3
+ from typing import Callable, Optional
4
+
5
+ import zmq
6
+
7
+ from scaler.config.types.zmq import ZMQConfig
8
+ from scaler.io.mixins import SyncSubscriber
9
+ from scaler.io.utility import deserialize
10
+ from scaler.protocol.python.mixins import Message
11
+
12
+
13
+ class ZMQSyncSubscriber(SyncSubscriber, threading.Thread):
14
+ def __init__(
15
+ self,
16
+ address: ZMQConfig,
17
+ callback: Callable[[Message], None],
18
+ topic: bytes,
19
+ exit_callback: Optional[Callable[[], None]] = None,
20
+ stop_event: threading.Event = threading.Event(),
21
+ daemonic: bool = False,
22
+ timeout_seconds: int = -1,
23
+ ):
24
+ threading.Thread.__init__(self)
25
+
26
+ self._stop_event = stop_event
27
+ self._address = address
28
+ self._callback = callback
29
+ self._exit_callback = exit_callback
30
+ self._topic = topic
31
+ self.daemon = bool(daemonic)
32
+ self._timeout_seconds = timeout_seconds
33
+
34
+ self._context: Optional[zmq.Context] = None
35
+ self._socket: Optional[zmq.Socket] = None
36
+
37
+ def __close(self):
38
+ self._socket.close()
39
+
40
+ def __stop_polling(self):
41
+ self._stop_event.set()
42
+
43
+ def destroy(self):
44
+ self.__stop_polling()
45
+
46
+ def run(self) -> None:
47
+ self.__initialize()
48
+
49
+ while not self._stop_event.is_set():
50
+ self.__routine_polling()
51
+
52
+ if self._exit_callback is not None:
53
+ self._exit_callback()
54
+
55
+ self.__close()
56
+
57
+ def __initialize(self):
58
+ self._context = zmq.Context.instance()
59
+ self._socket = self._context.socket(zmq.SUB)
60
+ self._socket.setsockopt(zmq.RCVHWM, 0)
61
+
62
+ if self._timeout_seconds == -1:
63
+ self._socket.setsockopt(zmq.RCVTIMEO, self._timeout_seconds)
64
+ else:
65
+ self._socket.setsockopt(zmq.RCVTIMEO, self._timeout_seconds * 1000)
66
+
67
+ self._socket.subscribe(self._topic)
68
+ self._socket.connect(self._address.to_address())
69
+ self._socket.connect(self._address.to_address())
70
+
71
+ def __routine_polling(self):
72
+ try:
73
+ self.__routine_receive(self._socket.recv(copy=False).bytes)
74
+ except zmq.Again:
75
+ raise TimeoutError(f"Cannot connect to {self._address.to_address()} in {self._timeout_seconds} seconds")
76
+
77
+ def __routine_receive(self, payload: bytes):
78
+ result: Optional[Message] = deserialize(payload)
79
+ if result is None:
80
+ logging.error(f"received unknown message: {payload!r}")
81
+ return None
82
+
83
+ self._callback(result)
scaler/io/utility.py ADDED
@@ -0,0 +1,80 @@
1
+ import logging
2
+ import os
3
+ from typing import List, Optional
4
+
5
+ from scaler.config.defaults import CAPNP_DATA_SIZE_LIMIT, CAPNP_MESSAGE_SIZE_LIMIT
6
+ from scaler.config.types.network_backend import NetworkBackend
7
+ from scaler.io.async_object_storage_connector import PyAsyncObjectStorageConnector
8
+ from scaler.io.mixins import AsyncObjectStorageConnector, SyncObjectStorageConnector
9
+ from scaler.io.sync_object_storage_connector import PySyncObjectStorageConnector
10
+ from scaler.protocol.capnp._python import _message # noqa
11
+ from scaler.protocol.python.message import PROTOCOL
12
+ from scaler.protocol.python.mixins import Message
13
+
14
+ try:
15
+ from collections.abc import Buffer # type: ignore[attr-defined]
16
+ except ImportError:
17
+ from typing_extensions import Buffer
18
+
19
+
20
+ def get_scaler_network_backend_from_env():
21
+ backend_str = os.environ.get("SCALER_NETWORK_BACKEND", "tcp_zmq") # Default to tcp_zmq
22
+ try:
23
+ return NetworkBackend[backend_str]
24
+ except KeyError:
25
+ return None
26
+
27
+
28
+ def create_async_object_storage_connector(*args, **kwargs) -> AsyncObjectStorageConnector:
29
+ connector_type = get_scaler_network_backend_from_env()
30
+ if connector_type == NetworkBackend.ymq:
31
+ from scaler.io.ymq_async_object_storage_connector import PyYMQAsyncObjectStorageConnector
32
+
33
+ return PyYMQAsyncObjectStorageConnector(*args, **kwargs)
34
+
35
+ elif connector_type == NetworkBackend.tcp_zmq:
36
+ return PyAsyncObjectStorageConnector(*args, **kwargs)
37
+
38
+ else:
39
+ raise ValueError(
40
+ f"Invalid SCALER_NETWORK_BACKEND value." f"Expected one of: {[e.name for e in NetworkBackend]}"
41
+ )
42
+
43
+
44
+ def create_sync_object_storage_connector(*args, **kwargs) -> SyncObjectStorageConnector:
45
+ connector_type = get_scaler_network_backend_from_env()
46
+ if connector_type == NetworkBackend.ymq:
47
+ from scaler.io.ymq_sync_object_storage_connector import PyYMQSyncObjectStorageConnector
48
+
49
+ return PyYMQSyncObjectStorageConnector(*args, **kwargs)
50
+
51
+ elif connector_type == NetworkBackend.tcp_zmq:
52
+ return PySyncObjectStorageConnector(*args, **kwargs)
53
+ else:
54
+ raise ValueError(
55
+ f"Invalid SCALER_NETWORK_BACKEND value." f"Expected one of: {[e.name for e in NetworkBackend]}"
56
+ )
57
+
58
+
59
+ def deserialize(data: Buffer) -> Optional[Message]:
60
+ with _message.Message.from_bytes(data, traversal_limit_in_words=CAPNP_MESSAGE_SIZE_LIMIT) as payload:
61
+ if not hasattr(payload, payload.which()):
62
+ logging.error(f"unknown message type: {payload.which()}")
63
+ return None
64
+
65
+ message = getattr(payload, payload.which())
66
+ return PROTOCOL[payload.which()](message)
67
+
68
+
69
+ def serialize(message: Message) -> bytes:
70
+ payload = _message.Message(**{PROTOCOL.inverse[type(message)]: message.get_message()})
71
+ return payload.to_bytes()
72
+
73
+
74
+ def chunk_to_list_of_bytes(data: bytes) -> List[bytes]:
75
+ # TODO: change to list of memoryview when capnp can support memoryview
76
+ return [data[i : i + CAPNP_DATA_SIZE_LIMIT] for i in range(0, len(data), CAPNP_DATA_SIZE_LIMIT)]
77
+
78
+
79
+ def concat_list_of_bytes(data: List[bytes]) -> bytes:
80
+ return bytearray().join(data)
File without changes
scaler/io/ymq/_ymq.pyi ADDED
@@ -0,0 +1,95 @@
1
+ # NOTE: NOT IMPLEMENTATION, TYPE INFORMATION ONLY
2
+ # This file contains type stubs for the Ymq Python C Extension module
3
+ from enum import IntEnum
4
+ from typing import Callable, Optional, SupportsBytes, Union
5
+
6
+ try:
7
+ from collections.abc import Buffer # type: ignore[attr-defined]
8
+ except ImportError:
9
+ from typing_extensions import Buffer
10
+
11
+ class Bytes(Buffer):
12
+ data: bytes | None
13
+ len: int
14
+
15
+ def __init__(self, data: Buffer | None = None) -> None: ...
16
+ def __repr__(self) -> str: ...
17
+ def __len__(self) -> int: ...
18
+
19
+ # this type signature is not 100% accurate because it's implemented in C
20
+ # but this satisfies the type check and is good enough
21
+ def __buffer__(self, flags: int, /) -> memoryview: ...
22
+
23
+ class Message:
24
+ address: Bytes | None
25
+ payload: Bytes
26
+
27
+ def __init__(
28
+ self, address: Bytes | bytes | SupportsBytes | None, payload: Bytes | bytes | SupportsBytes
29
+ ) -> None: ...
30
+ def __repr__(self) -> str: ...
31
+ def __str__(self) -> str: ...
32
+
33
+ class IOSocketType(IntEnum):
34
+ Uninit = 0
35
+ Binder = 1
36
+ Connector = 2
37
+ Unicast = 3
38
+ Multicast = 4
39
+
40
+ class BaseIOContext:
41
+ num_threads: int
42
+
43
+ def __init__(self, num_threads: int = 1) -> None: ...
44
+ def __repr__(self) -> str: ...
45
+ def createIOSocket(
46
+ self, callback: Callable[[Union[BaseIOSocket, Exception]], None], identity: str, socket_type: IOSocketType
47
+ ) -> None:
48
+ """Create an io socket with an identity and socket type"""
49
+
50
+ class BaseIOSocket:
51
+ identity: str
52
+ socket_type: IOSocketType
53
+
54
+ def __repr__(self) -> str: ...
55
+ def send(self, callback: Callable[[Optional[Exception]], None], message: Message) -> None:
56
+ """Send a message to one of the socket's peers"""
57
+
58
+ def recv(self, callback: Callable[[Union[Message, Exception]], None]) -> None:
59
+ """Receive a message from one of the socket's peers"""
60
+
61
+ def bind(self, callback: Callable[[Optional[Exception]], None], address: str) -> None:
62
+ """Bind the socket to an address and listen for incoming connections"""
63
+
64
+ def connect(self, callback: Callable[[Optional[Exception]], None], address: str) -> None:
65
+ """Connect to a remote socket"""
66
+
67
+ class ErrorCode(IntEnum):
68
+ Uninit = 0
69
+ InvalidPortFormat = 1
70
+ InvalidAddressFormat = 2
71
+ ConfigurationError = 3
72
+ SignalNotSupported = 4
73
+ CoreBug = 5
74
+ RepetetiveIOSocketIdentity = 6
75
+ RedundantIOSocketRefCount = 7
76
+ MultipleConnectToNotSupported = 8
77
+ MultipleBindToNotSupported = 9
78
+ InitialConnectFailedWithInProgress = 10
79
+ SendMessageRequestCouldNotComplete = 11
80
+ SetSockOptNonFatalFailure = 12
81
+ IPv6NotSupported = 13
82
+ RemoteEndDisconnectedOnSocketWithoutGuaranteedDelivery = 14
83
+ ConnectorSocketClosedByRemoteEnd = 15
84
+ IOSocketStopRequested = 16
85
+ BinderSendMessageWithNoAddress = 17
86
+
87
+ def explanation(self) -> str: ...
88
+
89
+ class YMQException(Exception):
90
+ code: ErrorCode
91
+ message: str
92
+
93
+ def __init__(self, /, code: ErrorCode, message: str) -> None: ...
94
+ def __repr__(self) -> str: ...
95
+ def __str__(self) -> str: ...
scaler/io/ymq/_ymq.so ADDED
Binary file
scaler/io/ymq/ymq.py ADDED
@@ -0,0 +1,138 @@
1
+ # This file wraps the interface exported by the C implementation of the module
2
+ # and provides a more ergonomic interface supporting both asynchronous and synchronous execution
3
+
4
+ __all__ = ["IOSocket", "IOContext", "Message", "IOSocketType", "YMQException", "Bytes", "ErrorCode"]
5
+
6
+ import asyncio
7
+ import concurrent.futures
8
+ from typing import Any, Callable, Optional, TypeVar, Union
9
+
10
+ try:
11
+ from typing import Concatenate, ParamSpec # type: ignore[attr-defined]
12
+ except ImportError:
13
+ from typing_extensions import ParamSpec, Concatenate # type: ignore[assignment]
14
+
15
+ from scaler.io.ymq._ymq import BaseIOContext, BaseIOSocket, Bytes, ErrorCode, IOSocketType, Message, YMQException
16
+
17
+
18
+ class IOSocket:
19
+ _base: BaseIOSocket
20
+
21
+ def __init__(self, base: BaseIOSocket) -> None:
22
+ self._base = base
23
+
24
+ @property
25
+ def socket_type(self) -> IOSocketType:
26
+ return self._base.socket_type
27
+
28
+ @property
29
+ def identity(self) -> str:
30
+ return self._base.identity
31
+
32
+ async def bind(self, address: str) -> None:
33
+ """Bind the socket to an address and listen for incoming connections"""
34
+ await call_async(self._base.bind, address)
35
+
36
+ def bind_sync(self, address: str, /, timeout: Optional[float] = None) -> None:
37
+ """Bind the socket to an address and listen for incoming connections"""
38
+ call_sync(self._base.bind, address, timeout=timeout)
39
+
40
+ async def connect(self, address: str) -> None:
41
+ """Connect to a remote socket"""
42
+ await call_async(self._base.connect, address)
43
+
44
+ def connect_sync(self, address: str, /, timeout: Optional[float] = None) -> None:
45
+ """Connect to a remote socket"""
46
+ call_sync(self._base.connect, address, timeout=timeout)
47
+
48
+ async def send(self, message: Message) -> None:
49
+ """Send a message to one of the socket's peers"""
50
+ await call_async(self._base.send, message)
51
+
52
+ def send_sync(self, message: Message, /, timeout: Optional[float] = None) -> None:
53
+ """Send a message to one of the socket's peers"""
54
+ call_sync(self._base.send, message, timeout=timeout)
55
+
56
+ async def recv(self) -> Message:
57
+ """Receive a message from one of the socket's peers"""
58
+ return await call_async(self._base.recv)
59
+
60
+ def recv_sync(self, /, timeout: Optional[float] = None) -> Message:
61
+ """Receive a message from one of the socket's peers"""
62
+ return call_sync(self._base.recv, timeout=timeout)
63
+
64
+
65
+ class IOContext:
66
+ _base: BaseIOContext
67
+
68
+ def __init__(self, num_threads: int = 1) -> None:
69
+ self._base = BaseIOContext(num_threads)
70
+
71
+ @property
72
+ def num_threads(self) -> int:
73
+ return self._base.num_threads
74
+
75
+ async def createIOSocket(self, identity: str, socket_type: IOSocketType) -> IOSocket:
76
+ """Create an io socket with an identity and socket type"""
77
+ return IOSocket(await call_async(self._base.createIOSocket, identity, socket_type))
78
+
79
+ def createIOSocket_sync(self, identity: str, socket_type: IOSocketType) -> IOSocket:
80
+ """Create an io socket with an identity and socket type"""
81
+ return IOSocket(call_sync(self._base.createIOSocket, identity, socket_type))
82
+
83
+
84
+ P = ParamSpec("P")
85
+ T = TypeVar("T")
86
+
87
+
88
+ async def call_async(
89
+ func: Callable[Concatenate[Callable[[Union[T, BaseException]], None], P], None], # type: ignore
90
+ *args: P.args, # type: ignore
91
+ **kwargs: P.kwargs, # type: ignore
92
+ ) -> T:
93
+ loop = asyncio.get_event_loop()
94
+ future = loop.create_future()
95
+
96
+ def callback(result: Union[T, BaseException]):
97
+ if isinstance(result, BaseException):
98
+ loop.call_soon_threadsafe(_safe_set_exception, future, result)
99
+ else:
100
+ loop.call_soon_threadsafe(_safe_set_result, future, result)
101
+
102
+ func(callback, *args, **kwargs)
103
+ return await future
104
+
105
+
106
+ # about the ignore directives: mypy cannot properly handle typing extension's ParamSpec and Concatenate in python <=3.9
107
+ # these type hints are correctly understood in Python 3.10+
108
+ def call_sync( # type: ignore[valid-type]
109
+ func: Callable[Concatenate[Callable[[Union[T, BaseException]], None], P], None], # type: ignore
110
+ *args: P.args, # type: ignore
111
+ timeout: Optional[float] = None,
112
+ **kwargs: P.kwargs, # type: ignore
113
+ ) -> T: # type: ignore
114
+ future: concurrent.futures.Future = concurrent.futures.Future()
115
+
116
+ def callback(result: Union[T, BaseException]):
117
+ if future.done():
118
+ return
119
+
120
+ if isinstance(result, BaseException):
121
+ future.set_exception(result)
122
+ else:
123
+ future.set_result(result)
124
+
125
+ func(callback, *args, **kwargs)
126
+ return future.result(timeout)
127
+
128
+
129
+ def _safe_set_result(future: asyncio.Future, result: Any) -> None:
130
+ if future.done():
131
+ return
132
+ future.set_result(result)
133
+
134
+
135
+ def _safe_set_exception(future: asyncio.Future, exc: BaseException) -> None:
136
+ if future.done():
137
+ return
138
+ future.set_exception(exc)
@@ -0,0 +1,184 @@
1
+ import asyncio
2
+ import logging
3
+ import socket
4
+ import uuid
5
+ from typing import Dict, Optional, Tuple
6
+
7
+ from scaler.io.mixins import AsyncObjectStorageConnector
8
+ from scaler.io.ymq.ymq import IOContext, IOSocketType, Message, YMQException
9
+ from scaler.protocol.capnp._python import _object_storage # noqa
10
+ from scaler.protocol.python.object_storage import ObjectRequestHeader, ObjectResponseHeader, to_capnp_object_id
11
+ from scaler.utility.exceptions import ObjectStorageException
12
+ from scaler.utility.identifiers import ObjectID
13
+
14
+
15
+ class PyYMQAsyncObjectStorageConnector(AsyncObjectStorageConnector):
16
+ """An asyncio connector that uses YMQ to connect to a Scaler's object storage instance."""
17
+
18
+ def __init__(self):
19
+ self._host: Optional[str] = None
20
+ self._port: Optional[int] = None
21
+
22
+ self._connected_event = asyncio.Event()
23
+
24
+ self._next_request_id = 0
25
+ self._pending_get_requests: Dict[ObjectID, asyncio.Future] = {}
26
+
27
+ self._lock = asyncio.Lock()
28
+ self._identity: str = f"{self.__class__.__name__}|{socket.gethostname().split('.')[0]}|{uuid.uuid4()}"
29
+ self._io_context: IOContext = IOContext()
30
+ self._io_socket = self._io_context.createIOSocket_sync(self._identity, IOSocketType.Connector)
31
+
32
+ def __del__(self):
33
+ if not self.is_connected():
34
+ return
35
+ self._io_socket = None
36
+
37
+ async def connect(self, host: str, port: int):
38
+ self._host = host
39
+ self._port = port
40
+
41
+ if self.is_connected():
42
+ raise ObjectStorageException("connector is already connected.")
43
+ await self._io_socket.connect(self.address)
44
+ self._connected_event.set()
45
+
46
+ async def wait_until_connected(self):
47
+ await self._connected_event.wait()
48
+
49
+ def is_connected(self) -> bool:
50
+ return self._connected_event.is_set()
51
+
52
+ async def destroy(self):
53
+ if not self.is_connected():
54
+ return
55
+ self._io_socket = None
56
+
57
+ @property
58
+ def address(self) -> str:
59
+ return f"tcp://{self._host}:{self._port}"
60
+
61
+ async def routine(self):
62
+ await self.wait_until_connected()
63
+
64
+ response = await self.__receive_response()
65
+ if response is None:
66
+ return
67
+
68
+ header, payload = response
69
+
70
+ if header.response_type != ObjectResponseHeader.ObjectResponseType.GetOK:
71
+ return
72
+
73
+ pending_get_future = self._pending_get_requests.pop(header.object_id, None)
74
+
75
+ if pending_get_future is None:
76
+ logging.warning(f"unknown get-ok response for unrequested object_id={repr(header.object_id)}.")
77
+ return
78
+
79
+ pending_get_future.set_result(payload)
80
+
81
+ async def set_object(self, object_id: ObjectID, payload: bytes) -> None:
82
+ await self.__send_request(object_id, len(payload), ObjectRequestHeader.ObjectRequestType.SetObject, payload)
83
+
84
+ async def get_object(self, object_id: ObjectID, max_payload_length: int = 2**64 - 1) -> bytes:
85
+ pending_get_future = self._pending_get_requests.get(object_id)
86
+
87
+ if pending_get_future is None:
88
+ pending_get_future = asyncio.Future()
89
+ self._pending_get_requests[object_id] = pending_get_future
90
+
91
+ await self.__send_request(
92
+ object_id, max_payload_length, ObjectRequestHeader.ObjectRequestType.GetObject, None
93
+ )
94
+
95
+ return await pending_get_future
96
+
97
+ async def delete_object(self, object_id: ObjectID) -> None:
98
+ await self.__send_request(object_id, 0, ObjectRequestHeader.ObjectRequestType.DeleteObject, None)
99
+
100
+ async def duplicate_object_id(self, object_id: ObjectID, new_object_id: ObjectID) -> None:
101
+ object_id_payload = to_capnp_object_id(object_id).to_bytes()
102
+
103
+ await self.__send_request(
104
+ new_object_id,
105
+ len(object_id_payload),
106
+ ObjectRequestHeader.ObjectRequestType.DuplicateObjectID,
107
+ object_id_payload,
108
+ )
109
+
110
+ def __ensure_is_connected(self):
111
+ if self._io_socket is None:
112
+ raise ObjectStorageException("connector is not connected.")
113
+
114
+ async def __send_request(
115
+ self,
116
+ object_id: ObjectID,
117
+ payload_length: int,
118
+ request_type: ObjectRequestHeader.ObjectRequestType,
119
+ payload: Optional[bytes],
120
+ ):
121
+ self.__ensure_is_connected()
122
+
123
+ request_id = self._next_request_id
124
+ self._next_request_id += 1
125
+ self._next_request_id %= 2**64 - 1 # UINT64_MAX
126
+
127
+ header = ObjectRequestHeader.new_msg(object_id, payload_length, request_id, request_type)
128
+
129
+ try:
130
+ async with self._lock:
131
+ await self.__write_request_header(header)
132
+
133
+ if payload is not None:
134
+ await self.__write_request_payload(payload)
135
+
136
+ except YMQException:
137
+ self._io_socket = None
138
+ self.__raise_connection_failure()
139
+
140
+ async def __write_request_header(self, header: ObjectRequestHeader):
141
+ assert self._io_socket is not None
142
+ await self._io_socket.send(Message(address=None, payload=header.get_message().to_bytes()))
143
+
144
+ async def __write_request_payload(self, payload: bytes):
145
+ assert self._io_socket is not None
146
+ await self._io_socket.send(Message(address=None, payload=payload))
147
+
148
+ async def __receive_response(self) -> Optional[Tuple[ObjectResponseHeader, bytes]]:
149
+ if self._io_socket is None:
150
+ return None
151
+
152
+ try:
153
+ header = await self.__read_response_header()
154
+ payload = await self.__read_response_payload(header)
155
+ except YMQException:
156
+ self._io_socket = None
157
+ self.__raise_connection_failure()
158
+
159
+ return header, payload
160
+
161
+ async def __read_response_header(self) -> ObjectResponseHeader:
162
+ assert self._io_socket is not None
163
+
164
+ msg = await self._io_socket.recv()
165
+ header_data = msg.payload.data
166
+ assert len(header_data) == ObjectResponseHeader.MESSAGE_LENGTH
167
+
168
+ with _object_storage.ObjectResponseHeader.from_bytes(header_data) as header_message:
169
+ return ObjectResponseHeader(header_message)
170
+
171
+ async def __read_response_payload(self, header: ObjectResponseHeader) -> bytes:
172
+ assert self._io_socket is not None
173
+ # assert self._reader is not None
174
+
175
+ if header.payload_length > 0:
176
+ res = await self._io_socket.recv()
177
+ assert len(res.payload) == header.payload_length
178
+ return res.payload.data
179
+ else:
180
+ return b""
181
+
182
+ @staticmethod
183
+ def __raise_connection_failure():
184
+ raise ObjectStorageException("connection failure to object storage server.")