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.
- opengris_scaler-1.12.37.dist-info/METADATA +730 -0
- opengris_scaler-1.12.37.dist-info/RECORD +196 -0
- opengris_scaler-1.12.37.dist-info/WHEEL +5 -0
- opengris_scaler-1.12.37.dist-info/entry_points.txt +10 -0
- opengris_scaler-1.12.37.dist-info/licenses/LICENSE +201 -0
- opengris_scaler-1.12.37.dist-info/licenses/LICENSE.spdx +7 -0
- opengris_scaler-1.12.37.dist-info/licenses/NOTICE +8 -0
- opengris_scaler.libs/libcapnp-1-e88d5415.0.1.so +0 -0
- opengris_scaler.libs/libgcc_s-2298274a.so.1 +0 -0
- opengris_scaler.libs/libkj-1-9bebd8ac.0.1.so +0 -0
- opengris_scaler.libs/libstdc++-08d5c7eb.so.6.0.33 +0 -0
- scaler/__init__.py +14 -0
- scaler/about.py +5 -0
- scaler/client/__init__.py +0 -0
- scaler/client/agent/__init__.py +0 -0
- scaler/client/agent/client_agent.py +218 -0
- scaler/client/agent/disconnect_manager.py +27 -0
- scaler/client/agent/future_manager.py +112 -0
- scaler/client/agent/heartbeat_manager.py +74 -0
- scaler/client/agent/mixins.py +89 -0
- scaler/client/agent/object_manager.py +98 -0
- scaler/client/agent/task_manager.py +64 -0
- scaler/client/client.py +672 -0
- scaler/client/future.py +252 -0
- scaler/client/object_buffer.py +129 -0
- scaler/client/object_reference.py +25 -0
- scaler/client/serializer/__init__.py +0 -0
- scaler/client/serializer/default.py +16 -0
- scaler/client/serializer/mixins.py +38 -0
- scaler/cluster/__init__.py +0 -0
- scaler/cluster/cluster.py +95 -0
- scaler/cluster/combo.py +157 -0
- scaler/cluster/object_storage_server.py +45 -0
- scaler/cluster/scheduler.py +86 -0
- scaler/config/__init__.py +0 -0
- scaler/config/common/__init__.py +0 -0
- scaler/config/common/logging.py +41 -0
- scaler/config/common/web.py +18 -0
- scaler/config/common/worker.py +65 -0
- scaler/config/common/worker_adapter.py +28 -0
- scaler/config/config_class.py +317 -0
- scaler/config/defaults.py +94 -0
- scaler/config/mixins.py +20 -0
- scaler/config/section/__init__.py +0 -0
- scaler/config/section/cluster.py +66 -0
- scaler/config/section/ecs_worker_adapter.py +78 -0
- scaler/config/section/native_worker_adapter.py +30 -0
- scaler/config/section/object_storage_server.py +13 -0
- scaler/config/section/scheduler.py +126 -0
- scaler/config/section/symphony_worker_adapter.py +35 -0
- scaler/config/section/top.py +16 -0
- scaler/config/section/webui.py +16 -0
- scaler/config/types/__init__.py +0 -0
- scaler/config/types/network_backend.py +12 -0
- scaler/config/types/object_storage_server.py +45 -0
- scaler/config/types/worker.py +67 -0
- scaler/config/types/zmq.py +83 -0
- scaler/entry_points/__init__.py +0 -0
- scaler/entry_points/cluster.py +10 -0
- scaler/entry_points/object_storage_server.py +26 -0
- scaler/entry_points/scheduler.py +51 -0
- scaler/entry_points/top.py +272 -0
- scaler/entry_points/webui.py +6 -0
- scaler/entry_points/worker_adapter_ecs.py +22 -0
- scaler/entry_points/worker_adapter_native.py +31 -0
- scaler/entry_points/worker_adapter_symphony.py +26 -0
- scaler/io/__init__.py +0 -0
- scaler/io/async_binder.py +89 -0
- scaler/io/async_connector.py +95 -0
- scaler/io/async_object_storage_connector.py +225 -0
- scaler/io/mixins.py +154 -0
- scaler/io/sync_connector.py +68 -0
- scaler/io/sync_object_storage_connector.py +249 -0
- scaler/io/sync_subscriber.py +83 -0
- scaler/io/utility.py +80 -0
- scaler/io/ymq/__init__.py +0 -0
- scaler/io/ymq/_ymq.pyi +95 -0
- scaler/io/ymq/_ymq.so +0 -0
- scaler/io/ymq/ymq.py +138 -0
- scaler/io/ymq_async_object_storage_connector.py +184 -0
- scaler/io/ymq_sync_object_storage_connector.py +184 -0
- scaler/object_storage/__init__.py +0 -0
- scaler/object_storage/object_storage_server.so +0 -0
- scaler/protocol/__init__.py +0 -0
- scaler/protocol/capnp/__init__.py +0 -0
- scaler/protocol/capnp/_python.py +6 -0
- scaler/protocol/capnp/common.capnp +68 -0
- scaler/protocol/capnp/message.capnp +218 -0
- scaler/protocol/capnp/object_storage.capnp +57 -0
- scaler/protocol/capnp/status.capnp +73 -0
- scaler/protocol/introduction.md +105 -0
- scaler/protocol/python/__init__.py +0 -0
- scaler/protocol/python/common.py +140 -0
- scaler/protocol/python/message.py +751 -0
- scaler/protocol/python/mixins.py +13 -0
- scaler/protocol/python/object_storage.py +118 -0
- scaler/protocol/python/status.py +279 -0
- scaler/protocol/worker.md +228 -0
- scaler/scheduler/__init__.py +0 -0
- scaler/scheduler/allocate_policy/__init__.py +0 -0
- scaler/scheduler/allocate_policy/allocate_policy.py +9 -0
- scaler/scheduler/allocate_policy/capability_allocate_policy.py +280 -0
- scaler/scheduler/allocate_policy/even_load_allocate_policy.py +159 -0
- scaler/scheduler/allocate_policy/mixins.py +55 -0
- scaler/scheduler/controllers/__init__.py +0 -0
- scaler/scheduler/controllers/balance_controller.py +65 -0
- scaler/scheduler/controllers/client_controller.py +131 -0
- scaler/scheduler/controllers/config_controller.py +31 -0
- scaler/scheduler/controllers/graph_controller.py +424 -0
- scaler/scheduler/controllers/information_controller.py +81 -0
- scaler/scheduler/controllers/mixins.py +194 -0
- scaler/scheduler/controllers/object_controller.py +147 -0
- scaler/scheduler/controllers/scaling_policies/__init__.py +0 -0
- scaler/scheduler/controllers/scaling_policies/fixed_elastic.py +145 -0
- scaler/scheduler/controllers/scaling_policies/mixins.py +10 -0
- scaler/scheduler/controllers/scaling_policies/null.py +14 -0
- scaler/scheduler/controllers/scaling_policies/types.py +9 -0
- scaler/scheduler/controllers/scaling_policies/utility.py +20 -0
- scaler/scheduler/controllers/scaling_policies/vanilla.py +95 -0
- scaler/scheduler/controllers/task_controller.py +376 -0
- scaler/scheduler/controllers/worker_controller.py +169 -0
- scaler/scheduler/object_usage/__init__.py +0 -0
- scaler/scheduler/object_usage/object_tracker.py +131 -0
- scaler/scheduler/scheduler.py +251 -0
- scaler/scheduler/task/__init__.py +0 -0
- scaler/scheduler/task/task_state_machine.py +92 -0
- scaler/scheduler/task/task_state_manager.py +61 -0
- scaler/ui/__init__.py +0 -0
- scaler/ui/common/__init__.py +0 -0
- scaler/ui/common/constants.py +9 -0
- scaler/ui/common/live_display.py +147 -0
- scaler/ui/common/memory_window.py +146 -0
- scaler/ui/common/setting_page.py +40 -0
- scaler/ui/common/task_graph.py +840 -0
- scaler/ui/common/task_log.py +111 -0
- scaler/ui/common/utility.py +66 -0
- scaler/ui/common/webui.py +80 -0
- scaler/ui/common/worker_processors.py +104 -0
- scaler/ui/v1.py +76 -0
- scaler/ui/v2.py +102 -0
- scaler/ui/webui.py +21 -0
- scaler/utility/__init__.py +0 -0
- scaler/utility/debug.py +19 -0
- scaler/utility/event_list.py +63 -0
- scaler/utility/event_loop.py +58 -0
- scaler/utility/exceptions.py +42 -0
- scaler/utility/formatter.py +44 -0
- scaler/utility/graph/__init__.py +0 -0
- scaler/utility/graph/optimization.py +27 -0
- scaler/utility/graph/topological_sorter.py +11 -0
- scaler/utility/graph/topological_sorter_graphblas.py +174 -0
- scaler/utility/identifiers.py +107 -0
- scaler/utility/logging/__init__.py +0 -0
- scaler/utility/logging/decorators.py +25 -0
- scaler/utility/logging/scoped_logger.py +33 -0
- scaler/utility/logging/utility.py +183 -0
- scaler/utility/many_to_many_dict.py +123 -0
- scaler/utility/metadata/__init__.py +0 -0
- scaler/utility/metadata/profile_result.py +31 -0
- scaler/utility/metadata/task_flags.py +30 -0
- scaler/utility/mixins.py +13 -0
- scaler/utility/network_util.py +7 -0
- scaler/utility/one_to_many_dict.py +72 -0
- scaler/utility/queues/__init__.py +0 -0
- scaler/utility/queues/async_indexed_queue.py +37 -0
- scaler/utility/queues/async_priority_queue.py +70 -0
- scaler/utility/queues/async_sorted_priority_queue.py +45 -0
- scaler/utility/queues/indexed_queue.py +114 -0
- scaler/utility/serialization.py +9 -0
- scaler/version.txt +1 -0
- scaler/worker/__init__.py +0 -0
- scaler/worker/agent/__init__.py +0 -0
- scaler/worker/agent/heartbeat_manager.py +110 -0
- scaler/worker/agent/mixins.py +137 -0
- scaler/worker/agent/processor/__init__.py +0 -0
- scaler/worker/agent/processor/object_cache.py +107 -0
- scaler/worker/agent/processor/processor.py +285 -0
- scaler/worker/agent/processor/streaming_buffer.py +28 -0
- scaler/worker/agent/processor_holder.py +147 -0
- scaler/worker/agent/processor_manager.py +369 -0
- scaler/worker/agent/profiling_manager.py +109 -0
- scaler/worker/agent/task_manager.py +150 -0
- scaler/worker/agent/timeout_manager.py +19 -0
- scaler/worker/preload.py +84 -0
- scaler/worker/worker.py +265 -0
- scaler/worker_adapter/__init__.py +0 -0
- scaler/worker_adapter/common.py +26 -0
- scaler/worker_adapter/ecs.py +241 -0
- scaler/worker_adapter/native.py +138 -0
- scaler/worker_adapter/symphony/__init__.py +0 -0
- scaler/worker_adapter/symphony/callback.py +45 -0
- scaler/worker_adapter/symphony/heartbeat_manager.py +82 -0
- scaler/worker_adapter/symphony/message.py +24 -0
- scaler/worker_adapter/symphony/task_manager.py +289 -0
- scaler/worker_adapter/symphony/worker.py +204 -0
- scaler/worker_adapter/symphony/worker_adapter.py +123 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import enum
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Awaitable, Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EventLoopType(enum.Enum):
|
|
8
|
+
builtin = enum.auto()
|
|
9
|
+
uvloop = enum.auto()
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def allowed_types():
|
|
13
|
+
return {m.name for m in EventLoopType}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register_event_loop(event_loop_type: str):
|
|
17
|
+
if event_loop_type not in EventLoopType.allowed_types():
|
|
18
|
+
raise TypeError(f"allowed event loop types are: {EventLoopType.allowed_types()}")
|
|
19
|
+
|
|
20
|
+
event_loop_type_enum = EventLoopType[event_loop_type]
|
|
21
|
+
if event_loop_type_enum == EventLoopType.uvloop:
|
|
22
|
+
try:
|
|
23
|
+
import uvloop # noqa
|
|
24
|
+
except ImportError:
|
|
25
|
+
raise ImportError("please use pip install uvloop if try to use uvloop as event loop")
|
|
26
|
+
|
|
27
|
+
uvloop.install()
|
|
28
|
+
|
|
29
|
+
assert event_loop_type in EventLoopType.allowed_types()
|
|
30
|
+
|
|
31
|
+
logging.info(f"use event loop: {event_loop_type}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_async_loop_routine(routine: Callable[[], Awaitable], seconds: int):
|
|
35
|
+
"""create async loop routine,
|
|
36
|
+
|
|
37
|
+
- if seconds is negative, means disable
|
|
38
|
+
- 0 means looping without any wait, as fast as possible
|
|
39
|
+
- positive number means execute routine every positive seconds, if passing 1 means run once every 1 seconds"""
|
|
40
|
+
|
|
41
|
+
async def loop():
|
|
42
|
+
if seconds < 0:
|
|
43
|
+
logging.info(f"{routine.__self__.__class__.__name__}: disabled") # type: ignore[attr-defined]
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
logging.info(f"{routine.__self__.__class__.__name__}: started") # type: ignore[attr-defined]
|
|
47
|
+
try:
|
|
48
|
+
while True:
|
|
49
|
+
await routine()
|
|
50
|
+
await asyncio.sleep(seconds)
|
|
51
|
+
except asyncio.CancelledError:
|
|
52
|
+
pass
|
|
53
|
+
except KeyboardInterrupt:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
logging.info(f"{routine.__self__.__class__.__name__}: exited") # type: ignore[attr-defined]
|
|
57
|
+
|
|
58
|
+
return loop()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
class TaskNotFoundError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class WorkerDiedError(Exception):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NoWorkerError(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DisconnectedError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ProcessorDiedError(Exception):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DeserializeObjectError(Exception):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MissingObjects(Exception):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ClientCancelledException(Exception):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ClientShutdownException(Exception):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ClientQuitException(Exception):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ObjectStorageException(Exception):
|
|
42
|
+
pass
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
STORAGE_SIZE_MODULUS = 1024.0
|
|
2
|
+
TIME_MODULUS = 1000
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def format_bytes(number) -> str:
|
|
6
|
+
for unit in ["B", "K", "M", "G", "T"]:
|
|
7
|
+
if number >= STORAGE_SIZE_MODULUS:
|
|
8
|
+
number /= STORAGE_SIZE_MODULUS
|
|
9
|
+
continue
|
|
10
|
+
|
|
11
|
+
if unit in {"B", "K"}:
|
|
12
|
+
return f"{int(number)}{unit}"
|
|
13
|
+
|
|
14
|
+
return f"{number:.1f}{unit}"
|
|
15
|
+
|
|
16
|
+
raise ValueError("This should not happen")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def format_integer(number):
|
|
20
|
+
return f"{number:,}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def format_percentage(number: int):
|
|
24
|
+
return f"{(number/1000):.1%}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def format_microseconds(number: int):
|
|
28
|
+
for unit in ["us", "ms", "s"]:
|
|
29
|
+
if number >= TIME_MODULUS:
|
|
30
|
+
number = int(number / TIME_MODULUS)
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
if unit == "us":
|
|
34
|
+
return f"{number/TIME_MODULUS:.1f}ms"
|
|
35
|
+
|
|
36
|
+
too_big_sign = "+" if unit == "s" and number > TIME_MODULUS else ""
|
|
37
|
+
return f"{int(number)}{too_big_sign}{unit}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def format_seconds(number: int):
|
|
41
|
+
if number > 60:
|
|
42
|
+
return "60+s"
|
|
43
|
+
|
|
44
|
+
return f"{number}s"
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
from typing import Any, Callable, Dict, List, Tuple, Union
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def cull_graph(
|
|
6
|
+
graph: Dict[str, Tuple[Union[Callable, Any], ...]], keys: List[str]
|
|
7
|
+
) -> Dict[str, Tuple[Union[Callable, Any], ...]]:
|
|
8
|
+
queue = deque(keys)
|
|
9
|
+
visited = set()
|
|
10
|
+
for target_key in keys:
|
|
11
|
+
visited.add(target_key)
|
|
12
|
+
|
|
13
|
+
while queue:
|
|
14
|
+
key = queue.popleft()
|
|
15
|
+
|
|
16
|
+
task = graph[key]
|
|
17
|
+
if not (isinstance(task, tuple) and task and callable(task[0])):
|
|
18
|
+
continue
|
|
19
|
+
|
|
20
|
+
dependencies = set(task[1:])
|
|
21
|
+
for predecessor_key in dependencies:
|
|
22
|
+
if predecessor_key in visited:
|
|
23
|
+
continue
|
|
24
|
+
visited.add(predecessor_key)
|
|
25
|
+
queue.append(predecessor_key)
|
|
26
|
+
|
|
27
|
+
return {key: graph[key] for key in visited}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from scaler.utility.graph.topological_sorter_graphblas import TopologicalSorter
|
|
5
|
+
|
|
6
|
+
logging.info("using GraphBLAS for calculate graph")
|
|
7
|
+
except ImportError as e:
|
|
8
|
+
assert isinstance(e, Exception)
|
|
9
|
+
from graphlib import TopologicalSorter # type: ignore[assignment, no-redef]
|
|
10
|
+
|
|
11
|
+
assert isinstance(TopologicalSorter, object)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import graphlib
|
|
3
|
+
import itertools
|
|
4
|
+
from typing import Generic, Hashable, Iterable, List, Mapping, Optional, Tuple, TypeVar
|
|
5
|
+
|
|
6
|
+
from bidict import bidict
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import graphblas as gb
|
|
10
|
+
import numpy as np # noqa
|
|
11
|
+
except ImportError:
|
|
12
|
+
raise ImportError("Please use 'pip install python-graphblas' to have graph blas support")
|
|
13
|
+
|
|
14
|
+
GraphKeyType = TypeVar("GraphKeyType", bound=Hashable)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TopologicalSorter(Generic[GraphKeyType]):
|
|
18
|
+
"""
|
|
19
|
+
Implements graphlib's TopologicalSorter, but the graph handling is backed by GraphBLAS
|
|
20
|
+
Reference: https://github.com/python/cpython/blob/4a3ea1fdd890e5e2ec26540dc3c958a52fba6556/Lib/graphlib.py
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, graph: Optional[Mapping[GraphKeyType, Iterable[GraphKeyType]]] = None):
|
|
24
|
+
# the layout of the matrix is (in-vertex, out-vertex)
|
|
25
|
+
self._matrix = gb.Matrix(gb.dtypes.BOOL)
|
|
26
|
+
self._key_to_id: bidict[GraphKeyType, int] = bidict()
|
|
27
|
+
|
|
28
|
+
self._graph_matrix_mask: Optional[np.ndarray] = None
|
|
29
|
+
self._visited_vertices_mask: Optional[np.ndarray] = None
|
|
30
|
+
self._ready_nodes: Optional[List[GraphKeyType]] = None
|
|
31
|
+
|
|
32
|
+
self._n_done = 0
|
|
33
|
+
self._n_visited = 0
|
|
34
|
+
|
|
35
|
+
if graph is not None:
|
|
36
|
+
self.merge_graph(graph)
|
|
37
|
+
|
|
38
|
+
def add(self, node: GraphKeyType, *predecessors: GraphKeyType) -> None:
|
|
39
|
+
self.merge_graph({node: predecessors})
|
|
40
|
+
|
|
41
|
+
def merge_graph(self, graph: Mapping[GraphKeyType, Iterable[GraphKeyType]]) -> None:
|
|
42
|
+
if self._ready_nodes is not None:
|
|
43
|
+
raise ValueError("nodes cannot be added after a call to prepare()")
|
|
44
|
+
|
|
45
|
+
# cache old dim to compare later when resizing matrix
|
|
46
|
+
old_dim = len(self._key_to_id)
|
|
47
|
+
|
|
48
|
+
# maintain iterable copies for iterable predecessors
|
|
49
|
+
graph_iterable_copy = {}
|
|
50
|
+
|
|
51
|
+
# update key to id mappings
|
|
52
|
+
for node, predecessors in graph.items():
|
|
53
|
+
if node not in self._key_to_id:
|
|
54
|
+
self._key_to_id[node] = len(self._key_to_id)
|
|
55
|
+
|
|
56
|
+
# copy iterator if predecessors is an iterable
|
|
57
|
+
if isinstance(predecessors, collections.abc.Iterable):
|
|
58
|
+
predecessors, graph_iterable_copy[node] = itertools.tee(predecessors)
|
|
59
|
+
|
|
60
|
+
for pred in predecessors:
|
|
61
|
+
if pred not in self._key_to_id:
|
|
62
|
+
self._key_to_id[pred] = len(self._key_to_id)
|
|
63
|
+
|
|
64
|
+
# resize at once as it is faster
|
|
65
|
+
if old_dim != len(self._key_to_id):
|
|
66
|
+
self._matrix.resize(len(self._key_to_id), len(self._key_to_id))
|
|
67
|
+
|
|
68
|
+
# update matrix
|
|
69
|
+
for node, predecessors in graph.items():
|
|
70
|
+
if node in graph_iterable_copy:
|
|
71
|
+
predecessors = graph_iterable_copy[node]
|
|
72
|
+
|
|
73
|
+
for pred in predecessors:
|
|
74
|
+
self._matrix[self._key_to_id[node], self._key_to_id[pred]] = True
|
|
75
|
+
|
|
76
|
+
def prepare(self) -> None:
|
|
77
|
+
if self._ready_nodes is not None:
|
|
78
|
+
raise ValueError("cannot prepare() more than once")
|
|
79
|
+
|
|
80
|
+
self._graph_matrix_mask = np.ones(len(self._key_to_id), bool)
|
|
81
|
+
self._visited_vertices_mask = np.zeros(len(self._key_to_id), bool)
|
|
82
|
+
|
|
83
|
+
self._ready_nodes = self._get_zero_degree_keys()
|
|
84
|
+
for node in self._ready_nodes:
|
|
85
|
+
self._visited_vertices_mask[self._key_to_id[node]] = True
|
|
86
|
+
self._n_visited += len(self._ready_nodes)
|
|
87
|
+
|
|
88
|
+
if self._has_cycle():
|
|
89
|
+
raise graphlib.CycleError("cycle detected")
|
|
90
|
+
|
|
91
|
+
def get_ready(self) -> Tuple[GraphKeyType, ...]:
|
|
92
|
+
if self._ready_nodes is None:
|
|
93
|
+
raise ValueError("prepare() must be called first")
|
|
94
|
+
|
|
95
|
+
result = tuple(self._ready_nodes)
|
|
96
|
+
self._ready_nodes.clear()
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
def is_active(self) -> bool:
|
|
100
|
+
if self._ready_nodes is None:
|
|
101
|
+
raise ValueError("prepare() must be called first")
|
|
102
|
+
return self._n_done < self._n_visited or bool(self._ready_nodes)
|
|
103
|
+
|
|
104
|
+
def __bool__(self) -> bool:
|
|
105
|
+
return self.is_active()
|
|
106
|
+
|
|
107
|
+
def done(self, *nodes: GraphKeyType) -> None:
|
|
108
|
+
if self._ready_nodes is None:
|
|
109
|
+
raise ValueError("prepare() must be called first")
|
|
110
|
+
|
|
111
|
+
for node in nodes:
|
|
112
|
+
if node not in self._key_to_id:
|
|
113
|
+
raise ValueError(f"node {node!r} was not added using add()")
|
|
114
|
+
|
|
115
|
+
_id = self._key_to_id[node]
|
|
116
|
+
|
|
117
|
+
if not self._visited_vertices_mask[_id]:
|
|
118
|
+
raise ValueError(f"node {node!r} is not ready")
|
|
119
|
+
|
|
120
|
+
if not self._graph_matrix_mask[_id]:
|
|
121
|
+
raise ValueError(f"node {node!r} is already done")
|
|
122
|
+
|
|
123
|
+
self._graph_matrix_mask[_id] = False
|
|
124
|
+
self._n_done += len(nodes)
|
|
125
|
+
|
|
126
|
+
new_ready_nodes = self._get_zero_degree_keys()
|
|
127
|
+
for node in new_ready_nodes:
|
|
128
|
+
self._visited_vertices_mask[self._key_to_id[node]] = True
|
|
129
|
+
self._ready_nodes.extend(new_ready_nodes)
|
|
130
|
+
self._n_visited += len(new_ready_nodes)
|
|
131
|
+
|
|
132
|
+
def static_order(self) -> Iterable[GraphKeyType]:
|
|
133
|
+
self.prepare()
|
|
134
|
+
while self.is_active():
|
|
135
|
+
node_group = self.get_ready()
|
|
136
|
+
yield from node_group
|
|
137
|
+
self.done(*node_group)
|
|
138
|
+
|
|
139
|
+
def _has_cycle(self) -> bool:
|
|
140
|
+
"""
|
|
141
|
+
Detect cycle using trace(A^n) != 0.
|
|
142
|
+
https://arxiv.org/pdf/1610.01200.pdf
|
|
143
|
+
|
|
144
|
+
:return: True if cycle is found, otherwise False
|
|
145
|
+
"""
|
|
146
|
+
matrix_n = gb.Vector.from_dense(np.ones(len(self._key_to_id), bool), missing_value=False).diag()
|
|
147
|
+
for _ in range(len(self._key_to_id)):
|
|
148
|
+
# use LOR_PAIR to compute matrix multiplication over boolean matrices
|
|
149
|
+
matrix_n << gb.semiring.lor_pair(matrix_n @ self._matrix)
|
|
150
|
+
# check diagonal for any truthy values
|
|
151
|
+
if matrix_n.diag().reduce(gb.monoid.lor):
|
|
152
|
+
return True
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
def _get_zero_degree_keys(self) -> List[GraphKeyType]:
|
|
156
|
+
ids = self._get_mask_diff(self._visited_vertices_mask, self._get_zero_degree_mask(self._get_masked_matrix()))
|
|
157
|
+
return [self._key_to_id.inverse[_id] for _id in ids]
|
|
158
|
+
|
|
159
|
+
def _get_masked_matrix(self) -> gb.Matrix:
|
|
160
|
+
# convert vector mask to matrix diagonal and then perform matrix multiplication to mask matrix
|
|
161
|
+
# https://github.com/DrTimothyAldenDavis/GraphBLAS/issues/48#issuecomment-858596341
|
|
162
|
+
return gb.semiring.lor_pair(
|
|
163
|
+
self._matrix @ gb.Vector.from_dense(self._graph_matrix_mask, missing_value=False).diag()
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def _get_zero_degree_mask(cls, masked_matrix: gb.Matrix) -> np.ndarray:
|
|
168
|
+
degrees = masked_matrix.reduce_rowwise(gb.monoid.lor)
|
|
169
|
+
indices, _ = degrees.to_coo(indices=True, values=False, sort=False)
|
|
170
|
+
return np.logical_not(np.in1d(np.arange(masked_matrix.nrows), indices)) # type: ignore[attr-defined]
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _get_mask_diff(old_mask: np.ndarray, new_mask: np.ndarray) -> List[int]:
|
|
174
|
+
return np.argwhere(old_mask != new_mask).ravel().tolist()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import hashlib
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Identifier(bytes, metaclass=abc.ABCMeta):
|
|
8
|
+
@abc.abstractmethod
|
|
9
|
+
def __repr__(self) -> str:
|
|
10
|
+
raise NotImplementedError()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ClientID(Identifier):
|
|
14
|
+
def __repr__(self) -> str:
|
|
15
|
+
return f"ClientID({self.decode()})"
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def generate_client_id(name: Optional[str] = None) -> "ClientID":
|
|
19
|
+
unique_client_tag = uuid.uuid4().bytes.hex()
|
|
20
|
+
|
|
21
|
+
if name is None:
|
|
22
|
+
return ClientID(f"Client|{unique_client_tag}".encode())
|
|
23
|
+
else:
|
|
24
|
+
return ClientID(f"Client|{name}|{unique_client_tag}".encode())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WorkerID(Identifier):
|
|
28
|
+
def __repr__(self) -> str:
|
|
29
|
+
return f"WorkerID({self.decode()})"
|
|
30
|
+
|
|
31
|
+
def is_valid(self) -> bool:
|
|
32
|
+
return self != _INVALID_WORKER_ID
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def invalid_worker_id() -> "WorkerID":
|
|
36
|
+
return _INVALID_WORKER_ID
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def generate_worker_id(name: str) -> "WorkerID":
|
|
40
|
+
unique_worker_tag = uuid.uuid4().bytes.hex()
|
|
41
|
+
return WorkerID(f"Worker|{name}|{unique_worker_tag}".encode())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_INVALID_WORKER_ID = WorkerID(b"")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ProcessorID(Identifier):
|
|
48
|
+
def __repr__(self) -> str:
|
|
49
|
+
return f"ProcessorID({self.hex()})"
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def generate_processor_id() -> "ProcessorID":
|
|
53
|
+
return ProcessorID(uuid.uuid4().bytes)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TaskID(Identifier):
|
|
57
|
+
def __repr__(self) -> str:
|
|
58
|
+
return f"TaskID({self.hex()})"
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def generate_task_id() -> "TaskID":
|
|
62
|
+
return TaskID(uuid.uuid4().bytes)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ObjectID(bytes):
|
|
66
|
+
SERIALIZER_TAG = hashlib.md5(b"serializer").digest()
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
Scaler 32-bytes object IDs.
|
|
70
|
+
|
|
71
|
+
Object ID are built from 2x16-bytes parts:
|
|
72
|
+
|
|
73
|
+
- the first 16-bytes uniquely identify the owner of the object (i.e. the Scaler client's hash);
|
|
74
|
+
- the second 16-bytes uniquely identify the object's content.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __new__(cls, value: bytes):
|
|
78
|
+
if len(value) != 32:
|
|
79
|
+
raise ValueError("Scaler object ID must be 32 bytes.")
|
|
80
|
+
|
|
81
|
+
return super().__new__(cls, value)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def generate_object_id(owner: ClientID) -> "ObjectID":
|
|
85
|
+
owner_hash = hashlib.md5(owner).digest()
|
|
86
|
+
unique_object_tag = uuid.uuid4().bytes
|
|
87
|
+
return ObjectID(owner_hash + unique_object_tag)
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def generate_serializer_object_id(owner: ClientID) -> "ObjectID":
|
|
91
|
+
owner_hash = hashlib.md5(owner).digest()
|
|
92
|
+
return ObjectID(owner_hash + ObjectID.SERIALIZER_TAG)
|
|
93
|
+
|
|
94
|
+
def owner_hash(self) -> bytes:
|
|
95
|
+
return self[:16]
|
|
96
|
+
|
|
97
|
+
def object_tag(self) -> bytes:
|
|
98
|
+
return self[16:]
|
|
99
|
+
|
|
100
|
+
def is_serializer(self) -> bool:
|
|
101
|
+
return self.object_tag() == ObjectID.SERIALIZER_TAG
|
|
102
|
+
|
|
103
|
+
def is_owner(self, owner: ClientID) -> bool:
|
|
104
|
+
return hashlib.md5(owner).digest() == self.owner_hash()
|
|
105
|
+
|
|
106
|
+
def __repr__(self) -> str:
|
|
107
|
+
return f"ObjectID(owner_hash={self.owner_hash().hex()}, object_tag={self.object_tag().hex()})"
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
from scaler.utility.logging.scoped_logger import ScopedLogger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def log_function(level_number: int = 2, logging_level: int = logging.INFO) -> typing.Callable:
|
|
10
|
+
def decorator(func: typing.Callable) -> typing.Callable:
|
|
11
|
+
@functools.wraps(func)
|
|
12
|
+
def wrapper(*args, **kwargs):
|
|
13
|
+
with ScopedLogger(
|
|
14
|
+
f"execute {func.__name__} at {get_caller_location(level_number)}", logging_level=logging_level
|
|
15
|
+
):
|
|
16
|
+
return func(*args, **kwargs)
|
|
17
|
+
|
|
18
|
+
return wrapper
|
|
19
|
+
|
|
20
|
+
return decorator
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_caller_location(stack_level: int):
|
|
24
|
+
caller = inspect.getframeinfo(inspect.stack()[stack_level][0])
|
|
25
|
+
return f"{caller.filename}:{caller.lineno}"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ScopedLogger:
|
|
8
|
+
def __init__(self, message: str, logging_level=logging.INFO):
|
|
9
|
+
self.timer = TimedLogger(message=message, logging_level=logging_level)
|
|
10
|
+
|
|
11
|
+
def __enter__(self):
|
|
12
|
+
self.timer.begin()
|
|
13
|
+
|
|
14
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
15
|
+
self.timer.end()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TimedLogger:
|
|
19
|
+
def __init__(self, message: str, logging_level=logging.INFO):
|
|
20
|
+
self.message = message
|
|
21
|
+
self.logging_level = logging_level
|
|
22
|
+
self.timer: Optional[int] = None
|
|
23
|
+
|
|
24
|
+
def begin(self):
|
|
25
|
+
self.timer = time.perf_counter_ns()
|
|
26
|
+
logging.log(self.logging_level, f"beginning {self.message}")
|
|
27
|
+
|
|
28
|
+
def end(self):
|
|
29
|
+
elapsed = time.perf_counter_ns() - self.timer
|
|
30
|
+
offset = datetime.timedelta(
|
|
31
|
+
seconds=int(elapsed / 1e9), milliseconds=int(elapsed % 1e9 / 1e6), microseconds=int(elapsed % 1e6 / 1e3)
|
|
32
|
+
)
|
|
33
|
+
logging.log(self.logging_level, f"completed {self.message} in {offset}")
|