wool 0.1rc3__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 +93 -40
- wool/_event.py +109 -0
- wool/_future.py +82 -11
- wool/_logging.py +14 -1
- 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 +174 -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.1rc3.dist-info → wool-0.1rc7.dist-info}/WHEEL +1 -2
- wool/_client.py +0 -206
- wool-0.1rc3.dist-info/METADATA +0 -137
- wool-0.1rc3.dist-info/RECORD +0 -17
- wool-0.1rc3.dist-info/top_level.txt +0 -1
- {wool-0.1rc3.dist-info → wool-0.1rc7.dist-info}/entry_points.txt +0 -0
wool/_task.py
CHANGED
|
@@ -4,52 +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
|
-
def wrapper(
|
|
108
|
+
def wrapper(
|
|
109
|
+
*args,
|
|
110
|
+
__wool_remote__: bool = False,
|
|
111
|
+
__wool_session__: WorkerPoolSession | None = None,
|
|
112
|
+
**kwargs,
|
|
113
|
+
) -> Coroutine:
|
|
51
114
|
# Handle static and class methods in a picklable way.
|
|
52
|
-
parent, function =
|
|
115
|
+
parent, function = _resolve(fn)
|
|
53
116
|
assert parent is not None
|
|
54
117
|
assert callable(function)
|
|
55
118
|
|
|
@@ -59,7 +122,7 @@ def task(fn: C) -> C:
|
|
|
59
122
|
else:
|
|
60
123
|
# Otherwise, submit the task to the pool.
|
|
61
124
|
return _put(
|
|
62
|
-
|
|
125
|
+
__wool_session__ or current_session(),
|
|
63
126
|
wrapper.__module__,
|
|
64
127
|
wrapper.__qualname__,
|
|
65
128
|
function,
|
|
@@ -71,14 +134,15 @@ def task(fn: C) -> C:
|
|
|
71
134
|
|
|
72
135
|
|
|
73
136
|
def _put(
|
|
74
|
-
|
|
137
|
+
session: WorkerPoolSession,
|
|
75
138
|
module: str,
|
|
76
139
|
qualname: str,
|
|
77
140
|
function: AsyncCallable,
|
|
78
141
|
*args,
|
|
79
142
|
**kwargs,
|
|
80
143
|
) -> Coroutine:
|
|
81
|
-
|
|
144
|
+
if not session.connected:
|
|
145
|
+
session.connect()
|
|
82
146
|
|
|
83
147
|
# Skip self argument if function is a method.
|
|
84
148
|
_args = args[1:] if hasattr(function, "__self__") else args
|
|
@@ -93,28 +157,29 @@ def _put(
|
|
|
93
157
|
# pool, so we set the `__wool_remote__` flag to true.
|
|
94
158
|
kwargs["__wool_remote__"] = True
|
|
95
159
|
|
|
96
|
-
task =
|
|
160
|
+
task = Task(
|
|
97
161
|
id=uuid4(),
|
|
98
162
|
callable=function,
|
|
99
163
|
args=args,
|
|
100
164
|
kwargs=kwargs,
|
|
101
165
|
tag=f"{module}.{qualname}({signature})",
|
|
102
166
|
)
|
|
103
|
-
assert isinstance(
|
|
104
|
-
future:
|
|
167
|
+
assert isinstance(session, WorkerPoolSession)
|
|
168
|
+
future: Future = session.put(task)
|
|
105
169
|
|
|
106
|
-
|
|
170
|
+
@wraps(function)
|
|
171
|
+
async def coroutine(future: Future):
|
|
107
172
|
try:
|
|
108
173
|
while not future.done():
|
|
109
174
|
await asyncio.sleep(0)
|
|
110
175
|
else:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
raise Exception().with_traceback(e.__traceback__) from e
|
|
176
|
+
return future.result()
|
|
177
|
+
except ConnectionResetError as e:
|
|
178
|
+
raise asyncio.CancelledError from e
|
|
115
179
|
except asyncio.CancelledError:
|
|
116
|
-
|
|
117
|
-
|
|
180
|
+
if not future.done():
|
|
181
|
+
logging.debug("Cancelling...")
|
|
182
|
+
future.cancel()
|
|
118
183
|
raise
|
|
119
184
|
|
|
120
185
|
return coroutine(future)
|
|
@@ -128,50 +193,78 @@ def _execute(fn: AsyncCallable, parent, *args, **kwargs):
|
|
|
128
193
|
|
|
129
194
|
|
|
130
195
|
# PUBLIC
|
|
131
|
-
def current_task() ->
|
|
196
|
+
def current_task() -> Task | None:
|
|
132
197
|
"""
|
|
133
198
|
Get the current task from the context variable if we are inside a task
|
|
134
199
|
context, otherwise return None.
|
|
200
|
+
|
|
201
|
+
:return: The current task or None if no task is active.
|
|
135
202
|
"""
|
|
136
203
|
return _current_task.get()
|
|
137
204
|
|
|
138
205
|
|
|
139
206
|
# PUBLIC
|
|
140
207
|
@dataclass
|
|
141
|
-
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
|
+
|
|
142
226
|
id: UUID
|
|
143
227
|
callable: AsyncCallable
|
|
144
228
|
args: Args
|
|
145
229
|
kwargs: Kwargs
|
|
146
230
|
timeout: Timeout = 0
|
|
147
231
|
caller: UUID | None = None
|
|
148
|
-
exception:
|
|
232
|
+
exception: TaskException | None = None
|
|
149
233
|
filename: str | None = None
|
|
150
234
|
function: str | None = None
|
|
151
235
|
line_no: int | None = None
|
|
152
236
|
tag: str | None = None
|
|
153
237
|
|
|
154
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
|
+
"""
|
|
155
244
|
if caller := _current_task.get():
|
|
156
245
|
self.caller = caller.id
|
|
157
|
-
|
|
246
|
+
TaskEvent("task-created", task=self).emit()
|
|
158
247
|
|
|
159
248
|
def __enter__(self) -> Callable[[], Coroutine]:
|
|
160
|
-
|
|
161
|
-
|
|
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}")
|
|
162
255
|
return self.run
|
|
163
256
|
|
|
164
257
|
def __exit__(
|
|
165
258
|
self,
|
|
166
|
-
exception_type: type,
|
|
167
|
-
exception_value:
|
|
168
|
-
exception_traceback: TracebackType,
|
|
259
|
+
exception_type: type[BaseException] | None,
|
|
260
|
+
exception_value: BaseException | None,
|
|
261
|
+
exception_traceback: TracebackType | None,
|
|
169
262
|
):
|
|
170
|
-
logging.
|
|
263
|
+
logging.debug(f"Exiting {self.__class__.__name__} with ID {self.id}")
|
|
171
264
|
if exception_value:
|
|
172
265
|
this = asyncio.current_task()
|
|
173
266
|
assert this
|
|
174
|
-
self.exception =
|
|
267
|
+
self.exception = TaskException(
|
|
175
268
|
exception_type.__qualname__,
|
|
176
269
|
traceback=[
|
|
177
270
|
y
|
|
@@ -184,9 +277,14 @@ class WoolTask:
|
|
|
184
277
|
this.add_done_callback(self._finish, context=Context())
|
|
185
278
|
|
|
186
279
|
def _finish(self, _):
|
|
187
|
-
|
|
280
|
+
TaskEvent("task-completed", task=self).emit()
|
|
188
281
|
|
|
189
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
|
+
"""
|
|
190
288
|
work = self._with_task(self.callable)
|
|
191
289
|
return work(*self.args, **self.kwargs)
|
|
192
290
|
|
|
@@ -205,67 +303,29 @@ class WoolTask:
|
|
|
205
303
|
|
|
206
304
|
|
|
207
305
|
# PUBLIC
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
Task events are emitted when a task is created, queued, started, stopped,
|
|
211
|
-
and completed. Tasks can be started and stopped multiple times by the event
|
|
212
|
-
loop. The cumulative time between start and stop events can be used to
|
|
213
|
-
determine a task's CPU utilization.
|
|
306
|
+
@dataclass
|
|
307
|
+
class TaskException:
|
|
214
308
|
"""
|
|
309
|
+
Represents an exception raised during task execution.
|
|
215
310
|
|
|
216
|
-
type:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
_handlers: dict[str, list[WoolTaskEventCallback]] = {}
|
|
220
|
-
|
|
221
|
-
def __init__(self, type: WoolTaskEventType, /, task: WoolTask) -> None:
|
|
222
|
-
self.type = type
|
|
223
|
-
self.task = task
|
|
224
|
-
|
|
225
|
-
@classmethod
|
|
226
|
-
def handler(
|
|
227
|
-
cls, *event_types: WoolTaskEventType
|
|
228
|
-
) -> PassthroughDecorator[WoolTaskEventCallback]:
|
|
229
|
-
def _handler(
|
|
230
|
-
fn: WoolTaskEventCallback,
|
|
231
|
-
) -> WoolTaskEventCallback:
|
|
232
|
-
for event_type in event_types:
|
|
233
|
-
cls._handlers.setdefault(event_type, []).append(fn)
|
|
234
|
-
return fn
|
|
235
|
-
|
|
236
|
-
return _handler
|
|
237
|
-
|
|
238
|
-
def emit(self):
|
|
239
|
-
logging.debug(f"Emitting {self.type} event for task {self.task.id}")
|
|
240
|
-
if handlers := self._handlers.get(self.type):
|
|
241
|
-
timestamp = perf_counter_ns()
|
|
242
|
-
for handler in handlers:
|
|
243
|
-
handler(self, timestamp)
|
|
244
|
-
|
|
311
|
+
:param type: The type of the exception.
|
|
312
|
+
:param traceback: The traceback of the exception.
|
|
313
|
+
"""
|
|
245
314
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
"task-created",
|
|
249
|
-
"task-queued",
|
|
250
|
-
"task-started",
|
|
251
|
-
"task-stopped",
|
|
252
|
-
"task-completed",
|
|
253
|
-
]
|
|
315
|
+
type: str
|
|
316
|
+
traceback: list[str]
|
|
254
317
|
|
|
255
318
|
|
|
256
319
|
# PUBLIC
|
|
257
|
-
class
|
|
258
|
-
|
|
259
|
-
|
|
320
|
+
class TaskEventCallback(Protocol):
|
|
321
|
+
"""
|
|
322
|
+
Protocol for WoolTaskEvent callback functions.
|
|
323
|
+
"""
|
|
260
324
|
|
|
261
|
-
|
|
262
|
-
@dataclass
|
|
263
|
-
class WoolTaskException:
|
|
264
|
-
type: str
|
|
265
|
-
traceback: list[str]
|
|
325
|
+
def __call__(self, event: TaskEvent, timestamp: Timestamp) -> None: ...
|
|
266
326
|
|
|
267
327
|
|
|
268
|
-
_current_task: ContextVar[
|
|
328
|
+
_current_task: ContextVar[Task | None] = ContextVar(
|
|
269
329
|
"_current_task", default=None
|
|
270
330
|
)
|
|
271
331
|
|
|
@@ -274,11 +334,11 @@ def _run(fn):
|
|
|
274
334
|
@wraps(fn)
|
|
275
335
|
def wrapper(self, *args, **kwargs):
|
|
276
336
|
if current_task := self._context.get(_current_task):
|
|
277
|
-
|
|
337
|
+
TaskEvent("task-started", task=current_task).emit()
|
|
278
338
|
try:
|
|
279
339
|
result = fn(self, *args, **kwargs)
|
|
280
340
|
finally:
|
|
281
|
-
|
|
341
|
+
TaskEvent("task-stopped", task=current_task).emit()
|
|
282
342
|
return result
|
|
283
343
|
else:
|
|
284
344
|
return fn(self, *args, **kwargs)
|
|
@@ -293,13 +353,14 @@ P = ParamSpec("P")
|
|
|
293
353
|
R = TypeVar("R")
|
|
294
354
|
|
|
295
355
|
|
|
296
|
-
def
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
"""
|
|
356
|
+
def _resolve(
|
|
357
|
+
method: Callable[P, R],
|
|
358
|
+
) -> Tuple[Type | ModuleType | None, Callable[P, R]]:
|
|
300
359
|
scope = modules[method.__module__]
|
|
301
360
|
parent = None
|
|
302
361
|
for name in method.__qualname__.split("."):
|
|
303
362
|
parent = scope
|
|
304
363
|
scope = getattr(scope, name)
|
|
364
|
+
assert scope
|
|
365
|
+
assert isinstance(parent, (Type, ModuleType))
|
|
305
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
|