baqueue 1.2.0__tar.gz → 1.2.1__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.
- {baqueue-1.2.0/baqueue.egg-info → baqueue-1.2.1}/PKG-INFO +1 -1
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/__init__.py +1 -1
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/drivers/redis_driver.py +107 -40
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/worker.py +11 -2
- {baqueue-1.2.0 → baqueue-1.2.1/baqueue.egg-info}/PKG-INFO +1 -1
- {baqueue-1.2.0 → baqueue-1.2.1}/LICENSE +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/MANIFEST.in +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/README.md +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/balancer.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/batch.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/cli.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/config.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/dashboard/__init__.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/dashboard/api.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/dashboard/server.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/dashboard/static/app.js +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/dashboard/static/index.html +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/dashboard/static/style.css +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/drivers/__init__.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/drivers/base.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/drivers/memory_driver.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/drivers/postgres_driver.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/drivers/sqlite_driver.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/events.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/job.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/pruner.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/queue.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/retry.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/scheduler.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/serializer.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue/supervisor.py +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue.egg-info/SOURCES.txt +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue.egg-info/dependency_links.txt +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue.egg-info/entry_points.txt +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue.egg-info/requires.txt +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/baqueue.egg-info/top_level.txt +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/pyproject.toml +0 -0
- {baqueue-1.2.0 → baqueue-1.2.1}/setup.cfg +0 -0
|
@@ -15,6 +15,7 @@ logger = logging.getLogger("baqueue.redis")
|
|
|
15
15
|
# the job hash is gone, so we can't read its status — we ZREM from every global
|
|
16
16
|
# status index to be sure the stale id is cleared.
|
|
17
17
|
_ALL_STATUSES = ("pending", "processing", "completed", "failed", "cancelled")
|
|
18
|
+
_MAX_STALE_POP_ATTEMPTS = 100
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class RedisDriver(BaseDriver):
|
|
@@ -202,28 +203,110 @@ class RedisDriver(BaseDriver):
|
|
|
202
203
|
await self._with_disk_full_recovery(_do)
|
|
203
204
|
return ids
|
|
204
205
|
|
|
206
|
+
async def _claim_pending_job(self, job_id: str) -> JobPayload | None:
|
|
207
|
+
from redis.exceptions import WatchError
|
|
208
|
+
|
|
209
|
+
job_key = self._key("job", job_id)
|
|
210
|
+
|
|
211
|
+
async def _attempt() -> JobPayload | None:
|
|
212
|
+
pipe = self._redis.pipeline()
|
|
213
|
+
try:
|
|
214
|
+
await pipe.watch(job_key)
|
|
215
|
+
raw = await pipe.hget(job_key, "data")
|
|
216
|
+
if not raw:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
payload = JobPayload.from_json(raw)
|
|
220
|
+
if payload.status != "pending":
|
|
221
|
+
return None
|
|
222
|
+
if payload.delay_until is not None and payload.delay_until > _now_ts():
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
payload.status = "processing"
|
|
226
|
+
now = _now_ts()
|
|
227
|
+
payload.started_at = now
|
|
228
|
+
payload.updated_at = now
|
|
229
|
+
payload.attempts += 1
|
|
230
|
+
|
|
231
|
+
pipe.multi()
|
|
232
|
+
pipe.hset(job_key, mapping={"data": payload.to_json()})
|
|
233
|
+
self._index_status_change(
|
|
234
|
+
pipe,
|
|
235
|
+
payload.id,
|
|
236
|
+
payload.queue,
|
|
237
|
+
"pending",
|
|
238
|
+
"processing",
|
|
239
|
+
payload.created_at,
|
|
240
|
+
)
|
|
241
|
+
await pipe.execute()
|
|
242
|
+
return payload
|
|
243
|
+
finally:
|
|
244
|
+
await pipe.reset()
|
|
245
|
+
|
|
246
|
+
for _ in range(5):
|
|
247
|
+
try:
|
|
248
|
+
return await self._with_disk_full_recovery(_attempt)
|
|
249
|
+
except WatchError:
|
|
250
|
+
continue
|
|
251
|
+
return None
|
|
252
|
+
|
|
205
253
|
async def pop(self, queue: str) -> JobPayload | None:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
payload.updated_at = now
|
|
218
|
-
payload.attempts += 1
|
|
254
|
+
for _ in range(_MAX_STALE_POP_ATTEMPTS):
|
|
255
|
+
job_id = await self._redis.lpop(self._key("queue", queue))
|
|
256
|
+
if not job_id:
|
|
257
|
+
return None
|
|
258
|
+
payload = await self._claim_pending_job(job_id)
|
|
259
|
+
if payload is not None:
|
|
260
|
+
return payload
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
async def _promote_delayed_job(self, job_id: str, now: float) -> JobPayload | None:
|
|
264
|
+
from redis.exceptions import WatchError
|
|
219
265
|
|
|
220
|
-
|
|
266
|
+
job_key = self._key("job", job_id)
|
|
267
|
+
|
|
268
|
+
async def _attempt() -> JobPayload | None:
|
|
221
269
|
pipe = self._redis.pipeline()
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
270
|
+
try:
|
|
271
|
+
await pipe.watch(job_key)
|
|
272
|
+
raw = await pipe.hget(job_key, "data")
|
|
273
|
+
if not raw:
|
|
274
|
+
pipe.multi()
|
|
275
|
+
pipe.zrem(self._key("delayed"), job_id)
|
|
276
|
+
await pipe.execute()
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
payload = JobPayload.from_json(raw)
|
|
280
|
+
if payload.status != "pending" or payload.delay_until is None:
|
|
281
|
+
pipe.multi()
|
|
282
|
+
pipe.zrem(self._key("delayed"), job_id)
|
|
283
|
+
await pipe.execute()
|
|
284
|
+
return None
|
|
285
|
+
if payload.delay_until > now:
|
|
286
|
+
pipe.multi()
|
|
287
|
+
pipe.zadd(self._key("delayed"), {job_id: payload.delay_until})
|
|
288
|
+
await pipe.execute()
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
payload.delay_until = None
|
|
292
|
+
payload.updated_at = now
|
|
293
|
+
|
|
294
|
+
pipe.multi()
|
|
295
|
+
pipe.hset(job_key, mapping={"data": payload.to_json()})
|
|
296
|
+
pipe.zrem(self._key("delayed"), job_id)
|
|
297
|
+
pipe.lrem(self._key("queue", payload.queue), 0, job_id)
|
|
298
|
+
pipe.rpush(self._key("queue", payload.queue), job_id)
|
|
299
|
+
await pipe.execute()
|
|
300
|
+
return payload
|
|
301
|
+
finally:
|
|
302
|
+
await pipe.reset()
|
|
303
|
+
|
|
304
|
+
for _ in range(5):
|
|
305
|
+
try:
|
|
306
|
+
return await self._with_disk_full_recovery(_attempt)
|
|
307
|
+
except WatchError:
|
|
308
|
+
continue
|
|
309
|
+
return None
|
|
227
310
|
|
|
228
311
|
async def pop_delayed(self) -> list[JobPayload]:
|
|
229
312
|
now = _now_ts()
|
|
@@ -231,27 +314,10 @@ class RedisDriver(BaseDriver):
|
|
|
231
314
|
if not job_ids:
|
|
232
315
|
return []
|
|
233
316
|
|
|
234
|
-
async def _remove_delayed():
|
|
235
|
-
pipe = self._redis.pipeline()
|
|
236
|
-
for job_id in job_ids:
|
|
237
|
-
pipe.zrem(self._key("delayed"), job_id)
|
|
238
|
-
await pipe.execute()
|
|
239
|
-
await self._with_disk_full_recovery(_remove_delayed)
|
|
240
|
-
|
|
241
317
|
moved: list[JobPayload] = []
|
|
242
318
|
for job_id in job_ids:
|
|
243
|
-
|
|
244
|
-
if
|
|
245
|
-
payload = JobPayload.from_json(raw)
|
|
246
|
-
payload.delay_until = None
|
|
247
|
-
payload.updated_at = _now_ts()
|
|
248
|
-
|
|
249
|
-
async def _move(payload=payload):
|
|
250
|
-
pipe = self._redis.pipeline()
|
|
251
|
-
pipe.hset(self._key("job", payload.id), mapping={"data": payload.to_json()})
|
|
252
|
-
pipe.rpush(self._key("queue", payload.queue), payload.id)
|
|
253
|
-
await pipe.execute()
|
|
254
|
-
await self._with_disk_full_recovery(_move)
|
|
319
|
+
payload = await self._promote_delayed_job(job_id, now)
|
|
320
|
+
if payload is not None:
|
|
255
321
|
moved.append(payload)
|
|
256
322
|
return moved
|
|
257
323
|
|
|
@@ -285,6 +351,7 @@ class RedisDriver(BaseDriver):
|
|
|
285
351
|
|
|
286
352
|
async def _do():
|
|
287
353
|
pipe = self._redis.pipeline()
|
|
354
|
+
pipe.lrem(self._key("queue", payload.queue), 0, payload.id)
|
|
288
355
|
if delay > 0:
|
|
289
356
|
payload.delay_until = _now_ts() + delay
|
|
290
357
|
pipe.hset(self._key("job", payload.id), mapping={"data": payload.to_json()})
|
|
@@ -402,8 +469,8 @@ class RedisDriver(BaseDriver):
|
|
|
402
469
|
now = _now_ts()
|
|
403
470
|
# Only a job actually sitting in the delayed ZSET needs to be moved into
|
|
404
471
|
# its ready list. A pending job that is already ready (delay_until None or
|
|
405
|
-
# in the past) must NOT be re-pushed, or
|
|
406
|
-
#
|
|
472
|
+
# in the past) must NOT be re-pushed, or it would leave duplicate ready
|
|
473
|
+
# entries that every worker has to discard later.
|
|
407
474
|
was_scheduled = payload.delay_until is not None and payload.delay_until > now
|
|
408
475
|
payload.delay_until = None
|
|
409
476
|
payload.updated_at = now
|
|
@@ -44,6 +44,7 @@ class Worker:
|
|
|
44
44
|
self._current_job: JobPayload | None = None
|
|
45
45
|
self._jobs_processed = 0
|
|
46
46
|
self._jobs_failed = 0
|
|
47
|
+
self._queue_cursor = 0
|
|
47
48
|
|
|
48
49
|
@property
|
|
49
50
|
def is_running(self) -> bool:
|
|
@@ -84,10 +85,18 @@ class Worker:
|
|
|
84
85
|
self._running = False
|
|
85
86
|
|
|
86
87
|
async def _fetch_next(self) -> JobPayload | None:
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
queue_count = len(self.queues)
|
|
89
|
+
if queue_count == 0:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
for offset in range(queue_count):
|
|
93
|
+
index = (self._queue_cursor + offset) % queue_count
|
|
94
|
+
job = await self.driver.pop(self.queues[index])
|
|
89
95
|
if job:
|
|
96
|
+
self._queue_cursor = (index + 1) % queue_count
|
|
90
97
|
return job
|
|
98
|
+
|
|
99
|
+
self._queue_cursor = (self._queue_cursor + 1) % queue_count
|
|
91
100
|
return None
|
|
92
101
|
|
|
93
102
|
@staticmethod
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|