opengris-scaler 1.12.28__cp313-cp313-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.

Potentially problematic release.


This version of opengris-scaler might be problematic. Click here for more details.

Files changed (187) hide show
  1. opengris_scaler-1.12.28.dist-info/METADATA +728 -0
  2. opengris_scaler-1.12.28.dist-info/RECORD +187 -0
  3. opengris_scaler-1.12.28.dist-info/WHEEL +5 -0
  4. opengris_scaler-1.12.28.dist-info/entry_points.txt +10 -0
  5. opengris_scaler-1.12.28.dist-info/licenses/LICENSE +201 -0
  6. opengris_scaler-1.12.28.dist-info/licenses/LICENSE.spdx +7 -0
  7. opengris_scaler-1.12.28.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 +210 -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 +658 -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 +115 -0
  32. scaler/cluster/combo.py +150 -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/defaults.py +94 -0
  37. scaler/config/loader.py +96 -0
  38. scaler/config/mixins.py +20 -0
  39. scaler/config/section/__init__.py +0 -0
  40. scaler/config/section/cluster.py +55 -0
  41. scaler/config/section/ecs_worker_adapter.py +85 -0
  42. scaler/config/section/native_worker_adapter.py +43 -0
  43. scaler/config/section/object_storage_server.py +8 -0
  44. scaler/config/section/scheduler.py +54 -0
  45. scaler/config/section/symphony_worker_adapter.py +47 -0
  46. scaler/config/section/top.py +13 -0
  47. scaler/config/section/webui.py +21 -0
  48. scaler/config/types/__init__.py +0 -0
  49. scaler/config/types/network_backend.py +12 -0
  50. scaler/config/types/object_storage_server.py +45 -0
  51. scaler/config/types/worker.py +62 -0
  52. scaler/config/types/zmq.py +83 -0
  53. scaler/entry_points/__init__.py +0 -0
  54. scaler/entry_points/cluster.py +133 -0
  55. scaler/entry_points/object_storage_server.py +45 -0
  56. scaler/entry_points/scheduler.py +144 -0
  57. scaler/entry_points/top.py +286 -0
  58. scaler/entry_points/webui.py +48 -0
  59. scaler/entry_points/worker_adapter_ecs.py +191 -0
  60. scaler/entry_points/worker_adapter_native.py +137 -0
  61. scaler/entry_points/worker_adapter_symphony.py +98 -0
  62. scaler/io/__init__.py +0 -0
  63. scaler/io/async_binder.py +89 -0
  64. scaler/io/async_connector.py +95 -0
  65. scaler/io/async_object_storage_connector.py +225 -0
  66. scaler/io/mixins.py +154 -0
  67. scaler/io/sync_connector.py +68 -0
  68. scaler/io/sync_object_storage_connector.py +247 -0
  69. scaler/io/sync_subscriber.py +83 -0
  70. scaler/io/utility.py +80 -0
  71. scaler/io/ymq/__init__.py +0 -0
  72. scaler/io/ymq/_ymq.pyi +95 -0
  73. scaler/io/ymq/ymq.py +138 -0
  74. scaler/io/ymq_async_object_storage_connector.py +184 -0
  75. scaler/io/ymq_sync_object_storage_connector.py +184 -0
  76. scaler/object_storage/__init__.py +0 -0
  77. scaler/protocol/__init__.py +0 -0
  78. scaler/protocol/capnp/__init__.py +0 -0
  79. scaler/protocol/capnp/_python.py +6 -0
  80. scaler/protocol/capnp/common.capnp +68 -0
  81. scaler/protocol/capnp/message.capnp +218 -0
  82. scaler/protocol/capnp/object_storage.capnp +57 -0
  83. scaler/protocol/capnp/status.capnp +73 -0
  84. scaler/protocol/introduction.md +105 -0
  85. scaler/protocol/python/__init__.py +0 -0
  86. scaler/protocol/python/common.py +140 -0
  87. scaler/protocol/python/message.py +751 -0
  88. scaler/protocol/python/mixins.py +13 -0
  89. scaler/protocol/python/object_storage.py +118 -0
  90. scaler/protocol/python/status.py +279 -0
  91. scaler/protocol/worker.md +228 -0
  92. scaler/scheduler/__init__.py +0 -0
  93. scaler/scheduler/allocate_policy/__init__.py +0 -0
  94. scaler/scheduler/allocate_policy/allocate_policy.py +9 -0
  95. scaler/scheduler/allocate_policy/capability_allocate_policy.py +280 -0
  96. scaler/scheduler/allocate_policy/even_load_allocate_policy.py +159 -0
  97. scaler/scheduler/allocate_policy/mixins.py +55 -0
  98. scaler/scheduler/controllers/__init__.py +0 -0
  99. scaler/scheduler/controllers/balance_controller.py +65 -0
  100. scaler/scheduler/controllers/client_controller.py +131 -0
  101. scaler/scheduler/controllers/config_controller.py +31 -0
  102. scaler/scheduler/controllers/graph_controller.py +424 -0
  103. scaler/scheduler/controllers/information_controller.py +81 -0
  104. scaler/scheduler/controllers/mixins.py +194 -0
  105. scaler/scheduler/controllers/object_controller.py +147 -0
  106. scaler/scheduler/controllers/scaling_policies/__init__.py +0 -0
  107. scaler/scheduler/controllers/scaling_policies/fixed_elastic.py +145 -0
  108. scaler/scheduler/controllers/scaling_policies/mixins.py +10 -0
  109. scaler/scheduler/controllers/scaling_policies/null.py +14 -0
  110. scaler/scheduler/controllers/scaling_policies/types.py +9 -0
  111. scaler/scheduler/controllers/scaling_policies/utility.py +20 -0
  112. scaler/scheduler/controllers/scaling_policies/vanilla.py +95 -0
  113. scaler/scheduler/controllers/task_controller.py +376 -0
  114. scaler/scheduler/controllers/worker_controller.py +169 -0
  115. scaler/scheduler/object_usage/__init__.py +0 -0
  116. scaler/scheduler/object_usage/object_tracker.py +131 -0
  117. scaler/scheduler/scheduler.py +251 -0
  118. scaler/scheduler/task/__init__.py +0 -0
  119. scaler/scheduler/task/task_state_machine.py +92 -0
  120. scaler/scheduler/task/task_state_manager.py +61 -0
  121. scaler/ui/__init__.py +0 -0
  122. scaler/ui/constants.py +9 -0
  123. scaler/ui/live_display.py +147 -0
  124. scaler/ui/memory_window.py +146 -0
  125. scaler/ui/setting_page.py +40 -0
  126. scaler/ui/task_graph.py +832 -0
  127. scaler/ui/task_log.py +107 -0
  128. scaler/ui/utility.py +66 -0
  129. scaler/ui/webui.py +147 -0
  130. scaler/ui/worker_processors.py +104 -0
  131. scaler/utility/__init__.py +0 -0
  132. scaler/utility/debug.py +19 -0
  133. scaler/utility/event_list.py +63 -0
  134. scaler/utility/event_loop.py +58 -0
  135. scaler/utility/exceptions.py +42 -0
  136. scaler/utility/formatter.py +44 -0
  137. scaler/utility/graph/__init__.py +0 -0
  138. scaler/utility/graph/optimization.py +27 -0
  139. scaler/utility/graph/topological_sorter.py +11 -0
  140. scaler/utility/graph/topological_sorter_graphblas.py +174 -0
  141. scaler/utility/identifiers.py +107 -0
  142. scaler/utility/logging/__init__.py +0 -0
  143. scaler/utility/logging/decorators.py +25 -0
  144. scaler/utility/logging/scoped_logger.py +33 -0
  145. scaler/utility/logging/utility.py +183 -0
  146. scaler/utility/many_to_many_dict.py +123 -0
  147. scaler/utility/metadata/__init__.py +0 -0
  148. scaler/utility/metadata/profile_result.py +31 -0
  149. scaler/utility/metadata/task_flags.py +30 -0
  150. scaler/utility/mixins.py +13 -0
  151. scaler/utility/network_util.py +7 -0
  152. scaler/utility/one_to_many_dict.py +72 -0
  153. scaler/utility/queues/__init__.py +0 -0
  154. scaler/utility/queues/async_indexed_queue.py +37 -0
  155. scaler/utility/queues/async_priority_queue.py +70 -0
  156. scaler/utility/queues/async_sorted_priority_queue.py +45 -0
  157. scaler/utility/queues/indexed_queue.py +114 -0
  158. scaler/utility/serialization.py +9 -0
  159. scaler/version.txt +1 -0
  160. scaler/worker/__init__.py +0 -0
  161. scaler/worker/agent/__init__.py +0 -0
  162. scaler/worker/agent/heartbeat_manager.py +107 -0
  163. scaler/worker/agent/mixins.py +137 -0
  164. scaler/worker/agent/processor/__init__.py +0 -0
  165. scaler/worker/agent/processor/object_cache.py +107 -0
  166. scaler/worker/agent/processor/processor.py +285 -0
  167. scaler/worker/agent/processor/streaming_buffer.py +28 -0
  168. scaler/worker/agent/processor_holder.py +147 -0
  169. scaler/worker/agent/processor_manager.py +369 -0
  170. scaler/worker/agent/profiling_manager.py +109 -0
  171. scaler/worker/agent/task_manager.py +150 -0
  172. scaler/worker/agent/timeout_manager.py +19 -0
  173. scaler/worker/preload.py +84 -0
  174. scaler/worker/worker.py +265 -0
  175. scaler/worker_adapter/__init__.py +0 -0
  176. scaler/worker_adapter/common.py +26 -0
  177. scaler/worker_adapter/ecs.py +269 -0
  178. scaler/worker_adapter/native.py +155 -0
  179. scaler/worker_adapter/symphony/__init__.py +0 -0
  180. scaler/worker_adapter/symphony/callback.py +45 -0
  181. scaler/worker_adapter/symphony/heartbeat_manager.py +79 -0
  182. scaler/worker_adapter/symphony/message.py +24 -0
  183. scaler/worker_adapter/symphony/task_manager.py +289 -0
  184. scaler/worker_adapter/symphony/worker.py +204 -0
  185. scaler/worker_adapter/symphony/worker_adapter.py +139 -0
  186. src/scaler/io/ymq/_ymq.so +0 -0
  187. src/scaler/object_storage/object_storage_server.so +0 -0
@@ -0,0 +1,137 @@
1
+ import argparse
2
+
3
+ from aiohttp import web
4
+
5
+ from scaler.config.loader import load_config
6
+ from scaler.config.section.native_worker_adapter import NativeWorkerAdapterConfig
7
+ from scaler.utility.event_loop import EventLoopType, register_event_loop
8
+ from scaler.utility.logging.utility import setup_logger
9
+ from scaler.worker_adapter.native import NativeWorkerAdapter
10
+
11
+
12
+ def get_args():
13
+ parser = argparse.ArgumentParser(
14
+ "scaler_native_worker_adapter", formatter_class=argparse.ArgumentDefaultsHelpFormatter
15
+ )
16
+
17
+ parser.add_argument("--config", "-c", type=str, default=None, help="Path to the TOML configuration file.")
18
+
19
+ # Server configuration
20
+ parser.add_argument("--adapter-web-host", type=str, help="Host for the native worker adapter HTTP server.")
21
+ parser.add_argument("--adapter-web-port", "-p", type=int, help="Port for the native worker adapter HTTP server.")
22
+
23
+ # Worker configuration
24
+ parser.add_argument("--io-threads", type=int, help="number of io threads for zmq")
25
+ parser.add_argument(
26
+ "--per-worker-capabilities",
27
+ "-pwc",
28
+ type=str,
29
+ help='comma-separated capabilities provided by the workers (e.g. "-pwc linux,cpu=4")',
30
+ )
31
+ parser.add_argument("--worker-task-queue-size", "-wtqs", type=int, default=10, help="specify worker queue size")
32
+ parser.add_argument(
33
+ "--max-workers", "-mw", type=int, help="maximum number of workers that can be started, -1 means no limit"
34
+ )
35
+ parser.add_argument(
36
+ "--heartbeat-interval", "-hi", type=int, help="number of seconds that worker agent send heartbeat to scheduler"
37
+ )
38
+ parser.add_argument(
39
+ "--task-timeout-seconds", "-tt", type=int, help="default task timeout seconds, 0 means never timeout"
40
+ )
41
+ parser.add_argument(
42
+ "--death-timeout-seconds",
43
+ "-dt",
44
+ type=int,
45
+ help="number of seconds without scheduler contact before worker shuts down",
46
+ )
47
+ parser.add_argument(
48
+ "--garbage-collect-interval-seconds", "-gc", type=int, help="number of seconds worker doing garbage collection"
49
+ )
50
+ parser.add_argument(
51
+ "--trim-memory-threshold-bytes",
52
+ "-tm",
53
+ type=int,
54
+ help="number of bytes threshold for worker process that trigger deep garbage collection",
55
+ )
56
+ parser.add_argument(
57
+ "--hard-processor-suspend",
58
+ "-hps",
59
+ action="store_true",
60
+ help="if true, suspended worker's processors will be actively suspended with a SIGTSTP signal",
61
+ )
62
+ parser.add_argument("--event-loop", "-e", choices=EventLoopType.allowed_types(), help="select event loop type")
63
+ parser.add_argument(
64
+ "--logging-paths",
65
+ "-lp",
66
+ nargs="*",
67
+ type=str,
68
+ help="specify where worker logs should be logged to, it can accept multiple files, default is /dev/stdout",
69
+ )
70
+ parser.add_argument(
71
+ "--logging-level",
72
+ "-ll",
73
+ type=str,
74
+ choices=("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"),
75
+ help="specify the logging level",
76
+ )
77
+ parser.add_argument(
78
+ "--logging-config-file",
79
+ "-lc",
80
+ type=str,
81
+ help="use standard python .conf file to specify python logging file configuration format",
82
+ )
83
+ parser.add_argument(
84
+ "--object-storage-address",
85
+ "-osa",
86
+ type=str,
87
+ help="specify the object storage server address, e.g.: tcp://localhost:2346",
88
+ )
89
+ parser.add_argument(
90
+ "scheduler_address",
91
+ nargs="?",
92
+ type=str,
93
+ help="scheduler address to connect workers to, e.g.: `tcp://localhost:6378",
94
+ )
95
+
96
+ return parser.parse_args()
97
+
98
+
99
+ def main():
100
+ args = get_args()
101
+ native_adapter_config = load_config(
102
+ NativeWorkerAdapterConfig, args.config, args, section_name="native_worker_adapter"
103
+ )
104
+
105
+ register_event_loop(native_adapter_config.event_loop)
106
+
107
+ setup_logger(
108
+ native_adapter_config.logging_paths,
109
+ native_adapter_config.logging_config_file,
110
+ native_adapter_config.logging_level,
111
+ )
112
+
113
+ native_worker_adapter = NativeWorkerAdapter(
114
+ address=native_adapter_config.scheduler_address,
115
+ object_storage_address=native_adapter_config.object_storage_address,
116
+ capabilities=native_adapter_config.per_worker_capabilities.capabilities,
117
+ io_threads=native_adapter_config.io_threads,
118
+ task_queue_size=native_adapter_config.worker_task_queue_size,
119
+ max_workers=native_adapter_config.max_workers,
120
+ heartbeat_interval_seconds=native_adapter_config.heartbeat_interval_seconds,
121
+ task_timeout_seconds=native_adapter_config.task_timeout_seconds,
122
+ death_timeout_seconds=native_adapter_config.death_timeout_seconds,
123
+ garbage_collect_interval_seconds=native_adapter_config.garbage_collect_interval_seconds,
124
+ trim_memory_threshold_bytes=native_adapter_config.trim_memory_threshold_bytes,
125
+ hard_processor_suspend=native_adapter_config.hard_processor_suspend,
126
+ event_loop=native_adapter_config.event_loop,
127
+ logging_paths=native_adapter_config.logging_paths,
128
+ logging_level=native_adapter_config.logging_level,
129
+ logging_config_file=native_adapter_config.logging_config_file,
130
+ )
131
+
132
+ app = native_worker_adapter.create_app()
133
+ web.run_app(app, host=native_adapter_config.adapter_web_host, port=native_adapter_config.adapter_web_port)
134
+
135
+
136
+ if __name__ == "__main__":
137
+ main()
@@ -0,0 +1,98 @@
1
+ import argparse
2
+
3
+ from aiohttp import web
4
+
5
+ from scaler.config.loader import load_config
6
+ from scaler.config.section.symphony_worker_adapter import SymphonyWorkerConfig
7
+ from scaler.utility.event_loop import EventLoopType, register_event_loop
8
+ from scaler.utility.logging.utility import setup_logger
9
+ from scaler.worker_adapter.symphony.worker_adapter import SymphonyWorkerAdapter
10
+
11
+
12
+ def get_args():
13
+ parser = argparse.ArgumentParser(
14
+ "scaler Symphony worker adapter", formatter_class=argparse.ArgumentDefaultsHelpFormatter
15
+ )
16
+ parser.add_argument("--config", "-c", type=str, default=None, help="Path to the TOML configuration file.")
17
+
18
+ # Server configuration
19
+ parser.add_argument("--adapter-web-host", type=str, help="host address for symphony worker adapter HTTP server")
20
+ parser.add_argument("--adapter-web-port", "-p", type=int, help="port for symphony worker adapter HTTP server")
21
+
22
+ # Symphony configuration
23
+ parser.add_argument("--service-name", "-sn", type=str, help="symphony service name")
24
+ parser.add_argument("--base-concurrency", "-n", type=int, help="base task concurrency")
25
+
26
+ # Worker configuration
27
+ parser.add_argument("--io-threads", "-it", help="specify number of io threads per worker")
28
+ parser.add_argument(
29
+ "--worker-capabilities",
30
+ "-wc",
31
+ type=str,
32
+ help='comma-separated capabilities provided by the worker (e.g. "-wr linux,cpu=4")',
33
+ )
34
+ parser.add_argument("--worker-task-queue-size", "-wtqs", type=int, help="specify symphony worker queue size")
35
+ parser.add_argument("--heartbeat-interval", "-hi", type=int, help="number of seconds to send heartbeat interval")
36
+ parser.add_argument("--death-timeout-seconds", "-ds", type=int, help="death timeout seconds")
37
+ parser.add_argument("--event-loop", "-el", choices=EventLoopType.allowed_types(), help="select event loop type")
38
+ parser.add_argument(
39
+ "--logging-paths",
40
+ "-lp",
41
+ nargs="*",
42
+ type=str,
43
+ help='specify where cluster log should logged to, it can be multiple paths, "/dev/stdout" is default for '
44
+ "standard output, each worker will have its own log file with process id appended to the path",
45
+ )
46
+ parser.add_argument(
47
+ "--logging-level",
48
+ "-ll",
49
+ type=str,
50
+ choices=("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"),
51
+ help="specify the logging level",
52
+ )
53
+ parser.add_argument(
54
+ "--logging-config-file",
55
+ type=str,
56
+ default=None,
57
+ help="use standard python the .conf file the specify python logging file configuration format, this will "
58
+ "bypass --logging-paths and --logging-level at the same time, and this will not work on per worker logging",
59
+ )
60
+ parser.add_argument(
61
+ "--object-storage-address",
62
+ "-osa",
63
+ default=None,
64
+ help="specify the object storage server address, e.g.: tcp://localhost:2346",
65
+ )
66
+ parser.add_argument("scheduler_address", nargs="?", type=str, help="scheduler address to connect to")
67
+ return parser.parse_args()
68
+
69
+
70
+ def main():
71
+ args = get_args()
72
+ symphony_config = load_config(SymphonyWorkerConfig, args.config, args, section_name="symphony_worker_adapter")
73
+ register_event_loop(symphony_config.event_loop)
74
+
75
+ setup_logger(symphony_config.logging_paths, symphony_config.logging_config_file, symphony_config.logging_level)
76
+
77
+ symphony_worker_adapter = SymphonyWorkerAdapter(
78
+ address=symphony_config.scheduler_address,
79
+ object_storage_address=symphony_config.object_storage_address,
80
+ capabilities=symphony_config.worker_capabilities.capabilities,
81
+ task_queue_size=symphony_config.worker_task_queue_size,
82
+ service_name=symphony_config.service_name,
83
+ base_concurrency=symphony_config.base_concurrency,
84
+ heartbeat_interval_seconds=symphony_config.heartbeat_interval,
85
+ death_timeout_seconds=symphony_config.death_timeout_seconds,
86
+ event_loop=symphony_config.event_loop,
87
+ io_threads=symphony_config.io_threads,
88
+ logging_paths=symphony_config.logging_paths,
89
+ logging_level=symphony_config.logging_level,
90
+ logging_config_file=symphony_config.logging_config_file,
91
+ )
92
+
93
+ app = symphony_worker_adapter.create_app()
94
+ web.run_app(app, host=symphony_config.adapter_web_host, port=symphony_config.adapter_web_port)
95
+
96
+
97
+ if __name__ == "__main__":
98
+ main()
scaler/io/__init__.py ADDED
File without changes
@@ -0,0 +1,89 @@
1
+ import logging
2
+ import os
3
+ import uuid
4
+ from collections import defaultdict
5
+ from typing import Awaitable, Callable, Dict, List, Optional
6
+
7
+ import zmq.asyncio
8
+ from zmq import Frame
9
+
10
+ from scaler.config.types.zmq import ZMQConfig
11
+ from scaler.io.mixins import AsyncBinder
12
+ from scaler.io.utility import deserialize, serialize
13
+ from scaler.protocol.python.mixins import Message
14
+ from scaler.protocol.python.status import BinderStatus
15
+
16
+
17
+ class ZMQAsyncBinder(AsyncBinder):
18
+ def __init__(self, context: zmq.asyncio.Context, name: str, address: ZMQConfig, identity: Optional[bytes] = None):
19
+ self._address = address
20
+
21
+ if identity is None:
22
+ identity = f"{os.getpid()}|{name}|{uuid.uuid4()}".encode()
23
+ self._identity = identity
24
+
25
+ self._context = context
26
+ self._socket = self._context.socket(zmq.ROUTER)
27
+ self.__set_socket_options()
28
+ self._socket.bind(self._address.to_address())
29
+
30
+ self._callback: Optional[Callable[[bytes, Message], Awaitable[None]]] = None
31
+
32
+ self._received: Dict[str, int] = defaultdict(lambda: 0)
33
+ self._sent: Dict[str, int] = defaultdict(lambda: 0)
34
+
35
+ @property
36
+ def identity(self):
37
+ return self._identity
38
+
39
+ def destroy(self):
40
+ self._context.destroy(linger=0)
41
+
42
+ def register(self, callback: Callable[[bytes, Message], Awaitable[None]]):
43
+ self._callback = callback
44
+
45
+ async def routine(self):
46
+ frames: List[Frame] = await self._socket.recv_multipart(copy=False)
47
+ if not self.__is_valid_message(frames):
48
+ return
49
+
50
+ source, payload = frames
51
+ try:
52
+ message: Optional[Message] = deserialize(payload.bytes)
53
+ if message is None:
54
+ logging.error(f"received unknown message from {source.bytes!r}: {payload!r}")
55
+ return
56
+ except Exception as e:
57
+ logging.error(f"{self.__get_prefix()} failed to deserialize message from {source.bytes!r}: {e}")
58
+ return
59
+
60
+ self.__count_received(message.__class__.__name__)
61
+ await self._callback(source.bytes, message)
62
+
63
+ async def send(self, to: bytes, message: Message):
64
+ self.__count_sent(message.__class__.__name__)
65
+ await self._socket.send_multipart([to, serialize(message)], copy=False)
66
+
67
+ def get_status(self) -> BinderStatus:
68
+ return BinderStatus.new_msg(received=self._received, sent=self._sent)
69
+
70
+ def __set_socket_options(self):
71
+ self._socket.setsockopt(zmq.IDENTITY, self._identity)
72
+ self._socket.setsockopt(zmq.SNDHWM, 0)
73
+ self._socket.setsockopt(zmq.RCVHWM, 0)
74
+
75
+ def __is_valid_message(self, frames: List[Frame]) -> bool:
76
+ if len(frames) != 2:
77
+ logging.error(f"{self.__get_prefix()} received unexpected frames {frames}")
78
+ return False
79
+
80
+ return True
81
+
82
+ def __count_received(self, message_type: str):
83
+ self._received[message_type] += 1
84
+
85
+ def __count_sent(self, message_type: str):
86
+ self._sent[message_type] += 1
87
+
88
+ def __get_prefix(self):
89
+ return f"{self.__class__.__name__}[{self._identity.decode()}]:"
@@ -0,0 +1,95 @@
1
+ import logging
2
+ import os
3
+ import uuid
4
+ from typing import Awaitable, Callable, Literal, Optional
5
+
6
+ import zmq.asyncio
7
+
8
+ from scaler.config.types.zmq import ZMQConfig
9
+ from scaler.io.mixins import AsyncConnector
10
+ from scaler.io.utility import deserialize, serialize
11
+ from scaler.protocol.python.mixins import Message
12
+
13
+
14
+ class ZMQAsyncConnector(AsyncConnector):
15
+ def __init__(
16
+ self,
17
+ context: zmq.asyncio.Context,
18
+ name: str,
19
+ socket_type: int,
20
+ address: ZMQConfig,
21
+ bind_or_connect: Literal["bind", "connect"],
22
+ callback: Optional[Callable[[Message], Awaitable[None]]],
23
+ identity: Optional[bytes],
24
+ ):
25
+ self._address = address
26
+
27
+ self._context = context
28
+ self._socket = self._context.socket(socket_type)
29
+
30
+ if identity is None:
31
+ identity = f"{os.getpid()}|{name}|{uuid.uuid4().bytes.hex()}".encode()
32
+ self._identity = identity
33
+
34
+ # set socket option
35
+ self._socket.setsockopt(zmq.IDENTITY, self._identity)
36
+ self._socket.setsockopt(zmq.SNDHWM, 0)
37
+ self._socket.setsockopt(zmq.RCVHWM, 0)
38
+
39
+ if bind_or_connect == "bind":
40
+ self._socket.bind(self.address)
41
+ elif bind_or_connect == "connect":
42
+ self._socket.connect(self.address)
43
+ else:
44
+ raise TypeError("bind_or_connect has to be 'bind' or 'connect'")
45
+
46
+ self._callback: Optional[Callable[[Message], Awaitable[None]]] = callback
47
+
48
+ def __del__(self):
49
+ self.destroy()
50
+
51
+ def destroy(self):
52
+ if self._socket.closed:
53
+ return
54
+
55
+ self._socket.close(linger=1)
56
+
57
+ @property
58
+ def identity(self) -> bytes:
59
+ return self._identity
60
+
61
+ @property
62
+ def socket(self) -> zmq.asyncio.Socket:
63
+ return self._socket
64
+
65
+ @property
66
+ def address(self) -> str:
67
+ return self._address.to_address()
68
+
69
+ async def routine(self):
70
+ if self._callback is None:
71
+ return
72
+
73
+ message: Optional[Message] = await self.receive()
74
+ if message is None:
75
+ return
76
+
77
+ await self._callback(message)
78
+
79
+ async def receive(self) -> Optional[Message]:
80
+ if self._context.closed:
81
+ return None
82
+
83
+ if self._socket.closed:
84
+ return None
85
+
86
+ payload = await self._socket.recv(copy=False)
87
+ result: Optional[Message] = deserialize(payload.bytes)
88
+ if result is None:
89
+ logging.error(f"received unknown message: {payload.bytes!r}")
90
+ return None
91
+
92
+ return result
93
+
94
+ async def send(self, message: Message):
95
+ await self._socket.send(serialize(message), copy=False)
@@ -0,0 +1,225 @@
1
+ import asyncio
2
+ import logging
3
+ import socket
4
+ import struct
5
+ import uuid
6
+ from typing import Dict, Optional, Tuple
7
+
8
+ from scaler.io.mixins import AsyncObjectStorageConnector
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 PyAsyncObjectStorageConnector(AsyncObjectStorageConnector):
16
+ """An asyncio connector that uses an raw TCP socket 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._reader: Optional[asyncio.StreamReader] = None
25
+ self._writer: Optional[asyncio.StreamWriter] = None
26
+
27
+ self._next_request_id = 0
28
+ self._pending_get_requests: Dict[ObjectID, asyncio.Future] = {}
29
+
30
+ self._identity: bytes = (
31
+ f"{self.__class__.__name__}|{socket.gethostname().split('.')[0]}|{uuid.uuid4()}".encode()
32
+ )
33
+
34
+ def __del__(self):
35
+ if not self.is_connected():
36
+ return
37
+
38
+ self._writer.close()
39
+
40
+ async def connect(self, host: str, port: int):
41
+ self._host = host
42
+ self._port = port
43
+
44
+ if self.is_connected():
45
+ raise ObjectStorageException("connector is already connected.")
46
+
47
+ self._reader, self._writer = await asyncio.open_connection(self._host, self._port)
48
+ await self.__read_framed_message()
49
+ self.__write_framed(self._identity)
50
+
51
+ try:
52
+ await self._writer.drain()
53
+ except ConnectionResetError:
54
+ self.__raise_connection_failure()
55
+
56
+ # Makes sure the socket is TCP_NODELAY. It seems to be the case by default, but that's not specified in the
57
+ # asyncio's documentation and might change in the future.
58
+ self._writer.get_extra_info("socket").setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
59
+
60
+ self._connected_event.set()
61
+
62
+ async def wait_until_connected(self):
63
+ await self._connected_event.wait()
64
+
65
+ def is_connected(self) -> bool:
66
+ return self._connected_event.is_set()
67
+
68
+ async def destroy(self):
69
+ if not self.is_connected():
70
+ return
71
+
72
+ if not self._writer.is_closing:
73
+ self._writer.close()
74
+
75
+ await self._writer.wait_closed()
76
+
77
+ @property
78
+ def reader(self) -> Optional[asyncio.StreamReader]:
79
+ return self._reader
80
+
81
+ @property
82
+ def writer(self) -> Optional[asyncio.StreamWriter]:
83
+ return self._writer
84
+
85
+ @property
86
+ def address(self) -> str:
87
+ self.__ensure_is_connected()
88
+ return f"tcp://{self._host}:{self._port}"
89
+
90
+ async def routine(self):
91
+ await self.wait_until_connected()
92
+
93
+ response = await self.__receive_response()
94
+ if response is None:
95
+ return
96
+
97
+ header, payload = response
98
+
99
+ if header.response_type != ObjectResponseHeader.ObjectResponseType.GetOK:
100
+ return
101
+
102
+ pending_get_future = self._pending_get_requests.pop(header.object_id, None)
103
+
104
+ if pending_get_future is None:
105
+ logging.warning(f"unknown get-ok response for unrequested object_id={repr(header.object_id)}.")
106
+ return
107
+
108
+ pending_get_future.set_result(payload)
109
+
110
+ async def set_object(self, object_id: ObjectID, payload: bytes) -> None:
111
+ await self.__send_request(object_id, len(payload), ObjectRequestHeader.ObjectRequestType.SetObject, payload)
112
+
113
+ async def get_object(self, object_id: ObjectID, max_payload_length: int = 2**64 - 1) -> bytes:
114
+ pending_get_future = self._pending_get_requests.get(object_id)
115
+
116
+ if pending_get_future is None:
117
+ pending_get_future = asyncio.Future()
118
+ self._pending_get_requests[object_id] = pending_get_future
119
+
120
+ await self.__send_request(
121
+ object_id, max_payload_length, ObjectRequestHeader.ObjectRequestType.GetObject, None
122
+ )
123
+
124
+ return await pending_get_future
125
+
126
+ async def delete_object(self, object_id: ObjectID) -> None:
127
+ await self.__send_request(object_id, 0, ObjectRequestHeader.ObjectRequestType.DeleteObject, None)
128
+
129
+ async def duplicate_object_id(self, object_id: ObjectID, new_object_id: ObjectID) -> None:
130
+ object_id_payload = to_capnp_object_id(object_id).to_bytes()
131
+
132
+ await self.__send_request(
133
+ new_object_id,
134
+ len(object_id_payload),
135
+ ObjectRequestHeader.ObjectRequestType.DuplicateObjectID,
136
+ object_id_payload,
137
+ )
138
+
139
+ def __ensure_is_connected(self):
140
+ if self._writer is None:
141
+ raise ObjectStorageException("connector is not connected.")
142
+
143
+ if self._writer.is_closing():
144
+ raise ObjectStorageException("connection is closed.")
145
+
146
+ async def __send_request(
147
+ self,
148
+ object_id: ObjectID,
149
+ payload_length: int,
150
+ request_type: ObjectRequestHeader.ObjectRequestType,
151
+ payload: Optional[bytes],
152
+ ):
153
+ self.__ensure_is_connected()
154
+ assert self._writer is not None
155
+
156
+ request_id = self._next_request_id
157
+ self._next_request_id += 1
158
+ self._next_request_id %= 2**64 - 1 # UINT64_MAX
159
+
160
+ header = ObjectRequestHeader.new_msg(object_id, payload_length, request_id, request_type)
161
+
162
+ self.__write_request_header(header)
163
+
164
+ if payload is not None:
165
+ self.__write_request_payload(payload)
166
+
167
+ try:
168
+ await self._writer.drain()
169
+ except ConnectionResetError:
170
+ self.__raise_connection_failure()
171
+
172
+ def __write_request_header(self, header: ObjectRequestHeader):
173
+ assert self._writer is not None
174
+ self.__write_framed(header.get_message().to_bytes())
175
+
176
+ def __write_request_payload(self, payload: bytes):
177
+ assert self._writer is not None
178
+ self.__write_framed(payload)
179
+
180
+ async def __receive_response(self) -> Optional[Tuple[ObjectResponseHeader, bytes]]:
181
+ assert self._reader is not None
182
+
183
+ if self._writer.is_closing():
184
+ return None
185
+
186
+ try:
187
+ header = await self.__read_response_header()
188
+ payload = await self.__read_response_payload(header)
189
+ except asyncio.IncompleteReadError:
190
+ self.__raise_connection_failure()
191
+
192
+ return header, payload
193
+
194
+ async def __read_response_header(self) -> ObjectResponseHeader:
195
+ assert self._reader is not None
196
+
197
+ header_data = await self.__read_framed_message()
198
+ assert len(header_data) == ObjectResponseHeader.MESSAGE_LENGTH
199
+
200
+ with _object_storage.ObjectResponseHeader.from_bytes(header_data) as header_message:
201
+ return ObjectResponseHeader(header_message)
202
+
203
+ async def __read_response_payload(self, header: ObjectResponseHeader) -> bytes:
204
+ assert self._reader is not None
205
+
206
+ if header.payload_length > 0:
207
+ res = await self.__read_framed_message()
208
+ assert len(res) == header.payload_length
209
+ return res
210
+ else:
211
+ return b""
212
+
213
+ async def __read_framed_message(self) -> bytes:
214
+ length_bytes = await self._reader.readexactly(8)
215
+ (payload_length,) = struct.unpack("<Q", length_bytes)
216
+ return await self._reader.readexactly(payload_length) if payload_length > 0 else bytes()
217
+
218
+ def __write_framed(self, payload: bytes):
219
+ self._writer.write(struct.pack("<Q", len(payload)))
220
+ self._writer.write(payload)
221
+ return
222
+
223
+ @staticmethod
224
+ def __raise_connection_failure():
225
+ raise ObjectStorageException("connection failure to object storage server.")