|
20 | 20 | abort_job_max_age, |
21 | 21 | abort_jobs_ss, |
22 | 22 | default_queue_name, |
| 23 | + default_worker_group, |
| 24 | + default_worker_name, |
23 | 25 | expires_extra_ms, |
24 | 26 | health_check_key_suffix, |
25 | 27 | in_progress_key_prefix, |
26 | 28 | job_key_prefix, |
27 | 29 | keep_cronjob_progress, |
28 | 30 | result_key_prefix, |
29 | 31 | retry_key_prefix, |
| 32 | + stream_prefix, |
30 | 33 | ) |
31 | 34 | from .utils import ( |
32 | 35 | args_to_string, |
@@ -144,10 +147,13 @@ class Worker: |
144 | 147 | :param functions: list of functions to register, can either be raw coroutine functions or the |
145 | 148 | result of :func:`arq.worker.func`. |
146 | 149 | :param queue_name: queue name to get jobs from |
| 150 | + :param worker_name: unique name to identify this worker |
| 151 | + :param worker_group: worker group that this worker belongs to |
147 | 152 | :param cron_jobs: list of cron jobs to run, use :func:`arq.cron.cron` to create them |
148 | 153 | :param redis_settings: settings for creating a redis connection |
149 | 154 | :param redis_pool: existing redis pool, generally None |
150 | 155 | :param burst: whether to stop the worker once all jobs have been run |
| 156 | + :param stream: whether to constantly listen for new jobs from a redis stream |
151 | 157 | :param on_startup: coroutine function to run at startup |
152 | 158 | :param on_shutdown: coroutine function to run at shutdown |
153 | 159 | :param on_job_start: coroutine function to run on job start |
@@ -188,10 +194,13 @@ def __init__( |
188 | 194 | functions: Sequence[Union[Function, 'WorkerCoroutine']] = (), |
189 | 195 | *, |
190 | 196 | queue_name: Optional[str] = default_queue_name, |
| 197 | + worker_name: Optional[str] = None, |
| 198 | + worker_group: Optional[str] = None, |
191 | 199 | cron_jobs: Optional[Sequence[CronJob]] = None, |
192 | 200 | redis_settings: Optional[RedisSettings] = None, |
193 | 201 | redis_pool: Optional[ArqRedis] = None, |
194 | 202 | burst: bool = False, |
| 203 | + stream: bool = False, |
195 | 204 | on_startup: Optional['StartupShutdown'] = None, |
196 | 205 | on_shutdown: Optional['StartupShutdown'] = None, |
197 | 206 | on_job_start: Optional['StartupShutdown'] = None, |
@@ -234,6 +243,10 @@ def __init__( |
234 | 243 | if len(self.functions) == 0: |
235 | 244 | raise RuntimeError('at least one function or cron_job must be registered') |
236 | 245 | self.burst = burst |
| 246 | + self.stream = stream |
| 247 | + if stream is True: |
| 248 | + self.worker_name = worker_name if worker_name is not None else default_worker_name |
| 249 | + self.worker_group = worker_group if worker_group is not None else default_worker_group |
237 | 250 | self.on_startup = on_startup |
238 | 251 | self.on_shutdown = on_shutdown |
239 | 252 | self.on_job_start = on_job_start |
@@ -357,17 +370,31 @@ async def main(self) -> None: |
357 | 370 | if self.on_startup: |
358 | 371 | await self.on_startup(self.ctx) |
359 | 372 |
|
360 | | - async for _ in poll(self.poll_delay_s): |
361 | | - await self._poll_iteration() |
362 | | - |
363 | | - if self.burst: |
364 | | - if 0 <= self.max_burst_jobs <= self._jobs_started(): |
365 | | - await asyncio.gather(*self.tasks.values()) |
366 | | - return None |
367 | | - queued_jobs = await self.pool.zcard(self.queue_name) |
368 | | - if queued_jobs == 0: |
369 | | - await asyncio.gather(*self.tasks.values()) |
370 | | - return None |
| 373 | + if self.stream is False: |
| 374 | + async for _ in poll(self.poll_delay_s): |
| 375 | + await self._poll_iteration() |
| 376 | + |
| 377 | + if self.burst: |
| 378 | + if 0 <= self.max_burst_jobs <= self._jobs_started(): |
| 379 | + await asyncio.gather(*self.tasks.values()) |
| 380 | + return None |
| 381 | + queued_jobs = await self.pool.zcard(self.queue_name) |
| 382 | + if queued_jobs == 0: |
| 383 | + await asyncio.gather(*self.tasks.values()) |
| 384 | + return None |
| 385 | + else: |
| 386 | + stream_name = stream_prefix + self.queue_name |
| 387 | + |
| 388 | + with contextlib.suppress(ResponseError): |
| 389 | + await self.pool.xgroup_create(stream_name, self.worker_group, '$', mkstream=True) |
| 390 | + logger.info('Stream consumer group created with name: %s', self.worker_group) |
| 391 | + |
| 392 | + while True: |
| 393 | + if event := await self.pool.xreadgroup( |
| 394 | + consumername=self.worker_name, groupname=self.worker_group, streams={stream_name: '>'}, block=0 |
| 395 | + ): |
| 396 | + await self._poll_iteration() |
| 397 | + await self.pool.xack(stream_name, self.worker_group, event[0][1][0][0]) # type: ignore[no-untyped-call] |
371 | 398 |
|
372 | 399 | async def _poll_iteration(self) -> None: |
373 | 400 | """ |
|
0 commit comments