wool 0.1rc6__py3-none-any.whl → 0.1rc7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wool might be problematic. Click here for more details.
- wool/__init__.py +37 -27
- wool/_cli.py +79 -38
- wool/_event.py +109 -0
- wool/_future.py +82 -11
- wool/_logging.py +7 -0
- wool/_manager.py +36 -20
- wool/_mempool/__init__.py +3 -0
- wool/_mempool/_mempool.py +204 -0
- wool/_mempool/_metadata/__init__.py +41 -0
- wool/_pool.py +357 -149
- wool/_protobuf/.gitkeep +0 -0
- wool/_protobuf/_mempool/_metadata/_metadata_pb2.py +36 -0
- wool/_protobuf/_mempool/_metadata/_metadata_pb2.pyi +17 -0
- wool/_queue.py +2 -1
- wool/_session.py +429 -0
- wool/_task.py +169 -113
- wool/_typing.py +5 -1
- wool/_utils.py +10 -17
- wool/_worker.py +120 -73
- wool-0.1rc7.dist-info/METADATA +343 -0
- wool-0.1rc7.dist-info/RECORD +23 -0
- {wool-0.1rc6.dist-info → wool-0.1rc7.dist-info}/WHEEL +1 -2
- wool/_client.py +0 -205
- wool-0.1rc6.dist-info/METADATA +0 -138
- wool-0.1rc6.dist-info/RECORD +0 -17
- wool-0.1rc6.dist-info/top_level.txt +0 -1
- {wool-0.1rc6.dist-info → wool-0.1rc7.dist-info}/entry_points.txt +0 -0
wool/_manager.py
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
import os
|
|
5
|
-
import signal
|
|
6
4
|
from functools import partial
|
|
7
|
-
from multiprocessing.managers import
|
|
8
|
-
|
|
9
|
-
DictProxy,
|
|
10
|
-
)
|
|
5
|
+
from multiprocessing.managers import BaseManager
|
|
6
|
+
from multiprocessing.managers import DictProxy
|
|
11
7
|
from queue import Empty
|
|
8
|
+
from threading import Event
|
|
12
9
|
from threading import Lock
|
|
13
|
-
from typing import TYPE_CHECKING
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
from typing import Any
|
|
12
|
+
from typing import Callable
|
|
13
|
+
from typing import TypeVar
|
|
14
|
+
from typing import overload
|
|
14
15
|
from uuid import UUID
|
|
15
16
|
from weakref import WeakValueDictionary
|
|
16
17
|
|
|
17
|
-
from wool.
|
|
18
|
+
from wool._event import TaskEvent
|
|
19
|
+
from wool._future import Future
|
|
18
20
|
from wool._queue import TaskQueue
|
|
19
21
|
from wool._typing import PassthroughDecorator
|
|
20
22
|
|
|
21
23
|
if TYPE_CHECKING:
|
|
22
|
-
from wool._task import
|
|
24
|
+
from wool._task import Task
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
C = TypeVar("C", bound=Callable[..., Any])
|
|
26
28
|
|
|
27
|
-
|
|
28
29
|
_manager_registry = {}
|
|
29
30
|
|
|
30
31
|
|
|
@@ -41,6 +42,7 @@ def register(
|
|
|
41
42
|
fn: C,
|
|
42
43
|
/,
|
|
43
44
|
*,
|
|
45
|
+
exposed: tuple[str, ...] | None = None,
|
|
44
46
|
proxytype: None = None,
|
|
45
47
|
method_to_typeid: None = None,
|
|
46
48
|
) -> C: ...
|
|
@@ -51,6 +53,7 @@ def register(
|
|
|
51
53
|
fn: None = None,
|
|
52
54
|
/,
|
|
53
55
|
*,
|
|
56
|
+
exposed: tuple[str, ...] | None = None,
|
|
54
57
|
proxytype: type | None = None,
|
|
55
58
|
method_to_typeid: dict[str, str] | None = None,
|
|
56
59
|
) -> PassthroughDecorator[C]: ...
|
|
@@ -60,10 +63,13 @@ def register(
|
|
|
60
63
|
fn: C | None = None,
|
|
61
64
|
/,
|
|
62
65
|
*,
|
|
66
|
+
exposed: tuple[str, ...] | None = None,
|
|
63
67
|
proxytype: type | None = None,
|
|
64
68
|
method_to_typeid: dict[str, str] | None = None,
|
|
65
69
|
) -> PassthroughDecorator[C] | C:
|
|
66
70
|
kwargs = {}
|
|
71
|
+
if exposed is not None:
|
|
72
|
+
kwargs["exposed"] = exposed
|
|
67
73
|
if proxytype is not None:
|
|
68
74
|
kwargs["proxytype"] = proxytype
|
|
69
75
|
if method_to_typeid is not None:
|
|
@@ -83,20 +89,20 @@ class FuturesProxy(DictProxy):
|
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
|
|
86
|
-
_task_queue: TaskQueue[
|
|
92
|
+
_task_queue: TaskQueue[Task] = TaskQueue(100000, None)
|
|
87
93
|
|
|
88
94
|
_task_queue_lock = Lock()
|
|
89
95
|
|
|
90
|
-
_task_futures: WeakValueDictionary[UUID,
|
|
96
|
+
_task_futures: WeakValueDictionary[UUID, Future] = WeakValueDictionary()
|
|
91
97
|
|
|
92
98
|
|
|
93
99
|
@register
|
|
94
|
-
def put(task:
|
|
100
|
+
def put(task: Task) -> Future:
|
|
95
101
|
try:
|
|
96
102
|
with queue_lock():
|
|
97
103
|
queue().put(task, block=False)
|
|
98
|
-
future = futures()[task.id] =
|
|
99
|
-
|
|
104
|
+
future = futures()[task.id] = Future()
|
|
105
|
+
TaskEvent("task-queued", task=task).emit()
|
|
100
106
|
return future
|
|
101
107
|
except Exception as e:
|
|
102
108
|
logging.exception(e)
|
|
@@ -104,7 +110,7 @@ def put(task: WoolTask) -> WoolFuture:
|
|
|
104
110
|
|
|
105
111
|
|
|
106
112
|
@register
|
|
107
|
-
def get() ->
|
|
113
|
+
def get() -> Task | Empty | None:
|
|
108
114
|
try:
|
|
109
115
|
return queue().get(block=False)
|
|
110
116
|
except Empty as e:
|
|
@@ -140,9 +146,18 @@ def queue_lock() -> Lock:
|
|
|
140
146
|
return _task_queue_lock
|
|
141
147
|
|
|
142
148
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
_stop_event = Event()
|
|
150
|
+
_wait_event = Event()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@register(exposed=("is_set", "set", "clear", "wait"))
|
|
154
|
+
def stopping() -> Event:
|
|
155
|
+
return _stop_event
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@register(exposed=("is_set", "set", "clear", "wait"))
|
|
159
|
+
def waiting() -> Event:
|
|
160
|
+
return _wait_event
|
|
146
161
|
|
|
147
162
|
|
|
148
163
|
class ManagerMeta(type):
|
|
@@ -162,4 +177,5 @@ class Manager(BaseManager, metaclass=ManagerMeta):
|
|
|
162
177
|
futures = staticmethod(futures)
|
|
163
178
|
queue = staticmethod(queue)
|
|
164
179
|
queue_lock = staticmethod(queue_lock)
|
|
165
|
-
|
|
180
|
+
stopping = staticmethod(stopping)
|
|
181
|
+
waiting = staticmethod(waiting)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import hashlib
|
|
3
|
+
import mmap
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
import shutil
|
|
7
|
+
from collections import namedtuple
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from dataclasses import asdict
|
|
10
|
+
from dataclasses import fields
|
|
11
|
+
from types import MappingProxyType
|
|
12
|
+
|
|
13
|
+
import shortuuid
|
|
14
|
+
|
|
15
|
+
from wool._mempool._metadata import MetadataMessage
|
|
16
|
+
|
|
17
|
+
Metadata = namedtuple(
|
|
18
|
+
"Metadata",
|
|
19
|
+
[field.name for field in fields(MetadataMessage)],
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MetadataMapping:
|
|
24
|
+
def __init__(self, mapping) -> None:
|
|
25
|
+
self._mapping = MappingProxyType(mapping)
|
|
26
|
+
|
|
27
|
+
def __getitem__(self, key: str) -> Metadata:
|
|
28
|
+
return Metadata(**asdict(self._mapping[key]))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MemoryPool:
|
|
32
|
+
def __init__(self, path: str | pathlib.Path = pathlib.Path(".mempool")):
|
|
33
|
+
if isinstance(path, str):
|
|
34
|
+
self._path = pathlib.Path(path)
|
|
35
|
+
else:
|
|
36
|
+
self._path = path
|
|
37
|
+
self._lockdir = self._path / "locks"
|
|
38
|
+
os.makedirs(self._lockdir, exist_ok=True)
|
|
39
|
+
self._files = {}
|
|
40
|
+
self._mmaps = {}
|
|
41
|
+
self._metadata: dict[str, MetadataMessage] = {}
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def metadata(self) -> MetadataMapping:
|
|
45
|
+
return MetadataMapping(self._metadata)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def path(self) -> pathlib.Path:
|
|
49
|
+
return self._path
|
|
50
|
+
|
|
51
|
+
async def map(self):
|
|
52
|
+
for entry in os.scandir(self._path):
|
|
53
|
+
if entry.is_dir() and (ref := entry.name) != "locks":
|
|
54
|
+
async with self._reflock(ref):
|
|
55
|
+
self._map(ref)
|
|
56
|
+
|
|
57
|
+
async def put(
|
|
58
|
+
self, dump: bytes, *, mutable: bool = False, ref: str | None = None
|
|
59
|
+
) -> str:
|
|
60
|
+
async with self._reflock(ref := ref or str(shortuuid.uuid())):
|
|
61
|
+
self._put(ref, dump, mutable=mutable, exist_ok=False)
|
|
62
|
+
return ref
|
|
63
|
+
|
|
64
|
+
async def post(self, ref: str, dump: bytes):
|
|
65
|
+
async with self._reflock(ref):
|
|
66
|
+
if ref not in self._mmaps:
|
|
67
|
+
self._map(ref)
|
|
68
|
+
metamap, dumpmap = self._mmaps[ref]
|
|
69
|
+
metamap.seek(0)
|
|
70
|
+
metadata = self._metadata.setdefault(
|
|
71
|
+
ref, MetadataMessage.loads(metamap.read())
|
|
72
|
+
)
|
|
73
|
+
if not metadata.mutable:
|
|
74
|
+
raise ValueError("Cannot modify an immutable reference")
|
|
75
|
+
if (size := len(dump)) != metadata.size:
|
|
76
|
+
try:
|
|
77
|
+
dumpmap.resize(size)
|
|
78
|
+
self._post(ref, dump, metadata)
|
|
79
|
+
except SystemError:
|
|
80
|
+
self._put(ref, dump, mutable=True, exist_ok=True)
|
|
81
|
+
return True
|
|
82
|
+
elif hashlib.md5(dump).digest() != metadata.md5:
|
|
83
|
+
self._post(ref, dump, metadata)
|
|
84
|
+
return True
|
|
85
|
+
else:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
async def get(self, ref: str) -> bytes:
|
|
89
|
+
async with self._reflock(ref):
|
|
90
|
+
if ref not in self._mmaps:
|
|
91
|
+
self._map(ref)
|
|
92
|
+
metamap, dumpmap = self._mmaps[ref]
|
|
93
|
+
metamap.seek(0)
|
|
94
|
+
metadata = MetadataMessage.loads(metamap.read())
|
|
95
|
+
cached_metadata = self._metadata.setdefault(ref, metadata)
|
|
96
|
+
if metadata.mutable and metadata.size != cached_metadata.size:
|
|
97
|
+
# Dump size has changed, so we need to re-map it
|
|
98
|
+
self._map(ref)
|
|
99
|
+
dumpmap.seek(0)
|
|
100
|
+
return dumpmap.read()
|
|
101
|
+
|
|
102
|
+
async def delete(self, ref: str):
|
|
103
|
+
async with self._reflock(ref):
|
|
104
|
+
if ref not in self._mmaps:
|
|
105
|
+
self._map(ref)
|
|
106
|
+
metamap, dumpmap = self._mmaps.pop(ref)
|
|
107
|
+
metafile, dumpfile = self._files.pop(ref)
|
|
108
|
+
if ref in self._metadata:
|
|
109
|
+
del self._metadata[ref]
|
|
110
|
+
if metamap:
|
|
111
|
+
metamap.close()
|
|
112
|
+
if dumpmap:
|
|
113
|
+
dumpmap.close()
|
|
114
|
+
if metafile:
|
|
115
|
+
metafile.close()
|
|
116
|
+
if dumpfile:
|
|
117
|
+
dumpfile.close()
|
|
118
|
+
try:
|
|
119
|
+
shutil.rmtree(self.path / ref)
|
|
120
|
+
except FileNotFoundError:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
def _put(
|
|
124
|
+
self,
|
|
125
|
+
ref: str,
|
|
126
|
+
dump: bytes,
|
|
127
|
+
*,
|
|
128
|
+
mutable: bool = False,
|
|
129
|
+
exist_ok: bool = False,
|
|
130
|
+
):
|
|
131
|
+
metadata = MetadataMessage(
|
|
132
|
+
ref=ref,
|
|
133
|
+
mutable=mutable,
|
|
134
|
+
size=len(dump),
|
|
135
|
+
md5=hashlib.md5(dump).digest(),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
refpath = pathlib.Path(self._path, f"{ref}")
|
|
139
|
+
os.makedirs(refpath, exist_ok=exist_ok)
|
|
140
|
+
|
|
141
|
+
with open(refpath / "meta", "wb") as metafile:
|
|
142
|
+
metafile.write(metadata.dumps())
|
|
143
|
+
|
|
144
|
+
with open(refpath / "dump", "wb") as dumpfile:
|
|
145
|
+
dumpfile.write(dump)
|
|
146
|
+
|
|
147
|
+
self._map(ref)
|
|
148
|
+
self._metadata[ref] = metadata
|
|
149
|
+
|
|
150
|
+
def _post(self, ref: str, dump: bytes, metadata: MetadataMessage):
|
|
151
|
+
metamap, dumpmap = self._mmaps[ref]
|
|
152
|
+
metadata.size = len(dump)
|
|
153
|
+
metadata.md5 = hashlib.md5(dump).digest()
|
|
154
|
+
metamap.seek(0)
|
|
155
|
+
metamap.write(metadata.dumps())
|
|
156
|
+
metamap.flush()
|
|
157
|
+
dumpmap.seek(0)
|
|
158
|
+
dumpmap.write(dump)
|
|
159
|
+
dumpmap.flush()
|
|
160
|
+
|
|
161
|
+
def _map(self, ref: str):
|
|
162
|
+
refpath = pathlib.Path(self._path, f"{ref}")
|
|
163
|
+
metafile = open(refpath / "meta", "r+b")
|
|
164
|
+
metamap = mmap.mmap(metafile.fileno(), 0, flags=mmap.MAP_SHARED)
|
|
165
|
+
dumpfile = open(refpath / "dump", "r+b")
|
|
166
|
+
dumpmap = mmap.mmap(dumpfile.fileno(), 0, flags=mmap.MAP_SHARED)
|
|
167
|
+
cached_metafile, cached_dumpfile = self._files.pop(ref, (None, None))
|
|
168
|
+
if cached_metafile:
|
|
169
|
+
cached_metafile.close()
|
|
170
|
+
if cached_dumpfile:
|
|
171
|
+
cached_dumpfile.close()
|
|
172
|
+
cached_metamap, cached_dumpmap = self._mmaps.pop(ref, (None, None))
|
|
173
|
+
if cached_metamap is not None and not cached_metamap.closed:
|
|
174
|
+
cached_metamap.close()
|
|
175
|
+
if cached_dumpmap is not None and not cached_dumpmap.closed:
|
|
176
|
+
cached_dumpmap.close()
|
|
177
|
+
self._files[ref] = (metafile, dumpfile)
|
|
178
|
+
self._mmaps[ref] = (metamap, dumpmap)
|
|
179
|
+
|
|
180
|
+
def _lockpath(self, ref: str):
|
|
181
|
+
return pathlib.Path(self._lockdir, f"{ref}.lock")
|
|
182
|
+
|
|
183
|
+
def _acquire(self, ref: str):
|
|
184
|
+
try:
|
|
185
|
+
os.symlink(f"{ref}", self._lockpath(ref))
|
|
186
|
+
return True
|
|
187
|
+
except FileExistsError:
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
def _release(self, ref: str):
|
|
191
|
+
try:
|
|
192
|
+
if os.path.islink(lock_path := self._lockpath(ref)):
|
|
193
|
+
os.unlink(lock_path)
|
|
194
|
+
except FileNotFoundError:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
@asynccontextmanager
|
|
198
|
+
async def _reflock(self, ref: str):
|
|
199
|
+
try:
|
|
200
|
+
while not self._acquire(ref):
|
|
201
|
+
await asyncio.sleep(0)
|
|
202
|
+
yield
|
|
203
|
+
finally:
|
|
204
|
+
self._release(ref)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from wool._protobuf._mempool._metadata._metadata_pb2 import (
|
|
8
|
+
_MetadataMessage,
|
|
9
|
+
)
|
|
10
|
+
except ImportError:
|
|
11
|
+
logging.error(
|
|
12
|
+
"Failed to import _MetadataMessage. "
|
|
13
|
+
"Ensure protocol buffers are compiled."
|
|
14
|
+
)
|
|
15
|
+
raise
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class MetadataMessage:
|
|
20
|
+
ref: str
|
|
21
|
+
mutable: bool
|
|
22
|
+
size: int
|
|
23
|
+
md5: bytes
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def loads(cls, data: bytes) -> MetadataMessage:
|
|
27
|
+
(metadata := _MetadataMessage()).ParseFromString(data)
|
|
28
|
+
return cls(
|
|
29
|
+
ref=metadata.ref,
|
|
30
|
+
mutable=metadata.mutable,
|
|
31
|
+
size=metadata.size,
|
|
32
|
+
md5=metadata.md5,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def dumps(self) -> bytes:
|
|
36
|
+
return _MetadataMessage(
|
|
37
|
+
ref=self.ref, mutable=self.mutable, size=self.size, md5=self.md5
|
|
38
|
+
).SerializeToString()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = ["MetadataMessage"]
|