-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
(#30): Refactor worker for readability, extensibility & configurability
Closes #30 * Refactor: Define WorkerManager & GruntWorker WorkerManager is the one who receives tasks and delegate them to its GruntWorkers running in background. GruntWorker executes the tasks and publish the result back to the queue. Both of these should implement the Interface IWorker. The main logic for the worker is in the `_main_loop`. * Refactor: pubsub client & concurrency manager In the future we want to enable users to configure and switch to a different pubsub client & concurrency manager if need be. We start with Redis as default pubsub and multiprocessing as default concurrency manager. Users should be able to configure these using envronment variables. * Use better interface We use Protocols as interfaces for worker, pubsub, concurrency manager * Refactor: Use pytest fixture for worker * Fix warning & zombie child procs on sigterm/sigkill Before, `./test.sh && pidof $(which python)` after `./test.sh` will give a list of pids which means we are not properly killing child processes. With this this change, we do not see zombie child processes anymore. We fix this making sure the worker manager handle TERM & INT signals and propagate it to the workers. Some helpful references regarding handling signals: * https://stackoverflow.com/questions/42628795/indirectly-stopping-a-python-asyncio-event-loop-through-sigterm-has-no-effect * https://stackoverflow.com/questions/67823770/how-to-propagate-sigterm-to-children-created-via-subprocess * Split worker.py into interfaces, pubsub & concurrency_manager worker.py is now split into: ├── interfaces.py ├── pubsub.py └── concurrency_manager.py all of which should be able to be imported by any worker.py or main.py. Hopefully this will make the code more organized and well-abstracted. * Attempt to ensure child processes are covered in unit tests The WorkerManager processes seems to be included in coverage but GruntWorker processes are still not (I guess because they are grandchild processes and coverage doesn't handle that?) * Make main.py more DRY: Re-use PubSub facade from pubsub.py * As a positive side effect, also closes #27 Small extras: * Re-organize test files * Split test_cli.py to test_cli.py and test_worker.py * Ignore __main__.py from coverage since it iss not coverable anyways
- Loading branch information
1 parent
136ae39
commit 6b1b2a8
Showing
19 changed files
with
723 additions
and
273 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,6 @@ | ||
[run] | ||
source = src/ | ||
parallel = True | ||
concurrency = multiprocessing | ||
omit = | ||
src/aiotaskq/__main__.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
from functools import cached_property | ||
import logging | ||
import multiprocessing | ||
import os | ||
import typing as t | ||
|
||
from .exceptions import ConcurrencyTypeNotSupported | ||
from .interfaces import ConcurrencyType, IConcurrencyManager, IProcess | ||
|
||
|
||
class ConcurrencyManager: | ||
"""The user-facing facade for creating the right concurrency manager implementation.""" | ||
|
||
_instance: "IConcurrencyManager" | ||
|
||
@classmethod | ||
def get(cls, concurrency_type: str, concurrency) -> IConcurrencyManager: | ||
if cls._instance: | ||
return cls._instance | ||
if concurrency_type == ConcurrencyType.MULTIPROCESSING: | ||
cls._instance = MultiProcessing(concurrency=concurrency) | ||
return cls._instance | ||
raise ConcurrencyTypeNotSupported( | ||
f'Concurrency type "{concurrency_type}" is not yet supported.' | ||
) | ||
|
||
|
||
class MultiProcessing: | ||
"""Implementation of a ConcurrencyManager that uses the `multiprocess` built-in module.""" | ||
|
||
def __init__(self, concurrency: int) -> None: | ||
self.concurrency = concurrency | ||
self.processes: dict[int, IProcess] = {} | ||
|
||
def start(self, func: t.Callable, *args: t.ParamSpecArgs) -> None: | ||
"""Start each processes under management.""" | ||
for _ in range(self.concurrency): | ||
proc = multiprocessing.Process(target=func, args=args) | ||
proc.start() | ||
assert proc.pid is not None | ||
self.processes[proc.pid] = proc | ||
|
||
def terminate(self) -> None: | ||
"""Terminate each process under management.""" | ||
for proc in self.processes.values(): | ||
self._logger.debug("Sending signal TERM to back worker process [pid=%s]", proc.pid) | ||
proc.terminate() | ||
|
||
@cached_property | ||
def _logger(self): | ||
return logging.getLogger(f"[{os.getpid()}] [{self.__class__.__qualname__}]") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
""" | ||
Define all interfaces for the library. | ||
Interfaces are mainly typing.Protocol classes, but may also include | ||
other declarative classes like enums or Types. | ||
""" | ||
|
||
import enum | ||
import typing as t | ||
|
||
|
||
Message = t.Union[str, bytes] | ||
|
||
|
||
class PollResponse(t.TypedDict): | ||
"""Define the dictionary returned from a pubsub.""" | ||
|
||
type: str | ||
data: Message | ||
pattern: t.Optional[str] | ||
channel: bytes | ||
|
||
|
||
class IProcess(t.Protocol): | ||
""" | ||
Define the interface for a process used in the library. | ||
It's more or less the same as the `multiprocessing.Process` except this | ||
one only has attributes that are necessary for the library, and also has | ||
slightly different typing e.g. pid in our case is always an `int`, whereas | ||
the one from `multiprocessing.Process` is `Optional[int]`. This way we're | ||
not limited to `multiprocessing.Process` and may switch to another implementation | ||
if needed. | ||
""" | ||
|
||
@property | ||
def pid(self) -> t.Optional[int]: | ||
"""Return the process id (pid).""" | ||
|
||
def start(self): | ||
"""Start running the process.""" | ||
|
||
def terminate(self): | ||
"""Send TERM signal to the process.""" | ||
|
||
|
||
class ConcurrencyType(str, enum.Enum): | ||
"""Define supported concurrency types.""" | ||
|
||
MULTIPROCESSING = "multiprocessing" | ||
|
||
|
||
class IConcurrencyManager(t.Protocol): | ||
""" | ||
Define the interface of a concurrency manager. | ||
It should be able to start x number of processes given & terminate them. | ||
""" | ||
|
||
concurrency: int | ||
processes: dict[int, IProcess] | ||
|
||
def __init__(self, concurrency: int) -> None: | ||
"""Initialize the concurrency manager.""" | ||
|
||
def start(self, func: t.Callable, *args: t.ParamSpecArgs) -> None: | ||
"""Start each process under management.""" | ||
|
||
def terminate(self) -> None: | ||
"""Terminate each process under management.""" | ||
|
||
|
||
class IPubSub(t.Protocol): | ||
def __init__(self, url: str, poll_interval_s: float, *args, **kwargs): | ||
"""Initialize the pubsub class.""" | ||
|
||
async def __aenter__(self) -> "IPubSub": | ||
"""Instantiate/start resources when entering the async context.""" | ||
|
||
async def __aexit__(self, exc_type, exc_value, traceback) -> None: | ||
"""Close resources when entering the async context.""" | ||
|
||
async def publish(self, channel: str, message: Message) -> None: | ||
"""Publish the given messaage to the given channel.""" | ||
|
||
async def subscribe(self, channel: str) -> None: | ||
"""Start subscribing to the given channel.""" | ||
|
||
async def poll(self) -> PollResponse: | ||
"""Poll for new message from the subscribed channel, and return it.""" | ||
|
||
|
||
class IWorker(t.Protocol): | ||
""" | ||
Define the interface for a worker. | ||
It should also be tied to a specific app. | ||
It should be able to subscribe, poll and publish messages to the other worker. | ||
""" | ||
|
||
pubsub: IPubSub | ||
app_import_path: str | ||
|
||
def run_forever(self) -> None: | ||
"""Run the worker forever in a loop.""" | ||
|
||
|
||
class IWorkerManager(IWorker): | ||
""" | ||
Define the interface for a worker manager. | ||
This is similar to a worker, but has more authority since it is the one | ||
one who create and kill other workers via its concurrency manager. | ||
""" | ||
|
||
concurrency_manager: IConcurrencyManager |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.