Skip to content

Commit 8ca2514

Browse files
committed
fix: updated sample & corrected command
1 parent f50fb4b commit 8ca2514

13 files changed

Lines changed: 245 additions & 104 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/basic.py

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

3-
from litestar import Litestar
3+
from typing import TYPE_CHECKING
44

5-
from litestar_saq import SAQConfig, SAQPlugin
5+
from litestar import Controller, Litestar, get
66

7-
saq = SAQPlugin(config=SAQConfig(redis_url="redis://cache:6379/0"))
8-
app = Litestar(plugins=[saq])
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])

litestar_saq/__init__.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
from __future__ import annotations
22

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

99
__all__ = [
1010
"SAQPlugin",
1111
"SAQConfig",
12+
"SAQController",
1213
"QueueConfig",
1314
"Queue",
1415
"CronJob",
1516
"Job",
1617
"Worker",
17-
"WorkerFunction",
18-
"create_worker_instance",
1918
"info",
2019
"controllers",
2120
]

litestar_saq/base.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +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 Queue(saq.Queue):
36+
class Queue(SaqQueue):
3437
"""[SAQ Queue](https://github.com/tobymao/saq/blob/master/saq/queue.py).
3538
3639
Configures `msgspec` for msgpack serialization/deserialization if not otherwise configured.
@@ -43,7 +46,19 @@ class Queue(saq.Queue):
4346
Passed through to `saq.Queue.__init__()`
4447
"""
4548

46-
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:
4762
"""Initialize a new queue."""
4863
self._namespace = kwargs.pop("queue_namespace", "saq")
4964
super().__init__(*args, **kwargs)
@@ -64,12 +79,39 @@ def job_id(self, job_key: str) -> str:
6479
return f"{self._namespace}:{self.name}:job:{job_key}"
6580

6681

67-
class Worker(saq.Worker):
82+
class Worker(SaqWorker):
6883
"""Worker."""
6984

7085
# same issue: https://github.com/samuelcolvin/arq/issues/182
7186
SIGNALS: list[Signals] = []
7287

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+
73115
async def on_app_startup(self) -> None:
74116
"""Attach the worker to the running event loop."""
75117
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)