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/__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 +4 -0
- wool/_mempool/_mempool.py +311 -0
- wool/_mempool/_metadata.py +39 -0
- wool/_mempool/_service.py +225 -0
- wool/_pool.py +357 -149
- wool/_protobuf/__init__.py +4 -0
- wool/_protobuf/mempool/mempool_pb2.py +66 -0
- wool/_protobuf/mempool/mempool_pb2.pyi +108 -0
- wool/_protobuf/mempool/mempool_pb2_grpc.py +312 -0
- wool/_protobuf/mempool/metadata/metadata_pb2.py +36 -0
- wool/_protobuf/mempool/metadata/metadata_pb2.pyi +17 -0
- wool/_protobuf/mempool/metadata/metadata_pb2_grpc.py +24 -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.1rc8.dist-info/METADATA +348 -0
- wool-0.1rc8.dist-info/RECORD +28 -0
- {wool-0.1rc6.dist-info → wool-0.1rc8.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.1rc8.dist-info}/entry_points.txt +0 -0
wool/_task.py
CHANGED
|
@@ -4,57 +4,115 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
import traceback
|
|
6
6
|
from collections.abc import Callable
|
|
7
|
-
from contextvars import Context
|
|
7
|
+
from contextvars import Context
|
|
8
|
+
from contextvars import ContextVar
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
from functools import wraps
|
|
10
11
|
from sys import modules
|
|
11
|
-
from
|
|
12
|
+
from types import ModuleType
|
|
12
13
|
from types import TracebackType
|
|
13
|
-
from typing import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
from uuid import UUID
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
from wool.
|
|
26
|
-
from wool.
|
|
27
|
-
from wool.
|
|
14
|
+
from typing import Coroutine
|
|
15
|
+
from typing import Dict
|
|
16
|
+
from typing import ParamSpec
|
|
17
|
+
from typing import Protocol
|
|
18
|
+
from typing import SupportsInt
|
|
19
|
+
from typing import Tuple
|
|
20
|
+
from typing import Type
|
|
21
|
+
from typing import TypeVar
|
|
22
|
+
from typing import cast
|
|
23
|
+
from uuid import UUID
|
|
24
|
+
from uuid import uuid4
|
|
25
|
+
|
|
26
|
+
from wool._event import TaskEvent
|
|
27
|
+
from wool._future import Future
|
|
28
|
+
from wool._pool import WorkerPoolSession
|
|
29
|
+
from wool._session import current_session
|
|
28
30
|
|
|
29
31
|
AsyncCallable = Callable[..., Coroutine]
|
|
30
32
|
C = TypeVar("C", bound=AsyncCallable)
|
|
31
33
|
|
|
32
|
-
Args =
|
|
33
|
-
Kwargs =
|
|
34
|
-
Timeout =
|
|
35
|
-
Timestamp =
|
|
34
|
+
Args = Tuple
|
|
35
|
+
Kwargs = Dict
|
|
36
|
+
Timeout = SupportsInt
|
|
37
|
+
Timestamp = SupportsInt
|
|
36
38
|
|
|
37
39
|
|
|
40
|
+
# PUBLIC
|
|
38
41
|
def task(fn: C) -> C:
|
|
39
42
|
"""
|
|
40
|
-
A decorator to declare
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
A decorator to declare an asynchronous function as remotely executable by a
|
|
44
|
+
worker pool. When the wrapped function is invoked, it is dispatched to the
|
|
45
|
+
worker pool associated with the current worker pool session context.
|
|
46
|
+
|
|
47
|
+
Tasks behave like coroutines, meaning they can be awaited as well as
|
|
48
|
+
cancelled.
|
|
49
|
+
|
|
50
|
+
:param fn: The task function.
|
|
51
|
+
:return: A Wool task declaration.
|
|
52
|
+
|
|
53
|
+
**Best practices and considerations for designing tasks:**
|
|
54
|
+
|
|
55
|
+
1. **Picklability**:
|
|
56
|
+
- Task arguments and return values must be picklable, as they are
|
|
57
|
+
serialized and transferred between processes. Avoid passing
|
|
58
|
+
unpicklable objects such as open file handles, database
|
|
59
|
+
connections, or lambda functions.
|
|
60
|
+
- Ensure that any custom objects used as arguments or return values
|
|
61
|
+
implement the necessary methods for pickling (e.g.,
|
|
62
|
+
``__getstate__`` and ``__setstate__``).
|
|
63
|
+
|
|
64
|
+
2. **Synchronization**:
|
|
65
|
+
- Tasks are not guaranteed to execute on the same process between
|
|
66
|
+
invocations. Each invocation may run on a different worker process.
|
|
67
|
+
- Standard ``asyncio`` synchronization primitives (e.g.,
|
|
68
|
+
``asyncio.Lock``) will not behave as expected in a multi-process
|
|
69
|
+
environment, as they are designed for single-process applications.
|
|
70
|
+
Use the specialized ``wool.locking`` synchronization primitives to
|
|
71
|
+
achieve inter-worker and inter-pool synchronization.
|
|
72
|
+
|
|
73
|
+
3. **Statelessness and idempotency**:
|
|
74
|
+
- Design tasks to be stateless and idemptoent. Avoid relying on global
|
|
75
|
+
variables or shared mutable state. This ensures predictable
|
|
76
|
+
behavior, avoids race conditions, and enables safe retries.
|
|
77
|
+
|
|
78
|
+
4. **Cancellation**:
|
|
79
|
+
- Task cancellation and propagation thereof mimics that of standard
|
|
80
|
+
Python coroutines.
|
|
81
|
+
|
|
82
|
+
5. **Error propagation**:
|
|
83
|
+
- Wool makes every effort to execute tasks transparently to the user,
|
|
84
|
+
and this includes error propagation. Unhandled exceptions raised
|
|
85
|
+
within a task will be propagated to the caller as they would
|
|
86
|
+
normally.
|
|
87
|
+
|
|
88
|
+
6. **Performance**:
|
|
89
|
+
- Minimize the size of arguments and return values to reduce
|
|
90
|
+
serialization overhead.
|
|
91
|
+
- For large datasets, consider using shared memory or passing
|
|
92
|
+
references (e.g., file paths) instead of transferring the entire
|
|
93
|
+
data.
|
|
94
|
+
|
|
95
|
+
**Usage**::
|
|
96
|
+
|
|
97
|
+
.. code-block:: python
|
|
98
|
+
|
|
99
|
+
import wool
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@wool.task
|
|
103
|
+
async def foo(...):
|
|
104
|
+
...
|
|
47
105
|
"""
|
|
48
106
|
|
|
49
107
|
@wraps(fn)
|
|
50
108
|
def wrapper(
|
|
51
109
|
*args,
|
|
52
110
|
__wool_remote__: bool = False,
|
|
53
|
-
|
|
111
|
+
__wool_session__: WorkerPoolSession | None = None,
|
|
54
112
|
**kwargs,
|
|
55
113
|
) -> Coroutine:
|
|
56
114
|
# Handle static and class methods in a picklable way.
|
|
57
|
-
parent, function =
|
|
115
|
+
parent, function = _resolve(fn)
|
|
58
116
|
assert parent is not None
|
|
59
117
|
assert callable(function)
|
|
60
118
|
|
|
@@ -64,7 +122,7 @@ def task(fn: C) -> C:
|
|
|
64
122
|
else:
|
|
65
123
|
# Otherwise, submit the task to the pool.
|
|
66
124
|
return _put(
|
|
67
|
-
|
|
125
|
+
__wool_session__ or current_session(),
|
|
68
126
|
wrapper.__module__,
|
|
69
127
|
wrapper.__qualname__,
|
|
70
128
|
function,
|
|
@@ -76,14 +134,15 @@ def task(fn: C) -> C:
|
|
|
76
134
|
|
|
77
135
|
|
|
78
136
|
def _put(
|
|
79
|
-
|
|
137
|
+
session: WorkerPoolSession,
|
|
80
138
|
module: str,
|
|
81
139
|
qualname: str,
|
|
82
140
|
function: AsyncCallable,
|
|
83
141
|
*args,
|
|
84
142
|
**kwargs,
|
|
85
143
|
) -> Coroutine:
|
|
86
|
-
|
|
144
|
+
if not session.connected:
|
|
145
|
+
session.connect()
|
|
87
146
|
|
|
88
147
|
# Skip self argument if function is a method.
|
|
89
148
|
_args = args[1:] if hasattr(function, "__self__") else args
|
|
@@ -98,28 +157,29 @@ def _put(
|
|
|
98
157
|
# pool, so we set the `__wool_remote__` flag to true.
|
|
99
158
|
kwargs["__wool_remote__"] = True
|
|
100
159
|
|
|
101
|
-
task =
|
|
160
|
+
task = Task(
|
|
102
161
|
id=uuid4(),
|
|
103
162
|
callable=function,
|
|
104
163
|
args=args,
|
|
105
164
|
kwargs=kwargs,
|
|
106
165
|
tag=f"{module}.{qualname}({signature})",
|
|
107
166
|
)
|
|
108
|
-
assert isinstance(
|
|
109
|
-
future:
|
|
167
|
+
assert isinstance(session, WorkerPoolSession)
|
|
168
|
+
future: Future = session.put(task)
|
|
110
169
|
|
|
111
|
-
|
|
170
|
+
@wraps(function)
|
|
171
|
+
async def coroutine(future: Future):
|
|
112
172
|
try:
|
|
113
173
|
while not future.done():
|
|
114
174
|
await asyncio.sleep(0)
|
|
115
175
|
else:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
raise Exception().with_traceback(e.__traceback__) from e
|
|
176
|
+
return future.result()
|
|
177
|
+
except ConnectionResetError as e:
|
|
178
|
+
raise asyncio.CancelledError from e
|
|
120
179
|
except asyncio.CancelledError:
|
|
121
|
-
|
|
122
|
-
|
|
180
|
+
if not future.done():
|
|
181
|
+
logging.debug("Cancelling...")
|
|
182
|
+
future.cancel()
|
|
123
183
|
raise
|
|
124
184
|
|
|
125
185
|
return coroutine(future)
|
|
@@ -133,50 +193,78 @@ def _execute(fn: AsyncCallable, parent, *args, **kwargs):
|
|
|
133
193
|
|
|
134
194
|
|
|
135
195
|
# PUBLIC
|
|
136
|
-
def current_task() ->
|
|
196
|
+
def current_task() -> Task | None:
|
|
137
197
|
"""
|
|
138
198
|
Get the current task from the context variable if we are inside a task
|
|
139
199
|
context, otherwise return None.
|
|
200
|
+
|
|
201
|
+
:return: The current task or None if no task is active.
|
|
140
202
|
"""
|
|
141
203
|
return _current_task.get()
|
|
142
204
|
|
|
143
205
|
|
|
144
206
|
# PUBLIC
|
|
145
207
|
@dataclass
|
|
146
|
-
class
|
|
208
|
+
class Task:
|
|
209
|
+
"""
|
|
210
|
+
Represents a task to be executed in the worker pool.
|
|
211
|
+
|
|
212
|
+
:param id: The unique identifier for the task.
|
|
213
|
+
:param callable: The asynchronous function to execute.
|
|
214
|
+
:param args: Positional arguments for the function.
|
|
215
|
+
:param kwargs: Keyword arguments for the function.
|
|
216
|
+
:param timeout: The timeout for the task in seconds.
|
|
217
|
+
Defaults to 0 (no timeout).
|
|
218
|
+
:param caller: The ID of the calling task, if any.
|
|
219
|
+
:param exception: The exception raised during task execution, if any.
|
|
220
|
+
:param filename: The filename where the task was defined.
|
|
221
|
+
:param function: The name of the function being executed.
|
|
222
|
+
:param line_no: The line number where the task was defined.
|
|
223
|
+
:param tag: An optional tag for the task.
|
|
224
|
+
"""
|
|
225
|
+
|
|
147
226
|
id: UUID
|
|
148
227
|
callable: AsyncCallable
|
|
149
228
|
args: Args
|
|
150
229
|
kwargs: Kwargs
|
|
151
230
|
timeout: Timeout = 0
|
|
152
231
|
caller: UUID | None = None
|
|
153
|
-
exception:
|
|
232
|
+
exception: TaskException | None = None
|
|
154
233
|
filename: str | None = None
|
|
155
234
|
function: str | None = None
|
|
156
235
|
line_no: int | None = None
|
|
157
236
|
tag: str | None = None
|
|
158
237
|
|
|
159
238
|
def __post_init__(self, **kwargs):
|
|
239
|
+
"""
|
|
240
|
+
Initialize the task and emit a "task-created" event.
|
|
241
|
+
|
|
242
|
+
:param kwargs: Additional keyword arguments.
|
|
243
|
+
"""
|
|
160
244
|
if caller := _current_task.get():
|
|
161
245
|
self.caller = caller.id
|
|
162
|
-
|
|
246
|
+
TaskEvent("task-created", task=self).emit()
|
|
163
247
|
|
|
164
248
|
def __enter__(self) -> Callable[[], Coroutine]:
|
|
165
|
-
|
|
166
|
-
|
|
249
|
+
"""
|
|
250
|
+
Enter the context of the task.
|
|
251
|
+
|
|
252
|
+
:return: The task's run method.
|
|
253
|
+
"""
|
|
254
|
+
logging.debug(f"Entering {self.__class__.__name__} with ID {self.id}")
|
|
167
255
|
return self.run
|
|
168
256
|
|
|
169
257
|
def __exit__(
|
|
170
258
|
self,
|
|
171
|
-
exception_type: type,
|
|
172
|
-
exception_value:
|
|
173
|
-
exception_traceback: TracebackType,
|
|
259
|
+
exception_type: type[BaseException] | None,
|
|
260
|
+
exception_value: BaseException | None,
|
|
261
|
+
exception_traceback: TracebackType | None,
|
|
174
262
|
):
|
|
175
|
-
logging.
|
|
263
|
+
logging.debug(f"Exiting {self.__class__.__name__} with ID {self.id}")
|
|
176
264
|
if exception_value:
|
|
177
265
|
this = asyncio.current_task()
|
|
178
266
|
assert this
|
|
179
|
-
self.exception =
|
|
267
|
+
self.exception = TaskException(
|
|
180
268
|
exception_type.__qualname__,
|
|
181
269
|
traceback=[
|
|
182
270
|
y
|
|
@@ -189,9 +277,14 @@ class WoolTask:
|
|
|
189
277
|
this.add_done_callback(self._finish, context=Context())
|
|
190
278
|
|
|
191
279
|
def _finish(self, _):
|
|
192
|
-
|
|
280
|
+
TaskEvent("task-completed", task=self).emit()
|
|
193
281
|
|
|
194
282
|
def run(self) -> Coroutine:
|
|
283
|
+
"""
|
|
284
|
+
Execute the task's callable with its arguments.
|
|
285
|
+
|
|
286
|
+
:return: A coroutine representing the task execution.
|
|
287
|
+
"""
|
|
195
288
|
work = self._with_task(self.callable)
|
|
196
289
|
return work(*self.args, **self.kwargs)
|
|
197
290
|
|
|
@@ -210,67 +303,29 @@ class WoolTask:
|
|
|
210
303
|
|
|
211
304
|
|
|
212
305
|
# PUBLIC
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
Task events are emitted when a task is created, queued, started, stopped,
|
|
216
|
-
and completed. Tasks can be started and stopped multiple times by the event
|
|
217
|
-
loop. The cumulative time between start and stop events can be used to
|
|
218
|
-
determine a task's CPU utilization.
|
|
306
|
+
@dataclass
|
|
307
|
+
class TaskException:
|
|
219
308
|
"""
|
|
309
|
+
Represents an exception raised during task execution.
|
|
220
310
|
|
|
221
|
-
type:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
_handlers: dict[str, list[WoolTaskEventCallback]] = {}
|
|
225
|
-
|
|
226
|
-
def __init__(self, type: WoolTaskEventType, /, task: WoolTask) -> None:
|
|
227
|
-
self.type = type
|
|
228
|
-
self.task = task
|
|
229
|
-
|
|
230
|
-
@classmethod
|
|
231
|
-
def handler(
|
|
232
|
-
cls, *event_types: WoolTaskEventType
|
|
233
|
-
) -> PassthroughDecorator[WoolTaskEventCallback]:
|
|
234
|
-
def _handler(
|
|
235
|
-
fn: WoolTaskEventCallback,
|
|
236
|
-
) -> WoolTaskEventCallback:
|
|
237
|
-
for event_type in event_types:
|
|
238
|
-
cls._handlers.setdefault(event_type, []).append(fn)
|
|
239
|
-
return fn
|
|
240
|
-
|
|
241
|
-
return _handler
|
|
242
|
-
|
|
243
|
-
def emit(self):
|
|
244
|
-
logging.debug(f"Emitting {self.type} event for task {self.task.id}")
|
|
245
|
-
if handlers := self._handlers.get(self.type):
|
|
246
|
-
timestamp = perf_counter_ns()
|
|
247
|
-
for handler in handlers:
|
|
248
|
-
handler(self, timestamp)
|
|
249
|
-
|
|
311
|
+
:param type: The type of the exception.
|
|
312
|
+
:param traceback: The traceback of the exception.
|
|
313
|
+
"""
|
|
250
314
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
"task-created",
|
|
254
|
-
"task-queued",
|
|
255
|
-
"task-started",
|
|
256
|
-
"task-stopped",
|
|
257
|
-
"task-completed",
|
|
258
|
-
]
|
|
315
|
+
type: str
|
|
316
|
+
traceback: list[str]
|
|
259
317
|
|
|
260
318
|
|
|
261
319
|
# PUBLIC
|
|
262
|
-
class
|
|
263
|
-
|
|
264
|
-
|
|
320
|
+
class TaskEventCallback(Protocol):
|
|
321
|
+
"""
|
|
322
|
+
Protocol for WoolTaskEvent callback functions.
|
|
323
|
+
"""
|
|
265
324
|
|
|
266
|
-
|
|
267
|
-
@dataclass
|
|
268
|
-
class WoolTaskException:
|
|
269
|
-
type: str
|
|
270
|
-
traceback: list[str]
|
|
325
|
+
def __call__(self, event: TaskEvent, timestamp: Timestamp) -> None: ...
|
|
271
326
|
|
|
272
327
|
|
|
273
|
-
_current_task: ContextVar[
|
|
328
|
+
_current_task: ContextVar[Task | None] = ContextVar(
|
|
274
329
|
"_current_task", default=None
|
|
275
330
|
)
|
|
276
331
|
|
|
@@ -279,11 +334,11 @@ def _run(fn):
|
|
|
279
334
|
@wraps(fn)
|
|
280
335
|
def wrapper(self, *args, **kwargs):
|
|
281
336
|
if current_task := self._context.get(_current_task):
|
|
282
|
-
|
|
337
|
+
TaskEvent("task-started", task=current_task).emit()
|
|
283
338
|
try:
|
|
284
339
|
result = fn(self, *args, **kwargs)
|
|
285
340
|
finally:
|
|
286
|
-
|
|
341
|
+
TaskEvent("task-stopped", task=current_task).emit()
|
|
287
342
|
return result
|
|
288
343
|
else:
|
|
289
344
|
return fn(self, *args, **kwargs)
|
|
@@ -298,13 +353,14 @@ P = ParamSpec("P")
|
|
|
298
353
|
R = TypeVar("R")
|
|
299
354
|
|
|
300
355
|
|
|
301
|
-
def
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
"""
|
|
356
|
+
def _resolve(
|
|
357
|
+
method: Callable[P, R],
|
|
358
|
+
) -> Tuple[Type | ModuleType | None, Callable[P, R]]:
|
|
305
359
|
scope = modules[method.__module__]
|
|
306
360
|
parent = None
|
|
307
361
|
for name in method.__qualname__.split("."):
|
|
308
362
|
parent = scope
|
|
309
363
|
scope = getattr(scope, name)
|
|
364
|
+
assert scope
|
|
365
|
+
assert isinstance(parent, (Type, ModuleType))
|
|
310
366
|
return parent, cast(Callable[P, R], scope)
|
wool/_typing.py
CHANGED
wool/_utils.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import threading
|
|
4
|
-
from typing import Callable
|
|
4
|
+
from typing import Callable
|
|
5
|
+
from typing import Final
|
|
6
|
+
from typing import Generic
|
|
7
|
+
from typing import TypeVar
|
|
5
8
|
|
|
6
9
|
|
|
7
10
|
class UndefinedType:
|
|
@@ -23,12 +26,6 @@ Undefined: Final = UndefinedType()
|
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
class PredicatedEvent(threading.Event):
|
|
26
|
-
"""
|
|
27
|
-
This class extends the threading.Event class and adds functionality to
|
|
28
|
-
automatically set the event if the given predicate function returns True
|
|
29
|
-
when the event's state is checked.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
29
|
def __init__(self, predicate: Callable[[], bool]):
|
|
33
30
|
self._predicate = predicate
|
|
34
31
|
super().__init__()
|
|
@@ -53,18 +50,14 @@ class Property(Generic[T]):
|
|
|
53
50
|
self._default = default
|
|
54
51
|
|
|
55
52
|
def get(self) -> T:
|
|
56
|
-
if self._value
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
assert not isinstance(self._default, UndefinedType)
|
|
53
|
+
if isinstance(self._value, UndefinedType):
|
|
54
|
+
if isinstance(self._default, UndefinedType):
|
|
55
|
+
raise ValueError("Property value is undefined")
|
|
60
56
|
return self._default
|
|
61
|
-
|
|
62
|
-
assert not isinstance(self._value, UndefinedType)
|
|
63
|
-
return self._value
|
|
57
|
+
return self._value
|
|
64
58
|
|
|
65
59
|
def set(self, value: T) -> None:
|
|
66
|
-
|
|
67
|
-
self._value = value
|
|
60
|
+
self._value = value
|
|
68
61
|
|
|
69
|
-
def
|
|
62
|
+
def reset(self) -> None:
|
|
70
63
|
self._value = Undefined
|