Skip to content

Commit d804016

Browse files
authored
Merge pull request #1 from cofin/config-updates
fix: config and CLI updates
2 parents 59bf4f1 + 8ca2514 commit d804016

15 files changed

Lines changed: 663 additions & 330 deletions

File tree

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,21 @@ Here is a basic application that demonstrates how to use the plugin.
1717
from __future__ import annotations
1818

1919
from litestar import Litestar
20-
from litestar_saq import SaqPlugin, SaqConfig
2120

22-
saq = SaqPlugin(config=SaqConfig())
21+
from litestar_saq import QueueConfig, SAQConfig, SAQPlugin
22+
23+
saq = SAQPlugin(config=SAQConfig(redis_url="redis://localhost:6397/0", queue_configs=[QueueConfig(name="samples")]))
2324
app = Litestar(plugins=[saq])
2425

26+
27+
```
28+
29+
You can start a background worker with the following command now:
30+
31+
```shell
32+
litestar --app-dir=examples/ --app basic:app tasks run-worker
33+
Using Litestar app from env: 'basic:app'
34+
Starting SAQ Workers ──────────────────────────────────────────────────────────────────
35+
INFO - 2023-10-04 17:39:03,255 - saq - worker - Worker starting: Queue<redis=Redis<ConnectionPool<Connection<host=localhost,port=6397,db=0>>>, name='samples'>
36+
INFO - 2023-10-04 17:39:06,545 - saq - worker - Worker shutting down
2537
```

examples/__init__.py

Whitespace-only changes.

examples/basic.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
from litestar import Controller, Litestar, get
6+
7+
from litestar_saq import QueueConfig, SAQConfig, SAQPlugin
8+
9+
if TYPE_CHECKING:
10+
from saq.types import QueueInfo
11+
12+
from litestar_saq.base import Queue
13+
14+
15+
class SampleController(Controller):
16+
@get(path="/samples")
17+
async def samples_queue_info(self, task_queues: dict[str, Queue]) -> QueueInfo:
18+
"""Check database available and returns app config info."""
19+
queue = task_queues.get("samples")
20+
return await queue.info() # type: ignore[union-attr]
21+
22+
23+
saq = SAQPlugin(config=SAQConfig(redis_url="redis://localhost:6397/0", queue_configs=[QueueConfig(name="samples")]))
24+
app = Litestar(plugins=[saq], route_handlers=[SampleController])

examples/tasks.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import asyncio
2+
from logging import getLogger
3+
4+
logger = getLogger(__name__)
5+
6+
7+
async def system_upkeep(_: dict) -> None:
8+
logger.info("Performing system upkeep operations.")
9+
logger.info("Simulating a long running operation. Sleeping for 60 seconds.")
10+
await asyncio.sleep(60)
11+
logger.info("Simulating an even longer running operation. Sleeping for 120 seconds.")
12+
await asyncio.sleep(120)
13+
logger.info("Long running process complete.")
14+
logger.info("Performing system upkeep operations.")
15+
16+
17+
async def background_worker_task(_: dict) -> None:
18+
logger.info("Performing background worker task.")
19+
await asyncio.sleep(20)
20+
logger.info("Performing system upkeep operations.")
21+
22+
23+
async def system_task(_: dict) -> None:
24+
logger.info("Performing simple system task")
25+
await asyncio.sleep(2)
26+
logger.info("System task complete.")

litestar_saq/__init__.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
from __future__ import annotations
22

33
from . import controllers, info
4-
from .base import BackgroundTaskError, CronJob, Job, Queue, Worker, WorkerFunction
5-
from .commands import create_worker_instance
4+
from .base import CronJob, Job, Queue, Worker
5+
from .config import QueueConfig, SAQConfig
6+
from .controllers import SAQController
7+
from .plugin import SAQPlugin
68

79
__all__ = [
10+
"SAQPlugin",
11+
"SAQConfig",
12+
"SAQController",
13+
"QueueConfig",
814
"Queue",
915
"CronJob",
1016
"Job",
1117
"Worker",
12-
"WorkerFunction",
13-
"create_worker_instance",
14-
"BackgroundTaskError",
1518
"info",
1619
"controllers",
1720
]

litestar_saq/base.py

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,39 @@
11
from __future__ import annotations
22

33
import asyncio
4-
from collections import abc
54
from dataclasses import dataclass
65
from typing import TYPE_CHECKING, Any
76

8-
import saq
7+
from saq import Job as SaqJob
8+
from saq import Worker as SaqWorker
9+
from saq.job import CronJob as SaqCronJob
10+
from saq.queue import Queue as SaqQueue
911

1012
if TYPE_CHECKING:
13+
from collections.abc import Collection
1114
from signal import Signals
1215

13-
14-
WorkerFunction = abc.Callable[..., abc.Awaitable[Any]]
16+
from redis.asyncio.client import Redis
17+
from saq.types import DumpType, Function, LoadType, PartialTimersDict, ReceivesContext
1518

1619

1720
@dataclass
18-
class Job(saq.Job):
21+
class Job(SaqJob):
1922
"""Job Details"""
2023

2124
job_name: str | None = None
2225
job_description: str | None = None
2326

2427

2528
@dataclass
26-
class CronJob(saq.CronJob):
29+
class CronJob(SaqCronJob):
2730
"""Cron Job Details"""
2831

2932
job_name: str | None = None
3033
job_description: str | None = None
3134

3235

33-
class BackgroundTaskError(Exception):
34-
"""Base class for `Task` related exceptions."""
35-
36-
37-
class Queue(saq.Queue):
36+
class Queue(SaqQueue):
3837
"""[SAQ Queue](https://github.com/tobymao/saq/blob/master/saq/queue.py).
3938
4039
Configures `msgspec` for msgpack serialization/deserialization if not otherwise configured.
@@ -47,22 +46,72 @@ class Queue(saq.Queue):
4746
Passed through to `saq.Queue.__init__()`
4847
"""
4948

50-
def __init__(self, *args: Any, **kwargs: Any) -> None:
49+
def __init__(
50+
self,
51+
redis: Redis[bytes],
52+
name: str = "default",
53+
dump: DumpType | None = None,
54+
load: LoadType | None = None,
55+
max_concurrent_ops: int = 20,
56+
queue_namespace: str | None = None,
57+
) -> None:
58+
self._namespace = queue_namespace if queue_namespace is not None else "saq"
59+
super().__init__(redis, name, dump, load, max_concurrent_ops)
60+
61+
def temp(self, *args: Any, **kwargs: Any) -> None:
5162
"""Initialize a new queue."""
52-
"""
53-
kwargs.setdefault("dump", serialization.to_json)
54-
kwargs.setdefault("load", serialization.from_json)
55-
kwargs.setdefault("name", "background-tasks")
56-
"""
63+
self._namespace = kwargs.pop("queue_namespace", "saq")
5764
super().__init__(*args, **kwargs)
5865

66+
def namespace(self, key: str) -> str:
67+
"""Make the namespace unique per app."""
68+
return f"{self._namespace}:{self.name}:{key}"
69+
70+
def job_id(self, job_key: str) -> str:
71+
"""Job ID.
5972
60-
class Worker(saq.Worker):
73+
Args:
74+
job_key (str): Sets the job ID for the given key
75+
76+
Returns:
77+
str: Job ID for the specified key
78+
"""
79+
return f"{self._namespace}:{self.name}:job:{job_key}"
80+
81+
82+
class Worker(SaqWorker):
6183
"""Worker."""
6284

6385
# same issue: https://github.com/samuelcolvin/arq/issues/182
6486
SIGNALS: list[Signals] = []
6587

88+
def __init__(
89+
self,
90+
queue: Queue | SaqQueue,
91+
functions: Collection[Function | tuple[str, Function]],
92+
*,
93+
concurrency: int = 10,
94+
cron_jobs: Collection[CronJob] | None = None,
95+
startup: ReceivesContext | None = None,
96+
shutdown: ReceivesContext | None = None,
97+
before_process: ReceivesContext | None = None,
98+
after_process: ReceivesContext | None = None,
99+
timers: PartialTimersDict | None = None,
100+
dequeue_timeout: float = 0,
101+
) -> None:
102+
super().__init__(
103+
queue,
104+
functions,
105+
concurrency=concurrency,
106+
cron_jobs=cron_jobs,
107+
startup=startup,
108+
shutdown=shutdown,
109+
before_process=before_process,
110+
after_process=after_process,
111+
timers=timers,
112+
dequeue_timeout=dequeue_timeout,
113+
)
114+
66115
async def on_app_startup(self) -> None:
67116
"""Attach the worker to the running event loop."""
68117
loop = asyncio.get_running_loop()

litestar_saq/cli.py

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,85 @@
11
from __future__ import annotations
22

3+
import asyncio
4+
import multiprocessing
5+
from contextlib import suppress
36
from typing import TYPE_CHECKING
47

5-
from click import group, option
8+
from click import IntRange, group, option
69
from litestar.cli._utils import LitestarGroup, console
710

11+
from litestar_saq.exceptions import ImproperConfigurationError
12+
from litestar_saq.plugin import SAQPlugin
13+
814
if TYPE_CHECKING:
915
from litestar import Litestar
1016

17+
from litestar_saq.base import Worker
18+
1119

12-
@group(cls=LitestarGroup, name="database")
20+
@group(cls=LitestarGroup, name="tasks")
1321
def background_worker_group() -> None:
1422
"""Manage background task workers."""
1523

1624

1725
@background_worker_group.command(
18-
name="show-current-revision",
19-
help="Shows the current revision for the database.",
26+
name="run-worker",
27+
help="Run background worker processes.",
28+
)
29+
@option(
30+
"--workers",
31+
help="The number of worker processes to start.",
32+
type=IntRange(min=1),
33+
default=1,
34+
required=False,
35+
show_default=True,
2036
)
21-
@option("--verbose", type=bool, help="Enable verbose output.", default=False, is_flag=True)
22-
def show_database_revision(app: Litestar, verbose: bool) -> None: # noqa: ARG001
23-
"""Show current database revision."""
37+
@option("-v", "--verbose", help="Enable verbose logging.", is_flag=True, default=None, type=bool, required=False)
38+
@option("-d", "--debug", help="Enable debugging.", is_flag=True, default=None, type=bool, required=False)
39+
def run_worker(
40+
app: Litestar,
41+
workers: int,
42+
verbose: bool | None, # noqa: ARG001
43+
debug: bool | None, # noqa: ARG001
44+
) -> None:
45+
"""Run the API server."""
2446
console.rule("[yellow]Starting SAQ Workers[/]", align="left")
47+
48+
plugin = get_saq_plugin(app)
49+
if workers > 1:
50+
for _ in range(workers - 1):
51+
p = multiprocessing.Process(target=run_worker_process, args=(plugin.get_workers(),))
52+
p.start()
53+
54+
with suppress(KeyboardInterrupt):
55+
run_worker_process(workers=plugin.get_workers())
56+
57+
58+
def get_saq_plugin(app: Litestar) -> SAQPlugin:
59+
"""Retrieve a SAQ plugin from the Litestar application's plugins.
60+
61+
This function attempts to find a SAQ plugin instance.
62+
If plugin is not found, it raises an ImproperlyConfiguredException.
63+
"""
64+
65+
with suppress(KeyError):
66+
return app.plugins.get(SAQPlugin)
67+
msg = "Failed to initialize SAQ. The required plugin (SAQPlugin) is missing."
68+
raise ImproperConfigurationError(
69+
msg,
70+
)
71+
72+
73+
def run_worker_process(workers: list[Worker]) -> None:
74+
"""Run a worker."""
75+
loop = asyncio.get_event_loop()
76+
77+
try:
78+
for i, worker_instance in enumerate(workers):
79+
if i < len(workers) - 1:
80+
loop.create_task(worker_instance.start())
81+
else:
82+
loop.run_until_complete(worker_instance.start())
83+
except KeyboardInterrupt:
84+
for worker in workers:
85+
loop.run_until_complete(worker.stop())

litestar_saq/commands.py

Lines changed: 0 additions & 51 deletions
This file was deleted.

0 commit comments

Comments
 (0)