queuerPy 0.18.0__tar.gz → 0.20.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.18.0/queuerPy.egg-info → queuerpy-0.20.0}/PKG-INFO +55 -12
- {queuerpy-0.18.0 → queuerpy-0.20.0}/README.md +54 -11
- {queuerpy-0.18.0 → queuerpy-0.20.0}/_version.py +1 -1
- {queuerpy-0.18.0 → queuerpy-0.20.0}/database/db_job.py +25 -12
- {queuerpy-0.18.0 → queuerpy-0.20.0}/helper/database.py +5 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/pyproject.toml +2 -2
- {queuerpy-0.18.0 → queuerpy-0.20.0/queuerPy.egg-info}/PKG-INFO +55 -12
- {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer_global.py +1 -1
- {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer_master.py +25 -14
- {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer_worker.py +13 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/sql/job.sql +58 -7
- {queuerpy-0.18.0 → queuerpy-0.20.0}/LICENSE +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/MANIFEST.in +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/__init__.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/core/__init__.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/core/broadcaster.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/core/listener.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/core/retryer.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/core/runner.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/core/scheduler.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/core/ticker.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/database/__init__.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/database/db_listener.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/database/db_master.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/database/db_worker.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/helper/__init__.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/helper/error.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/helper/logging.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/helper/sql.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/helper/task.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/model/__init__.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/model/batch_job.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/model/connection.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/model/job.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/model/master.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/model/options.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/model/options_on_error.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/model/task.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/model/worker.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/py.typed +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/queuerPy.egg-info/SOURCES.txt +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/queuerPy.egg-info/dependency_links.txt +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/queuerPy.egg-info/requires.txt +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/queuerPy.egg-info/top_level.txt +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer_job.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer_listener.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer_next_interval.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer_task.py +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/setup.cfg +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/sql/master.sql +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.0}/sql/notify.sql +0 -0
- {queuerpy-0.18.0 → queuerpy-0.20.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.20.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>
|
|
@@ -58,9 +58,7 @@ The job table contains only queued, scheduled and running tasks. The ended jobs
|
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
## PyPI Installation (Recommended)
|
|
61
|
+
## 🛠️ Installation
|
|
64
62
|
|
|
65
63
|
```bash
|
|
66
64
|
pip install queuerPy
|
|
@@ -134,10 +132,10 @@ You can find a full example in the example folder.
|
|
|
134
132
|
def new_queuer(name: str, max_concurrency: int, *options: OnError) -> Queuer
|
|
135
133
|
|
|
136
134
|
def new_queuer_with_db(
|
|
137
|
-
name: str,
|
|
138
|
-
max_concurrency: int,
|
|
139
|
-
encryption_key: str,
|
|
140
|
-
db_config: DatabaseConfiguration,
|
|
135
|
+
name: str,
|
|
136
|
+
max_concurrency: int,
|
|
137
|
+
encryption_key: str,
|
|
138
|
+
db_config: DatabaseConfiguration,
|
|
141
139
|
*options: OnError
|
|
142
140
|
) -> Queuer
|
|
143
141
|
```
|
|
@@ -149,6 +147,7 @@ def new_queuer_with_db(
|
|
|
149
147
|
- `options`: Optional `OnError` configurations to apply to the worker.
|
|
150
148
|
|
|
151
149
|
This function performs the following setup:
|
|
150
|
+
|
|
152
151
|
- Initializes a logger.
|
|
153
152
|
- Sets up the database connection using the provided `db_config` or environment variables.
|
|
154
153
|
- Creates `JobDBHandler`, `WorkerDBHandler` instances for database interactions.
|
|
@@ -167,6 +166,7 @@ def start(self) -> None
|
|
|
167
166
|
```
|
|
168
167
|
|
|
169
168
|
Upon calling `start`:
|
|
169
|
+
|
|
170
170
|
- It performs a basic check to ensure internal listeners are initialized.
|
|
171
171
|
- Database listeners are created to listen to job events (inserts, updates, deletes) via PostgreSQL NOTIFY/LISTEN.
|
|
172
172
|
- It starts a poller to periodically poll the database for new jobs to process.
|
|
@@ -185,7 +185,50 @@ The `stop` method gracefully shuts down the Queuer instance, releasing resources
|
|
|
185
185
|
def stop(self) -> None
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
-
The `stop` method cancels all jobs, closes database listeners, and cleans up resources.
|
|
188
|
+
The `stop` method cancels all jobs, closes database listeners, and cleans up resources. **Note:** This method can only be used to stop the current worker instance that the code is running in. To stop other workers, use `stop_worker` or `stop_worker_gracefully`.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## stop_worker
|
|
193
|
+
|
|
194
|
+
The `stop_worker` method immediately stops a worker by setting its status to `STOPPED`. This will cancel all running jobs on that worker.
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
def stop_worker(self, worker_rid: UUID) -> None
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- `worker_rid`: The `UUID` identifying the worker to stop.
|
|
201
|
+
|
|
202
|
+
When a worker is stopped:
|
|
203
|
+
|
|
204
|
+
- The worker status is immediately set to `STOPPED` in the database
|
|
205
|
+
- The heartbeat ticker detects the `STOPPED` status and calls `stop()` on that worker
|
|
206
|
+
- All running jobs on that worker are cancelled immediately
|
|
207
|
+
- The worker will no longer accept new jobs
|
|
208
|
+
|
|
209
|
+
This method is useful for immediately shutting down a worker, for example in emergency situations or when you need to take a worker offline quickly.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## stop_worker_gracefully
|
|
214
|
+
|
|
215
|
+
The `stop_worker_gracefully` method gracefully stops a worker by setting its status to `STOPPING`. This allows currently running jobs to complete before the worker shuts down.
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
def stop_worker_gracefully(self, worker_rid: UUID) -> None
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
- `worker_rid`: The `UUID` identifying the worker to stop gracefully.
|
|
222
|
+
|
|
223
|
+
When a worker is stopped gracefully:
|
|
224
|
+
|
|
225
|
+
- The worker status is set to `STOPPING` in the database
|
|
226
|
+
- The heartbeat ticker detects the `STOPPING` status and sets `max_concurrency` to `0`
|
|
227
|
+
- Currently running jobs are allowed to complete normally
|
|
228
|
+
- No new jobs will be accepted by this worker
|
|
229
|
+
- Once all running jobs have finished, the worker status is automatically set to `STOPPED` and the worker shuts down
|
|
230
|
+
|
|
231
|
+
This method is ideal for maintenance scenarios where you want to ensure all in-progress work completes before shutting down the worker.
|
|
189
232
|
|
|
190
233
|
---
|
|
191
234
|
|
|
@@ -212,8 +255,8 @@ The `add_job` method adds a new job to the queue for execution. Jobs are units o
|
|
|
212
255
|
|
|
213
256
|
```python
|
|
214
257
|
def add_job(
|
|
215
|
-
self,
|
|
216
|
-
task: Union[Callable, str],
|
|
258
|
+
self,
|
|
259
|
+
task: Union[Callable, str],
|
|
217
260
|
*parameters: Any,
|
|
218
261
|
**parameters_keyed: Any
|
|
219
262
|
) -> Job
|
|
@@ -322,7 +365,7 @@ class RetryBackoff(str, Enum):
|
|
|
322
365
|
|
|
323
366
|
- `RETRY_BACKOFF_NONE`: No backoff. The retry_delay remains constant for all retries.
|
|
324
367
|
- `RETRY_BACKOFF_LINEAR`: The retry delay increases linearly with each attempt (e.g., delay, 2*delay, 3*delay).
|
|
325
|
-
- `RETRY_BACKOFF_EXPONENTIAL`: The retry delay increases exponentially with each attempt (e.g., delay, delay*2, delay*2
|
|
368
|
+
- `RETRY_BACKOFF_EXPONENTIAL`: The retry delay increases exponentially with each attempt (e.g., delay, delay*2, delay*2\*2).
|
|
326
369
|
|
|
327
370
|
---
|
|
328
371
|
|
|
@@ -15,9 +15,7 @@ The job table contains only queued, scheduled and running tasks. The ended jobs
|
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
## PyPI Installation (Recommended)
|
|
18
|
+
## 🛠️ Installation
|
|
21
19
|
|
|
22
20
|
```bash
|
|
23
21
|
pip install queuerPy
|
|
@@ -91,10 +89,10 @@ You can find a full example in the example folder.
|
|
|
91
89
|
def new_queuer(name: str, max_concurrency: int, *options: OnError) -> Queuer
|
|
92
90
|
|
|
93
91
|
def new_queuer_with_db(
|
|
94
|
-
name: str,
|
|
95
|
-
max_concurrency: int,
|
|
96
|
-
encryption_key: str,
|
|
97
|
-
db_config: DatabaseConfiguration,
|
|
92
|
+
name: str,
|
|
93
|
+
max_concurrency: int,
|
|
94
|
+
encryption_key: str,
|
|
95
|
+
db_config: DatabaseConfiguration,
|
|
98
96
|
*options: OnError
|
|
99
97
|
) -> Queuer
|
|
100
98
|
```
|
|
@@ -106,6 +104,7 @@ def new_queuer_with_db(
|
|
|
106
104
|
- `options`: Optional `OnError` configurations to apply to the worker.
|
|
107
105
|
|
|
108
106
|
This function performs the following setup:
|
|
107
|
+
|
|
109
108
|
- Initializes a logger.
|
|
110
109
|
- Sets up the database connection using the provided `db_config` or environment variables.
|
|
111
110
|
- Creates `JobDBHandler`, `WorkerDBHandler` instances for database interactions.
|
|
@@ -124,6 +123,7 @@ def start(self) -> None
|
|
|
124
123
|
```
|
|
125
124
|
|
|
126
125
|
Upon calling `start`:
|
|
126
|
+
|
|
127
127
|
- It performs a basic check to ensure internal listeners are initialized.
|
|
128
128
|
- Database listeners are created to listen to job events (inserts, updates, deletes) via PostgreSQL NOTIFY/LISTEN.
|
|
129
129
|
- It starts a poller to periodically poll the database for new jobs to process.
|
|
@@ -142,7 +142,50 @@ The `stop` method gracefully shuts down the Queuer instance, releasing resources
|
|
|
142
142
|
def stop(self) -> None
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
The `stop` method cancels all jobs, closes database listeners, and cleans up resources.
|
|
145
|
+
The `stop` method cancels all jobs, closes database listeners, and cleans up resources. **Note:** This method can only be used to stop the current worker instance that the code is running in. To stop other workers, use `stop_worker` or `stop_worker_gracefully`.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## stop_worker
|
|
150
|
+
|
|
151
|
+
The `stop_worker` method immediately stops a worker by setting its status to `STOPPED`. This will cancel all running jobs on that worker.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
def stop_worker(self, worker_rid: UUID) -> None
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- `worker_rid`: The `UUID` identifying the worker to stop.
|
|
158
|
+
|
|
159
|
+
When a worker is stopped:
|
|
160
|
+
|
|
161
|
+
- The worker status is immediately set to `STOPPED` in the database
|
|
162
|
+
- The heartbeat ticker detects the `STOPPED` status and calls `stop()` on that worker
|
|
163
|
+
- All running jobs on that worker are cancelled immediately
|
|
164
|
+
- The worker will no longer accept new jobs
|
|
165
|
+
|
|
166
|
+
This method is useful for immediately shutting down a worker, for example in emergency situations or when you need to take a worker offline quickly.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## stop_worker_gracefully
|
|
171
|
+
|
|
172
|
+
The `stop_worker_gracefully` method gracefully stops a worker by setting its status to `STOPPING`. This allows currently running jobs to complete before the worker shuts down.
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
def stop_worker_gracefully(self, worker_rid: UUID) -> None
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
- `worker_rid`: The `UUID` identifying the worker to stop gracefully.
|
|
179
|
+
|
|
180
|
+
When a worker is stopped gracefully:
|
|
181
|
+
|
|
182
|
+
- The worker status is set to `STOPPING` in the database
|
|
183
|
+
- The heartbeat ticker detects the `STOPPING` status and sets `max_concurrency` to `0`
|
|
184
|
+
- Currently running jobs are allowed to complete normally
|
|
185
|
+
- No new jobs will be accepted by this worker
|
|
186
|
+
- Once all running jobs have finished, the worker status is automatically set to `STOPPED` and the worker shuts down
|
|
187
|
+
|
|
188
|
+
This method is ideal for maintenance scenarios where you want to ensure all in-progress work completes before shutting down the worker.
|
|
146
189
|
|
|
147
190
|
---
|
|
148
191
|
|
|
@@ -169,8 +212,8 @@ The `add_job` method adds a new job to the queue for execution. Jobs are units o
|
|
|
169
212
|
|
|
170
213
|
```python
|
|
171
214
|
def add_job(
|
|
172
|
-
self,
|
|
173
|
-
task: Union[Callable, str],
|
|
215
|
+
self,
|
|
216
|
+
task: Union[Callable, str],
|
|
174
217
|
*parameters: Any,
|
|
175
218
|
**parameters_keyed: Any
|
|
176
219
|
) -> Job
|
|
@@ -279,7 +322,7 @@ class RetryBackoff(str, Enum):
|
|
|
279
322
|
|
|
280
323
|
- `RETRY_BACKOFF_NONE`: No backoff. The retry_delay remains constant for all retries.
|
|
281
324
|
- `RETRY_BACKOFF_LINEAR`: The retry delay increases linearly with each attempt (e.g., delay, 2*delay, 3*delay).
|
|
282
|
-
- `RETRY_BACKOFF_EXPONENTIAL`: The retry delay increases exponentially with each attempt (e.g., delay, delay*2, delay*2
|
|
325
|
+
- `RETRY_BACKOFF_EXPONENTIAL`: The retry delay increases exponentially with each attempt (e.g., delay, delay*2, delay*2\*2).
|
|
283
326
|
|
|
284
327
|
---
|
|
285
328
|
|
|
@@ -9,7 +9,7 @@ from typing import List, Optional, Tuple
|
|
|
9
9
|
from uuid import UUID
|
|
10
10
|
from psycopg.rows import dict_row
|
|
11
11
|
|
|
12
|
-
from ..helper.database import Database
|
|
12
|
+
from ..helper.database import Database, DatabaseConfiguration
|
|
13
13
|
from ..helper.error import QueuerError
|
|
14
14
|
from ..helper.sql import SQLLoader, run_ddl
|
|
15
15
|
from ..model.job import Job
|
|
@@ -25,7 +25,7 @@ class JobDBHandler:
|
|
|
25
25
|
def __init__(
|
|
26
26
|
self,
|
|
27
27
|
db_connection: Database,
|
|
28
|
-
|
|
28
|
+
db_config: DatabaseConfiguration,
|
|
29
29
|
encryption_key: str = "",
|
|
30
30
|
):
|
|
31
31
|
"""Initialize job database handler."""
|
|
@@ -35,12 +35,13 @@ class JobDBHandler:
|
|
|
35
35
|
raise ValueError("Database connection is not established")
|
|
36
36
|
|
|
37
37
|
self.encryption_key: str = encryption_key
|
|
38
|
+
self.with_timescale: bool = db_config.with_timescale
|
|
38
39
|
|
|
39
40
|
sql_loader: SQLLoader = SQLLoader()
|
|
40
|
-
sql_loader.load_notify_sql(self.db.instance, force=with_table_drop)
|
|
41
|
-
sql_loader.load_job_sql(self.db.instance, force=with_table_drop)
|
|
41
|
+
sql_loader.load_notify_sql(self.db.instance, force=db_config.with_table_drop)
|
|
42
|
+
sql_loader.load_job_sql(self.db.instance, force=db_config.with_table_drop)
|
|
42
43
|
|
|
43
|
-
if with_table_drop:
|
|
44
|
+
if db_config.with_table_drop:
|
|
44
45
|
self.drop_tables()
|
|
45
46
|
|
|
46
47
|
self.create_table()
|
|
@@ -72,7 +73,7 @@ class JobDBHandler:
|
|
|
72
73
|
if self.db.instance is None:
|
|
73
74
|
raise ValueError("Database connection is not established")
|
|
74
75
|
|
|
75
|
-
run_ddl(self.db.instance, "SELECT init_job();")
|
|
76
|
+
run_ddl(self.db.instance, f"SELECT init_job({self.with_timescale});")
|
|
76
77
|
|
|
77
78
|
def drop_tables(self) -> None:
|
|
78
79
|
"""Drop job tables with DDL deadlock protection."""
|
|
@@ -379,9 +380,15 @@ class JobDBHandler:
|
|
|
379
380
|
if self.db.instance is None:
|
|
380
381
|
raise ValueError("Database connection is not established")
|
|
381
382
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
383
|
+
try:
|
|
384
|
+
with self.db.instance.cursor() as cur:
|
|
385
|
+
cur.execute(
|
|
386
|
+
"SELECT add_retention_archive(%s, %s);", (days, self.with_timescale)
|
|
387
|
+
)
|
|
388
|
+
self.db.instance.commit()
|
|
389
|
+
except Exception as e:
|
|
390
|
+
self.db.instance.rollback()
|
|
391
|
+
raise e
|
|
385
392
|
|
|
386
393
|
def remove_retention_archive(self) -> None:
|
|
387
394
|
"""
|
|
@@ -391,9 +398,15 @@ class JobDBHandler:
|
|
|
391
398
|
if self.db.instance is None:
|
|
392
399
|
raise ValueError("Database connection is not established")
|
|
393
400
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
401
|
+
try:
|
|
402
|
+
with self.db.instance.cursor() as cur:
|
|
403
|
+
cur.execute(
|
|
404
|
+
"SELECT remove_retention_archive(%s);", (self.with_timescale,)
|
|
405
|
+
)
|
|
406
|
+
self.db.instance.commit()
|
|
407
|
+
except Exception as e:
|
|
408
|
+
self.db.instance.rollback()
|
|
409
|
+
raise e
|
|
397
410
|
|
|
398
411
|
def select_job_from_archive(self, rid: UUID) -> Optional[Job]:
|
|
399
412
|
"""
|
|
@@ -30,6 +30,7 @@ class DatabaseConfiguration:
|
|
|
30
30
|
schema: str = "public"
|
|
31
31
|
sslmode: str = "require"
|
|
32
32
|
with_table_drop: bool = False
|
|
33
|
+
with_timescale: bool = True
|
|
33
34
|
|
|
34
35
|
@classmethod
|
|
35
36
|
def from_env(cls) -> "DatabaseConfiguration":
|
|
@@ -45,6 +46,9 @@ class DatabaseConfiguration:
|
|
|
45
46
|
with_table_drop = (
|
|
46
47
|
os.getenv("QUEUER_DB_WITH_TABLE_DROP", "false").lower() == "true"
|
|
47
48
|
)
|
|
49
|
+
with_timescale = (
|
|
50
|
+
os.getenv("QUEUER_DB_WITH_TIMESCALE", "true").lower() != "false"
|
|
51
|
+
)
|
|
48
52
|
|
|
49
53
|
# Validate required fields
|
|
50
54
|
if not all(
|
|
@@ -72,6 +76,7 @@ class DatabaseConfiguration:
|
|
|
72
76
|
schema=schema,
|
|
73
77
|
sslmode=sslmode,
|
|
74
78
|
with_table_drop=with_table_drop,
|
|
79
|
+
with_timescale=with_timescale,
|
|
75
80
|
)
|
|
76
81
|
|
|
77
82
|
def connection_string(self) -> str:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "queuerPy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.20.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.20.0"
|
|
96
96
|
warn_return_any = true
|
|
97
97
|
warn_unused_configs = true
|
|
98
98
|
disallow_untyped_defs = true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: queuerPy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.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>
|
|
@@ -58,9 +58,7 @@ The job table contains only queued, scheduled and running tasks. The ended jobs
|
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
## PyPI Installation (Recommended)
|
|
61
|
+
## 🛠️ Installation
|
|
64
62
|
|
|
65
63
|
```bash
|
|
66
64
|
pip install queuerPy
|
|
@@ -134,10 +132,10 @@ You can find a full example in the example folder.
|
|
|
134
132
|
def new_queuer(name: str, max_concurrency: int, *options: OnError) -> Queuer
|
|
135
133
|
|
|
136
134
|
def new_queuer_with_db(
|
|
137
|
-
name: str,
|
|
138
|
-
max_concurrency: int,
|
|
139
|
-
encryption_key: str,
|
|
140
|
-
db_config: DatabaseConfiguration,
|
|
135
|
+
name: str,
|
|
136
|
+
max_concurrency: int,
|
|
137
|
+
encryption_key: str,
|
|
138
|
+
db_config: DatabaseConfiguration,
|
|
141
139
|
*options: OnError
|
|
142
140
|
) -> Queuer
|
|
143
141
|
```
|
|
@@ -149,6 +147,7 @@ def new_queuer_with_db(
|
|
|
149
147
|
- `options`: Optional `OnError` configurations to apply to the worker.
|
|
150
148
|
|
|
151
149
|
This function performs the following setup:
|
|
150
|
+
|
|
152
151
|
- Initializes a logger.
|
|
153
152
|
- Sets up the database connection using the provided `db_config` or environment variables.
|
|
154
153
|
- Creates `JobDBHandler`, `WorkerDBHandler` instances for database interactions.
|
|
@@ -167,6 +166,7 @@ def start(self) -> None
|
|
|
167
166
|
```
|
|
168
167
|
|
|
169
168
|
Upon calling `start`:
|
|
169
|
+
|
|
170
170
|
- It performs a basic check to ensure internal listeners are initialized.
|
|
171
171
|
- Database listeners are created to listen to job events (inserts, updates, deletes) via PostgreSQL NOTIFY/LISTEN.
|
|
172
172
|
- It starts a poller to periodically poll the database for new jobs to process.
|
|
@@ -185,7 +185,50 @@ The `stop` method gracefully shuts down the Queuer instance, releasing resources
|
|
|
185
185
|
def stop(self) -> None
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
-
The `stop` method cancels all jobs, closes database listeners, and cleans up resources.
|
|
188
|
+
The `stop` method cancels all jobs, closes database listeners, and cleans up resources. **Note:** This method can only be used to stop the current worker instance that the code is running in. To stop other workers, use `stop_worker` or `stop_worker_gracefully`.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## stop_worker
|
|
193
|
+
|
|
194
|
+
The `stop_worker` method immediately stops a worker by setting its status to `STOPPED`. This will cancel all running jobs on that worker.
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
def stop_worker(self, worker_rid: UUID) -> None
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- `worker_rid`: The `UUID` identifying the worker to stop.
|
|
201
|
+
|
|
202
|
+
When a worker is stopped:
|
|
203
|
+
|
|
204
|
+
- The worker status is immediately set to `STOPPED` in the database
|
|
205
|
+
- The heartbeat ticker detects the `STOPPED` status and calls `stop()` on that worker
|
|
206
|
+
- All running jobs on that worker are cancelled immediately
|
|
207
|
+
- The worker will no longer accept new jobs
|
|
208
|
+
|
|
209
|
+
This method is useful for immediately shutting down a worker, for example in emergency situations or when you need to take a worker offline quickly.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## stop_worker_gracefully
|
|
214
|
+
|
|
215
|
+
The `stop_worker_gracefully` method gracefully stops a worker by setting its status to `STOPPING`. This allows currently running jobs to complete before the worker shuts down.
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
def stop_worker_gracefully(self, worker_rid: UUID) -> None
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
- `worker_rid`: The `UUID` identifying the worker to stop gracefully.
|
|
222
|
+
|
|
223
|
+
When a worker is stopped gracefully:
|
|
224
|
+
|
|
225
|
+
- The worker status is set to `STOPPING` in the database
|
|
226
|
+
- The heartbeat ticker detects the `STOPPING` status and sets `max_concurrency` to `0`
|
|
227
|
+
- Currently running jobs are allowed to complete normally
|
|
228
|
+
- No new jobs will be accepted by this worker
|
|
229
|
+
- Once all running jobs have finished, the worker status is automatically set to `STOPPED` and the worker shuts down
|
|
230
|
+
|
|
231
|
+
This method is ideal for maintenance scenarios where you want to ensure all in-progress work completes before shutting down the worker.
|
|
189
232
|
|
|
190
233
|
---
|
|
191
234
|
|
|
@@ -212,8 +255,8 @@ The `add_job` method adds a new job to the queue for execution. Jobs are units o
|
|
|
212
255
|
|
|
213
256
|
```python
|
|
214
257
|
def add_job(
|
|
215
|
-
self,
|
|
216
|
-
task: Union[Callable, str],
|
|
258
|
+
self,
|
|
259
|
+
task: Union[Callable, str],
|
|
217
260
|
*parameters: Any,
|
|
218
261
|
**parameters_keyed: Any
|
|
219
262
|
) -> Job
|
|
@@ -322,7 +365,7 @@ class RetryBackoff(str, Enum):
|
|
|
322
365
|
|
|
323
366
|
- `RETRY_BACKOFF_NONE`: No backoff. The retry_delay remains constant for all retries.
|
|
324
367
|
- `RETRY_BACKOFF_LINEAR`: The retry delay increases linearly with each attempt (e.g., delay, 2*delay, 3*delay).
|
|
325
|
-
- `RETRY_BACKOFF_EXPONENTIAL`: The retry delay increases exponentially with each attempt (e.g., delay, delay*2, delay*2
|
|
368
|
+
- `RETRY_BACKOFF_EXPONENTIAL`: The retry delay increases exponentially with each attempt (e.g., delay, delay*2, delay*2\*2).
|
|
326
369
|
|
|
327
370
|
---
|
|
328
371
|
|
|
@@ -56,7 +56,7 @@ class QueuerGlobalMixin:
|
|
|
56
56
|
|
|
57
57
|
# Database handlers
|
|
58
58
|
self.db_job: JobDBHandler = JobDBHandler(
|
|
59
|
-
self.database, db_config
|
|
59
|
+
self.database, db_config, encryption_key
|
|
60
60
|
)
|
|
61
61
|
self.db_worker: WorkerDBHandler = WorkerDBHandler(
|
|
62
62
|
self.database, db_config.with_table_drop
|
|
@@ -38,14 +38,27 @@ class QueuerMasterMixin(QueuerGlobalMixin):
|
|
|
38
38
|
|
|
39
39
|
# Handle retention archive configuration
|
|
40
40
|
try:
|
|
41
|
-
if
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
if master_settings.retention_archive > 0:
|
|
42
|
+
if old_master.settings.retention_archive == 0:
|
|
43
|
+
self.db_job.add_retention_archive(master_settings.retention_archive)
|
|
44
|
+
elif (
|
|
45
|
+
old_master.settings.retention_archive
|
|
46
|
+
!= master_settings.retention_archive
|
|
47
|
+
):
|
|
48
|
+
try:
|
|
49
|
+
self.db_job.remove_retention_archive()
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.warning(
|
|
52
|
+
f"Could not remove old retention policy (may not exist): {e}"
|
|
53
|
+
)
|
|
54
|
+
self.db_job.add_retention_archive(master_settings.retention_archive)
|
|
55
|
+
elif old_master.settings.retention_archive > 0:
|
|
56
|
+
try:
|
|
57
|
+
self.db_job.remove_retention_archive()
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.warning(
|
|
60
|
+
f"Could not remove retention policy (may not exist): {e}"
|
|
61
|
+
)
|
|
49
62
|
except Exception as e:
|
|
50
63
|
raise QueuerError("managing retention archive", e)
|
|
51
64
|
|
|
@@ -83,10 +96,9 @@ class QueuerMasterMixin(QueuerGlobalMixin):
|
|
|
83
96
|
logger.error(f"Error in master ticker task: {e}")
|
|
84
97
|
|
|
85
98
|
ticker = Ticker(
|
|
86
|
-
task=ticker_task,
|
|
87
|
-
name="_master_ticker",
|
|
88
99
|
interval=timedelta(seconds=master_settings.master_poll_interval),
|
|
89
|
-
|
|
100
|
+
task=ticker_task,
|
|
101
|
+
use_mp=False, # Use threading instead of multiprocessing to avoid pickling issues
|
|
90
102
|
)
|
|
91
103
|
logger.info("Starting master ticker...")
|
|
92
104
|
ticker.go()
|
|
@@ -150,10 +162,9 @@ class QueuerMasterMixin(QueuerGlobalMixin):
|
|
|
150
162
|
|
|
151
163
|
# Create and start the polling ticker
|
|
152
164
|
ticker = Ticker(
|
|
153
|
-
task=ticker_task,
|
|
154
|
-
name="_poll_master_ticker",
|
|
155
165
|
interval=timedelta(seconds=master_settings.master_poll_interval),
|
|
156
|
-
|
|
166
|
+
task=ticker_task,
|
|
167
|
+
use_mp=False, # Use threading instead of multiprocessing to avoid pickling issues
|
|
157
168
|
)
|
|
158
169
|
|
|
159
170
|
logger.info("Starting master poll ticker...")
|
|
@@ -15,6 +15,19 @@ from .queuer_global import QueuerGlobalMixin
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def get_current_worker_rid(self) -> Optional[UUID]:
|
|
19
|
+
"""
|
|
20
|
+
Get the current worker's RID safely with mutex locking.
|
|
21
|
+
Mirrors Go's GetCurrentWorkerRID method.
|
|
22
|
+
|
|
23
|
+
:return: Worker RID or None if no worker
|
|
24
|
+
"""
|
|
25
|
+
with self.worker_mutex:
|
|
26
|
+
if self.worker is not None:
|
|
27
|
+
return self.worker.rid
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
18
31
|
class QueuerWorkerMixin(QueuerGlobalMixin):
|
|
19
32
|
"""
|
|
20
33
|
Mixin class containing worker-related methods for the Queuer.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
CREATE OR REPLACE FUNCTION init_job() RETURNS VOID AS $$
|
|
1
|
+
CREATE OR REPLACE FUNCTION init_job(enable_timescaledb BOOLEAN DEFAULT TRUE) RETURNS VOID AS $$
|
|
2
2
|
BEGIN
|
|
3
3
|
-- Create TimescaleDB extension
|
|
4
|
-
|
|
4
|
+
IF enable_timescaledb THEN
|
|
5
|
+
CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;
|
|
6
|
+
END IF;
|
|
5
7
|
|
|
6
8
|
-- Create pgcrypto extension
|
|
7
9
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
@@ -37,7 +39,9 @@ BEGIN
|
|
|
37
39
|
);
|
|
38
40
|
|
|
39
41
|
-- Create hypertable (TimescaleDB feature)
|
|
40
|
-
|
|
42
|
+
IF enable_timescaledb THEN
|
|
43
|
+
PERFORM create_hypertable('job_archive', by_range('updated_at'), if_not_exists => TRUE);
|
|
44
|
+
END IF;
|
|
41
45
|
|
|
42
46
|
-- Create triggers for notifications
|
|
43
47
|
CREATE OR REPLACE TRIGGER job_notify_event
|
|
@@ -698,17 +702,64 @@ BEGIN
|
|
|
698
702
|
END;
|
|
699
703
|
$$ LANGUAGE plpgsql;
|
|
700
704
|
|
|
701
|
-
CREATE OR REPLACE FUNCTION add_retention_archive(input_retention_days INT)
|
|
705
|
+
CREATE OR REPLACE FUNCTION add_retention_archive(input_retention_days INT, enable_timescaledb BOOLEAN DEFAULT TRUE)
|
|
702
706
|
RETURNS VOID AS $$
|
|
703
707
|
BEGIN
|
|
704
|
-
|
|
708
|
+
IF enable_timescaledb THEN
|
|
709
|
+
-- Use TimescaleDB retention policy
|
|
710
|
+
PERFORM add_retention_policy('job_archive', (input_retention_days * INTERVAL '1 day'));
|
|
711
|
+
ELSE
|
|
712
|
+
-- Create sequence for tracking inserts
|
|
713
|
+
CREATE SEQUENCE IF NOT EXISTS job_archive_insert_counter;
|
|
714
|
+
|
|
715
|
+
-- Create trigger function that deletes old records every 10th insert
|
|
716
|
+
EXECUTE format('
|
|
717
|
+
CREATE OR REPLACE FUNCTION cleanup_job_archive_trigger()
|
|
718
|
+
RETURNS TRIGGER AS $func$
|
|
719
|
+
DECLARE
|
|
720
|
+
insert_count BIGINT;
|
|
721
|
+
deleted_count INT;
|
|
722
|
+
BEGIN
|
|
723
|
+
-- Only run cleanup every 10th insert
|
|
724
|
+
insert_count := nextval(''job_archive_insert_counter'');
|
|
725
|
+
IF insert_count %% 10 = 0 THEN
|
|
726
|
+
DELETE FROM job_archive
|
|
727
|
+
WHERE updated_at < (CURRENT_TIMESTAMP - INTERVAL ''%s days'');
|
|
728
|
+
|
|
729
|
+
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
|
730
|
+
|
|
731
|
+
-- Log cleanup if any rows were deleted (optional)
|
|
732
|
+
IF deleted_count > 0 THEN
|
|
733
|
+
RAISE NOTICE ''Cleaned up %% old job_archive records'', deleted_count;
|
|
734
|
+
END IF;
|
|
735
|
+
END IF;
|
|
736
|
+
RETURN NEW;
|
|
737
|
+
END;
|
|
738
|
+
$func$ LANGUAGE plpgsql;
|
|
739
|
+
', input_retention_days);
|
|
740
|
+
|
|
741
|
+
-- Create trigger on job_archive
|
|
742
|
+
DROP TRIGGER IF EXISTS job_archive_cleanup_trigger ON job_archive;
|
|
743
|
+
CREATE TRIGGER job_archive_cleanup_trigger
|
|
744
|
+
AFTER INSERT ON job_archive
|
|
745
|
+
FOR EACH ROW
|
|
746
|
+
EXECUTE FUNCTION cleanup_job_archive_trigger();
|
|
747
|
+
END IF;
|
|
705
748
|
END;
|
|
706
749
|
$$ LANGUAGE plpgsql;
|
|
707
750
|
|
|
708
|
-
CREATE OR REPLACE FUNCTION remove_retention_archive()
|
|
751
|
+
CREATE OR REPLACE FUNCTION remove_retention_archive(enable_timescaledb BOOLEAN DEFAULT TRUE)
|
|
709
752
|
RETURNS VOID AS $$
|
|
710
753
|
BEGIN
|
|
711
|
-
|
|
754
|
+
IF enable_timescaledb THEN
|
|
755
|
+
-- Remove TimescaleDB retention policy
|
|
756
|
+
PERFORM remove_retention_policy('job_archive');
|
|
757
|
+
ELSE
|
|
758
|
+
-- Drop trigger and related objects
|
|
759
|
+
DROP TRIGGER IF EXISTS job_archive_cleanup_trigger ON job_archive;
|
|
760
|
+
DROP FUNCTION IF EXISTS cleanup_job_archive_trigger();
|
|
761
|
+
DROP SEQUENCE IF EXISTS job_archive_insert_counter;
|
|
762
|
+
END IF;
|
|
712
763
|
END;
|
|
713
764
|
$$ LANGUAGE plpgsql;
|
|
714
765
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|