queuerPy 0.21.0__tar.gz → 0.23.0__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.
- {queuerpy-0.21.0/queuerPy.egg-info → queuerpy-0.23.0}/PKG-INFO +14 -2
- {queuerpy-0.21.0 → queuerpy-0.23.0}/README.md +13 -1
- {queuerpy-0.21.0 → queuerpy-0.23.0}/_version.py +1 -1
- queuerpy-0.23.0/core/context.py +11 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/core/runner.py +8 -1
- {queuerpy-0.21.0 → queuerpy-0.23.0}/database/db_job.py +112 -66
- {queuerpy-0.21.0 → queuerpy-0.23.0}/database/db_worker.py +44 -35
- {queuerpy-0.21.0 → queuerpy-0.23.0}/helper/database.py +2 -1
- {queuerpy-0.21.0 → queuerpy-0.23.0}/model/options.py +8 -2
- {queuerpy-0.21.0 → queuerpy-0.23.0}/pyproject.toml +2 -2
- {queuerpy-0.21.0 → queuerpy-0.23.0}/queuer.py +5 -7
- {queuerpy-0.21.0 → queuerpy-0.23.0/queuerPy.egg-info}/PKG-INFO +14 -2
- {queuerpy-0.21.0 → queuerpy-0.23.0}/queuerPy.egg-info/SOURCES.txt +2 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/queuer_job.py +23 -3
- {queuerpy-0.21.0 → queuerpy-0.23.0}/sql/job.sql +4 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/LICENSE +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/MANIFEST.in +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/__init__.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/core/__init__.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/core/broadcaster.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/core/listener.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/core/retryer.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/core/scheduler.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/core/ticker.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/database/__init__.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/database/db_listener.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/database/db_master.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/helper/__init__.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/helper/error.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/helper/logging.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/helper/sql.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/helper/task.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/model/__init__.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/model/batch_job.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/model/connection.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/model/job.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/model/master.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/model/options_on_error.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/model/task.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/model/worker.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/py.typed +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/queuerPy.egg-info/dependency_links.txt +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/queuerPy.egg-info/requires.txt +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/queuerPy.egg-info/top_level.txt +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/queuer_global.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/queuer_listener.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/queuer_master.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/queuer_next_interval.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/queuer_task.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/queuer_worker.py +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/setup.cfg +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/sql/master.sql +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/sql/notify.sql +0 -0
- {queuerpy-0.21.0 → queuerpy-0.23.0}/sql/worker.sql +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: queuerPy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.23.0
|
|
4
4
|
Summary: A Python implementation of the queuer system - a job queuing and processing system with PostgreSQL backend
|
|
5
5
|
Author-email: Simon Herrmann <siherrmann@users.noreply.github.com>
|
|
6
6
|
Maintainer-email: Simon Herrmann <siherrmann@users.noreply.github.com>
|
|
@@ -43,7 +43,7 @@ Dynamic: license-file
|
|
|
43
43
|
|
|
44
44
|
# queuerPy
|
|
45
45
|
|
|
46
|
-
[](https://pypi.org/project/queuerPy)
|
|
47
47
|
[](https://www.python.org/)
|
|
48
48
|

|
|
49
49
|
[](https://github.com/siherrmann/queuer/blob/master/LICENSE)
|
|
@@ -107,6 +107,18 @@ job = q.add_job(example_task, paramKeyed1="test", debug=True)
|
|
|
107
107
|
job = q.add_job(example_task, 5, "12", paramKeyed1="test", debug=True)
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
### 🔗 Automatic Parent-Child Job Tracking
|
|
111
|
+
|
|
112
|
+
If you add a new job *from within* the execution of another job, queuerPy will automatically track this relationship. Thanks to native Python `ContextVar` propagation, the new job will seamlessly inherit the `parent_rid` of the current job.
|
|
113
|
+
|
|
114
|
+
This enables you to create deep, scalable trees of sub-jobs without having to manually pass job IDs around in your function parameters!
|
|
115
|
+
|
|
116
|
+
### 🛡️ Database Stability & Timeouts
|
|
117
|
+
|
|
118
|
+
queuerPy is built for robust, uninterrupted long-term polling. It handles PostgreSQL connection losses, cursor iteration interruptions, and blocking calls seamlessly:
|
|
119
|
+
- **Statement & Connection Timeouts**: To prevent workers from hanging indefinitely, queuerPy enforces strict `connect_timeout` (10s) and query `statement_timeout` (30s) at the connection layer.
|
|
120
|
+
- **Resilient Polling**: Iterative fetches and polling loops are wrapped in comprehensive error checking. If the database crashes, queuerPy will gracefully wait and reconnect rather than crashing the worker.
|
|
121
|
+
|
|
110
122
|
In the initialisation of the queuer the existence of the necessary database tables is checked and if they don't exist they get created. The database is configured with these environment variables:
|
|
111
123
|
|
|
112
124
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# queuerPy
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/queuerPy)
|
|
4
4
|
[](https://www.python.org/)
|
|
5
5
|

|
|
6
6
|
[](https://github.com/siherrmann/queuer/blob/master/LICENSE)
|
|
@@ -64,6 +64,18 @@ job = q.add_job(example_task, paramKeyed1="test", debug=True)
|
|
|
64
64
|
job = q.add_job(example_task, 5, "12", paramKeyed1="test", debug=True)
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
### 🔗 Automatic Parent-Child Job Tracking
|
|
68
|
+
|
|
69
|
+
If you add a new job *from within* the execution of another job, queuerPy will automatically track this relationship. Thanks to native Python `ContextVar` propagation, the new job will seamlessly inherit the `parent_rid` of the current job.
|
|
70
|
+
|
|
71
|
+
This enables you to create deep, scalable trees of sub-jobs without having to manually pass job IDs around in your function parameters!
|
|
72
|
+
|
|
73
|
+
### 🛡️ Database Stability & Timeouts
|
|
74
|
+
|
|
75
|
+
queuerPy is built for robust, uninterrupted long-term polling. It handles PostgreSQL connection losses, cursor iteration interruptions, and blocking calls seamlessly:
|
|
76
|
+
- **Statement & Connection Timeouts**: To prevent workers from hanging indefinitely, queuerPy enforces strict `connect_timeout` (10s) and query `statement_timeout` (30s) at the connection layer.
|
|
77
|
+
- **Resilient Polling**: Iterative fetches and polling loops are wrapped in comprehensive error checking. If the database crashes, queuerPy will gracefully wait and reconnect rather than crashing the worker.
|
|
78
|
+
|
|
67
79
|
In the initialisation of the queuer the existence of the necessary database tables is checked and if they don't exist they get created. The database is configured with these environment variables:
|
|
68
80
|
|
|
69
81
|
```bash
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context management for the queuer implementation.
|
|
3
|
+
Provides a way to store tracking information across asynchronous and concurrent flows.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
# Context variable to track the RID of the currently executing job.
|
|
11
|
+
current_job_rid: ContextVar[Optional[UUID]] = ContextVar("current_job_rid", default=None)
|
|
@@ -27,6 +27,7 @@ class Runner:
|
|
|
27
27
|
:param task: The synchronous or asynchronous function to execute.
|
|
28
28
|
:param args: Arguments to pass to the task function.
|
|
29
29
|
:param pool: Multiprocessing.Pool for process reuse (required).
|
|
30
|
+
:param setup_func: Optional callable to run before the task (e.g., for setting ContextVars).
|
|
30
31
|
:param kwargs: Keyword arguments to pass to the task function.
|
|
31
32
|
"""
|
|
32
33
|
|
|
@@ -35,6 +36,7 @@ class Runner:
|
|
|
35
36
|
task: Callable[..., Any],
|
|
36
37
|
*args: Any,
|
|
37
38
|
pool: Any,
|
|
39
|
+
setup_func: Optional[Callable[[], None]] = None,
|
|
38
40
|
**kwargs: Any,
|
|
39
41
|
):
|
|
40
42
|
"""
|
|
@@ -51,6 +53,7 @@ class Runner:
|
|
|
51
53
|
self.is_async = inspect.iscoroutinefunction(task)
|
|
52
54
|
self.name = task.__name__
|
|
53
55
|
self._pool = pool
|
|
56
|
+
self._setup_func = setup_func
|
|
54
57
|
self._async_result: Optional[Any] = None
|
|
55
58
|
# Use Manager Event for cross-process sharing (pickleable)
|
|
56
59
|
manager = multiprocessing.Manager()
|
|
@@ -78,6 +81,7 @@ class Runner:
|
|
|
78
81
|
kwargs: dict,
|
|
79
82
|
is_async: bool,
|
|
80
83
|
cancel_event: Any,
|
|
84
|
+
setup_func: Optional[Callable[[], None]] = None,
|
|
81
85
|
) -> Any:
|
|
82
86
|
"""
|
|
83
87
|
Static method to execute tasks in pool workers.
|
|
@@ -88,6 +92,9 @@ class Runner:
|
|
|
88
92
|
if cancel_event.is_set():
|
|
89
93
|
raise asyncio.CancelledError("Task was cancelled before execution")
|
|
90
94
|
|
|
95
|
+
if setup_func is not None:
|
|
96
|
+
setup_func()
|
|
97
|
+
|
|
91
98
|
if is_async:
|
|
92
99
|
# For async tasks, monitor cancellation during execution
|
|
93
100
|
result = asyncio.run(
|
|
@@ -139,7 +146,7 @@ class Runner:
|
|
|
139
146
|
"""Starts the Runner in the background using the pool."""
|
|
140
147
|
self._async_result = self._pool.apply_async(
|
|
141
148
|
Runner._execute_in_pool_worker,
|
|
142
|
-
(self.task, self.args, self.kwargs, self.is_async, self._cancel_event),
|
|
149
|
+
(self.task, self.args, self.kwargs, self.is_async, self._cancel_event, self._setup_func),
|
|
143
150
|
)
|
|
144
151
|
|
|
145
152
|
def get_results(self, timeout: Optional[float] = None) -> Any:
|
|
@@ -186,19 +186,22 @@ class JobDBHandler:
|
|
|
186
186
|
if self.db.instance is None:
|
|
187
187
|
raise ValueError("Database connection is not established")
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
189
|
+
try:
|
|
190
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
191
|
+
cur.execute(
|
|
192
|
+
"""
|
|
193
|
+
SELECT * FROM update_job_initial(%s);
|
|
194
|
+
""",
|
|
195
|
+
(worker.id,),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
jobs: List[Job] = []
|
|
199
|
+
for row in cur.fetchall():
|
|
200
|
+
jobs.append(Job.from_row(row))
|
|
201
|
+
|
|
202
|
+
return jobs
|
|
203
|
+
except Exception as e:
|
|
204
|
+
raise QueuerError("update jobs initial", e)
|
|
202
205
|
|
|
203
206
|
def update_job_final(self, job: Job) -> Job:
|
|
204
207
|
"""
|
|
@@ -303,17 +306,20 @@ class JobDBHandler:
|
|
|
303
306
|
if self.db.instance is None:
|
|
304
307
|
raise ValueError("Database connection is not established")
|
|
305
308
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
309
|
+
try:
|
|
310
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
311
|
+
cur.execute(
|
|
312
|
+
"SELECT * FROM select_all_jobs(%s, %s, %s);",
|
|
313
|
+
(self.encryption_key or "", last_id, entries),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
jobs: List[Job] = []
|
|
317
|
+
for row in cur.fetchall():
|
|
318
|
+
jobs.append(Job.from_row(row))
|
|
319
|
+
|
|
320
|
+
return jobs
|
|
321
|
+
except Exception as e:
|
|
322
|
+
raise QueuerError("select all jobs", e)
|
|
317
323
|
|
|
318
324
|
def select_all_jobs_by_worker_rid(
|
|
319
325
|
self, worker_rid: UUID, last_id: int = 0, entries: int = 100
|
|
@@ -330,17 +336,20 @@ class JobDBHandler:
|
|
|
330
336
|
if self.db.instance is None:
|
|
331
337
|
raise ValueError("Database connection is not established")
|
|
332
338
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
339
|
+
try:
|
|
340
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
341
|
+
cur.execute(
|
|
342
|
+
"SELECT * FROM select_all_jobs_by_worker_rid(%s, %s, %s, %s);",
|
|
343
|
+
(self.encryption_key or "", str(worker_rid), last_id, entries),
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
jobs: List[Job] = []
|
|
347
|
+
for row in cur.fetchall():
|
|
348
|
+
jobs.append(Job.from_row(row))
|
|
349
|
+
|
|
350
|
+
return jobs
|
|
351
|
+
except Exception as e:
|
|
352
|
+
raise QueuerError("select all jobs by worker rid", e)
|
|
344
353
|
|
|
345
354
|
def select_all_jobs_by_search(
|
|
346
355
|
self, search: str, last_id: int = 0, entries: int = 100
|
|
@@ -358,17 +367,48 @@ class JobDBHandler:
|
|
|
358
367
|
if self.db.instance is None:
|
|
359
368
|
raise ValueError("Database connection is not established")
|
|
360
369
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
370
|
+
try:
|
|
371
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
372
|
+
cur.execute(
|
|
373
|
+
"SELECT * FROM select_all_jobs_by_search(%s, %s, %s, %s);",
|
|
374
|
+
(self.encryption_key or "", search, last_id, entries),
|
|
375
|
+
)
|
|
366
376
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
377
|
+
jobs: List[Job] = []
|
|
378
|
+
for row in cur.fetchall():
|
|
379
|
+
jobs.append(Job.from_row(row))
|
|
370
380
|
|
|
371
|
-
|
|
381
|
+
return jobs
|
|
382
|
+
except Exception as e:
|
|
383
|
+
raise QueuerError("select all jobs by search", e)
|
|
384
|
+
|
|
385
|
+
def select_jobs_without_worker(
|
|
386
|
+
self, last_id: int = 0, entries: int = 100
|
|
387
|
+
) -> List[Job]:
|
|
388
|
+
"""
|
|
389
|
+
Select all jobs that are not assigned to any worker.
|
|
390
|
+
|
|
391
|
+
:param last_id: Last job ID from previous page.
|
|
392
|
+
:param entries: Number of entries to retrieve.
|
|
393
|
+
:return: List of Job instances.
|
|
394
|
+
"""
|
|
395
|
+
if self.db.instance is None:
|
|
396
|
+
raise ValueError("Database connection is not established")
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
400
|
+
cur.execute(
|
|
401
|
+
"SELECT * FROM select_jobs_without_worker(%s, %s, %s);",
|
|
402
|
+
(self.encryption_key or "", last_id, entries),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
jobs: List[Job] = []
|
|
406
|
+
for row in cur.fetchall():
|
|
407
|
+
jobs.append(Job.from_row(row))
|
|
408
|
+
|
|
409
|
+
return jobs
|
|
410
|
+
except Exception as e:
|
|
411
|
+
raise QueuerError("select jobs without worker", e)
|
|
372
412
|
|
|
373
413
|
def add_retention_archive(self, days: int) -> None:
|
|
374
414
|
"""
|
|
@@ -441,17 +481,20 @@ class JobDBHandler:
|
|
|
441
481
|
if self.db.instance is None:
|
|
442
482
|
raise ValueError("Database connection is not established")
|
|
443
483
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
484
|
+
try:
|
|
485
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
486
|
+
cur.execute(
|
|
487
|
+
"SELECT * FROM select_all_jobs_from_archive(%s, %s, %s);",
|
|
488
|
+
(self.encryption_key or "", last_id, entries),
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
jobs: List[Job] = []
|
|
492
|
+
for row in cur.fetchall():
|
|
493
|
+
jobs.append(Job.from_row(row))
|
|
494
|
+
|
|
495
|
+
return jobs
|
|
496
|
+
except Exception as e:
|
|
497
|
+
raise QueuerError("select all jobs from archive", e)
|
|
455
498
|
|
|
456
499
|
def select_all_jobs_from_archive_by_search(
|
|
457
500
|
self, search: str, last_id: int = 0, entries: int = 100
|
|
@@ -468,14 +511,17 @@ class JobDBHandler:
|
|
|
468
511
|
if self.db.instance is None:
|
|
469
512
|
raise ValueError("Database connection is not established")
|
|
470
513
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
514
|
+
try:
|
|
515
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
516
|
+
cur.execute(
|
|
517
|
+
"SELECT * FROM select_all_jobs_from_archive_by_search(%s, %s, %s, %s);",
|
|
518
|
+
(self.encryption_key or "", search, last_id, entries),
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
jobs: List[Job] = []
|
|
522
|
+
for row in cur.fetchall():
|
|
523
|
+
jobs.append(Job.from_row(row))
|
|
524
|
+
|
|
525
|
+
return jobs
|
|
526
|
+
except Exception as e:
|
|
527
|
+
raise QueuerError("select all jobs from archive by search", e)
|
|
@@ -195,14 +195,17 @@ class WorkerDBHandler:
|
|
|
195
195
|
if self.db.instance is None:
|
|
196
196
|
raise ValueError("Database connection is not established")
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
198
|
+
try:
|
|
199
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
200
|
+
cur.execute("SELECT * FROM select_all_workers(%s, %s);", (last_id, entries))
|
|
201
|
+
|
|
202
|
+
workers: List[Worker] = []
|
|
203
|
+
for row in cur.fetchall():
|
|
204
|
+
workers.append(Worker.from_row(row))
|
|
205
|
+
|
|
206
|
+
return workers
|
|
207
|
+
except Exception as e:
|
|
208
|
+
raise QueuerError("select all workers", e)
|
|
206
209
|
|
|
207
210
|
def select_all_workers_by_search(
|
|
208
211
|
self, search: str, last_id: int = 0, entries: int = 100
|
|
@@ -215,17 +218,20 @@ class WorkerDBHandler:
|
|
|
215
218
|
if self.db.instance is None:
|
|
216
219
|
raise ValueError("Database connection is not established")
|
|
217
220
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
221
|
+
try:
|
|
222
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
223
|
+
cur.execute(
|
|
224
|
+
"SELECT * FROM select_all_workers_by_search(%s, %s, %s);",
|
|
225
|
+
(search, last_id, entries),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
workers: List[Worker] = []
|
|
229
|
+
for row in cur.fetchall():
|
|
230
|
+
workers.append(Worker.from_row(row))
|
|
231
|
+
|
|
232
|
+
return workers
|
|
233
|
+
except Exception as e:
|
|
234
|
+
raise QueuerError("select all workers by search", e)
|
|
229
235
|
|
|
230
236
|
def select_all_connections(self) -> List[ConnectionModel]:
|
|
231
237
|
"""
|
|
@@ -235,19 +241,22 @@ class WorkerDBHandler:
|
|
|
235
241
|
if self.db.instance is None:
|
|
236
242
|
raise ValueError("Database connection is not established")
|
|
237
243
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
244
|
+
try:
|
|
245
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
246
|
+
cur.execute("SELECT * FROM select_all_connections();")
|
|
247
|
+
|
|
248
|
+
connections: List[ConnectionModel] = []
|
|
249
|
+
for row in cur.fetchall():
|
|
250
|
+
connection = ConnectionModel(
|
|
251
|
+
pid=row["output_pid"],
|
|
252
|
+
database=row["output_datname"],
|
|
253
|
+
username=row["output_usename"],
|
|
254
|
+
application_name=row["output_application_name"],
|
|
255
|
+
query=row["output_query"],
|
|
256
|
+
state=row["output_state"],
|
|
257
|
+
)
|
|
258
|
+
connections.append(connection)
|
|
259
|
+
|
|
260
|
+
return connections
|
|
261
|
+
except Exception as e:
|
|
262
|
+
raise QueuerError("select all connections", e)
|
|
@@ -88,8 +88,9 @@ class DatabaseConfiguration:
|
|
|
88
88
|
f"user={self.username} "
|
|
89
89
|
f"password={self.password} "
|
|
90
90
|
f"sslmode={self.sslmode} "
|
|
91
|
+
f"connect_timeout=10 "
|
|
91
92
|
f"application_name=queuer "
|
|
92
|
-
f"options='-c search_path={self.schema}'"
|
|
93
|
+
f"options='-c search_path={self.schema} -c statement_timeout=30000'"
|
|
93
94
|
)
|
|
94
95
|
|
|
95
96
|
|
|
@@ -78,6 +78,7 @@ class Options:
|
|
|
78
78
|
|
|
79
79
|
on_error: Optional[OnError] = None
|
|
80
80
|
schedule: Optional[Schedule] = None
|
|
81
|
+
parent_rid: Optional[str] = None
|
|
81
82
|
|
|
82
83
|
def is_valid(self) -> bool:
|
|
83
84
|
"""
|
|
@@ -100,6 +101,7 @@ class Options:
|
|
|
100
101
|
return {
|
|
101
102
|
"on_error": self.on_error.to_dict() if self.on_error else None,
|
|
102
103
|
"schedule": self.schedule.to_dict() if self.schedule else None,
|
|
104
|
+
"parent_rid": self.parent_rid,
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
@classmethod
|
|
@@ -115,11 +117,15 @@ class Options:
|
|
|
115
117
|
options.on_error = OnError.from_dict(data["on_error"])
|
|
116
118
|
if data.get("schedule"):
|
|
117
119
|
options.schedule = Schedule.from_dict(data["schedule"])
|
|
120
|
+
if data.get("parent_rid"):
|
|
121
|
+
options.parent_rid = data["parent_rid"]
|
|
118
122
|
return options
|
|
119
123
|
|
|
120
124
|
|
|
121
125
|
def new_options(
|
|
122
|
-
on_error: Optional[OnError] = None,
|
|
126
|
+
on_error: Optional[OnError] = None,
|
|
127
|
+
schedule: Optional[Schedule] = None,
|
|
128
|
+
parent_rid: Optional[str] = None,
|
|
123
129
|
) -> Options:
|
|
124
130
|
"""
|
|
125
131
|
Create new options. Mirrors Go's pattern.
|
|
@@ -128,4 +134,4 @@ def new_options(
|
|
|
128
134
|
:param schedule: Schedule options.
|
|
129
135
|
:return: Options instance.
|
|
130
136
|
"""
|
|
131
|
-
return Options(on_error=on_error, schedule=schedule)
|
|
137
|
+
return Options(on_error=on_error, schedule=schedule, parent_rid=parent_rid)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "queuerPy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.23.0"
|
|
8
8
|
description = "A Python implementation of the queuer system - a job queuing and processing system with PostgreSQL backend"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -92,7 +92,7 @@ pythonVersion = "3.8"
|
|
|
92
92
|
pythonPlatform = "All"
|
|
93
93
|
|
|
94
94
|
[tool.mypy]
|
|
95
|
-
python_version = "0.
|
|
95
|
+
python_version = "0.23.0"
|
|
96
96
|
warn_return_any = true
|
|
97
97
|
warn_unused_configs = true
|
|
98
98
|
disallow_untyped_defs = true
|
|
@@ -319,7 +319,6 @@ class Queuer(
|
|
|
319
319
|
logger.info("Closing process pool")
|
|
320
320
|
try:
|
|
321
321
|
self._process_pool.close()
|
|
322
|
-
self._process_pool.join(timeout=5.0)
|
|
323
322
|
# Force terminate if workers didn't finish gracefully
|
|
324
323
|
self._process_pool.terminate()
|
|
325
324
|
self._process_pool.join()
|
|
@@ -335,12 +334,11 @@ class Queuer(
|
|
|
335
334
|
time.sleep(0.1)
|
|
336
335
|
|
|
337
336
|
# Close database connection
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
logger.error(f"Error closing database connection: {e}")
|
|
337
|
+
logger.info("Closing database connection")
|
|
338
|
+
try:
|
|
339
|
+
self.database.close()
|
|
340
|
+
except Exception as e:
|
|
341
|
+
logger.error(f"Error closing database connection: {e}")
|
|
344
342
|
|
|
345
343
|
logger.info("Queuer stopped")
|
|
346
344
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: queuerPy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.23.0
|
|
4
4
|
Summary: A Python implementation of the queuer system - a job queuing and processing system with PostgreSQL backend
|
|
5
5
|
Author-email: Simon Herrmann <siherrmann@users.noreply.github.com>
|
|
6
6
|
Maintainer-email: Simon Herrmann <siherrmann@users.noreply.github.com>
|
|
@@ -43,7 +43,7 @@ Dynamic: license-file
|
|
|
43
43
|
|
|
44
44
|
# queuerPy
|
|
45
45
|
|
|
46
|
-
[](https://pypi.org/project/queuerPy)
|
|
47
47
|
[](https://www.python.org/)
|
|
48
48
|

|
|
49
49
|
[](https://github.com/siherrmann/queuer/blob/master/LICENSE)
|
|
@@ -107,6 +107,18 @@ job = q.add_job(example_task, paramKeyed1="test", debug=True)
|
|
|
107
107
|
job = q.add_job(example_task, 5, "12", paramKeyed1="test", debug=True)
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
### 🔗 Automatic Parent-Child Job Tracking
|
|
111
|
+
|
|
112
|
+
If you add a new job *from within* the execution of another job, queuerPy will automatically track this relationship. Thanks to native Python `ContextVar` propagation, the new job will seamlessly inherit the `parent_rid` of the current job.
|
|
113
|
+
|
|
114
|
+
This enables you to create deep, scalable trees of sub-jobs without having to manually pass job IDs around in your function parameters!
|
|
115
|
+
|
|
116
|
+
### 🛡️ Database Stability & Timeouts
|
|
117
|
+
|
|
118
|
+
queuerPy is built for robust, uninterrupted long-term polling. It handles PostgreSQL connection losses, cursor iteration interruptions, and blocking calls seamlessly:
|
|
119
|
+
- **Statement & Connection Timeouts**: To prevent workers from hanging indefinitely, queuerPy enforces strict `connect_timeout` (10s) and query `statement_timeout` (30s) at the connection layer.
|
|
120
|
+
- **Resilient Polling**: Iterative fetches and polling loops are wrapped in comprehensive error checking. If the database crashes, queuerPy will gracefully wait and reconnect rather than crashing the worker.
|
|
121
|
+
|
|
110
122
|
In the initialisation of the queuer the existence of the necessary database tables is checked and if they don't exist they get created. The database is configured with these environment variables:
|
|
111
123
|
|
|
112
124
|
```bash
|
|
@@ -26,6 +26,7 @@ queuer_worker.py
|
|
|
26
26
|
./queuer_worker.py
|
|
27
27
|
./core/__init__.py
|
|
28
28
|
./core/broadcaster.py
|
|
29
|
+
./core/context.py
|
|
29
30
|
./core/listener.py
|
|
30
31
|
./core/retryer.py
|
|
31
32
|
./core/runner.py
|
|
@@ -57,6 +58,7 @@ queuer_worker.py
|
|
|
57
58
|
./sql/worker.sql
|
|
58
59
|
core/__init__.py
|
|
59
60
|
core/broadcaster.py
|
|
61
|
+
core/context.py
|
|
60
62
|
core/listener.py
|
|
61
63
|
core/retryer.py
|
|
62
64
|
core/runner.py
|
|
@@ -20,11 +20,19 @@ from .model.job import Job, JobStatus, new_job as create_job
|
|
|
20
20
|
from .model.options import Options
|
|
21
21
|
from .model.worker import Worker
|
|
22
22
|
from .queuer_global import QueuerGlobalMixin
|
|
23
|
+
from .core.context import current_job_rid
|
|
24
|
+
from functools import partial
|
|
23
25
|
|
|
24
26
|
# Set up logger
|
|
25
27
|
logger = logging.getLogger(__name__)
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
def _setup_job_context(job_rid: UUID) -> None:
|
|
31
|
+
"""Helper to set the context variable in the worker process."""
|
|
32
|
+
current_job_rid.set(job_rid)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
28
36
|
class QueuerJobMixin(QueuerGlobalMixin):
|
|
29
37
|
"""
|
|
30
38
|
Mixin class containing job-related methods for the Queuer.
|
|
@@ -50,6 +58,12 @@ class QueuerJobMixin(QueuerGlobalMixin):
|
|
|
50
58
|
:raises Exception: If something goes wrong
|
|
51
59
|
"""
|
|
52
60
|
options: Optional[Options] = self._merge_options(None)
|
|
61
|
+
current_rid = current_job_rid.get()
|
|
62
|
+
if current_rid:
|
|
63
|
+
if not options:
|
|
64
|
+
options = Options()
|
|
65
|
+
options.parent_rid = str(current_rid)
|
|
66
|
+
|
|
53
67
|
job: Job = self._add_job(task, options, *parameters, **parameters_keyed)
|
|
54
68
|
|
|
55
69
|
logger.info(f"Job added: {job.rid}")
|
|
@@ -67,6 +81,12 @@ class QueuerJobMixin(QueuerGlobalMixin):
|
|
|
67
81
|
# Merge default options with provided options
|
|
68
82
|
options_merged: Optional[Options] = self._merge_options(options)
|
|
69
83
|
|
|
84
|
+
current_rid = current_job_rid.get()
|
|
85
|
+
if current_rid:
|
|
86
|
+
if not options_merged:
|
|
87
|
+
options_merged = Options()
|
|
88
|
+
options_merged.parent_rid = str(current_rid)
|
|
89
|
+
|
|
70
90
|
try:
|
|
71
91
|
# Create new job
|
|
72
92
|
new_job: Job = create_job(
|
|
@@ -422,8 +442,9 @@ class QueuerJobMixin(QueuerGlobalMixin):
|
|
|
422
442
|
try:
|
|
423
443
|
parameters = getattr(job, "parameters", [])
|
|
424
444
|
parameters_keyed = getattr(job, "parameters_keyed", {})
|
|
445
|
+
setup_func = partial(_setup_job_context, job.rid)
|
|
425
446
|
runner = Runner(
|
|
426
|
-
task, *parameters, pool=self._process_pool, **parameters_keyed
|
|
447
|
+
task, *parameters, pool=self._process_pool, setup_func=setup_func, **parameters_keyed
|
|
427
448
|
)
|
|
428
449
|
|
|
429
450
|
logger.info(f"Created runner for job {job.rid}")
|
|
@@ -439,8 +460,7 @@ class QueuerJobMixin(QueuerGlobalMixin):
|
|
|
439
460
|
return [results], False, None
|
|
440
461
|
|
|
441
462
|
except asyncio.CancelledError:
|
|
442
|
-
|
|
443
|
-
runner.cancel()
|
|
463
|
+
runner.cancel()
|
|
444
464
|
return [], True, None
|
|
445
465
|
except Exception as e:
|
|
446
466
|
logger.error(f"Process runner task {job.task_name} failed: {e}")
|
|
@@ -57,6 +57,10 @@ BEGIN
|
|
|
57
57
|
ON job
|
|
58
58
|
USING HASH ((options->'schedule'->>'next_interval'));
|
|
59
59
|
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_job_parent_rid
|
|
61
|
+
ON job
|
|
62
|
+
USING BTREE ((options->>'parent_rid'));
|
|
63
|
+
|
|
60
64
|
CREATE INDEX IF NOT EXISTS idx_job_worker_id ON job (worker_id);
|
|
61
65
|
CREATE INDEX IF NOT EXISTS idx_job_worker_rid ON job (worker_rid);
|
|
62
66
|
CREATE INDEX IF NOT EXISTS idx_job_status ON job (status);
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|