wool 0.1rc7__py3-none-any.whl → 0.1rc9__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/_mempool/__init__.py +2 -1
- wool/_mempool/_client.py +167 -0
- wool/_mempool/_mempool.py +215 -108
- wool/_mempool/{_metadata/__init__.py → _metadata.py} +8 -14
- wool/_mempool/_service.py +227 -0
- wool/_protobuf/__init__.py +11 -0
- wool/_protobuf/{_mempool/_metadata/_metadata_pb2.py → mempool/metadata_pb2.py} +8 -8
- wool/_protobuf/{_mempool/_metadata/_metadata_pb2.pyi → mempool/metadata_pb2.pyi} +1 -1
- wool/_protobuf/mempool/metadata_pb2_grpc.py +24 -0
- wool/_protobuf/mempool/service_pb2.py +66 -0
- wool/_protobuf/mempool/service_pb2.pyi +108 -0
- wool/_protobuf/mempool/service_pb2_grpc.py +355 -0
- {wool-0.1rc7.dist-info → wool-0.1rc9.dist-info}/METADATA +8 -2
- wool-0.1rc9.dist-info/RECORD +29 -0
- wool/_protobuf/.gitkeep +0 -0
- wool-0.1rc7.dist-info/RECORD +0 -23
- {wool-0.1rc7.dist-info → wool-0.1rc9.dist-info}/WHEEL +0 -0
- {wool-0.1rc7.dist-info → wool-0.1rc9.dist-info}/entry_points.txt +0 -0
wool/_mempool/__init__.py
CHANGED
wool/_mempool/_client.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Final
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from typing import Self
|
|
9
|
+
except ImportError:
|
|
10
|
+
from typing_extensions import Self
|
|
11
|
+
|
|
12
|
+
import grpc
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from wool._protobuf.mempool import service_pb2 as pb
|
|
16
|
+
from wool._protobuf.mempool import service_pb2_grpc as rpc
|
|
17
|
+
except ImportError as e:
|
|
18
|
+
from wool._protobuf import ProtobufImportError
|
|
19
|
+
|
|
20
|
+
raise ProtobufImportError(e) from e
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EventHandler(Protocol):
|
|
24
|
+
@staticmethod
|
|
25
|
+
def __call__(client: MemoryPoolClient, response: pb.Event) -> None: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MemoryPoolClient:
|
|
29
|
+
_event_stream: Final[grpc.aio.UnaryStreamCall]
|
|
30
|
+
_listener: asyncio.Task
|
|
31
|
+
_session: pb.Session | None
|
|
32
|
+
|
|
33
|
+
def __init__(self, channel: grpc.aio.Channel, event_handler: EventHandler):
|
|
34
|
+
self._stub = rpc.MemoryPoolStub(channel)
|
|
35
|
+
self._event_stream = self._stub.session(pb.SessionRequest())
|
|
36
|
+
self._event_handler = event_handler
|
|
37
|
+
self._session = None
|
|
38
|
+
|
|
39
|
+
async def __aenter__(self) -> Self:
|
|
40
|
+
if self._session:
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
"Client session may not be re-entered. "
|
|
43
|
+
"Use 'async with MemoryPoolClient(...)' only once."
|
|
44
|
+
)
|
|
45
|
+
response = await self._event_stream.read()
|
|
46
|
+
assert isinstance(response, pb.SessionResponse)
|
|
47
|
+
self._session = response.session
|
|
48
|
+
self._listener = asyncio.create_task(self._listen())
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
async def __aexit__(self, *_):
|
|
52
|
+
if not self._session:
|
|
53
|
+
raise RuntimeError(
|
|
54
|
+
"Client session has not been entered. "
|
|
55
|
+
"Use 'async with MemoryPoolClient(...)'."
|
|
56
|
+
)
|
|
57
|
+
assert self._listener
|
|
58
|
+
self._listener.cancel()
|
|
59
|
+
try:
|
|
60
|
+
await self._listener
|
|
61
|
+
except asyncio.CancelledError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def __del__(self):
|
|
65
|
+
try:
|
|
66
|
+
self._event_stream.cancel()
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def id(self) -> str:
|
|
72
|
+
if not self._session:
|
|
73
|
+
raise RuntimeError(
|
|
74
|
+
"Client session has not been entered. "
|
|
75
|
+
"Use 'async with MemoryPoolClient(...)'."
|
|
76
|
+
)
|
|
77
|
+
return self._session.id
|
|
78
|
+
|
|
79
|
+
async def map(self, ref: str):
|
|
80
|
+
if not self._session:
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
"Client session has not been entered. "
|
|
83
|
+
"Use 'async with MemoryPoolClient(...)'."
|
|
84
|
+
)
|
|
85
|
+
request = pb.AcquireRequest(
|
|
86
|
+
session=self._session,
|
|
87
|
+
reference=pb.Reference(id=ref),
|
|
88
|
+
)
|
|
89
|
+
await self._stub.map(request)
|
|
90
|
+
|
|
91
|
+
async def get(self, ref: str) -> bytes:
|
|
92
|
+
if not self._session:
|
|
93
|
+
raise RuntimeError(
|
|
94
|
+
"Client session has not been entered. "
|
|
95
|
+
"Use 'async with MemoryPoolClient(...)'."
|
|
96
|
+
)
|
|
97
|
+
request = pb.GetRequest(
|
|
98
|
+
session=self._session,
|
|
99
|
+
reference=pb.Reference(id=ref),
|
|
100
|
+
)
|
|
101
|
+
response: pb.GetResponse = await self._stub.get(request)
|
|
102
|
+
return response.dump
|
|
103
|
+
|
|
104
|
+
async def put(self, dump: bytes, *, mutable: bool = False) -> str:
|
|
105
|
+
if not self._session:
|
|
106
|
+
raise RuntimeError(
|
|
107
|
+
"Client session has not been entered. "
|
|
108
|
+
"Use 'async with MemoryPoolClient(...)'."
|
|
109
|
+
)
|
|
110
|
+
request = pb.PutRequest(
|
|
111
|
+
session=self._session,
|
|
112
|
+
dump=dump,
|
|
113
|
+
mutable=mutable,
|
|
114
|
+
)
|
|
115
|
+
response: pb.PutResponse = await self._stub.put(request)
|
|
116
|
+
return response.reference.id
|
|
117
|
+
|
|
118
|
+
async def post(self, ref: str, dump: bytes) -> bool:
|
|
119
|
+
if not self._session:
|
|
120
|
+
raise RuntimeError(
|
|
121
|
+
"Client session has not been entered. "
|
|
122
|
+
"Use 'async with MemoryPoolClient(...)'."
|
|
123
|
+
)
|
|
124
|
+
request = pb.PostRequest(
|
|
125
|
+
session=self._session,
|
|
126
|
+
reference=pb.Reference(id=ref),
|
|
127
|
+
dump=dump,
|
|
128
|
+
)
|
|
129
|
+
response: pb.PostResponse = await self._stub.post(request)
|
|
130
|
+
return response.updated
|
|
131
|
+
|
|
132
|
+
async def acquire(self, ref: str):
|
|
133
|
+
if not self._session:
|
|
134
|
+
raise RuntimeError(
|
|
135
|
+
"Client session has not been entered. "
|
|
136
|
+
"Use 'async with MemoryPoolClient(...)'."
|
|
137
|
+
)
|
|
138
|
+
request = pb.AcquireRequest(
|
|
139
|
+
session=self._session,
|
|
140
|
+
reference=pb.Reference(id=ref),
|
|
141
|
+
)
|
|
142
|
+
await self._stub.acquire(request)
|
|
143
|
+
|
|
144
|
+
async def release(self, ref: str):
|
|
145
|
+
if not self._session:
|
|
146
|
+
raise RuntimeError(
|
|
147
|
+
"Client session has not been entered. "
|
|
148
|
+
"Use 'async with MemoryPoolClient(...)'."
|
|
149
|
+
)
|
|
150
|
+
request = pb.ReleaseRequest(
|
|
151
|
+
session=self._session,
|
|
152
|
+
reference=pb.Reference(id=ref),
|
|
153
|
+
)
|
|
154
|
+
await self._stub.release(request)
|
|
155
|
+
|
|
156
|
+
async def _listen(self):
|
|
157
|
+
assert self._event_stream
|
|
158
|
+
try:
|
|
159
|
+
while True:
|
|
160
|
+
if (response := await self._event_stream.read()) is None:
|
|
161
|
+
break
|
|
162
|
+
assert isinstance(response, pb.SessionResponse), (
|
|
163
|
+
f"Unexpected event type: {type(response)}"
|
|
164
|
+
)
|
|
165
|
+
self._event_handler(self, response.event)
|
|
166
|
+
except asyncio.CancelledError:
|
|
167
|
+
self._event_stream.cancel()
|
wool/_mempool/_mempool.py
CHANGED
|
@@ -1,34 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
import hashlib
|
|
3
|
-
import mmap
|
|
4
5
|
import os
|
|
5
6
|
import pathlib
|
|
6
7
|
import shutil
|
|
7
|
-
from collections import namedtuple
|
|
8
8
|
from contextlib import asynccontextmanager
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
|
|
9
|
+
from mmap import mmap
|
|
10
|
+
from typing import BinaryIO
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from typing import Self
|
|
14
|
+
except ImportError:
|
|
15
|
+
from typing_extensions import Self
|
|
12
16
|
|
|
13
17
|
import shortuuid
|
|
14
18
|
|
|
15
19
|
from wool._mempool._metadata import MetadataMessage
|
|
16
20
|
|
|
17
|
-
Metadata = namedtuple(
|
|
18
|
-
"Metadata",
|
|
19
|
-
[field.name for field in fields(MetadataMessage)],
|
|
20
|
-
)
|
|
21
21
|
|
|
22
|
+
class SharedObject:
|
|
23
|
+
_id: str
|
|
24
|
+
_mempool: MemoryPool
|
|
25
|
+
_file: BinaryIO
|
|
26
|
+
_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(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:
|
|
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(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
|
|
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(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:
|
|
119
|
+
return self._mmap
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def _path(self) -> pathlib.Path:
|
|
123
|
+
return pathlib.Path(self._mempool.path, self.id)
|
|
22
124
|
|
|
23
|
-
|
|
24
|
-
def
|
|
25
|
-
self.
|
|
125
|
+
@property
|
|
126
|
+
def _metadata(self) -> MetadataMessage:
|
|
127
|
+
return MetadataMessage.loads(bytes(self._mmap))
|
|
26
128
|
|
|
27
|
-
def
|
|
28
|
-
|
|
129
|
+
def close(self):
|
|
130
|
+
self._mmap.close()
|
|
131
|
+
self._file.close()
|
|
132
|
+
del self._instances[self.id]
|
|
29
133
|
|
|
30
134
|
|
|
31
135
|
class MemoryPool:
|
|
136
|
+
_objects: dict[str, SharedObject]
|
|
137
|
+
_path: pathlib.Path
|
|
138
|
+
|
|
32
139
|
def __init__(self, path: str | pathlib.Path = pathlib.Path(".mempool")):
|
|
33
140
|
if isinstance(path, str):
|
|
34
141
|
self._path = pathlib.Path(path)
|
|
@@ -36,89 +143,86 @@ class MemoryPool:
|
|
|
36
143
|
self._path = path
|
|
37
144
|
self._lockdir = self._path / "locks"
|
|
38
145
|
os.makedirs(self._lockdir, exist_ok=True)
|
|
39
|
-
self.
|
|
40
|
-
self.
|
|
41
|
-
self._metadata: dict[str, MetadataMessage] = {}
|
|
146
|
+
self._acquire(f"pid-{os.getpid()}")
|
|
147
|
+
self._objects = dict()
|
|
42
148
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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()}")
|
|
46
154
|
|
|
47
155
|
@property
|
|
48
156
|
def path(self) -> pathlib.Path:
|
|
49
157
|
return self._path
|
|
50
158
|
|
|
51
|
-
async def map(self):
|
|
52
|
-
|
|
53
|
-
if
|
|
54
|
-
|
|
55
|
-
|
|
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)
|
|
56
173
|
|
|
57
174
|
async def put(
|
|
58
175
|
self, dump: bytes, *, mutable: bool = False, ref: str | None = None
|
|
59
176
|
) -> str:
|
|
60
|
-
|
|
177
|
+
ref = ref or str(shortuuid.uuid())
|
|
178
|
+
async with self._reference_lock(ref):
|
|
61
179
|
self._put(ref, dump, mutable=mutable, exist_ok=False)
|
|
62
180
|
return ref
|
|
63
181
|
|
|
64
|
-
async def post(self, ref: str, dump: bytes):
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
metamap, dumpmap = self._mmaps[ref]
|
|
69
|
-
metamap.seek(0)
|
|
70
|
-
metadata = self._metadata.setdefault(
|
|
71
|
-
ref, MetadataMessage.loads(metamap.read())
|
|
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"
|
|
72
186
|
)
|
|
73
|
-
|
|
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:
|
|
74
192
|
raise ValueError("Cannot modify an immutable reference")
|
|
75
|
-
if (size := len(dump)) != metadata.size:
|
|
193
|
+
if (size := len(dump)) != obj.metadata.size:
|
|
76
194
|
try:
|
|
77
|
-
|
|
78
|
-
self._post(ref,
|
|
195
|
+
obj.mmap.resize(size)
|
|
196
|
+
self._post(ref, obj, dump)
|
|
79
197
|
except SystemError:
|
|
80
198
|
self._put(ref, dump, mutable=True, exist_ok=True)
|
|
81
199
|
return True
|
|
82
|
-
elif hashlib.md5(dump).digest() != metadata.md5:
|
|
83
|
-
self._post(ref,
|
|
200
|
+
elif hashlib.md5(dump).digest() != obj.metadata.md5:
|
|
201
|
+
self._post(ref, obj, dump)
|
|
84
202
|
return True
|
|
85
203
|
else:
|
|
86
204
|
return False
|
|
87
205
|
|
|
88
206
|
async def get(self, ref: str) -> bytes:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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:
|
|
98
213
|
self._map(ref)
|
|
99
|
-
|
|
100
|
-
return dumpmap.read()
|
|
214
|
+
return bytes(self._objects[ref].refresh().mmap)
|
|
101
215
|
|
|
102
216
|
async def delete(self, ref: str):
|
|
103
|
-
async with self.
|
|
104
|
-
|
|
105
|
-
self.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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
|
|
122
226
|
|
|
123
227
|
def _put(
|
|
124
228
|
self,
|
|
@@ -145,60 +249,63 @@ class MemoryPool:
|
|
|
145
249
|
dumpfile.write(dump)
|
|
146
250
|
|
|
147
251
|
self._map(ref)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
metadata
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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)
|
|
160
266
|
|
|
161
267
|
def _map(self, ref: str):
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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):
|
|
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:
|
|
184
277
|
try:
|
|
185
|
-
os.symlink(f"{
|
|
278
|
+
os.symlink(f"{key}", self._lockpath(key))
|
|
186
279
|
return True
|
|
187
280
|
except FileExistsError:
|
|
188
281
|
return False
|
|
189
282
|
|
|
190
|
-
def _release(self,
|
|
283
|
+
def _release(self, key: str):
|
|
191
284
|
try:
|
|
192
|
-
if os.path.islink(lock_path := self._lockpath(
|
|
285
|
+
if os.path.islink(lock_path := self._lockpath(key)):
|
|
193
286
|
os.unlink(lock_path)
|
|
194
287
|
except FileNotFoundError:
|
|
195
288
|
pass
|
|
196
289
|
|
|
290
|
+
def _locked(self, key: str) -> bool:
|
|
291
|
+
return os.path.islink(self._lockpath(key))
|
|
292
|
+
|
|
197
293
|
@asynccontextmanager
|
|
198
|
-
async def
|
|
294
|
+
async def _reference_lock(self, ref: str):
|
|
199
295
|
try:
|
|
200
296
|
while not self._acquire(ref):
|
|
201
297
|
await asyncio.sleep(0)
|
|
202
298
|
yield
|
|
203
299
|
finally:
|
|
204
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)
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from dataclasses import asdict
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
|
|
6
6
|
try:
|
|
7
|
-
from wool._protobuf.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"Failed to import _MetadataMessage. "
|
|
13
|
-
"Ensure protocol buffers are compiled."
|
|
14
|
-
)
|
|
15
|
-
raise
|
|
7
|
+
from wool._protobuf.mempool import metadata_pb2 as pb
|
|
8
|
+
except ImportError as e:
|
|
9
|
+
from wool._protobuf import ProtobufImportError
|
|
10
|
+
|
|
11
|
+
raise ProtobufImportError(e) from e
|
|
16
12
|
|
|
17
13
|
|
|
18
14
|
@dataclass
|
|
@@ -24,7 +20,7 @@ class MetadataMessage:
|
|
|
24
20
|
|
|
25
21
|
@classmethod
|
|
26
22
|
def loads(cls, data: bytes) -> MetadataMessage:
|
|
27
|
-
(metadata :=
|
|
23
|
+
(metadata := pb.MetadataMessage()).ParseFromString(data)
|
|
28
24
|
return cls(
|
|
29
25
|
ref=metadata.ref,
|
|
30
26
|
mutable=metadata.mutable,
|
|
@@ -33,9 +29,7 @@ class MetadataMessage:
|
|
|
33
29
|
)
|
|
34
30
|
|
|
35
31
|
def dumps(self) -> bytes:
|
|
36
|
-
return
|
|
37
|
-
ref=self.ref, mutable=self.mutable, size=self.size, md5=self.md5
|
|
38
|
-
).SerializeToString()
|
|
32
|
+
return pb.MetadataMessage(**asdict(self)).SerializeToString()
|
|
39
33
|
|
|
40
34
|
|
|
41
35
|
__all__ = ["MetadataMessage"]
|