wool 0.1rc11__tar.gz → 0.1rc12__tar.gz
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-0.1rc11 → wool-0.1rc12}/PKG-INFO +1 -1
- {wool-0.1rc11 → wool-0.1rc12}/wool/__init__.py +11 -9
- {wool-0.1rc11 → wool-0.1rc12}/wool/_worker.py +91 -54
- {wool-0.1rc11 → wool-0.1rc12}/wool/_worker_discovery.py +71 -65
- {wool-0.1rc11 → wool-0.1rc12}/wool/_worker_pool.py +26 -34
- {wool-0.1rc11 → wool-0.1rc12}/wool/_worker_proxy.py +38 -41
- {wool-0.1rc11 → wool-0.1rc12}/.gitignore +0 -0
- {wool-0.1rc11 → wool-0.1rc12}/README.md +0 -0
- {wool-0.1rc11 → wool-0.1rc12}/pyproject.toml +0 -0
- {wool-0.1rc11 → wool-0.1rc12}/wool/_protobuf/__init__.py +0 -0
- {wool-0.1rc11 → wool-0.1rc12}/wool/_protobuf/exception.py +0 -0
- {wool-0.1rc11 → wool-0.1rc12}/wool/_protobuf/task.py +0 -0
- {wool-0.1rc11 → wool-0.1rc12}/wool/_protobuf/task_pb2.py +0 -0
- {wool-0.1rc11 → wool-0.1rc12}/wool/_protobuf/task_pb2.pyi +0 -0
- {wool-0.1rc11 → wool-0.1rc12}/wool/_protobuf/task_pb2_grpc.py +0 -0
- {wool-0.1rc11 → wool-0.1rc12}/wool/_protobuf/worker.py +0 -0
- {wool-0.1rc11 → wool-0.1rc12}/wool/_protobuf/worker_pb2.py +0 -0
- {wool-0.1rc11 → wool-0.1rc12}/wool/_protobuf/worker_pb2.pyi +0 -0
- {wool-0.1rc11 → wool-0.1rc12}/wool/_protobuf/worker_pb2_grpc.py +0 -0
- {wool-0.1rc11 → wool-0.1rc12}/wool/_resource_pool.py +0 -0
- {wool-0.1rc11 → wool-0.1rc12}/wool/_typing.py +0 -0
- {wool-0.1rc11 → wool-0.1rc12}/wool/_work.py +0 -0
|
@@ -19,10 +19,10 @@ from wool._work import routine
|
|
|
19
19
|
from wool._work import work
|
|
20
20
|
from wool._worker import Worker
|
|
21
21
|
from wool._worker import WorkerService
|
|
22
|
-
from wool._worker_discovery import
|
|
23
|
-
from wool._worker_discovery import
|
|
24
|
-
from wool._worker_discovery import
|
|
25
|
-
from wool._worker_discovery import
|
|
22
|
+
from wool._worker_discovery import Discovery
|
|
23
|
+
from wool._worker_discovery import LanDiscovery
|
|
24
|
+
from wool._worker_discovery import LanRegistrar
|
|
25
|
+
from wool._worker_discovery import Registrar
|
|
26
26
|
from wool._worker_pool import WorkerPool
|
|
27
27
|
from wool._worker_proxy import WorkerProxy
|
|
28
28
|
|
|
@@ -54,20 +54,22 @@ try:
|
|
|
54
54
|
except PackageNotFoundError:
|
|
55
55
|
__version__ = "unknown"
|
|
56
56
|
|
|
57
|
-
__proxy__: Final[ContextVar[WorkerProxy | None]] = ContextVar(
|
|
57
|
+
__proxy__: Final[ContextVar[WorkerProxy | None]] = ContextVar(
|
|
58
|
+
"__proxy__", default=None
|
|
59
|
+
)
|
|
58
60
|
|
|
59
61
|
__proxy_pool__: Final[ContextVar[ResourcePool[WorkerProxy] | None]] = ContextVar(
|
|
60
62
|
"__proxy_pool__", default=None
|
|
61
63
|
)
|
|
62
64
|
|
|
63
65
|
__all__ = [
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
+
"LanDiscovery",
|
|
67
|
+
"LanRegistrar",
|
|
66
68
|
"Worker",
|
|
67
|
-
"
|
|
69
|
+
"Discovery",
|
|
68
70
|
"WorkerPool",
|
|
69
71
|
"WorkerProxy",
|
|
70
|
-
"
|
|
72
|
+
"Registrar",
|
|
71
73
|
"WorkerService",
|
|
72
74
|
"WoolTask",
|
|
73
75
|
"WoolTaskEvent",
|
|
@@ -13,7 +13,10 @@ from multiprocessing import Process
|
|
|
13
13
|
from multiprocessing.connection import Connection
|
|
14
14
|
from typing import TYPE_CHECKING
|
|
15
15
|
from typing import Any
|
|
16
|
+
from typing import AsyncContextManager
|
|
16
17
|
from typing import AsyncIterator
|
|
18
|
+
from typing import Awaitable
|
|
19
|
+
from typing import ContextManager
|
|
17
20
|
from typing import Final
|
|
18
21
|
from typing import Generic
|
|
19
22
|
from typing import Protocol
|
|
@@ -31,15 +34,13 @@ from wool import _protobuf as pb
|
|
|
31
34
|
from wool._resource_pool import ResourcePool
|
|
32
35
|
from wool._work import WoolTask
|
|
33
36
|
from wool._work import WoolTaskEvent
|
|
34
|
-
from wool._worker_discovery import
|
|
37
|
+
from wool._worker_discovery import Factory
|
|
38
|
+
from wool._worker_discovery import RegistrarLike
|
|
35
39
|
from wool._worker_discovery import WorkerInfo
|
|
36
40
|
|
|
37
41
|
if TYPE_CHECKING:
|
|
38
42
|
from wool._worker_proxy import WorkerProxy
|
|
39
43
|
|
|
40
|
-
_ip_address: str | None = None
|
|
41
|
-
_EXTERNAL_DNS_SERVER: Final[str] = "8.8.8.8" # Google DNS for IP detection
|
|
42
|
-
|
|
43
44
|
|
|
44
45
|
@contextmanager
|
|
45
46
|
def _signal_handlers(service: "WorkerService"):
|
|
@@ -80,13 +81,8 @@ def _signal_handlers(service: "WorkerService"):
|
|
|
80
81
|
signal.signal(signal.SIGINT, old_sigint)
|
|
81
82
|
|
|
82
83
|
|
|
83
|
-
_T_RegistryService = TypeVar(
|
|
84
|
-
"_T_RegistryService", bound=RegistryServiceLike, covariant=True
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
84
|
# public
|
|
89
|
-
class Worker(ABC
|
|
85
|
+
class Worker(ABC):
|
|
90
86
|
"""Abstract base class for worker implementations in the wool framework.
|
|
91
87
|
|
|
92
88
|
Workers are individual processes that execute distributed tasks within
|
|
@@ -94,36 +90,40 @@ class Worker(ABC, Generic[_T_RegistryService]):
|
|
|
94
90
|
a discovery service to be found by client sessions.
|
|
95
91
|
|
|
96
92
|
This class defines the core interface that all worker implementations
|
|
97
|
-
must provide, including lifecycle management and
|
|
93
|
+
must provide, including lifecycle management and registrar service
|
|
98
94
|
integration for peer-to-peer discovery.
|
|
99
95
|
|
|
100
96
|
:param tags:
|
|
101
97
|
Capability tags associated with this worker for filtering and
|
|
102
98
|
selection by client sessions.
|
|
103
|
-
:param
|
|
104
|
-
Service instance for worker registration and discovery
|
|
105
|
-
the distributed pool.
|
|
99
|
+
:param registrar:
|
|
100
|
+
Service instance or factory for worker registration and discovery
|
|
101
|
+
within the distributed pool. Can be provided as:
|
|
102
|
+
|
|
103
|
+
- **Instance**: Direct registrar service object
|
|
104
|
+
- **Factory function**: Function returning a registrar service instance
|
|
105
|
+
- **Context manager factory**: Function returning a context manager
|
|
106
|
+
that yields a registrar service
|
|
106
107
|
:param extra:
|
|
107
108
|
Additional arbitrary metadata as key-value pairs.
|
|
108
109
|
"""
|
|
109
110
|
|
|
110
111
|
_info: WorkerInfo | None = None
|
|
111
112
|
_started: bool = False
|
|
112
|
-
|
|
113
|
+
_registrar: RegistrarLike | Factory[RegistrarLike]
|
|
114
|
+
_registrar_service: RegistrarLike | None = None
|
|
115
|
+
_registrar_context: Any | None = None
|
|
113
116
|
_uid: Final[str]
|
|
114
117
|
_tags: Final[set[str]]
|
|
115
118
|
_extra: Final[dict[str, Any]]
|
|
116
119
|
|
|
117
120
|
def __init__(
|
|
118
|
-
self,
|
|
119
|
-
*tags: str,
|
|
120
|
-
registry_service: _T_RegistryService,
|
|
121
|
-
**extra: Any,
|
|
121
|
+
self, *tags: str, registrar: RegistrarLike | Factory[RegistrarLike], **extra: Any
|
|
122
122
|
):
|
|
123
123
|
self._uid = f"worker-{uuid.uuid4().hex}"
|
|
124
124
|
self._tags = set(tags)
|
|
125
125
|
self._extra = extra
|
|
126
|
-
self.
|
|
126
|
+
self._registrar = registrar
|
|
127
127
|
|
|
128
128
|
@property
|
|
129
129
|
def uid(self) -> str:
|
|
@@ -167,14 +167,21 @@ class Worker(ABC, Generic[_T_RegistryService]):
|
|
|
167
167
|
|
|
168
168
|
This method is a final implementation that calls the abstract
|
|
169
169
|
`_start` method to initialize the worker process and register
|
|
170
|
-
it with the
|
|
170
|
+
it with the registrar service.
|
|
171
171
|
"""
|
|
172
172
|
if self._started:
|
|
173
173
|
raise RuntimeError("Worker has already been started")
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
|
|
175
|
+
self._registrar_service, self._registrar_context = await self._enter_context(
|
|
176
|
+
self._registrar
|
|
177
|
+
)
|
|
178
|
+
if not isinstance(self._registrar_service, RegistrarLike):
|
|
179
|
+
raise ValueError("Registrar factory must return a RegistrarLike instance")
|
|
180
|
+
|
|
176
181
|
await self._start()
|
|
177
182
|
self._started = True
|
|
183
|
+
assert self._info
|
|
184
|
+
await self._registrar_service.register(self._info)
|
|
178
185
|
|
|
179
186
|
@final
|
|
180
187
|
async def stop(self):
|
|
@@ -182,13 +189,23 @@ class Worker(ABC, Generic[_T_RegistryService]):
|
|
|
182
189
|
|
|
183
190
|
This method is a final implementation that calls the abstract
|
|
184
191
|
`_stop` method to gracefully shut down the worker process and
|
|
185
|
-
unregister it from the
|
|
192
|
+
unregister it from the registrar service.
|
|
186
193
|
"""
|
|
187
194
|
if not self._started:
|
|
188
195
|
raise RuntimeError("Worker has not been started")
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
196
|
+
try:
|
|
197
|
+
if not self._info:
|
|
198
|
+
raise RuntimeError("Cannot unregister - worker has no info")
|
|
199
|
+
assert self._registrar_service is not None
|
|
200
|
+
await self._registrar_service.unregister(self._info)
|
|
201
|
+
finally:
|
|
202
|
+
try:
|
|
203
|
+
await self._stop()
|
|
204
|
+
finally:
|
|
205
|
+
await self._exit_context(self._registrar_context)
|
|
206
|
+
self._registrar_service = None
|
|
207
|
+
self._registrar_context = None
|
|
208
|
+
self._started = False
|
|
192
209
|
|
|
193
210
|
@abstractmethod
|
|
194
211
|
async def _start(self):
|
|
@@ -208,10 +225,36 @@ class Worker(ABC, Generic[_T_RegistryService]):
|
|
|
208
225
|
"""
|
|
209
226
|
...
|
|
210
227
|
|
|
228
|
+
async def _enter_context(self, factory):
|
|
229
|
+
"""Enter context for factory objects, handling different factory types."""
|
|
230
|
+
ctx = None
|
|
231
|
+
if isinstance(factory, ContextManager):
|
|
232
|
+
ctx = factory
|
|
233
|
+
obj = ctx.__enter__()
|
|
234
|
+
elif isinstance(factory, AsyncContextManager):
|
|
235
|
+
ctx = factory
|
|
236
|
+
obj = await ctx.__aenter__()
|
|
237
|
+
elif callable(factory):
|
|
238
|
+
return await self._enter_context(factory())
|
|
239
|
+
elif isinstance(factory, Awaitable):
|
|
240
|
+
obj = await factory
|
|
241
|
+
else:
|
|
242
|
+
obj = factory
|
|
243
|
+
return obj, ctx
|
|
244
|
+
|
|
245
|
+
async def _exit_context(
|
|
246
|
+
self, ctx: AsyncContextManager | ContextManager | None, *args
|
|
247
|
+
):
|
|
248
|
+
"""Exit context for context managers."""
|
|
249
|
+
if isinstance(ctx, AsyncContextManager):
|
|
250
|
+
await ctx.__aexit__(*args)
|
|
251
|
+
elif isinstance(ctx, ContextManager):
|
|
252
|
+
ctx.__exit__(*args)
|
|
253
|
+
|
|
211
254
|
|
|
212
255
|
# public
|
|
213
|
-
class WorkerFactory(
|
|
214
|
-
"""Protocol for creating worker instances with
|
|
256
|
+
class WorkerFactory(Protocol):
|
|
257
|
+
"""Protocol for creating worker instances with registrar integration.
|
|
215
258
|
|
|
216
259
|
Defines the callable interface for worker factory implementations
|
|
217
260
|
that can create :py:class:`Worker` instances configured with specific
|
|
@@ -221,7 +264,7 @@ class WorkerFactory(Generic[_T_RegistryService], Protocol):
|
|
|
221
264
|
worker processes with consistent configuration.
|
|
222
265
|
"""
|
|
223
266
|
|
|
224
|
-
def __call__(self, *tags: str, **_) -> Worker
|
|
267
|
+
def __call__(self, *tags: str, **_) -> Worker:
|
|
225
268
|
"""Create a new worker instance.
|
|
226
269
|
|
|
227
270
|
:param tags:
|
|
@@ -235,12 +278,12 @@ class WorkerFactory(Generic[_T_RegistryService], Protocol):
|
|
|
235
278
|
|
|
236
279
|
|
|
237
280
|
# public
|
|
238
|
-
class LocalWorker(Worker
|
|
281
|
+
class LocalWorker(Worker):
|
|
239
282
|
"""Local worker implementation that runs tasks in a separate process.
|
|
240
283
|
|
|
241
284
|
:py:class:`LocalWorker` creates and manages a dedicated worker process
|
|
242
285
|
that hosts a gRPC server for executing distributed wool tasks. Each
|
|
243
|
-
worker automatically registers itself with the provided
|
|
286
|
+
worker automatically registers itself with the provided registrar service
|
|
244
287
|
for discovery by client sessions.
|
|
245
288
|
|
|
246
289
|
The worker process runs independently and can handle multiple concurrent
|
|
@@ -250,8 +293,8 @@ class LocalWorker(Worker[_T_RegistryService]):
|
|
|
250
293
|
:param tags:
|
|
251
294
|
Capability tags to associate with this worker for filtering
|
|
252
295
|
and selection by client sessions.
|
|
253
|
-
:param
|
|
254
|
-
Service instance for worker registration and discovery.
|
|
296
|
+
:param registrar:
|
|
297
|
+
Service instance or factory for worker registration and discovery.
|
|
255
298
|
:param extra:
|
|
256
299
|
Additional arbitrary metadata as key-value pairs.
|
|
257
300
|
"""
|
|
@@ -263,10 +306,10 @@ class LocalWorker(Worker[_T_RegistryService]):
|
|
|
263
306
|
*tags: str,
|
|
264
307
|
host: str = "127.0.0.1",
|
|
265
308
|
port: int = 0,
|
|
266
|
-
|
|
309
|
+
registrar: RegistrarLike | Factory[RegistrarLike],
|
|
267
310
|
**extra: Any,
|
|
268
311
|
):
|
|
269
|
-
super().__init__(*tags,
|
|
312
|
+
super().__init__(*tags, registrar=registrar, **extra)
|
|
270
313
|
self._worker_process = WorkerProcess(host=host, port=port)
|
|
271
314
|
|
|
272
315
|
@property
|
|
@@ -299,9 +342,9 @@ class LocalWorker(Worker[_T_RegistryService]):
|
|
|
299
342
|
async def _start(self):
|
|
300
343
|
"""Start the worker process and register it with the pool.
|
|
301
344
|
|
|
302
|
-
Initializes the
|
|
345
|
+
Initializes the registrar service, starts the worker process
|
|
303
346
|
with its gRPC server, and registers the worker's network
|
|
304
|
-
address with the
|
|
347
|
+
address with the registrar for discovery by client sessions.
|
|
305
348
|
"""
|
|
306
349
|
loop = asyncio.get_running_loop()
|
|
307
350
|
await loop.run_in_executor(None, self._worker_process.start)
|
|
@@ -324,30 +367,24 @@ class LocalWorker(Worker[_T_RegistryService]):
|
|
|
324
367
|
tags=self._tags,
|
|
325
368
|
extra=self._extra,
|
|
326
369
|
)
|
|
327
|
-
await self._registry_service.register(self._info)
|
|
328
370
|
|
|
329
371
|
async def _stop(self):
|
|
330
372
|
"""Stop the worker process and unregister it from the pool.
|
|
331
373
|
|
|
332
|
-
Unregisters the worker from the
|
|
374
|
+
Unregisters the worker from the registrar service, gracefully
|
|
333
375
|
shuts down the worker process using SIGINT, and cleans up
|
|
334
|
-
the
|
|
376
|
+
the registrar service. If graceful shutdown fails, the process
|
|
335
377
|
is forcefully terminated.
|
|
336
378
|
"""
|
|
379
|
+
if not self._worker_process.is_alive():
|
|
380
|
+
return
|
|
337
381
|
try:
|
|
338
|
-
if
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if
|
|
343
|
-
|
|
344
|
-
try:
|
|
345
|
-
if self._worker_process.pid:
|
|
346
|
-
os.kill(self._worker_process.pid, signal.SIGINT)
|
|
347
|
-
self._worker_process.join()
|
|
348
|
-
except OSError:
|
|
349
|
-
if self._worker_process.is_alive():
|
|
350
|
-
self._worker_process.kill()
|
|
382
|
+
if self._worker_process.pid:
|
|
383
|
+
os.kill(self._worker_process.pid, signal.SIGINT)
|
|
384
|
+
self._worker_process.join()
|
|
385
|
+
except OSError:
|
|
386
|
+
if self._worker_process.is_alive():
|
|
387
|
+
self._worker_process.kill()
|
|
351
388
|
|
|
352
389
|
|
|
353
390
|
class WorkerProcess(Process):
|
|
@@ -11,7 +11,6 @@ from abc import abstractmethod
|
|
|
11
11
|
from collections import deque
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from dataclasses import field
|
|
14
|
-
from typing import TYPE_CHECKING
|
|
15
14
|
from typing import Any
|
|
16
15
|
from typing import AsyncContextManager
|
|
17
16
|
from typing import AsyncIterator
|
|
@@ -23,15 +22,13 @@ from typing import Dict
|
|
|
23
22
|
from typing import Generic
|
|
24
23
|
from typing import Literal
|
|
25
24
|
from typing import Protocol
|
|
25
|
+
from typing import Self
|
|
26
26
|
from typing import Tuple
|
|
27
27
|
from typing import TypeAlias
|
|
28
28
|
from typing import TypeVar
|
|
29
29
|
from typing import final
|
|
30
30
|
from typing import runtime_checkable
|
|
31
31
|
|
|
32
|
-
if TYPE_CHECKING:
|
|
33
|
-
pass
|
|
34
|
-
|
|
35
32
|
from zeroconf import IPVersion
|
|
36
33
|
from zeroconf import ServiceInfo
|
|
37
34
|
from zeroconf import ServiceListener
|
|
@@ -39,9 +36,6 @@ from zeroconf import Zeroconf
|
|
|
39
36
|
from zeroconf.asyncio import AsyncServiceBrowser
|
|
40
37
|
from zeroconf.asyncio import AsyncZeroconf
|
|
41
38
|
|
|
42
|
-
if TYPE_CHECKING:
|
|
43
|
-
pass
|
|
44
|
-
|
|
45
39
|
|
|
46
40
|
# public
|
|
47
41
|
@dataclass
|
|
@@ -314,7 +308,6 @@ class DiscoveryEvent:
|
|
|
314
308
|
_T_co = TypeVar("_T_co", covariant=True)
|
|
315
309
|
|
|
316
310
|
|
|
317
|
-
# public
|
|
318
311
|
class Reducible(Protocol):
|
|
319
312
|
"""Protocol for objects that support pickling via __reduce__."""
|
|
320
313
|
|
|
@@ -322,21 +315,22 @@ class Reducible(Protocol):
|
|
|
322
315
|
|
|
323
316
|
|
|
324
317
|
# public
|
|
325
|
-
|
|
318
|
+
@runtime_checkable
|
|
319
|
+
class DiscoveryLike(Protocol):
|
|
326
320
|
"""Protocol for async iterators that yield discovery events.
|
|
327
321
|
|
|
328
322
|
Implementations must be pickleable via __reduce__ to support
|
|
329
323
|
task-specific session contexts in distributed environments.
|
|
330
324
|
"""
|
|
331
325
|
|
|
332
|
-
def __aiter__(self) ->
|
|
326
|
+
def __aiter__(self) -> DiscoveryLike: ...
|
|
333
327
|
|
|
334
|
-
def __anext__(self) -> Awaitable[
|
|
328
|
+
def __anext__(self) -> Awaitable[DiscoveryEvent]: ...
|
|
335
329
|
|
|
336
330
|
|
|
337
331
|
# public
|
|
338
332
|
@runtime_checkable
|
|
339
|
-
class
|
|
333
|
+
class _Factory(Protocol, Generic[_T_co]):
|
|
340
334
|
def __call__(
|
|
341
335
|
self,
|
|
342
336
|
) -> (
|
|
@@ -344,8 +338,16 @@ class Factory(Protocol, Generic[_T_co]):
|
|
|
344
338
|
): ...
|
|
345
339
|
|
|
346
340
|
|
|
341
|
+
Factory: TypeAlias = (
|
|
342
|
+
Awaitable[_T_co]
|
|
343
|
+
| AsyncContextManager[_T_co]
|
|
344
|
+
| ContextManager[_T_co]
|
|
345
|
+
| _Factory[_T_co]
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
347
349
|
# public
|
|
348
|
-
class
|
|
350
|
+
class Discovery(ABC):
|
|
349
351
|
"""Abstract base class for discovering worker services.
|
|
350
352
|
|
|
351
353
|
When started, implementations should discover all existing services that
|
|
@@ -358,7 +360,7 @@ class DiscoveryService(ABC):
|
|
|
358
360
|
- Workers updated to satisfy the filter should trigger worker-added events
|
|
359
361
|
- Workers updated to no longer satisfy the filter should trigger
|
|
360
362
|
worker-removed events
|
|
361
|
-
- Tracked workers removed from the
|
|
363
|
+
- Tracked workers removed from the registrar entirely should always
|
|
362
364
|
trigger worker-removed
|
|
363
365
|
|
|
364
366
|
:param filter:
|
|
@@ -436,7 +438,7 @@ class DiscoveryService(ABC):
|
|
|
436
438
|
"""Yields discovery events as they occur.
|
|
437
439
|
|
|
438
440
|
Returns an asynchronous iterator that yields discovery events for
|
|
439
|
-
workers being added, updated, or removed from the
|
|
441
|
+
workers being added, updated, or removed from the registrar. Events
|
|
440
442
|
are filtered according to the filter function provided during
|
|
441
443
|
initialization.
|
|
442
444
|
|
|
@@ -457,21 +459,18 @@ class DiscoveryService(ABC):
|
|
|
457
459
|
...
|
|
458
460
|
|
|
459
461
|
|
|
460
|
-
|
|
462
|
+
_T_DiscoveryLike = TypeVar("_T_DiscoveryLike", bound=Discovery)
|
|
461
463
|
|
|
462
464
|
|
|
463
465
|
# public
|
|
464
|
-
|
|
466
|
+
@runtime_checkable
|
|
467
|
+
class RegistrarLike(Protocol):
|
|
465
468
|
"""Abstract base class for a service where workers can register themselves.
|
|
466
469
|
|
|
467
470
|
Provides the interface for worker registration, unregistration, and
|
|
468
471
|
property updates within a distributed worker pool system.
|
|
469
472
|
"""
|
|
470
473
|
|
|
471
|
-
async def start(self) -> None: ...
|
|
472
|
-
|
|
473
|
-
async def stop(self) -> None: ...
|
|
474
|
-
|
|
475
474
|
async def register(self, worker_info: WorkerInfo) -> None: ...
|
|
476
475
|
|
|
477
476
|
async def unregister(self, worker_info: WorkerInfo) -> None: ...
|
|
@@ -480,7 +479,7 @@ class RegistryServiceLike(Protocol):
|
|
|
480
479
|
|
|
481
480
|
|
|
482
481
|
# public
|
|
483
|
-
class
|
|
482
|
+
class Registrar(Generic[_T_DiscoveryLike], ABC):
|
|
484
483
|
"""Abstract base class for a service where workers can register themselves.
|
|
485
484
|
|
|
486
485
|
Provides the interface for worker registration, unregistration, and
|
|
@@ -494,19 +493,26 @@ class RegistryService(Generic[_T_DiscoveryServiceLike], ABC):
|
|
|
494
493
|
self._started = False
|
|
495
494
|
self._stopped = False
|
|
496
495
|
|
|
496
|
+
async def __aenter__(self) -> Self:
|
|
497
|
+
await self.start()
|
|
498
|
+
return self
|
|
499
|
+
|
|
500
|
+
async def __aexit__(self, *_):
|
|
501
|
+
await self.stop()
|
|
502
|
+
|
|
497
503
|
async def start(self) -> None:
|
|
498
|
-
"""Starts the
|
|
504
|
+
"""Starts the registrar service, making it ready to accept registrations.
|
|
499
505
|
|
|
500
506
|
:raises RuntimeError:
|
|
501
507
|
If the service has already been started.
|
|
502
508
|
"""
|
|
503
509
|
if self._started:
|
|
504
|
-
raise RuntimeError("
|
|
510
|
+
raise RuntimeError("Registrar service already started")
|
|
505
511
|
await asyncio.wait_for(self._start(), timeout=60)
|
|
506
512
|
self._started = True
|
|
507
513
|
|
|
508
514
|
async def stop(self) -> None:
|
|
509
|
-
"""Stops the
|
|
515
|
+
"""Stops the registrar service and cleans up any resources.
|
|
510
516
|
|
|
511
517
|
:raises RuntimeError:
|
|
512
518
|
If the service has not been started.
|
|
@@ -514,18 +520,18 @@ class RegistryService(Generic[_T_DiscoveryServiceLike], ABC):
|
|
|
514
520
|
if self._stopped:
|
|
515
521
|
return
|
|
516
522
|
if not self._started:
|
|
517
|
-
raise RuntimeError("
|
|
523
|
+
raise RuntimeError("Registrar service not started")
|
|
518
524
|
await self._stop()
|
|
519
525
|
self._stopped = True
|
|
520
526
|
|
|
521
527
|
@abstractmethod
|
|
522
528
|
async def _start(self) -> None:
|
|
523
|
-
"""Starts the
|
|
529
|
+
"""Starts the registrar service, making it ready to accept registrations."""
|
|
524
530
|
...
|
|
525
531
|
|
|
526
532
|
@abstractmethod
|
|
527
533
|
async def _stop(self) -> None:
|
|
528
|
-
"""Stops the
|
|
534
|
+
"""Stops the registrar service and cleans up any resources."""
|
|
529
535
|
...
|
|
530
536
|
|
|
531
537
|
async def register(
|
|
@@ -538,12 +544,12 @@ class RegistryService(Generic[_T_DiscoveryServiceLike], ABC):
|
|
|
538
544
|
The :class:`~wool._worker_discovery.WorkerInfo` instance containing all
|
|
539
545
|
worker details.
|
|
540
546
|
:raises RuntimeError:
|
|
541
|
-
If the
|
|
547
|
+
If the registrar service is not running.
|
|
542
548
|
"""
|
|
543
549
|
if not self._started:
|
|
544
|
-
raise RuntimeError("
|
|
550
|
+
raise RuntimeError("Registrar service not started - call start() first")
|
|
545
551
|
if self._stopped:
|
|
546
|
-
raise RuntimeError("
|
|
552
|
+
raise RuntimeError("Registrar service already stopped")
|
|
547
553
|
await self._register(worker_info)
|
|
548
554
|
|
|
549
555
|
@abstractmethod
|
|
@@ -566,12 +572,12 @@ class RegistryService(Generic[_T_DiscoveryServiceLike], ABC):
|
|
|
566
572
|
The :class:`~wool._worker_discovery.WorkerInfo` instance of the worker to
|
|
567
573
|
unregister.
|
|
568
574
|
:raises RuntimeError:
|
|
569
|
-
If the
|
|
575
|
+
If the registrar service is not running.
|
|
570
576
|
"""
|
|
571
577
|
if not self._started:
|
|
572
|
-
raise RuntimeError("
|
|
578
|
+
raise RuntimeError("Registrar service not started - call start() first")
|
|
573
579
|
if self._stopped:
|
|
574
|
-
raise RuntimeError("
|
|
580
|
+
raise RuntimeError("Registrar service already stopped")
|
|
575
581
|
await self._unregister(worker_info)
|
|
576
582
|
|
|
577
583
|
@abstractmethod
|
|
@@ -590,12 +596,12 @@ class RegistryService(Generic[_T_DiscoveryServiceLike], ABC):
|
|
|
590
596
|
:param worker_info:
|
|
591
597
|
The updated :class:`~wool._worker_discovery.WorkerInfo` instance.
|
|
592
598
|
:raises RuntimeError:
|
|
593
|
-
If the
|
|
599
|
+
If the registrar service is not running.
|
|
594
600
|
"""
|
|
595
601
|
if not self._started:
|
|
596
|
-
raise RuntimeError("
|
|
602
|
+
raise RuntimeError("Registrar service not started - call start() first")
|
|
597
603
|
if self._stopped:
|
|
598
|
-
raise RuntimeError("
|
|
604
|
+
raise RuntimeError("Registrar service already stopped")
|
|
599
605
|
await self._update(worker_info)
|
|
600
606
|
|
|
601
607
|
@abstractmethod
|
|
@@ -609,7 +615,7 @@ class RegistryService(Generic[_T_DiscoveryServiceLike], ABC):
|
|
|
609
615
|
|
|
610
616
|
|
|
611
617
|
# public
|
|
612
|
-
class
|
|
618
|
+
class LanDiscovery(Discovery):
|
|
613
619
|
"""Implements worker discovery on the local network using Zeroconf.
|
|
614
620
|
|
|
615
621
|
This service browses the local network for DNS-SD services and delivers
|
|
@@ -705,12 +711,12 @@ class LanDiscoveryService(DiscoveryService):
|
|
|
705
711
|
|
|
706
712
|
def add_service(self, zc: Zeroconf, type_: str, name: str):
|
|
707
713
|
"""Called by Zeroconf when a service is added."""
|
|
708
|
-
if type_ ==
|
|
714
|
+
if type_ == LanRegistrar.service_type:
|
|
709
715
|
asyncio.create_task(self._handle_add_service(type_, name))
|
|
710
716
|
|
|
711
717
|
def remove_service(self, zc: Zeroconf, type_: str, name: str):
|
|
712
718
|
"""Called by Zeroconf when a service is removed."""
|
|
713
|
-
if type_ ==
|
|
719
|
+
if type_ == LanRegistrar.service_type:
|
|
714
720
|
if worker := self._service_cache.pop(name, None):
|
|
715
721
|
asyncio.create_task(
|
|
716
722
|
self._event_queue.put(
|
|
@@ -720,7 +726,7 @@ class LanDiscoveryService(DiscoveryService):
|
|
|
720
726
|
|
|
721
727
|
def update_service(self, zc: Zeroconf, type_, name):
|
|
722
728
|
"""Called by Zeroconf when a service is updated."""
|
|
723
|
-
if type_ ==
|
|
729
|
+
if type_ == LanRegistrar.service_type:
|
|
724
730
|
asyncio.create_task(self._handle_update_service(type_, name))
|
|
725
731
|
|
|
726
732
|
async def _handle_add_service(self, type_: str, name: str):
|
|
@@ -787,11 +793,11 @@ class LanDiscoveryService(DiscoveryService):
|
|
|
787
793
|
|
|
788
794
|
|
|
789
795
|
# public
|
|
790
|
-
class
|
|
791
|
-
"""Implements a worker
|
|
796
|
+
class LanRegistrar(Registrar[LanDiscovery]):
|
|
797
|
+
"""Implements a worker registrar using Zeroconf to advertise on the LAN.
|
|
792
798
|
|
|
793
799
|
This service registers workers by publishing a DNS-SD service record on
|
|
794
|
-
the local network, allowing :class:`
|
|
800
|
+
the local network, allowing :class:`LanDiscovery` to find them.
|
|
795
801
|
"""
|
|
796
802
|
|
|
797
803
|
aiozc: AsyncZeroconf | None
|
|
@@ -824,10 +830,10 @@ class LanRegistryService(RegistryService[LanDiscoveryService]):
|
|
|
824
830
|
The :class:`~wool._worker_discovery.WorkerInfo` instance containing all
|
|
825
831
|
worker details.
|
|
826
832
|
:raises RuntimeError:
|
|
827
|
-
If the
|
|
833
|
+
If the registrar service is not properly initialized.
|
|
828
834
|
"""
|
|
829
835
|
if self.aiozc is None:
|
|
830
|
-
raise RuntimeError("
|
|
836
|
+
raise RuntimeError("Registrar service not properly initialized")
|
|
831
837
|
address = f"{worker_info.host}:{worker_info.port}"
|
|
832
838
|
ip_address, port = self._resolve_address(address)
|
|
833
839
|
service_name = f"{worker_info.uid}.{self.service_type}"
|
|
@@ -848,10 +854,10 @@ class LanRegistryService(RegistryService[LanDiscoveryService]):
|
|
|
848
854
|
The :class:`~wool._worker_discovery.WorkerInfo` instance of the worker to
|
|
849
855
|
unregister.
|
|
850
856
|
:raises RuntimeError:
|
|
851
|
-
If the
|
|
857
|
+
If the registrar service is not properly initialized.
|
|
852
858
|
"""
|
|
853
859
|
if self.aiozc is None:
|
|
854
|
-
raise RuntimeError("
|
|
860
|
+
raise RuntimeError("Registrar service not properly initialized")
|
|
855
861
|
service = self.services[worker_info.uid]
|
|
856
862
|
await self.aiozc.async_unregister_service(service)
|
|
857
863
|
del self.services[worker_info.uid]
|
|
@@ -866,12 +872,12 @@ class LanRegistryService(RegistryService[LanDiscoveryService]):
|
|
|
866
872
|
:param worker_info:
|
|
867
873
|
The updated :class:`~wool._worker_discovery.WorkerInfo` instance.
|
|
868
874
|
:raises RuntimeError:
|
|
869
|
-
If the
|
|
875
|
+
If the registrar service is not properly initialized.
|
|
870
876
|
:raises Exception:
|
|
871
877
|
If the Zeroconf service update fails.
|
|
872
878
|
"""
|
|
873
879
|
if self.aiozc is None:
|
|
874
|
-
raise RuntimeError("
|
|
880
|
+
raise RuntimeError("Registrar service not properly initialized")
|
|
875
881
|
|
|
876
882
|
service = self.services[worker_info.uid]
|
|
877
883
|
new_properties = _serialize_worker_info(worker_info)
|
|
@@ -971,12 +977,12 @@ def _deserialize_worker_info(info: ServiceInfo) -> WorkerInfo:
|
|
|
971
977
|
|
|
972
978
|
|
|
973
979
|
# public
|
|
974
|
-
class
|
|
975
|
-
"""Implements a worker
|
|
980
|
+
class LocalRegistrar(Registrar):
|
|
981
|
+
"""Implements a worker registrar using shared memory for local pools.
|
|
976
982
|
|
|
977
983
|
This service registers workers by writing their information to a shared memory
|
|
978
|
-
block, allowing
|
|
979
|
-
The
|
|
984
|
+
block, allowing LocalDiscovery instances to find them efficiently.
|
|
985
|
+
The registrar stores worker ports as integers in a simple array format,
|
|
980
986
|
providing fast local discovery without network overhead.
|
|
981
987
|
|
|
982
988
|
:param uri:
|
|
@@ -1018,7 +1024,7 @@ class LocalRegistryService(RegistryService):
|
|
|
1018
1024
|
if self._shared_memory:
|
|
1019
1025
|
try:
|
|
1020
1026
|
self._shared_memory.close()
|
|
1021
|
-
# Unlink the shared memory if this
|
|
1027
|
+
# Unlink the shared memory if this registrar created it
|
|
1022
1028
|
if self._created_shared_memory:
|
|
1023
1029
|
self._shared_memory.unlink()
|
|
1024
1030
|
except Exception:
|
|
@@ -1033,10 +1039,10 @@ class LocalRegistryService(RegistryService):
|
|
|
1033
1039
|
The :class:`~wool._worker_discovery.WorkerInfo` instance containing all
|
|
1034
1040
|
worker details. Only the port is stored in shared memory.
|
|
1035
1041
|
:raises RuntimeError:
|
|
1036
|
-
If the
|
|
1042
|
+
If the registrar service is not properly initialized.
|
|
1037
1043
|
"""
|
|
1038
1044
|
if self._shared_memory is None:
|
|
1039
|
-
raise RuntimeError("
|
|
1045
|
+
raise RuntimeError("Registrar service not properly initialized")
|
|
1040
1046
|
|
|
1041
1047
|
if worker_info.port is None:
|
|
1042
1048
|
raise ValueError("Worker port must be specified")
|
|
@@ -1048,7 +1054,7 @@ class LocalRegistryService(RegistryService):
|
|
|
1048
1054
|
struct.pack_into("I", self._shared_memory.buf, i, worker_info.port)
|
|
1049
1055
|
break
|
|
1050
1056
|
else:
|
|
1051
|
-
raise RuntimeError("No available slots in shared memory
|
|
1057
|
+
raise RuntimeError("No available slots in shared memory registrar")
|
|
1052
1058
|
|
|
1053
1059
|
async def _unregister(self, worker_info: WorkerInfo) -> None:
|
|
1054
1060
|
"""Unregister a worker by removing its port from shared memory.
|
|
@@ -1057,10 +1063,10 @@ class LocalRegistryService(RegistryService):
|
|
|
1057
1063
|
The :class:`~wool._worker_discovery.WorkerInfo` instance of the worker to
|
|
1058
1064
|
unregister.
|
|
1059
1065
|
:raises RuntimeError:
|
|
1060
|
-
If the
|
|
1066
|
+
If the registrar service is not properly initialized.
|
|
1061
1067
|
"""
|
|
1062
1068
|
if self._shared_memory is None:
|
|
1063
|
-
raise RuntimeError("
|
|
1069
|
+
raise RuntimeError("Registrar service not properly initialized")
|
|
1064
1070
|
|
|
1065
1071
|
if worker_info.port is None:
|
|
1066
1072
|
return
|
|
@@ -1075,7 +1081,7 @@ class LocalRegistryService(RegistryService):
|
|
|
1075
1081
|
async def _update(self, worker_info: WorkerInfo) -> None:
|
|
1076
1082
|
"""Update a worker's properties in shared memory.
|
|
1077
1083
|
|
|
1078
|
-
For the simple port-based
|
|
1084
|
+
For the simple port-based registrar, update is the same as register.
|
|
1079
1085
|
|
|
1080
1086
|
:param worker_info:
|
|
1081
1087
|
The updated :class:`~wool._worker_discovery.WorkerInfo` instance.
|
|
@@ -1084,7 +1090,7 @@ class LocalRegistryService(RegistryService):
|
|
|
1084
1090
|
|
|
1085
1091
|
|
|
1086
1092
|
# public
|
|
1087
|
-
class
|
|
1093
|
+
class LocalDiscovery(Discovery):
|
|
1088
1094
|
"""Implements worker discovery using shared memory for local pools.
|
|
1089
1095
|
|
|
1090
1096
|
This service reads worker ports from a shared memory block and
|
|
@@ -1172,8 +1178,8 @@ class LocalDiscoveryService(DiscoveryService):
|
|
|
1172
1178
|
uid=f"worker-{port}",
|
|
1173
1179
|
host="localhost",
|
|
1174
1180
|
port=port,
|
|
1175
|
-
pid=0, # Not available in simple
|
|
1176
|
-
version="unknown", # Not available in simple
|
|
1181
|
+
pid=0, # Not available in simple registrar
|
|
1182
|
+
version="unknown", # Not available in simple registrar
|
|
1177
1183
|
tags=set(),
|
|
1178
1184
|
extra={},
|
|
1179
1185
|
)
|
|
@@ -1211,7 +1217,7 @@ class LocalDiscoveryService(DiscoveryService):
|
|
|
1211
1217
|
event = DiscoveryEvent(type="worker_removed", worker_info=worker_info)
|
|
1212
1218
|
await self._event_queue.put(event)
|
|
1213
1219
|
|
|
1214
|
-
# Find updated workers (minimal for port-only
|
|
1220
|
+
# Find updated workers (minimal for port-only registrar)
|
|
1215
1221
|
for uid, worker_info in current_workers.items():
|
|
1216
1222
|
if uid in self._service_cache:
|
|
1217
1223
|
old_worker = self._service_cache[uid]
|
|
@@ -4,22 +4,17 @@ import asyncio
|
|
|
4
4
|
import hashlib
|
|
5
5
|
import os
|
|
6
6
|
import uuid
|
|
7
|
-
from functools import partial
|
|
8
7
|
from multiprocessing.shared_memory import SharedMemory
|
|
9
|
-
from typing import AsyncIterator
|
|
10
8
|
from typing import Final
|
|
11
9
|
from typing import overload
|
|
12
10
|
|
|
13
11
|
from wool._worker import LocalWorker
|
|
14
12
|
from wool._worker import Worker
|
|
15
13
|
from wool._worker import WorkerFactory
|
|
16
|
-
from wool._worker_discovery import
|
|
14
|
+
from wool._worker_discovery import DiscoveryLike
|
|
17
15
|
from wool._worker_discovery import Factory
|
|
18
|
-
from wool._worker_discovery import
|
|
19
|
-
from wool._worker_discovery import
|
|
20
|
-
from wool._worker_discovery import ReducibleAsyncIteratorLike
|
|
21
|
-
from wool._worker_discovery import RegistryServiceLike
|
|
22
|
-
from wool._worker_proxy import LoadBalancerFactory
|
|
16
|
+
from wool._worker_discovery import LocalDiscovery
|
|
17
|
+
from wool._worker_discovery import LocalRegistrar
|
|
23
18
|
from wool._worker_proxy import LoadBalancerLike
|
|
24
19
|
from wool._worker_proxy import RoundRobinLoadBalancer
|
|
25
20
|
from wool._worker_proxy import WorkerProxy
|
|
@@ -65,13 +60,11 @@ class WorkerPool:
|
|
|
65
60
|
.. code-block:: python
|
|
66
61
|
|
|
67
62
|
from wool import WorkerPool, LocalWorker
|
|
68
|
-
from wool._worker_discovery import
|
|
63
|
+
from wool._worker_discovery import LocalRegistrar
|
|
69
64
|
from functools import partial
|
|
70
65
|
|
|
71
66
|
# Custom worker factory with specific tags
|
|
72
|
-
worker_factory = partial(
|
|
73
|
-
LocalWorker, registry_service=LocalRegistryService("my-pool")
|
|
74
|
-
)
|
|
67
|
+
worker_factory = partial(LocalWorker, registrar=LocalRegistrar("my-pool"))
|
|
75
68
|
|
|
76
69
|
async with WorkerPool(
|
|
77
70
|
"gpu-capable",
|
|
@@ -86,10 +79,10 @@ class WorkerPool:
|
|
|
86
79
|
.. code-block:: python
|
|
87
80
|
|
|
88
81
|
from wool import WorkerPool
|
|
89
|
-
from wool._worker_discovery import
|
|
82
|
+
from wool._worker_discovery import LanDiscovery
|
|
90
83
|
|
|
91
84
|
# Connect to existing workers on the network
|
|
92
|
-
discovery =
|
|
85
|
+
discovery = LanDiscovery(filter=lambda w: "production" in w.tags)
|
|
93
86
|
|
|
94
87
|
async with WorkerPool(discovery=discovery) as pool:
|
|
95
88
|
results = await gather_metrics()
|
|
@@ -158,15 +151,15 @@ class WorkerPool:
|
|
|
158
151
|
Examples::
|
|
159
152
|
|
|
160
153
|
# Direct instance
|
|
161
|
-
discovery=
|
|
154
|
+
discovery=LanDiscovery(filter=lambda w: "prod" in w.tags)
|
|
162
155
|
|
|
163
156
|
# Instance factory
|
|
164
|
-
discovery=lambda:
|
|
157
|
+
discovery=lambda: LocalDiscovery("pool-123")
|
|
165
158
|
|
|
166
159
|
# Context manager factory
|
|
167
160
|
@asynccontextmanager
|
|
168
161
|
async def discovery():
|
|
169
|
-
service = await
|
|
162
|
+
service = await DatabaseDiscovery.create(connection_string)
|
|
170
163
|
try:
|
|
171
164
|
...
|
|
172
165
|
yield service
|
|
@@ -186,8 +179,10 @@ class WorkerPool:
|
|
|
186
179
|
self,
|
|
187
180
|
*tags: str,
|
|
188
181
|
size: int = 0,
|
|
189
|
-
worker: WorkerFactory
|
|
190
|
-
loadbalancer:
|
|
182
|
+
worker: WorkerFactory = LocalWorker,
|
|
183
|
+
loadbalancer: (
|
|
184
|
+
LoadBalancerLike | Factory[LoadBalancerLike]
|
|
185
|
+
) = RoundRobinLoadBalancer,
|
|
191
186
|
):
|
|
192
187
|
"""
|
|
193
188
|
Create an ephemeral pool of workers, spawning the specified quantity of workers
|
|
@@ -199,11 +194,10 @@ class WorkerPool:
|
|
|
199
194
|
def __init__(
|
|
200
195
|
self,
|
|
201
196
|
*,
|
|
202
|
-
discovery:
|
|
203
|
-
|
|
204
|
-
| Factory[
|
|
205
|
-
),
|
|
206
|
-
loadbalancer: LoadBalancerLike | LoadBalancerFactory = RoundRobinLoadBalancer,
|
|
197
|
+
discovery: DiscoveryLike | Factory[DiscoveryLike],
|
|
198
|
+
loadbalancer: (
|
|
199
|
+
LoadBalancerLike | Factory[LoadBalancerLike]
|
|
200
|
+
) = RoundRobinLoadBalancer,
|
|
207
201
|
):
|
|
208
202
|
"""
|
|
209
203
|
Connect to an existing pool of workers discovered by the specified discovery
|
|
@@ -216,12 +210,10 @@ class WorkerPool:
|
|
|
216
210
|
*tags: str,
|
|
217
211
|
size: int | None = None,
|
|
218
212
|
worker: WorkerFactory | None = None,
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
| None
|
|
224
|
-
) = None,
|
|
213
|
+
discovery: DiscoveryLike | Factory[DiscoveryLike] | None = None,
|
|
214
|
+
loadbalancer: (
|
|
215
|
+
LoadBalancerLike | Factory[LoadBalancerLike]
|
|
216
|
+
) = RoundRobinLoadBalancer,
|
|
225
217
|
):
|
|
226
218
|
self._workers = []
|
|
227
219
|
|
|
@@ -244,7 +236,7 @@ class WorkerPool:
|
|
|
244
236
|
self._shared_memory.buf[i] = 0
|
|
245
237
|
await self._spawn_workers(uri, *tags, size=size, factory=worker)
|
|
246
238
|
return WorkerProxy(
|
|
247
|
-
discovery=
|
|
239
|
+
discovery=LocalDiscovery(uri),
|
|
248
240
|
loadbalancer=loadbalancer,
|
|
249
241
|
)
|
|
250
242
|
|
|
@@ -269,7 +261,7 @@ class WorkerPool:
|
|
|
269
261
|
self._shared_memory.buf[i] = 0
|
|
270
262
|
await self._spawn_workers(uri, *tags, size=size, factory=worker)
|
|
271
263
|
return WorkerProxy(
|
|
272
|
-
discovery=
|
|
264
|
+
discovery=LocalDiscovery(uri),
|
|
273
265
|
loadbalancer=loadbalancer,
|
|
274
266
|
)
|
|
275
267
|
|
|
@@ -289,7 +281,7 @@ class WorkerPool:
|
|
|
289
281
|
async def __aenter__(self) -> WorkerPool:
|
|
290
282
|
"""Starts the worker pool and its services, returning a session.
|
|
291
283
|
|
|
292
|
-
This method starts the worker
|
|
284
|
+
This method starts the worker registrar, creates a client session,
|
|
293
285
|
launches all worker processes, and registers them.
|
|
294
286
|
|
|
295
287
|
:returns:
|
|
@@ -332,6 +324,6 @@ class WorkerPool:
|
|
|
332
324
|
|
|
333
325
|
def _default_worker_factory(self, uri):
|
|
334
326
|
def factory(*tags, **_):
|
|
335
|
-
return LocalWorker(*tags,
|
|
327
|
+
return LocalWorker(*tags, registrar=LocalRegistrar(uri))
|
|
336
328
|
|
|
337
329
|
return factory
|
|
@@ -27,9 +27,9 @@ from wool._resource_pool import Resource
|
|
|
27
27
|
from wool._resource_pool import ResourcePool
|
|
28
28
|
from wool._worker import WorkerClient
|
|
29
29
|
from wool._worker_discovery import DiscoveryEvent
|
|
30
|
+
from wool._worker_discovery import DiscoveryLike
|
|
30
31
|
from wool._worker_discovery import Factory
|
|
31
|
-
from wool._worker_discovery import
|
|
32
|
-
from wool._worker_discovery import ReducibleAsyncIteratorLike
|
|
32
|
+
from wool._worker_discovery import LocalDiscovery
|
|
33
33
|
from wool._worker_discovery import WorkerInfo
|
|
34
34
|
|
|
35
35
|
if TYPE_CHECKING:
|
|
@@ -106,6 +106,7 @@ class NoWorkersAvailable(Exception):
|
|
|
106
106
|
"""
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
# public
|
|
109
110
|
@runtime_checkable
|
|
110
111
|
class LoadBalancerLike(Protocol):
|
|
111
112
|
"""Protocol for load balancer v2 that directly dispatches tasks.
|
|
@@ -122,19 +123,16 @@ class LoadBalancerLike(Protocol):
|
|
|
122
123
|
def dispatch(self, task: WoolTask) -> AsyncIterator: ...
|
|
123
124
|
|
|
124
125
|
def worker_added_callback(
|
|
125
|
-
self, client: Resource[WorkerClient], info: WorkerInfo
|
|
126
|
+
self, client: Callable[[], Resource[WorkerClient]], info: WorkerInfo
|
|
126
127
|
): ...
|
|
127
128
|
|
|
128
129
|
def worker_updated_callback(
|
|
129
|
-
self, client: Resource[WorkerClient], info: WorkerInfo
|
|
130
|
+
self, client: Callable[[], Resource[WorkerClient]], info: WorkerInfo
|
|
130
131
|
): ...
|
|
131
132
|
|
|
132
133
|
def worker_removed_callback(self, info: WorkerInfo): ...
|
|
133
134
|
|
|
134
135
|
|
|
135
|
-
LoadBalancerFactory: TypeAlias = Factory[LoadBalancerLike]
|
|
136
|
-
|
|
137
|
-
|
|
138
136
|
DispatchCall: TypeAlias = grpc.aio.UnaryStreamCall[pb.task.Task, pb.worker.Response]
|
|
139
137
|
|
|
140
138
|
|
|
@@ -213,10 +211,14 @@ class RoundRobinLoadBalancer:
|
|
|
213
211
|
f"All {len(self._workers)} workers failed with transient errors"
|
|
214
212
|
)
|
|
215
213
|
|
|
216
|
-
def worker_added_callback(
|
|
214
|
+
def worker_added_callback(
|
|
215
|
+
self, client: Callable[[], Resource[WorkerClient]], info: WorkerInfo
|
|
216
|
+
):
|
|
217
217
|
self._workers[info] = client
|
|
218
218
|
|
|
219
|
-
def worker_updated_callback(
|
|
219
|
+
def worker_updated_callback(
|
|
220
|
+
self, client: Callable[[], Resource[WorkerClient]], info: WorkerInfo
|
|
221
|
+
):
|
|
220
222
|
self._workers[info] = client
|
|
221
223
|
|
|
222
224
|
def worker_removed_callback(self, info: WorkerInfo):
|
|
@@ -251,15 +253,12 @@ class WorkerProxy:
|
|
|
251
253
|
Load balancer implementation or factory for task distribution.
|
|
252
254
|
"""
|
|
253
255
|
|
|
254
|
-
_discovery:
|
|
255
|
-
ReducibleAsyncIteratorLike[DiscoveryEvent]
|
|
256
|
-
| Factory[AsyncIterator[DiscoveryEvent]]
|
|
257
|
-
)
|
|
256
|
+
_discovery: DiscoveryLike | Factory[DiscoveryLike]
|
|
258
257
|
_discovery_manager: (
|
|
259
|
-
AsyncContextManager[
|
|
260
|
-
| ContextManager[AsyncIterator[DiscoveryEvent]]
|
|
258
|
+
AsyncContextManager[DiscoveryLike] | ContextManager[DiscoveryLike]
|
|
261
259
|
)
|
|
262
|
-
|
|
260
|
+
|
|
261
|
+
_loadbalancer = LoadBalancerLike | Factory[LoadBalancerLike]
|
|
263
262
|
_loadbalancer_manager: (
|
|
264
263
|
AsyncContextManager[LoadBalancerLike] | ContextManager[LoadBalancerLike]
|
|
265
264
|
)
|
|
@@ -268,11 +267,10 @@ class WorkerProxy:
|
|
|
268
267
|
def __init__(
|
|
269
268
|
self,
|
|
270
269
|
*,
|
|
271
|
-
discovery:
|
|
272
|
-
|
|
273
|
-
| Factory[
|
|
274
|
-
),
|
|
275
|
-
loadbalancer: LoadBalancerLike | LoadBalancerFactory = RoundRobinLoadBalancer,
|
|
270
|
+
discovery: DiscoveryLike | Factory[DiscoveryLike],
|
|
271
|
+
loadbalancer: (
|
|
272
|
+
LoadBalancerLike | Factory[LoadBalancerLike]
|
|
273
|
+
) = RoundRobinLoadBalancer,
|
|
276
274
|
): ...
|
|
277
275
|
|
|
278
276
|
@overload
|
|
@@ -280,7 +278,8 @@ class WorkerProxy:
|
|
|
280
278
|
self,
|
|
281
279
|
*,
|
|
282
280
|
workers: Sequence[WorkerInfo],
|
|
283
|
-
loadbalancer: LoadBalancerLike
|
|
281
|
+
loadbalancer: LoadBalancerLike
|
|
282
|
+
| Factory[LoadBalancerLike] = RoundRobinLoadBalancer,
|
|
284
283
|
): ...
|
|
285
284
|
|
|
286
285
|
@overload
|
|
@@ -288,20 +287,18 @@ class WorkerProxy:
|
|
|
288
287
|
self,
|
|
289
288
|
pool_uri: str,
|
|
290
289
|
*tags: str,
|
|
291
|
-
loadbalancer: LoadBalancerLike
|
|
290
|
+
loadbalancer: LoadBalancerLike
|
|
291
|
+
| Factory[LoadBalancerLike] = RoundRobinLoadBalancer,
|
|
292
292
|
): ...
|
|
293
293
|
|
|
294
294
|
def __init__(
|
|
295
295
|
self,
|
|
296
296
|
pool_uri: str | None = None,
|
|
297
297
|
*tags: str,
|
|
298
|
-
discovery: (
|
|
299
|
-
ReducibleAsyncIteratorLike[DiscoveryEvent]
|
|
300
|
-
| Factory[AsyncIterator[DiscoveryEvent]]
|
|
301
|
-
| None
|
|
302
|
-
) = None,
|
|
298
|
+
discovery: (DiscoveryLike | Factory[DiscoveryLike] | None) = None,
|
|
303
299
|
workers: Sequence[WorkerInfo] | None = None,
|
|
304
|
-
loadbalancer: LoadBalancerLike
|
|
300
|
+
loadbalancer: LoadBalancerLike
|
|
301
|
+
| Factory[LoadBalancerLike] = RoundRobinLoadBalancer,
|
|
305
302
|
):
|
|
306
303
|
if not (pool_uri or discovery or workers):
|
|
307
304
|
raise ValueError(
|
|
@@ -316,7 +313,7 @@ class WorkerProxy:
|
|
|
316
313
|
|
|
317
314
|
match (pool_uri, discovery, workers):
|
|
318
315
|
case (pool_uri, None, None) if pool_uri is not None:
|
|
319
|
-
self._discovery =
|
|
316
|
+
self._discovery = LocalDiscovery(
|
|
320
317
|
pool_uri, filter=lambda w: bool({pool_uri, *tags} & w.tags)
|
|
321
318
|
)
|
|
322
319
|
case (None, discovery, None) if discovery is not None:
|
|
@@ -399,7 +396,7 @@ class WorkerProxy:
|
|
|
399
396
|
self._discovery_service, self._discovery_ctx = await self._enter_context(
|
|
400
397
|
self._discovery
|
|
401
398
|
)
|
|
402
|
-
if not isinstance(self._discovery_service,
|
|
399
|
+
if not isinstance(self._discovery_service, DiscoveryLike):
|
|
403
400
|
raise ValueError
|
|
404
401
|
|
|
405
402
|
self._proxy_token = wool.__proxy__.set(self)
|
|
@@ -463,16 +460,16 @@ class WorkerProxy:
|
|
|
463
460
|
|
|
464
461
|
async def _enter_context(self, factory):
|
|
465
462
|
ctx = None
|
|
466
|
-
if
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
463
|
+
if isinstance(factory, ContextManager):
|
|
464
|
+
ctx = factory
|
|
465
|
+
obj = ctx.__enter__()
|
|
466
|
+
elif isinstance(factory, AsyncContextManager):
|
|
467
|
+
ctx = factory
|
|
468
|
+
obj = await ctx.__aenter__()
|
|
469
|
+
elif callable(factory):
|
|
470
|
+
return await self._enter_context(factory())
|
|
471
|
+
elif isinstance(factory, Awaitable):
|
|
472
|
+
obj = await factory
|
|
476
473
|
else:
|
|
477
474
|
obj = factory
|
|
478
475
|
return obj, ctx
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|