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/_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,3 @@
1
+ from wool._mempool._mempool import MemoryPool
2
+
3
+ __all__ = ["MemoryPool"]
@@ -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"]