wool 0.1rc6__py3-none-any.whl → 0.1rc8__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/_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
- BaseManager,
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, Any, Callable, TypeVar, overload
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._future import WoolFuture
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 WoolTask
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[WoolTask] = TaskQueue(1000, None)
92
+ _task_queue: TaskQueue[Task] = TaskQueue(100000, None)
87
93
 
88
94
  _task_queue_lock = Lock()
89
95
 
90
- _task_futures: WeakValueDictionary[UUID, WoolFuture] = WeakValueDictionary()
96
+ _task_futures: WeakValueDictionary[UUID, Future] = WeakValueDictionary()
91
97
 
92
98
 
93
99
  @register
94
- def put(task: WoolTask) -> WoolFuture:
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] = WoolFuture()
99
- logging.debug(f"Pushed task {task.id} to queue: {task.tag}")
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() -> WoolTask | Empty | None:
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
- @register
144
- def stop(wait: bool = True) -> None:
145
- os.kill(os.getpid(), signal.SIGINT if wait else signal.SIGTERM)
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
- stop = staticmethod(stop)
180
+ stopping = staticmethod(stopping)
181
+ waiting = staticmethod(waiting)
@@ -0,0 +1,4 @@
1
+ from wool._mempool._mempool import MemoryPool
2
+ from wool._mempool._service import MemoryPoolService
3
+
4
+ __all__ = ["MemoryPool", "MemoryPoolService"]
@@ -0,0 +1,311 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import hashlib
5
+ import mmap
6
+ import os
7
+ import pathlib
8
+ import shutil
9
+ from contextlib import asynccontextmanager
10
+ from typing import BinaryIO
11
+
12
+ try:
13
+ from typing import Self
14
+ except ImportError:
15
+ from typing_extensions import Self
16
+
17
+ import shortuuid
18
+
19
+ from wool._mempool._metadata import MetadataMessage
20
+
21
+
22
+ class SharedObject:
23
+ _id: str
24
+ _mempool: MemoryPool
25
+ _file: BinaryIO
26
+ _mmap: mmap.mmap
27
+ _size: int
28
+ _md5: bytes
29
+
30
+ def __init__(self, id: str, *, mempool: MemoryPool):
31
+ self._id = id
32
+ self._mempool = mempool
33
+ self._file = open(self._path / "dump", "r+b")
34
+ self._mmap = mmap.mmap(self._file.fileno(), 0)
35
+ self._size = self.metadata.size
36
+ self._md5 = self.metadata.md5
37
+
38
+ def __del__(self):
39
+ try:
40
+ self.close()
41
+ except Exception:
42
+ pass
43
+
44
+ @property
45
+ def id(self) -> str:
46
+ return self._id
47
+
48
+ @property
49
+ def metadata(self) -> SharedObjectMetadata:
50
+ return SharedObjectMetadata(self.id, mempool=self._mempool)
51
+
52
+ @property
53
+ def mmap(self) -> mmap.mmap:
54
+ return self._mmap
55
+
56
+ @property
57
+ def _path(self) -> pathlib.Path:
58
+ return pathlib.Path(self._mempool.path, self.id)
59
+
60
+ def close(self):
61
+ self.metadata.close()
62
+ self._mmap.close()
63
+ self._file.close()
64
+
65
+ def refresh(self) -> Self:
66
+ if self._size != self.metadata.size or self._md5 != self.metadata.md5:
67
+ self._mmap.close()
68
+ self._file.close()
69
+ self._file = open(self._path / "dump", "r+b")
70
+ self._mmap = mmap.mmap(self._file.fileno(), 0)
71
+ self._size = self.metadata.size
72
+ self._md5 = self.metadata.md5
73
+ return self
74
+
75
+
76
+ class SharedObjectMetadata:
77
+ _id: str
78
+ _mempool: MemoryPool
79
+ _file: BinaryIO
80
+ _mmap: mmap.mmap
81
+ _instances: dict[str, SharedObjectMetadata] = {}
82
+
83
+ def __new__(cls, id: str, *, mempool: MemoryPool):
84
+ if id in cls._instances:
85
+ return cls._instances[id]
86
+ return super().__new__(cls)
87
+
88
+ def __init__(self, id: str, mempool: MemoryPool):
89
+ self._id = id
90
+ self._mempool = mempool
91
+ self._file = open(self._path / "meta", "r+b")
92
+ self._mmap = mmap.mmap(self._file.fileno(), 0)
93
+ self._instances[id] = self
94
+
95
+ def __del__(self):
96
+ try:
97
+ self.close()
98
+ except Exception:
99
+ pass
100
+
101
+ @property
102
+ def id(self) -> str:
103
+ return self._id
104
+
105
+ @property
106
+ def mutable(self) -> bool:
107
+ return self._metadata.mutable
108
+
109
+ @property
110
+ def size(self) -> int:
111
+ return self._metadata.size
112
+
113
+ @property
114
+ def md5(self) -> bytes:
115
+ return self._metadata.md5
116
+
117
+ @property
118
+ def mmap(self) -> mmap.mmap:
119
+ return self._mmap
120
+
121
+ @property
122
+ def _path(self) -> pathlib.Path:
123
+ return pathlib.Path(self._mempool.path, self.id)
124
+
125
+ @property
126
+ def _metadata(self) -> MetadataMessage:
127
+ return MetadataMessage.loads(bytes(self._mmap))
128
+
129
+ def close(self):
130
+ self._mmap.close()
131
+ self._file.close()
132
+ del self._instances[self.id]
133
+
134
+
135
+ class MemoryPool:
136
+ _objects: dict[str, SharedObject]
137
+ _path: pathlib.Path
138
+
139
+ def __init__(self, path: str | pathlib.Path = pathlib.Path(".mempool")):
140
+ if isinstance(path, str):
141
+ self._path = pathlib.Path(path)
142
+ else:
143
+ self._path = path
144
+ self._lockdir = self._path / "locks"
145
+ os.makedirs(self._lockdir, exist_ok=True)
146
+ self._acquire(f"pid-{os.getpid()}")
147
+ self._objects = dict()
148
+
149
+ def __contains__(self, ref: str) -> bool:
150
+ return ref in self._objects
151
+
152
+ def __del__(self):
153
+ self._release(f"pid-{os.getpid()}")
154
+
155
+ @property
156
+ def path(self) -> pathlib.Path:
157
+ return self._path
158
+
159
+ async def map(self, ref: str | None = None):
160
+ if ref is not None and ref not in self._objects:
161
+ if self._locked(f"delete-{ref}"):
162
+ raise RuntimeError(
163
+ f"Reference {ref} is currently locked for deletion"
164
+ )
165
+ async with self._reference_lock(ref):
166
+ self._map(ref)
167
+ else:
168
+ for entry in os.scandir(self._path):
169
+ if entry.is_dir() and (ref := entry.name) != "locks":
170
+ if not self._locked(f"delete-{ref}"):
171
+ async with self._reference_lock(ref):
172
+ self._map(ref)
173
+
174
+ async def put(
175
+ self, dump: bytes, *, mutable: bool = False, ref: str | None = None
176
+ ) -> str:
177
+ ref = ref or str(shortuuid.uuid())
178
+ async with self._reference_lock(ref):
179
+ self._put(ref, dump, mutable=mutable, exist_ok=False)
180
+ return ref
181
+
182
+ async def post(self, ref: str, dump: bytes) -> bool:
183
+ if self._locked(f"delete-{ref}"):
184
+ raise RuntimeError(
185
+ f"Reference {ref} is currently locked for deletion"
186
+ )
187
+ async with self._reference_lock(ref):
188
+ if ref not in self._objects:
189
+ self._map(ref)
190
+ obj = self._objects[ref]
191
+ if not obj.metadata.mutable:
192
+ raise ValueError("Cannot modify an immutable reference")
193
+ if (size := len(dump)) != obj.metadata.size:
194
+ try:
195
+ obj.mmap.resize(size)
196
+ self._post(ref, obj, dump)
197
+ except SystemError:
198
+ self._put(ref, dump, mutable=True, exist_ok=True)
199
+ return True
200
+ elif hashlib.md5(dump).digest() != obj.metadata.md5:
201
+ self._post(ref, obj, dump)
202
+ return True
203
+ else:
204
+ return False
205
+
206
+ async def get(self, ref: str) -> bytes:
207
+ if self._locked(f"delete-{ref}"):
208
+ raise RuntimeError(
209
+ f"Reference {ref} is currently locked for deletion"
210
+ )
211
+ async with self._reference_lock(ref):
212
+ if ref not in self._objects:
213
+ self._map(ref)
214
+ return bytes(self._objects[ref].refresh().mmap)
215
+
216
+ async def delete(self, ref: str):
217
+ async with self._delete_lock(ref):
218
+ async with self._reference_lock(ref):
219
+ if ref not in self._objects:
220
+ self._map(ref)
221
+ self._objects.pop(ref).close()
222
+ try:
223
+ shutil.rmtree(self.path / ref)
224
+ except FileNotFoundError:
225
+ pass
226
+
227
+ def _put(
228
+ self,
229
+ ref: str,
230
+ dump: bytes,
231
+ *,
232
+ mutable: bool = False,
233
+ exist_ok: bool = False,
234
+ ):
235
+ metadata = MetadataMessage(
236
+ ref=ref,
237
+ mutable=mutable,
238
+ size=len(dump),
239
+ md5=hashlib.md5(dump).digest(),
240
+ )
241
+
242
+ refpath = pathlib.Path(self._path, f"{ref}")
243
+ os.makedirs(refpath, exist_ok=exist_ok)
244
+
245
+ with open(refpath / "meta", "wb") as metafile:
246
+ metafile.write(metadata.dumps())
247
+
248
+ with open(refpath / "dump", "wb") as dumpfile:
249
+ dumpfile.write(dump)
250
+
251
+ self._map(ref)
252
+
253
+ def _post(self, ref: str, obj: SharedObject, dump: bytes):
254
+ if not obj.metadata.mutable:
255
+ raise ValueError("Cannot modify an immutable reference")
256
+ metadata = MetadataMessage(
257
+ ref=ref,
258
+ mutable=True,
259
+ size=len(dump),
260
+ md5=hashlib.md5(dump).digest(),
261
+ )
262
+ obj.metadata.mmap[:] = metadata.dumps()
263
+ obj.metadata.mmap.flush()
264
+ obj.mmap.seek(0)
265
+ obj.mmap.write(dump)
266
+
267
+ def _map(self, ref: str):
268
+ obj = self._objects.pop(ref, None)
269
+ if obj:
270
+ obj.close()
271
+ self._objects[ref] = SharedObject(id=ref, mempool=self)
272
+
273
+ def _lockpath(self, key: str) -> pathlib.Path:
274
+ return pathlib.Path(self._lockdir, f"{key}.lock")
275
+
276
+ def _acquire(self, key: str) -> bool:
277
+ try:
278
+ os.symlink(f"{key}", self._lockpath(key))
279
+ return True
280
+ except FileExistsError:
281
+ return False
282
+
283
+ def _release(self, key: str):
284
+ try:
285
+ if os.path.islink(lock_path := self._lockpath(key)):
286
+ os.unlink(lock_path)
287
+ except FileNotFoundError:
288
+ pass
289
+
290
+ def _locked(self, key: str) -> bool:
291
+ return os.path.islink(self._lockpath(key))
292
+
293
+ @asynccontextmanager
294
+ async def _reference_lock(self, ref: str):
295
+ try:
296
+ while not self._acquire(ref):
297
+ await asyncio.sleep(0)
298
+ yield
299
+ finally:
300
+ self._release(ref)
301
+
302
+ @asynccontextmanager
303
+ async def _delete_lock(self, ref: str):
304
+ key = f"delete-{ref}"
305
+ if not self._acquire(f"delete-{ref}"):
306
+ raise RuntimeError(
307
+ f"Reference {ref} is currently locked for deletion"
308
+ )
309
+ else:
310
+ yield
311
+ self._release(key)
@@ -0,0 +1,39 @@
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 _MetadataMessage
8
+ except ImportError:
9
+ logging.error(
10
+ "Failed to import _MetadataMessage. "
11
+ "Ensure protocol buffers are compiled."
12
+ )
13
+ raise
14
+
15
+
16
+ @dataclass
17
+ class MetadataMessage:
18
+ ref: str
19
+ mutable: bool
20
+ size: int
21
+ md5: bytes
22
+
23
+ @classmethod
24
+ def loads(cls, data: bytes) -> MetadataMessage:
25
+ (metadata := _MetadataMessage()).ParseFromString(data)
26
+ return cls(
27
+ ref=metadata.ref,
28
+ mutable=metadata.mutable,
29
+ size=metadata.size,
30
+ md5=metadata.md5,
31
+ )
32
+
33
+ def dumps(self) -> bytes:
34
+ return _MetadataMessage(
35
+ ref=self.ref, mutable=self.mutable, size=self.size, md5=self.md5
36
+ ).SerializeToString()
37
+
38
+
39
+ __all__ = ["MetadataMessage"]