wool 0.1rc20__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.
- wool/__init__.py +122 -0
- wool/_context.py +29 -0
- wool/_protobuf/worker.py +26 -0
- wool/_resource_pool.py +376 -0
- wool/_typing.py +7 -0
- wool/_undefined.py +11 -0
- wool/_work.py +554 -0
- wool/core/__init__.py +0 -0
- wool/core/discovery/__init__.py +0 -0
- wool/core/discovery/base.py +249 -0
- wool/core/discovery/lan.py +534 -0
- wool/core/discovery/local.py +822 -0
- wool/core/loadbalancer/__init__.py +0 -0
- wool/core/loadbalancer/base.py +125 -0
- wool/core/loadbalancer/roundrobin.py +101 -0
- wool/core/protobuf/__init__.py +18 -0
- wool/core/protobuf/exception.py +3 -0
- wool/core/protobuf/task.py +11 -0
- wool/core/protobuf/task_pb2.py +42 -0
- wool/core/protobuf/task_pb2.pyi +43 -0
- wool/core/protobuf/task_pb2_grpc.py +24 -0
- wool/core/protobuf/worker.py +26 -0
- wool/core/protobuf/worker_pb2.py +53 -0
- wool/core/protobuf/worker_pb2.pyi +65 -0
- wool/core/protobuf/worker_pb2_grpc.py +141 -0
- wool/core/typing.py +22 -0
- wool/core/worker/__init__.py +0 -0
- wool/core/worker/base.py +300 -0
- wool/core/worker/connection.py +250 -0
- wool/core/worker/local.py +148 -0
- wool/core/worker/pool.py +386 -0
- wool/core/worker/process.py +249 -0
- wool/core/worker/proxy.py +427 -0
- wool/core/worker/service.py +231 -0
- wool-0.1rc20.dist-info/METADATA +463 -0
- wool-0.1rc20.dist-info/RECORD +38 -0
- wool-0.1rc20.dist-info/WHEEL +4 -0
- wool-0.1rc20.dist-info/entry_points.txt +2 -0
wool/_work.py
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
import traceback
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from contextvars import Context
|
|
9
|
+
from contextvars import ContextVar
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from functools import wraps
|
|
12
|
+
from sys import modules
|
|
13
|
+
from time import perf_counter_ns
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
from types import TracebackType
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
from typing import Coroutine
|
|
18
|
+
from typing import Dict
|
|
19
|
+
from typing import Literal
|
|
20
|
+
from typing import ParamSpec
|
|
21
|
+
from typing import Protocol
|
|
22
|
+
from typing import SupportsInt
|
|
23
|
+
from typing import Tuple
|
|
24
|
+
from typing import Type
|
|
25
|
+
from typing import TypeVar
|
|
26
|
+
from typing import cast
|
|
27
|
+
from uuid import UUID
|
|
28
|
+
from uuid import uuid4
|
|
29
|
+
|
|
30
|
+
import cloudpickle
|
|
31
|
+
|
|
32
|
+
import wool
|
|
33
|
+
from wool import _context as ctx
|
|
34
|
+
from wool._typing import PassthroughDecorator
|
|
35
|
+
from wool.core import protobuf as pb
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from wool.core.worker.proxy import WorkerProxy
|
|
39
|
+
|
|
40
|
+
AsyncCallable = Callable[..., Coroutine]
|
|
41
|
+
C = TypeVar("C", bound=AsyncCallable)
|
|
42
|
+
|
|
43
|
+
Args = Tuple
|
|
44
|
+
Kwargs = Dict
|
|
45
|
+
Timeout = SupportsInt
|
|
46
|
+
Timestamp = SupportsInt
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# public
|
|
50
|
+
def work(fn: C) -> C:
|
|
51
|
+
"""Decorator to declare an asynchronous function as remotely executable.
|
|
52
|
+
|
|
53
|
+
Converts an asynchronous function into a distributed task that can be
|
|
54
|
+
executed by a worker pool. When the decorated function is invoked, it
|
|
55
|
+
is dispatched to the worker pool associated with the current worker
|
|
56
|
+
pool session context.
|
|
57
|
+
|
|
58
|
+
:param fn:
|
|
59
|
+
The asynchronous function to convert into a distributed routine.
|
|
60
|
+
:returns:
|
|
61
|
+
The decorated function that dispatches to the worker pool when
|
|
62
|
+
called.
|
|
63
|
+
:raises ValueError:
|
|
64
|
+
If the decorated function is not a coroutine function.
|
|
65
|
+
|
|
66
|
+
.. note::
|
|
67
|
+
Decorated functions behave like regular coroutines and can
|
|
68
|
+
be awaited and cancelled normally. Task execution occurs
|
|
69
|
+
transparently across the distributed worker pool.
|
|
70
|
+
|
|
71
|
+
Best practices and considerations for designing tasks:
|
|
72
|
+
|
|
73
|
+
1. **Picklability**: Arguments and return values must be picklable for
|
|
74
|
+
serialization between processes. Avoid unpicklable objects like
|
|
75
|
+
open file handles, database connections, or lambda functions.
|
|
76
|
+
Custom objects should implement ``__getstate__`` and
|
|
77
|
+
``__setstate__`` methods if needed.
|
|
78
|
+
|
|
79
|
+
2. **Synchronization**: Tasks are not guaranteed to execute on the
|
|
80
|
+
same process between invocations. Standard :mod:`asyncio`
|
|
81
|
+
synchronization primitives will not work across processes.
|
|
82
|
+
Use file-based or other distributed synchronization utilities.
|
|
83
|
+
|
|
84
|
+
3. **Statelessness**: Design tasks to be stateless and idempotent.
|
|
85
|
+
Avoid global variables or shared mutable state to ensure
|
|
86
|
+
predictable behavior and enable safe retries.
|
|
87
|
+
|
|
88
|
+
4. **Cancellation**: Task cancellation behaves like standard Python
|
|
89
|
+
coroutine cancellation and is properly propagated across the
|
|
90
|
+
distributed system.
|
|
91
|
+
|
|
92
|
+
5. **Error propagation**: Unhandled exceptions raised within tasks are
|
|
93
|
+
transparently propagated to the caller as they would be normally.
|
|
94
|
+
|
|
95
|
+
6. **Performance**: Minimize argument and return value sizes to reduce
|
|
96
|
+
serialization overhead. For large datasets, consider using shared
|
|
97
|
+
memory or passing references instead of the data itself.
|
|
98
|
+
|
|
99
|
+
Example usage:
|
|
100
|
+
|
|
101
|
+
.. code-block:: python
|
|
102
|
+
|
|
103
|
+
import wool
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@wool.work
|
|
107
|
+
async def fibonacci(n: int) -> int:
|
|
108
|
+
if n <= 1:
|
|
109
|
+
return n
|
|
110
|
+
return await fibonacci(n - 1) + await fibonacci(n - 2)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async def main():
|
|
114
|
+
async with wool.WorkerPool():
|
|
115
|
+
result = await fibonacci(10)
|
|
116
|
+
"""
|
|
117
|
+
if not inspect.iscoroutinefunction(fn):
|
|
118
|
+
raise ValueError("Expected a coroutine function")
|
|
119
|
+
|
|
120
|
+
@wraps(fn)
|
|
121
|
+
def wrapper(*args, **kwargs) -> Coroutine:
|
|
122
|
+
# Handle static and class methods in a picklable way.
|
|
123
|
+
parent, function = _resolve(fn)
|
|
124
|
+
assert parent is not None
|
|
125
|
+
assert callable(function)
|
|
126
|
+
|
|
127
|
+
if _do_dispatch.get():
|
|
128
|
+
proxy = wool.__proxy__.get()
|
|
129
|
+
assert proxy
|
|
130
|
+
stream = _dispatch(
|
|
131
|
+
proxy,
|
|
132
|
+
wrapper.__module__,
|
|
133
|
+
wrapper.__qualname__,
|
|
134
|
+
function,
|
|
135
|
+
*args,
|
|
136
|
+
**kwargs,
|
|
137
|
+
)
|
|
138
|
+
if inspect.iscoroutinefunction(fn):
|
|
139
|
+
return _stream_to_coroutine(stream)
|
|
140
|
+
else:
|
|
141
|
+
raise ValueError("Expected a coroutine function")
|
|
142
|
+
else:
|
|
143
|
+
return _execute(fn, parent, *args, **kwargs)
|
|
144
|
+
|
|
145
|
+
return cast(C, wrapper)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
routine = work
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _dispatch(
|
|
152
|
+
proxy: WorkerProxy,
|
|
153
|
+
module: str,
|
|
154
|
+
qualname: str,
|
|
155
|
+
function: AsyncCallable,
|
|
156
|
+
*args,
|
|
157
|
+
**kwargs,
|
|
158
|
+
):
|
|
159
|
+
# Skip self argument if function is a method.
|
|
160
|
+
args = args[1:] if hasattr(function, "__self__") else args
|
|
161
|
+
signature = ", ".join(
|
|
162
|
+
(
|
|
163
|
+
*(repr(v) for v in args),
|
|
164
|
+
*(f"{k}={repr(v)}" for k, v in kwargs.items()),
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
task = WoolTask(
|
|
168
|
+
id=uuid4(),
|
|
169
|
+
callable=function,
|
|
170
|
+
args=args,
|
|
171
|
+
kwargs=kwargs,
|
|
172
|
+
tag=f"{module}.{qualname}({signature})",
|
|
173
|
+
proxy=proxy,
|
|
174
|
+
)
|
|
175
|
+
return proxy.dispatch(task, timeout=ctx.dispatch_timeout.get())
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def _execute(fn: AsyncCallable, parent, *args, **kwargs):
|
|
179
|
+
token = _do_dispatch.set(True)
|
|
180
|
+
try:
|
|
181
|
+
if isinstance(fn, classmethod):
|
|
182
|
+
return await fn.__func__(parent, *args, **kwargs)
|
|
183
|
+
else:
|
|
184
|
+
return await fn(*args, **kwargs)
|
|
185
|
+
finally:
|
|
186
|
+
_do_dispatch.reset(token)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async def _stream_to_coroutine(stream):
|
|
190
|
+
result = None
|
|
191
|
+
async for result in await stream:
|
|
192
|
+
continue
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# public
|
|
197
|
+
def current_task() -> WoolTask | None:
|
|
198
|
+
"""
|
|
199
|
+
Get the current task from the context variable if we are inside a task
|
|
200
|
+
context, otherwise return None.
|
|
201
|
+
|
|
202
|
+
:returns:
|
|
203
|
+
The current task or None if no task is active.
|
|
204
|
+
"""
|
|
205
|
+
return _current_task.get()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# public
|
|
209
|
+
@dataclass
|
|
210
|
+
class WoolTask:
|
|
211
|
+
"""
|
|
212
|
+
Represents a distributed task to be executed in the worker pool.
|
|
213
|
+
|
|
214
|
+
Each task encapsulates a function call along with its arguments and
|
|
215
|
+
execution context. Tasks are created when decorated functions are
|
|
216
|
+
invoked and contain all necessary information for remote execution
|
|
217
|
+
including serialization, routing, and result handling.
|
|
218
|
+
|
|
219
|
+
:param id:
|
|
220
|
+
Unique identifier for this task instance.
|
|
221
|
+
:param callable:
|
|
222
|
+
The asynchronous function to execute.
|
|
223
|
+
:param args:
|
|
224
|
+
Positional arguments for the function.
|
|
225
|
+
:param kwargs:
|
|
226
|
+
Keyword arguments for the function.
|
|
227
|
+
:param proxy:
|
|
228
|
+
Worker proxy for task dispatch and routing.
|
|
229
|
+
:param timeout:
|
|
230
|
+
Task timeout in seconds (0 means no timeout).
|
|
231
|
+
:param caller:
|
|
232
|
+
UUID of the calling task if this is a nested task.
|
|
233
|
+
:param exception:
|
|
234
|
+
Exception information if task execution failed.
|
|
235
|
+
:param filename:
|
|
236
|
+
Source filename where the task was defined.
|
|
237
|
+
:param function:
|
|
238
|
+
Name of the function being executed.
|
|
239
|
+
:param line_no:
|
|
240
|
+
Line number where the task was defined.
|
|
241
|
+
:param tag:
|
|
242
|
+
Optional descriptive tag for the task.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
id: UUID
|
|
246
|
+
callable: AsyncCallable
|
|
247
|
+
args: Args
|
|
248
|
+
kwargs: Kwargs
|
|
249
|
+
proxy: WorkerProxy
|
|
250
|
+
timeout: Timeout = 0
|
|
251
|
+
caller: UUID | None = None
|
|
252
|
+
exception: WoolTaskException | None = None
|
|
253
|
+
filename: str | None = None
|
|
254
|
+
function: str | None = None
|
|
255
|
+
line_no: int | None = None
|
|
256
|
+
tag: str | None = None
|
|
257
|
+
|
|
258
|
+
def __post_init__(self, **kwargs):
|
|
259
|
+
"""
|
|
260
|
+
Initialize the task and emit a "task-created" event.
|
|
261
|
+
|
|
262
|
+
Sets up the task context including caller tracking and event emission
|
|
263
|
+
for monitoring and debugging purposes.
|
|
264
|
+
|
|
265
|
+
:param kwargs:
|
|
266
|
+
Additional keyword arguments (unused).
|
|
267
|
+
"""
|
|
268
|
+
if caller := _current_task.get():
|
|
269
|
+
self.caller = caller.id
|
|
270
|
+
WoolTaskEvent("task-created", task=self).emit()
|
|
271
|
+
|
|
272
|
+
def __enter__(self) -> Callable[[], Coroutine]:
|
|
273
|
+
"""
|
|
274
|
+
Enter the task context for execution.
|
|
275
|
+
|
|
276
|
+
:returns:
|
|
277
|
+
The task's run method as a callable coroutine.
|
|
278
|
+
"""
|
|
279
|
+
logging.debug(f"Entering {self.__class__.__name__} with ID {self.id}")
|
|
280
|
+
return self.run
|
|
281
|
+
|
|
282
|
+
def __exit__(
|
|
283
|
+
self,
|
|
284
|
+
exception_type: type[BaseException] | None,
|
|
285
|
+
exception_value: BaseException | None,
|
|
286
|
+
exception_traceback: TracebackType | None,
|
|
287
|
+
):
|
|
288
|
+
"""Exit the task context and handle any exceptions.
|
|
289
|
+
|
|
290
|
+
Captures exception information for later processing and allows
|
|
291
|
+
exceptions to propagate normally for proper error handling.
|
|
292
|
+
|
|
293
|
+
:param exception_type:
|
|
294
|
+
Type of exception that occurred, if any.
|
|
295
|
+
:param exception_value:
|
|
296
|
+
Exception instance that occurred, if any.
|
|
297
|
+
:param exception_traceback:
|
|
298
|
+
Traceback of the exception, if any.
|
|
299
|
+
:returns:
|
|
300
|
+
False to allow exceptions to propagate.
|
|
301
|
+
"""
|
|
302
|
+
logging.debug(f"Exiting {self.__class__.__name__} with ID {self.id}")
|
|
303
|
+
if exception_value:
|
|
304
|
+
this = asyncio.current_task()
|
|
305
|
+
assert this
|
|
306
|
+
self.exception = WoolTaskException(
|
|
307
|
+
exception_type.__qualname__,
|
|
308
|
+
traceback=[
|
|
309
|
+
y
|
|
310
|
+
for x in traceback.format_exception(
|
|
311
|
+
exception_type, exception_value, exception_traceback
|
|
312
|
+
)
|
|
313
|
+
for y in x.split("\n")
|
|
314
|
+
],
|
|
315
|
+
)
|
|
316
|
+
this.add_done_callback(self._finish, context=Context())
|
|
317
|
+
# Return False to allow exceptions to propagate
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
@classmethod
|
|
321
|
+
def from_protobuf(cls, task: pb.task.Task) -> WoolTask:
|
|
322
|
+
return cls(
|
|
323
|
+
id=UUID(task.id),
|
|
324
|
+
callable=cloudpickle.loads(task.callable),
|
|
325
|
+
args=cloudpickle.loads(task.args),
|
|
326
|
+
kwargs=cloudpickle.loads(task.kwargs),
|
|
327
|
+
caller=UUID(task.caller) if task.caller else None,
|
|
328
|
+
proxy=cloudpickle.loads(task.proxy),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def to_protobuf(self) -> pb.task.Task:
|
|
332
|
+
return pb.task.Task(
|
|
333
|
+
id=str(self.id),
|
|
334
|
+
callable=cloudpickle.dumps(self.callable),
|
|
335
|
+
args=cloudpickle.dumps(self.args),
|
|
336
|
+
kwargs=cloudpickle.dumps(self.kwargs),
|
|
337
|
+
caller=str(self.caller) if self.caller else "",
|
|
338
|
+
proxy=cloudpickle.dumps(self.proxy),
|
|
339
|
+
proxy_id=str(self.proxy.id),
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
async def run(self) -> Coroutine:
|
|
343
|
+
"""
|
|
344
|
+
Execute the task's callable with its arguments in proxy context.
|
|
345
|
+
|
|
346
|
+
:returns:
|
|
347
|
+
A coroutine representing the routine execution.
|
|
348
|
+
:raises RuntimeError:
|
|
349
|
+
If no proxy is available for task execution.
|
|
350
|
+
"""
|
|
351
|
+
proxy_pool = wool.__proxy_pool__.get()
|
|
352
|
+
assert proxy_pool
|
|
353
|
+
async with proxy_pool.get(self.proxy) as proxy:
|
|
354
|
+
# Set the proxy in context variable for nested task dispatch
|
|
355
|
+
token = wool.__proxy__.set(proxy)
|
|
356
|
+
try:
|
|
357
|
+
work = self._with_self(self.callable)
|
|
358
|
+
return await work(*self.args, **self.kwargs)
|
|
359
|
+
finally:
|
|
360
|
+
wool.__proxy__.reset(token)
|
|
361
|
+
|
|
362
|
+
def _finish(self, _):
|
|
363
|
+
WoolTaskEvent("task-completed", task=self).emit()
|
|
364
|
+
|
|
365
|
+
def _with_self(self, fn: AsyncCallable) -> AsyncCallable:
|
|
366
|
+
@wraps(fn)
|
|
367
|
+
async def wrapper(*args, **kwargs):
|
|
368
|
+
with self:
|
|
369
|
+
current_task_token = _current_task.set(self)
|
|
370
|
+
# Do not re-submit this task, execute it locally
|
|
371
|
+
local_token = _do_dispatch.set(False)
|
|
372
|
+
# Yield to event loop with context set
|
|
373
|
+
await asyncio.sleep(0)
|
|
374
|
+
try:
|
|
375
|
+
result = await fn(*args, **kwargs)
|
|
376
|
+
return result
|
|
377
|
+
finally:
|
|
378
|
+
_current_task.reset(current_task_token)
|
|
379
|
+
_do_dispatch.reset(local_token)
|
|
380
|
+
|
|
381
|
+
return wrapper
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# public
|
|
385
|
+
@dataclass
|
|
386
|
+
class WoolTaskException:
|
|
387
|
+
"""
|
|
388
|
+
Represents an exception that occurred during distributed task execution.
|
|
389
|
+
|
|
390
|
+
Captures exception information from remote task execution for proper
|
|
391
|
+
error reporting and debugging. The exception details are serialized
|
|
392
|
+
and transmitted back to the calling context.
|
|
393
|
+
|
|
394
|
+
:param type:
|
|
395
|
+
Qualified name of the exception class.
|
|
396
|
+
:param traceback:
|
|
397
|
+
List of formatted traceback lines from the exception.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
type: str
|
|
401
|
+
traceback: list[str]
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# public
|
|
405
|
+
class WoolTaskEvent:
|
|
406
|
+
"""
|
|
407
|
+
Represents a lifecycle event for a distributed task.
|
|
408
|
+
|
|
409
|
+
Events are emitted at key points during task execution and can be used
|
|
410
|
+
for monitoring, debugging, and performance analysis. Event handlers can
|
|
411
|
+
be registered to respond to specific event types.
|
|
412
|
+
|
|
413
|
+
:param type:
|
|
414
|
+
The type of task event (e.g., "task-created", "task-scheduled").
|
|
415
|
+
:param task:
|
|
416
|
+
The :class:`WoolTask` instance associated with this event.
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
type: WoolTaskEventType
|
|
420
|
+
task: WoolTask
|
|
421
|
+
|
|
422
|
+
_handlers: dict[str, list[WoolTaskEventCallback]] = {}
|
|
423
|
+
|
|
424
|
+
def __init__(self, type: WoolTaskEventType, /, task: WoolTask) -> None:
|
|
425
|
+
"""
|
|
426
|
+
Initialize a WoolTaskEvent instance.
|
|
427
|
+
|
|
428
|
+
:param type:
|
|
429
|
+
The type of the task event.
|
|
430
|
+
:param task:
|
|
431
|
+
The :class:`WoolTask` instance associated with the event.
|
|
432
|
+
"""
|
|
433
|
+
self.type = type
|
|
434
|
+
self.task = task
|
|
435
|
+
|
|
436
|
+
@classmethod
|
|
437
|
+
def handler(
|
|
438
|
+
cls, *event_types: WoolTaskEventType
|
|
439
|
+
) -> PassthroughDecorator[WoolTaskEventCallback]:
|
|
440
|
+
"""
|
|
441
|
+
Register a handler function for specific task event types.
|
|
442
|
+
|
|
443
|
+
:param event_types:
|
|
444
|
+
The event types to handle.
|
|
445
|
+
:returns:
|
|
446
|
+
A decorator to register the handler function.
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
def _handler(
|
|
450
|
+
fn: WoolTaskEventCallback,
|
|
451
|
+
) -> WoolTaskEventCallback:
|
|
452
|
+
for event_type in event_types:
|
|
453
|
+
cls._handlers.setdefault(event_type, []).append(fn)
|
|
454
|
+
return fn
|
|
455
|
+
|
|
456
|
+
return _handler
|
|
457
|
+
|
|
458
|
+
def emit(self):
|
|
459
|
+
"""
|
|
460
|
+
Emit the task event, invoking all registered handlers for the
|
|
461
|
+
event type.
|
|
462
|
+
|
|
463
|
+
Handlers are called with the event instance and a timestamp.
|
|
464
|
+
|
|
465
|
+
:raises Exception:
|
|
466
|
+
If any handler raises an exception.
|
|
467
|
+
"""
|
|
468
|
+
logging.debug(
|
|
469
|
+
f"Emitting {self.type} event for "
|
|
470
|
+
f"task {self.task.id} "
|
|
471
|
+
f"({self.task.callable.__qualname__})"
|
|
472
|
+
)
|
|
473
|
+
if handlers := self._handlers.get(self.type):
|
|
474
|
+
timestamp = perf_counter_ns()
|
|
475
|
+
for handler in handlers:
|
|
476
|
+
handler(self, timestamp)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
# public
|
|
480
|
+
WoolTaskEventType = Literal[
|
|
481
|
+
"task-created",
|
|
482
|
+
"task-queued",
|
|
483
|
+
"task-scheduled",
|
|
484
|
+
"task-started",
|
|
485
|
+
"task-stopped",
|
|
486
|
+
"task-completed",
|
|
487
|
+
]
|
|
488
|
+
"""
|
|
489
|
+
Defines the types of events that can occur during the lifecycle of a Wool
|
|
490
|
+
task.
|
|
491
|
+
|
|
492
|
+
- "task-created":
|
|
493
|
+
Emitted when a task is created.
|
|
494
|
+
- "task-queued":
|
|
495
|
+
Emitted when a task is added to the queue.
|
|
496
|
+
- "task-scheduled":
|
|
497
|
+
Emitted when a task is scheduled for execution in a worker's event
|
|
498
|
+
loop.
|
|
499
|
+
- "task-started":
|
|
500
|
+
Emitted when a task starts execution.
|
|
501
|
+
- "task-stopped":
|
|
502
|
+
Emitted when a task stops execution.
|
|
503
|
+
- "task-completed":
|
|
504
|
+
Emitted when a task completes execution.
|
|
505
|
+
"""
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# public
|
|
509
|
+
class WoolTaskEventCallback(Protocol):
|
|
510
|
+
"""
|
|
511
|
+
Protocol for WoolTaskEvent callback functions.
|
|
512
|
+
"""
|
|
513
|
+
|
|
514
|
+
def __call__(self, event: WoolTaskEvent, timestamp: Timestamp) -> None: ...
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
_do_dispatch: ContextVar[bool] = ContextVar("_do_dispatch", default=True)
|
|
518
|
+
_current_task: ContextVar[WoolTask | None] = ContextVar("_current_task", default=None)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _run(fn):
|
|
522
|
+
@wraps(fn)
|
|
523
|
+
def wrapper(self, *args, **kwargs):
|
|
524
|
+
if current_task := self._context.get(_current_task):
|
|
525
|
+
WoolTaskEvent("task-started", task=current_task).emit()
|
|
526
|
+
try:
|
|
527
|
+
result = fn(self, *args, **kwargs)
|
|
528
|
+
finally:
|
|
529
|
+
WoolTaskEvent("task-stopped", task=current_task).emit()
|
|
530
|
+
return result
|
|
531
|
+
else:
|
|
532
|
+
return fn(self, *args, **kwargs)
|
|
533
|
+
|
|
534
|
+
return wrapper
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
asyncio.Handle._run = _run(asyncio.Handle._run)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
P = ParamSpec("P")
|
|
541
|
+
R = TypeVar("R")
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _resolve(
|
|
545
|
+
method: Callable[P, R],
|
|
546
|
+
) -> Tuple[Type | ModuleType | None, Callable[P, R]]:
|
|
547
|
+
scope = modules[method.__module__]
|
|
548
|
+
parent = None
|
|
549
|
+
for name in method.__qualname__.split("."):
|
|
550
|
+
parent = scope
|
|
551
|
+
scope = getattr(scope, name)
|
|
552
|
+
assert scope
|
|
553
|
+
assert isinstance(parent, (Type, ModuleType))
|
|
554
|
+
return parent, cast(Callable[P, R], scope)
|
wool/core/__init__.py
ADDED
|
File without changes
|
|
File without changes
|