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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wool
3
- Version: 0.1rc11
3
+ Version: 0.1rc12
4
4
  Summary: A Python framework for distributed multiprocessing.
5
5
  Author-email: Conrad Bzura <conrad@wool.io>
6
6
  Maintainer-email: maintainers@wool.io
@@ -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 DiscoveryService
23
- from wool._worker_discovery import LanDiscoveryService
24
- from wool._worker_discovery import LanRegistryService
25
- from wool._worker_discovery import RegistryService
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("__proxy__", default=None)
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
- "LanDiscoveryService",
65
- "LanRegistryService",
66
+ "LanDiscovery",
67
+ "LanRegistrar",
66
68
  "Worker",
67
- "DiscoveryService",
69
+ "Discovery",
68
70
  "WorkerPool",
69
71
  "WorkerProxy",
70
- "RegistryService",
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 RegistryServiceLike
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, Generic[_T_RegistryService]):
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 registry service
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 registry_service:
104
- Service instance for worker registration and discovery within
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
- _registry_service: RegistryServiceLike
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._registry_service = registry_service
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 registry service.
170
+ it with the registrar service.
171
171
  """
172
172
  if self._started:
173
173
  raise RuntimeError("Worker has already been started")
174
- if self._registry_service:
175
- await self._registry_service.start()
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 registry service.
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
- await self._stop()
190
- if self._registry_service:
191
- await self._registry_service.stop()
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(Generic[_T_RegistryService], Protocol):
214
- """Protocol for creating worker instances with registry integration.
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[_T_RegistryService]:
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[_T_RegistryService]):
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 registry service
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 registry_service:
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
- registry_service: _T_RegistryService,
309
+ registrar: RegistrarLike | Factory[RegistrarLike],
267
310
  **extra: Any,
268
311
  ):
269
- super().__init__(*tags, registry_service=registry_service, **extra)
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 registry service, starts the worker process
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 registry for discovery by client sessions.
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 registry service, gracefully
374
+ Unregisters the worker from the registrar service, gracefully
333
375
  shuts down the worker process using SIGINT, and cleans up
334
- the registry service. If graceful shutdown fails, the process
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 not self._info:
339
- raise RuntimeError("Cannot unregister - worker has no info")
340
- await self._registry_service.unregister(self._info)
341
- finally:
342
- if not self._worker_process.is_alive():
343
- return
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
- class ReducibleAsyncIteratorLike(Reducible, Protocol, Generic[_T_co]):
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) -> ReducibleAsyncIteratorLike[_T_co]: ...
326
+ def __aiter__(self) -> DiscoveryLike: ...
333
327
 
334
- def __anext__(self) -> Awaitable[_T_co]: ...
328
+ def __anext__(self) -> Awaitable[DiscoveryEvent]: ...
335
329
 
336
330
 
337
331
  # public
338
332
  @runtime_checkable
339
- class Factory(Protocol, Generic[_T_co]):
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 DiscoveryService(ABC):
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 registry entirely should always
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 registry. Events
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
- _T_DiscoveryServiceLike = TypeVar("_T_DiscoveryServiceLike", bound=DiscoveryService)
462
+ _T_DiscoveryLike = TypeVar("_T_DiscoveryLike", bound=Discovery)
461
463
 
462
464
 
463
465
  # public
464
- class RegistryServiceLike(Protocol):
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 RegistryService(Generic[_T_DiscoveryServiceLike], ABC):
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 registry service, making it ready to accept registrations.
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("Registry service already started")
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 registry service and cleans up any resources.
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("Registry service not started")
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 registry service, making it ready to accept registrations."""
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 registry service and cleans up any resources."""
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 registry service is not running.
547
+ If the registrar service is not running.
542
548
  """
543
549
  if not self._started:
544
- raise RuntimeError("Registry service not started - call start() first")
550
+ raise RuntimeError("Registrar service not started - call start() first")
545
551
  if self._stopped:
546
- raise RuntimeError("Registry service already stopped")
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 registry service is not running.
575
+ If the registrar service is not running.
570
576
  """
571
577
  if not self._started:
572
- raise RuntimeError("Registry service not started - call start() first")
578
+ raise RuntimeError("Registrar service not started - call start() first")
573
579
  if self._stopped:
574
- raise RuntimeError("Registry service already stopped")
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 registry service is not running.
599
+ If the registrar service is not running.
594
600
  """
595
601
  if not self._started:
596
- raise RuntimeError("Registry service not started - call start() first")
602
+ raise RuntimeError("Registrar service not started - call start() first")
597
603
  if self._stopped:
598
- raise RuntimeError("Registry service already stopped")
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 LanDiscoveryService(DiscoveryService):
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_ == LanRegistryService.service_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_ == LanRegistryService.service_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_ == LanRegistryService.service_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 LanRegistryService(RegistryService[LanDiscoveryService]):
791
- """Implements a worker registry using Zeroconf to advertise on the LAN.
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:`LanDiscoveryService` to find them.
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 registry service is not properly initialized.
833
+ If the registrar service is not properly initialized.
828
834
  """
829
835
  if self.aiozc is None:
830
- raise RuntimeError("Registry service not properly initialized")
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 registry service is not properly initialized.
857
+ If the registrar service is not properly initialized.
852
858
  """
853
859
  if self.aiozc is None:
854
- raise RuntimeError("Registry service not properly initialized")
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 registry service is not properly initialized.
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("Registry service not properly initialized")
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 LocalRegistryService(RegistryService):
975
- """Implements a worker registry using shared memory for local pools.
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 LocalDiscoveryService instances to find them efficiently.
979
- The registry stores worker ports as integers in a simple array format,
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 registry created it
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 registry service is not properly initialized.
1042
+ If the registrar service is not properly initialized.
1037
1043
  """
1038
1044
  if self._shared_memory is None:
1039
- raise RuntimeError("Registry service not properly initialized")
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 registry")
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 registry service is not properly initialized.
1066
+ If the registrar service is not properly initialized.
1061
1067
  """
1062
1068
  if self._shared_memory is None:
1063
- raise RuntimeError("Registry service not properly initialized")
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 registry, update is the same as register.
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 LocalDiscoveryService(DiscoveryService):
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 registry
1176
- version="unknown", # Not available in simple registry
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 registry)
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 DiscoveryEvent
14
+ from wool._worker_discovery import DiscoveryLike
17
15
  from wool._worker_discovery import Factory
18
- from wool._worker_discovery import LocalDiscoveryService
19
- from wool._worker_discovery import LocalRegistryService
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 LocalRegistryService
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 LanDiscoveryService
82
+ from wool._worker_discovery import LanDiscovery
90
83
 
91
84
  # Connect to existing workers on the network
92
- discovery = LanDiscoveryService(filter=lambda w: "production" in w.tags)
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=LanDiscoveryService(filter=lambda w: "prod" in w.tags)
154
+ discovery=LanDiscovery(filter=lambda w: "prod" in w.tags)
162
155
 
163
156
  # Instance factory
164
- discovery=lambda: LocalDiscoveryService("pool-123")
157
+ discovery=lambda: LocalDiscovery("pool-123")
165
158
 
166
159
  # Context manager factory
167
160
  @asynccontextmanager
168
161
  async def discovery():
169
- service = await DatabaseDiscoveryService.create(connection_string)
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[RegistryServiceLike] = LocalWorker[LocalRegistryService],
190
- loadbalancer: LoadBalancerLike | LoadBalancerFactory = RoundRobinLoadBalancer,
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
- ReducibleAsyncIteratorLike[DiscoveryEvent]
204
- | Factory[AsyncIterator[DiscoveryEvent]]
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
- loadbalancer: LoadBalancerLike | LoadBalancerFactory = RoundRobinLoadBalancer,
220
- discovery: (
221
- ReducibleAsyncIteratorLike[DiscoveryEvent]
222
- | Factory[AsyncIterator[DiscoveryEvent]]
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=LocalDiscoveryService(uri),
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=LocalDiscoveryService(uri),
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 registry, creates a client session,
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, registry_service=LocalRegistryService(uri))
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 LocalDiscoveryService
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(self, client: Resource[WorkerClient], info: WorkerInfo):
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(self, client: Resource[WorkerClient], info: WorkerInfo):
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[AsyncIterator[DiscoveryEvent]]
260
- | ContextManager[AsyncIterator[DiscoveryEvent]]
258
+ AsyncContextManager[DiscoveryLike] | ContextManager[DiscoveryLike]
261
259
  )
262
- _loadbalancer = LoadBalancerLike | LoadBalancerFactory
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
- ReducibleAsyncIteratorLike[DiscoveryEvent]
273
- | Factory[AsyncIterator[DiscoveryEvent]]
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 | LoadBalancerFactory = RoundRobinLoadBalancer,
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 | LoadBalancerFactory = RoundRobinLoadBalancer,
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 | LoadBalancerFactory = RoundRobinLoadBalancer,
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 = LocalDiscoveryService(
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, AsyncIterator):
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 callable(factory):
467
- obj = factory()
468
- if isinstance(obj, ContextManager):
469
- ctx = obj
470
- obj = obj.__enter__()
471
- elif isinstance(obj, AsyncContextManager):
472
- ctx = obj
473
- obj = await obj.__aenter__()
474
- elif isinstance(obj, Awaitable):
475
- obj = await obj
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