queuerPy 0.16.0__tar.gz → 0.18.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.16.0/queuerPy.egg-info → queuerpy-0.18.0}/PKG-INFO +82 -7
- {queuerpy-0.16.0 → queuerpy-0.18.0}/README.md +81 -6
- {queuerpy-0.16.0 → queuerpy-0.18.0}/_version.py +1 -1
- {queuerpy-0.16.0 → queuerpy-0.18.0}/model/worker.py +1 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/pyproject.toml +2 -2
- {queuerpy-0.16.0 → queuerpy-0.18.0}/queuer.py +111 -143
- {queuerpy-0.16.0 → queuerpy-0.18.0/queuerPy.egg-info}/PKG-INFO +82 -7
- {queuerpy-0.16.0 → queuerpy-0.18.0}/queuerPy.egg-info/SOURCES.txt +2 -0
- queuerpy-0.18.0/queuer_worker.py +96 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/LICENSE +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/MANIFEST.in +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/__init__.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/core/__init__.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/core/broadcaster.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/core/listener.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/core/retryer.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/core/runner.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/core/scheduler.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/core/ticker.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/database/__init__.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/database/db_job.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/database/db_listener.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/database/db_master.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/database/db_worker.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/helper/__init__.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/helper/database.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/helper/error.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/helper/logging.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/helper/sql.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/helper/task.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/model/__init__.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/model/batch_job.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/model/connection.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/model/job.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/model/master.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/model/options.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/model/options_on_error.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/model/task.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/py.typed +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/queuerPy.egg-info/dependency_links.txt +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/queuerPy.egg-info/requires.txt +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/queuerPy.egg-info/top_level.txt +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/queuer_global.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/queuer_job.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/queuer_listener.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/queuer_master.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/queuer_next_interval.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/queuer_task.py +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/setup.cfg +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/sql/job.sql +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/sql/master.sql +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.0}/sql/notify.sql +0 -0
- {queuerpy-0.16.0 → queuerpy-0.18.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.18.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,9 +43,10 @@ Dynamic: license-file
|
|
|
43
43
|
|
|
44
44
|
# queuerPy
|
|
45
45
|
|
|
46
|
+
[](https://pypi.org/project/queuerPy/)
|
|
46
47
|
[](https://www.python.org/)
|
|
47
|
-
[](https://github.com/siherrmann/queuer/blob/master/LICENSE)
|
|
48
48
|

|
|
49
|
+
[](https://github.com/siherrmann/queuer/blob/master/LICENSE)
|
|
49
50
|
|
|
50
51
|
Python port of the queuer package - a queueing system based on PostgreSQL.
|
|
51
52
|
|
|
@@ -97,9 +98,15 @@ def example_task():
|
|
|
97
98
|
That's easy, right? Adding a job is just as easy:
|
|
98
99
|
|
|
99
100
|
```python
|
|
100
|
-
# Add a job to the queue
|
|
101
|
+
# Add a job to the queue with positional arguments
|
|
101
102
|
job = q.add_job(example_task, 5, "12")
|
|
102
103
|
print(f"Job added: {job.rid}")
|
|
104
|
+
|
|
105
|
+
# Add a job with keyword arguments (kwargs)
|
|
106
|
+
job = q.add_job(example_task, paramKeyed1="test", debug=True)
|
|
107
|
+
|
|
108
|
+
# Add a job with both positional and keyword arguments
|
|
109
|
+
job = q.add_job(example_task, 5, "12", paramKeyed1="test", debug=True)
|
|
103
110
|
```
|
|
104
111
|
|
|
105
112
|
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:
|
|
@@ -199,6 +206,72 @@ This method handles the registration of a task, making the worker able to pick u
|
|
|
199
206
|
|
|
200
207
|
---
|
|
201
208
|
|
|
209
|
+
## add_job
|
|
210
|
+
|
|
211
|
+
The `add_job` method adds a new job to the queue for execution. Jobs are units of work that will be processed by the queuer.
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
def add_job(
|
|
215
|
+
self,
|
|
216
|
+
task: Union[Callable, str],
|
|
217
|
+
*parameters: Any,
|
|
218
|
+
**parameters_keyed: Any
|
|
219
|
+
) -> Job
|
|
220
|
+
|
|
221
|
+
def add_job_with_options(
|
|
222
|
+
self,
|
|
223
|
+
options: Optional[Options],
|
|
224
|
+
task: Union[Callable, str],
|
|
225
|
+
*parameters: Any,
|
|
226
|
+
**parameters_keyed: Any
|
|
227
|
+
) -> Job
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- `task`: A `Callable` or `str` representing the task to execute. If a callable, it must be registered with `add_task` first.
|
|
231
|
+
- `options`: Optional `Options` for custom error handling or scheduling behavior. Only available in `add_job_with_options()`.
|
|
232
|
+
- `*parameters`: Positional arguments to pass to the task function.
|
|
233
|
+
- `**parameters_keyed`: Keyword arguments to pass to the task function (Python-specific feature).
|
|
234
|
+
|
|
235
|
+
**Examples:**
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
# Job with both positional and keyword arguments
|
|
239
|
+
job = queuer.add_job(my_task, "arg1", paramKeyed1="test", paramKeyed2=1)
|
|
240
|
+
|
|
241
|
+
# Job with custom options (requires add_job_with_options)
|
|
242
|
+
options = Options(on_error=OnError(max_retries=5))
|
|
243
|
+
job = queuer.add_job_with_options(options, my_task, "arg1", debug=True)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Note:** Keyword arguments (`**parameters_keyed`) are stored separately in the database and enable Python functions to be called with named parameters. This is useful for optional parameters, default values, and improved code clarity. Go jobs continue to use only positional parameters.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## add_jobs
|
|
251
|
+
|
|
252
|
+
The `add_jobs` method allows efficient batch insertion of multiple jobs at once.
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
def add_jobs(self, batch_jobs: List[BatchJob]) -> List[UUID]
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
- `batch_jobs`: A list of `BatchJob` instances to insert.
|
|
259
|
+
|
|
260
|
+
**Example:**
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from model.batch_job import BatchJob
|
|
264
|
+
|
|
265
|
+
batch = [
|
|
266
|
+
BatchJob(task=my_task, parameters=[1, "a"]),
|
|
267
|
+
BatchJob(task=my_task, parameters=[2], parameters_keyed={"name": "b"}),
|
|
268
|
+
BatchJob(task=my_task, parameters_keyed={"id": 3, "name": "c"})
|
|
269
|
+
]
|
|
270
|
+
job_rids = queuer.add_jobs(batch)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
202
275
|
## add_next_interval_func
|
|
203
276
|
|
|
204
277
|
The `add_next_interval_func` method registers a custom function that determines the next execution time for scheduled jobs. This is useful for implementing complex scheduling logic beyond simple fixed intervals.
|
|
@@ -296,19 +369,20 @@ class Schedule:
|
|
|
296
369
|
### Job Management
|
|
297
370
|
|
|
298
371
|
```python
|
|
299
|
-
# Add a
|
|
300
|
-
job = queuer.add_job(my_task, param1, param2)
|
|
372
|
+
# Add a job with both positional and keyword arguments
|
|
373
|
+
job = queuer.add_job(my_task, param1, param2, paramKeyed1="test", paramKeyed2=1)
|
|
301
374
|
|
|
302
375
|
# Add a job with custom options
|
|
303
376
|
from model.options import Options, OnError
|
|
304
377
|
options = Options(on_error=OnError(max_retries=5, timeout=60.0))
|
|
305
|
-
job = queuer.add_job_with_options(options, my_task, param1)
|
|
378
|
+
job = queuer.add_job_with_options(options, my_task, param1, paramKeyed1="test")
|
|
306
379
|
|
|
307
380
|
# Add multiple jobs as a batch
|
|
308
381
|
from model.batch_job import BatchJob
|
|
309
382
|
batch = [
|
|
310
383
|
BatchJob(task=my_task, parameters=[1, "a"]),
|
|
311
|
-
BatchJob(task=my_task, parameters=[2, "b"
|
|
384
|
+
BatchJob(task=my_task, parameters=[2], parameters_keyed={"name": "b"}),
|
|
385
|
+
BatchJob(task=my_task, parameters_keyed={"id": 3, "name": "c"})
|
|
312
386
|
]
|
|
313
387
|
queuer.add_jobs(batch)
|
|
314
388
|
|
|
@@ -335,6 +409,7 @@ worker_jobs = queuer.get_jobs_by_worker_rid(worker.rid)
|
|
|
335
409
|
|
|
336
410
|
# ⭐ Features
|
|
337
411
|
|
|
412
|
+
- **Keyword Arguments Support**: Python functions can use both positional arguments (`*args`) and keyword arguments (`**kwargs`), while maintaining compatibility with Go jobs that use only positional parameters.
|
|
338
413
|
- **Async/Await Support**: Full asyncio integration with threading fallbacks.
|
|
339
414
|
- **PostgreSQL NOTIFY/LISTEN**: Real-time job notifications without polling overhead.
|
|
340
415
|
- **Batch Job Processing**: Insert job batches efficiently using PostgreSQL's `COPY FROM` feature.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# queuerPy
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/queuerPy/)
|
|
3
4
|
[](https://www.python.org/)
|
|
4
|
-
[](https://github.com/siherrmann/queuer/blob/master/LICENSE)
|
|
5
5
|

|
|
6
|
+
[](https://github.com/siherrmann/queuer/blob/master/LICENSE)
|
|
6
7
|
|
|
7
8
|
Python port of the queuer package - a queueing system based on PostgreSQL.
|
|
8
9
|
|
|
@@ -54,9 +55,15 @@ def example_task():
|
|
|
54
55
|
That's easy, right? Adding a job is just as easy:
|
|
55
56
|
|
|
56
57
|
```python
|
|
57
|
-
# Add a job to the queue
|
|
58
|
+
# Add a job to the queue with positional arguments
|
|
58
59
|
job = q.add_job(example_task, 5, "12")
|
|
59
60
|
print(f"Job added: {job.rid}")
|
|
61
|
+
|
|
62
|
+
# Add a job with keyword arguments (kwargs)
|
|
63
|
+
job = q.add_job(example_task, paramKeyed1="test", debug=True)
|
|
64
|
+
|
|
65
|
+
# Add a job with both positional and keyword arguments
|
|
66
|
+
job = q.add_job(example_task, 5, "12", paramKeyed1="test", debug=True)
|
|
60
67
|
```
|
|
61
68
|
|
|
62
69
|
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:
|
|
@@ -156,6 +163,72 @@ This method handles the registration of a task, making the worker able to pick u
|
|
|
156
163
|
|
|
157
164
|
---
|
|
158
165
|
|
|
166
|
+
## add_job
|
|
167
|
+
|
|
168
|
+
The `add_job` method adds a new job to the queue for execution. Jobs are units of work that will be processed by the queuer.
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
def add_job(
|
|
172
|
+
self,
|
|
173
|
+
task: Union[Callable, str],
|
|
174
|
+
*parameters: Any,
|
|
175
|
+
**parameters_keyed: Any
|
|
176
|
+
) -> Job
|
|
177
|
+
|
|
178
|
+
def add_job_with_options(
|
|
179
|
+
self,
|
|
180
|
+
options: Optional[Options],
|
|
181
|
+
task: Union[Callable, str],
|
|
182
|
+
*parameters: Any,
|
|
183
|
+
**parameters_keyed: Any
|
|
184
|
+
) -> Job
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
- `task`: A `Callable` or `str` representing the task to execute. If a callable, it must be registered with `add_task` first.
|
|
188
|
+
- `options`: Optional `Options` for custom error handling or scheduling behavior. Only available in `add_job_with_options()`.
|
|
189
|
+
- `*parameters`: Positional arguments to pass to the task function.
|
|
190
|
+
- `**parameters_keyed`: Keyword arguments to pass to the task function (Python-specific feature).
|
|
191
|
+
|
|
192
|
+
**Examples:**
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
# Job with both positional and keyword arguments
|
|
196
|
+
job = queuer.add_job(my_task, "arg1", paramKeyed1="test", paramKeyed2=1)
|
|
197
|
+
|
|
198
|
+
# Job with custom options (requires add_job_with_options)
|
|
199
|
+
options = Options(on_error=OnError(max_retries=5))
|
|
200
|
+
job = queuer.add_job_with_options(options, my_task, "arg1", debug=True)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Note:** Keyword arguments (`**parameters_keyed`) are stored separately in the database and enable Python functions to be called with named parameters. This is useful for optional parameters, default values, and improved code clarity. Go jobs continue to use only positional parameters.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## add_jobs
|
|
208
|
+
|
|
209
|
+
The `add_jobs` method allows efficient batch insertion of multiple jobs at once.
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
def add_jobs(self, batch_jobs: List[BatchJob]) -> List[UUID]
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
- `batch_jobs`: A list of `BatchJob` instances to insert.
|
|
216
|
+
|
|
217
|
+
**Example:**
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from model.batch_job import BatchJob
|
|
221
|
+
|
|
222
|
+
batch = [
|
|
223
|
+
BatchJob(task=my_task, parameters=[1, "a"]),
|
|
224
|
+
BatchJob(task=my_task, parameters=[2], parameters_keyed={"name": "b"}),
|
|
225
|
+
BatchJob(task=my_task, parameters_keyed={"id": 3, "name": "c"})
|
|
226
|
+
]
|
|
227
|
+
job_rids = queuer.add_jobs(batch)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
159
232
|
## add_next_interval_func
|
|
160
233
|
|
|
161
234
|
The `add_next_interval_func` method registers a custom function that determines the next execution time for scheduled jobs. This is useful for implementing complex scheduling logic beyond simple fixed intervals.
|
|
@@ -253,19 +326,20 @@ class Schedule:
|
|
|
253
326
|
### Job Management
|
|
254
327
|
|
|
255
328
|
```python
|
|
256
|
-
# Add a
|
|
257
|
-
job = queuer.add_job(my_task, param1, param2)
|
|
329
|
+
# Add a job with both positional and keyword arguments
|
|
330
|
+
job = queuer.add_job(my_task, param1, param2, paramKeyed1="test", paramKeyed2=1)
|
|
258
331
|
|
|
259
332
|
# Add a job with custom options
|
|
260
333
|
from model.options import Options, OnError
|
|
261
334
|
options = Options(on_error=OnError(max_retries=5, timeout=60.0))
|
|
262
|
-
job = queuer.add_job_with_options(options, my_task, param1)
|
|
335
|
+
job = queuer.add_job_with_options(options, my_task, param1, paramKeyed1="test")
|
|
263
336
|
|
|
264
337
|
# Add multiple jobs as a batch
|
|
265
338
|
from model.batch_job import BatchJob
|
|
266
339
|
batch = [
|
|
267
340
|
BatchJob(task=my_task, parameters=[1, "a"]),
|
|
268
|
-
BatchJob(task=my_task, parameters=[2, "b"
|
|
341
|
+
BatchJob(task=my_task, parameters=[2], parameters_keyed={"name": "b"}),
|
|
342
|
+
BatchJob(task=my_task, parameters_keyed={"id": 3, "name": "c"})
|
|
269
343
|
]
|
|
270
344
|
queuer.add_jobs(batch)
|
|
271
345
|
|
|
@@ -292,6 +366,7 @@ worker_jobs = queuer.get_jobs_by_worker_rid(worker.rid)
|
|
|
292
366
|
|
|
293
367
|
# ⭐ Features
|
|
294
368
|
|
|
369
|
+
- **Keyword Arguments Support**: Python functions can use both positional arguments (`*args`) and keyword arguments (`**kwargs`), while maintaining compatibility with Go jobs that use only positional parameters.
|
|
295
370
|
- **Async/Await Support**: Full asyncio integration with threading fallbacks.
|
|
296
371
|
- **PostgreSQL NOTIFY/LISTEN**: Real-time job notifications without polling overhead.
|
|
297
372
|
- **Batch Job Processing**: Insert job batches efficiently using PostgreSQL's `COPY FROM` feature.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "queuerPy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.18.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.18.0"
|
|
96
96
|
warn_return_any = true
|
|
97
97
|
warn_unused_configs = true
|
|
98
98
|
disallow_untyped_defs = true
|
|
@@ -28,6 +28,7 @@ from .queuer_task import QueuerTaskMixin
|
|
|
28
28
|
from .queuer_next_interval import QueuerNextIntervalMixin
|
|
29
29
|
from .queuer_listener import QueuerListenerMixin
|
|
30
30
|
from .queuer_master import QueuerMasterMixin
|
|
31
|
+
from .queuer_worker import QueuerWorkerMixin
|
|
31
32
|
|
|
32
33
|
logger = get_logger(__name__)
|
|
33
34
|
|
|
@@ -68,6 +69,7 @@ class Queuer(
|
|
|
68
69
|
QueuerNextIntervalMixin,
|
|
69
70
|
QueuerListenerMixin,
|
|
70
71
|
QueuerMasterMixin,
|
|
72
|
+
QueuerWorkerMixin,
|
|
71
73
|
):
|
|
72
74
|
"""
|
|
73
75
|
Main queuing system class.
|
|
@@ -113,6 +115,7 @@ class Queuer(
|
|
|
113
115
|
|
|
114
116
|
# Configuration
|
|
115
117
|
self.job_poll_interval: timedelta = timedelta(minutes=5)
|
|
118
|
+
self.worker_poll_interval: timedelta = timedelta(seconds=30)
|
|
116
119
|
self.retention_archive: timedelta = timedelta(days=30)
|
|
117
120
|
|
|
118
121
|
# Active runners (jobs currently executing)
|
|
@@ -219,177 +222,96 @@ class Queuer(
|
|
|
219
222
|
logger.info("Queuer started")
|
|
220
223
|
|
|
221
224
|
def stop(self) -> None:
|
|
222
|
-
"""
|
|
223
|
-
|
|
224
|
-
|
|
225
|
+
"""
|
|
226
|
+
Stop stops the queuer by closing the job listeners, cancelling all queued and running jobs,
|
|
227
|
+
and cancelling the context to stop the queuer.
|
|
228
|
+
"""
|
|
229
|
+
# Check if already stopped (database is None means we've already cleaned up)
|
|
230
|
+
if self.database is None:
|
|
231
|
+
return # Already stopped
|
|
232
|
+
|
|
233
|
+
# Mark as not running immediately to prevent re-entrant calls
|
|
234
|
+
was_running = self.running
|
|
235
|
+
self.running = False
|
|
225
236
|
|
|
226
|
-
# Stop tickers first to prevent
|
|
227
|
-
if self.heartbeat_ticker:
|
|
237
|
+
# Stop tickers first to prevent them from calling stop() recursively
|
|
238
|
+
if hasattr(self, "heartbeat_ticker") and self.heartbeat_ticker:
|
|
228
239
|
try:
|
|
229
240
|
self.heartbeat_ticker.stop()
|
|
230
241
|
except Exception as e:
|
|
231
242
|
logger.warning(f"Error stopping heartbeat ticker: {e}")
|
|
232
243
|
|
|
233
|
-
if self.poll_job_ticker:
|
|
244
|
+
if hasattr(self, "poll_job_ticker") and self.poll_job_ticker:
|
|
234
245
|
try:
|
|
235
246
|
self.poll_job_ticker.stop()
|
|
236
247
|
except Exception as e:
|
|
237
248
|
logger.warning(f"Error stopping poll job ticker: {e}")
|
|
238
249
|
|
|
239
|
-
# Close
|
|
250
|
+
# Close db listeners
|
|
240
251
|
if self.job_db_listener:
|
|
241
252
|
try:
|
|
242
|
-
# Use go_func but wait for completion
|
|
243
253
|
task = go_func(self.job_db_listener.stop, use_mp=False)
|
|
244
|
-
task.get_results(timeout=3.0)
|
|
254
|
+
task.get_results(timeout=3.0)
|
|
245
255
|
except Exception as e:
|
|
246
256
|
# Only log if it's not already closed
|
|
247
|
-
if "closed" not in str(e)
|
|
248
|
-
logger.
|
|
249
|
-
|
|
250
|
-
# Wait for database listener runners to complete
|
|
251
|
-
if self.job_db_listener_runner:
|
|
252
|
-
try:
|
|
253
|
-
self.job_db_listener_runner.get_results(timeout=2.0)
|
|
254
|
-
self.job_db_listener_runner = None
|
|
255
|
-
except Exception as e:
|
|
256
|
-
logger.warning(f"Error waiting for job listener runner: {e}")
|
|
257
|
+
if "Listener has been closed" not in str(e):
|
|
258
|
+
logger.error(f"Error closing job insert listener: {e}")
|
|
257
259
|
|
|
258
260
|
if self.job_archive_db_listener:
|
|
259
261
|
try:
|
|
260
262
|
task = go_func(self.job_archive_db_listener.stop, use_mp=False)
|
|
261
|
-
task.get_results(timeout=3.0)
|
|
263
|
+
task.get_results(timeout=3.0)
|
|
262
264
|
except Exception as e:
|
|
263
|
-
if "closed" not in str(e)
|
|
264
|
-
logger.
|
|
265
|
+
if "Listener has been closed" not in str(e):
|
|
266
|
+
logger.error(f"Error closing job archive listener: {e}")
|
|
265
267
|
|
|
266
|
-
#
|
|
267
|
-
|
|
268
|
-
try:
|
|
269
|
-
self.job_archive_db_listener_runner.get_results(timeout=2.0)
|
|
270
|
-
self.job_archive_db_listener_runner = None
|
|
271
|
-
except Exception as e:
|
|
272
|
-
logger.warning(f"Error waiting for job archive listener runner: {e}")
|
|
273
|
-
|
|
274
|
-
# Update worker status to stopped (only if we were running)
|
|
268
|
+
# Update worker status to stopped
|
|
269
|
+
err = None
|
|
275
270
|
worker_rid = None
|
|
276
|
-
if
|
|
277
|
-
|
|
278
|
-
|
|
271
|
+
if self.database and self.database.instance:
|
|
272
|
+
with self.worker_mutex:
|
|
273
|
+
if self.worker:
|
|
279
274
|
self.worker.status = WorkerStatus.STOPPED
|
|
280
275
|
worker_updated = self.db_worker.update_worker(self.worker)
|
|
281
276
|
if worker_updated:
|
|
277
|
+
self.worker = worker_updated
|
|
282
278
|
worker_rid = self.worker.rid
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
else:
|
|
286
|
-
# If we were never running, just get the worker_rid for job cancellation
|
|
287
|
-
worker_rid = self.worker.rid
|
|
279
|
+
else:
|
|
280
|
+
err = Exception("Failed to update worker")
|
|
288
281
|
|
|
289
|
-
|
|
282
|
+
if err:
|
|
283
|
+
logger.error(f"Error updating worker status to stopped: {err}")
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
# Cancel all queued and running jobs (only if we have a valid worker RID)
|
|
290
287
|
if worker_rid:
|
|
291
288
|
try:
|
|
292
289
|
self.cancel_all_jobs_by_worker(worker_rid, 100)
|
|
293
290
|
except Exception as e:
|
|
294
291
|
logger.error(f"Error cancelling all jobs by worker: {e}")
|
|
292
|
+
return
|
|
295
293
|
|
|
296
|
-
# Cancel
|
|
297
|
-
if self._cancel_event:
|
|
298
|
-
try:
|
|
299
|
-
self._cancel_event.set()
|
|
300
|
-
except Exception as e:
|
|
301
|
-
logger.warning(f"Error setting cancel event: {e}")
|
|
302
|
-
|
|
303
|
-
# Signal that we're stopping
|
|
304
|
-
self.running = False
|
|
305
|
-
self._stopped.set()
|
|
306
|
-
|
|
307
|
-
# Cancel all active runners - similar to Go's job cancellation
|
|
308
|
-
active_runner_count = len(self.active_runners)
|
|
309
|
-
if active_runner_count > 0:
|
|
310
|
-
logger.info(f"Cancelling {active_runner_count} active runners")
|
|
311
|
-
|
|
294
|
+
# Cancel all active runners
|
|
312
295
|
for runner_id, runner in list(self.active_runners.items()):
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if runner_id in self.active_runners:
|
|
316
|
-
del self.active_runners[runner_id]
|
|
317
|
-
logger.debug(f"Removed runner {runner_id} from active_runners")
|
|
296
|
+
runner.cancel()
|
|
297
|
+
del self.active_runners[runner_id]
|
|
318
298
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
# Final check
|
|
323
|
-
remaining_runners = len(self.active_runners)
|
|
324
|
-
if remaining_runners > 0:
|
|
325
|
-
logger.warning(f"{remaining_runners} runners could not be cleaned up")
|
|
326
|
-
else:
|
|
327
|
-
logger.debug("All active runners cleaned up successfully")
|
|
328
|
-
|
|
329
|
-
# Cleanup broadcasters and listeners
|
|
330
|
-
try:
|
|
331
|
-
broadcaster_count = 0
|
|
332
|
-
if self.job_insert_broadcaster:
|
|
333
|
-
listener_count = len(self.job_insert_broadcaster.listeners)
|
|
334
|
-
if listener_count > 0:
|
|
335
|
-
logger.debug(
|
|
336
|
-
f"Clearing {listener_count} listeners from job_insert_broadcaster"
|
|
337
|
-
)
|
|
338
|
-
self.job_insert_broadcaster.listeners.clear()
|
|
339
|
-
broadcaster_count += 1
|
|
340
|
-
|
|
341
|
-
if self.job_update_broadcaster:
|
|
342
|
-
listener_count = len(self.job_update_broadcaster.listeners)
|
|
343
|
-
if listener_count > 0:
|
|
344
|
-
logger.debug(
|
|
345
|
-
f"Clearing {listener_count} listeners from job_update_broadcaster"
|
|
346
|
-
)
|
|
347
|
-
self.job_update_broadcaster.listeners.clear()
|
|
348
|
-
broadcaster_count += 1
|
|
349
|
-
|
|
350
|
-
if self.job_delete_broadcaster:
|
|
351
|
-
listener_count = len(self.job_delete_broadcaster.listeners)
|
|
352
|
-
if listener_count > 0:
|
|
353
|
-
logger.debug(
|
|
354
|
-
f"Clearing {listener_count} listeners from job_delete_broadcaster"
|
|
355
|
-
)
|
|
356
|
-
self.job_delete_broadcaster.listeners.clear()
|
|
357
|
-
broadcaster_count += 1
|
|
358
|
-
|
|
359
|
-
if broadcaster_count > 0:
|
|
360
|
-
logger.debug(f"Cleaned up {broadcaster_count} broadcasters")
|
|
361
|
-
|
|
362
|
-
except Exception as e:
|
|
363
|
-
logger.warning(f"Error during broadcaster cleanup: {e}")
|
|
364
|
-
|
|
365
|
-
# Cleanup AsyncIO resources
|
|
366
|
-
try:
|
|
367
|
-
try:
|
|
368
|
-
loop = asyncio.get_running_loop()
|
|
369
|
-
pending_tasks = [
|
|
370
|
-
task for task in asyncio.all_tasks(loop) if not task.done()
|
|
371
|
-
]
|
|
372
|
-
if pending_tasks:
|
|
373
|
-
logger.debug(f"Cancelling {len(pending_tasks)} pending async tasks")
|
|
374
|
-
for task in pending_tasks:
|
|
375
|
-
task.cancel()
|
|
376
|
-
except RuntimeError:
|
|
377
|
-
pass
|
|
378
|
-
|
|
379
|
-
# Clear the cancel event
|
|
380
|
-
self._cancel_event = None
|
|
299
|
+
# Cancel the context to stop the queuer
|
|
300
|
+
if self._cancel_event:
|
|
301
|
+
self._cancel_event.set()
|
|
381
302
|
|
|
382
|
-
|
|
383
|
-
|
|
303
|
+
# Wait a moment for background goroutines to finish gracefully
|
|
304
|
+
time.sleep(0.1)
|
|
384
305
|
|
|
306
|
+
# Close database connection
|
|
385
307
|
if self.database:
|
|
308
|
+
logger.info("Closing database connection")
|
|
386
309
|
try:
|
|
387
|
-
logger.info(f"Closing database '{self.name}'")
|
|
388
310
|
self.database.close()
|
|
389
311
|
except Exception as e:
|
|
390
|
-
logger.error(f"Error closing
|
|
312
|
+
logger.error(f"Error closing database connection: {e}")
|
|
391
313
|
|
|
392
|
-
logger.info(
|
|
314
|
+
logger.info("Queuer stopped")
|
|
393
315
|
|
|
394
316
|
# Job notification listeners
|
|
395
317
|
async def _handle_job_notification(self, notification: str) -> None:
|
|
@@ -524,31 +446,77 @@ class Queuer(
|
|
|
524
446
|
|
|
525
447
|
# Tickers
|
|
526
448
|
def _heartbeat_func(self) -> None:
|
|
527
|
-
"""Send periodic heartbeats
|
|
449
|
+
"""Send periodic heartbeats and handle worker status changes."""
|
|
528
450
|
try:
|
|
529
|
-
|
|
451
|
+
logger.debug("Sending worker heartbeat...")
|
|
452
|
+
|
|
453
|
+
# Get current worker with read lock
|
|
530
454
|
with self.worker_mutex:
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
455
|
+
worker = self.worker
|
|
456
|
+
|
|
457
|
+
if worker is None:
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
# Select worker from database for heartbeat
|
|
461
|
+
worker_from_db = self.db_worker.select_worker(worker.rid)
|
|
462
|
+
if not worker_from_db:
|
|
463
|
+
logger.error("Error selecting worker for heartbeat")
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
# Handle worker status
|
|
467
|
+
if worker_from_db.status == WorkerStatus.STOPPED:
|
|
468
|
+
logger.info(
|
|
469
|
+
f"Stopping worker... (worker_status: {worker_from_db.status})"
|
|
540
470
|
)
|
|
471
|
+
try:
|
|
472
|
+
self.stop()
|
|
473
|
+
except Exception as e:
|
|
474
|
+
logger.error(f"Error stopping queuer: {e}")
|
|
475
|
+
return
|
|
476
|
+
|
|
477
|
+
elif worker_from_db.status == WorkerStatus.STOPPING:
|
|
478
|
+
if worker_from_db.max_concurrency != 0:
|
|
479
|
+
logger.info(
|
|
480
|
+
f"Gracefully stopping worker... (worker_status: {worker_from_db.status})"
|
|
481
|
+
)
|
|
482
|
+
worker_from_db.max_concurrency = 0
|
|
483
|
+
worker_from_db = self.db_worker.update_worker(worker_from_db)
|
|
484
|
+
if not worker_from_db:
|
|
485
|
+
logger.error("Error updating worker concurrency")
|
|
486
|
+
return
|
|
487
|
+
elif len(self.active_runners) == 0:
|
|
488
|
+
logger.info(
|
|
489
|
+
f"All running jobs finished, stopping worker... (worker_status: {worker_from_db.status})"
|
|
490
|
+
)
|
|
491
|
+
worker_from_db.status = WorkerStatus.STOPPED
|
|
492
|
+
worker_from_db = self.db_worker.update_worker(worker_from_db)
|
|
493
|
+
if not worker_from_db:
|
|
494
|
+
logger.error("Error updating worker status to stopped")
|
|
495
|
+
return
|
|
496
|
+
try:
|
|
497
|
+
self.stop()
|
|
498
|
+
except Exception as e:
|
|
499
|
+
logger.error(f"Error stopping queuer: {e}")
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
else:
|
|
503
|
+
# Default case: update worker heartbeat
|
|
504
|
+
worker_from_db = self.db_worker.update_worker(worker)
|
|
505
|
+
if not worker_from_db:
|
|
506
|
+
logger.error("Error updating worker heartbeat")
|
|
507
|
+
return
|
|
508
|
+
|
|
509
|
+
# Update local worker with write lock
|
|
510
|
+
with self.worker_mutex:
|
|
511
|
+
self.worker = worker_from_db
|
|
541
512
|
|
|
542
|
-
if updated_worker:
|
|
543
|
-
with self.worker_mutex:
|
|
544
|
-
self.worker = updated_worker
|
|
545
513
|
except Exception as e:
|
|
546
514
|
logger.error(f"Heartbeat error: {e}")
|
|
547
515
|
|
|
548
516
|
def _start_heartbeat_ticker(self) -> None:
|
|
549
517
|
"""Start heartbeat ticker using threading."""
|
|
550
518
|
self.heartbeat_ticker = Ticker(
|
|
551
|
-
|
|
519
|
+
self.worker_poll_interval,
|
|
552
520
|
self._heartbeat_func,
|
|
553
521
|
use_mp=False,
|
|
554
522
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: queuerPy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.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,9 +43,10 @@ Dynamic: license-file
|
|
|
43
43
|
|
|
44
44
|
# queuerPy
|
|
45
45
|
|
|
46
|
+
[](https://pypi.org/project/queuerPy/)
|
|
46
47
|
[](https://www.python.org/)
|
|
47
|
-
[](https://github.com/siherrmann/queuer/blob/master/LICENSE)
|
|
48
48
|

|
|
49
|
+
[](https://github.com/siherrmann/queuer/blob/master/LICENSE)
|
|
49
50
|
|
|
50
51
|
Python port of the queuer package - a queueing system based on PostgreSQL.
|
|
51
52
|
|
|
@@ -97,9 +98,15 @@ def example_task():
|
|
|
97
98
|
That's easy, right? Adding a job is just as easy:
|
|
98
99
|
|
|
99
100
|
```python
|
|
100
|
-
# Add a job to the queue
|
|
101
|
+
# Add a job to the queue with positional arguments
|
|
101
102
|
job = q.add_job(example_task, 5, "12")
|
|
102
103
|
print(f"Job added: {job.rid}")
|
|
104
|
+
|
|
105
|
+
# Add a job with keyword arguments (kwargs)
|
|
106
|
+
job = q.add_job(example_task, paramKeyed1="test", debug=True)
|
|
107
|
+
|
|
108
|
+
# Add a job with both positional and keyword arguments
|
|
109
|
+
job = q.add_job(example_task, 5, "12", paramKeyed1="test", debug=True)
|
|
103
110
|
```
|
|
104
111
|
|
|
105
112
|
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:
|
|
@@ -199,6 +206,72 @@ This method handles the registration of a task, making the worker able to pick u
|
|
|
199
206
|
|
|
200
207
|
---
|
|
201
208
|
|
|
209
|
+
## add_job
|
|
210
|
+
|
|
211
|
+
The `add_job` method adds a new job to the queue for execution. Jobs are units of work that will be processed by the queuer.
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
def add_job(
|
|
215
|
+
self,
|
|
216
|
+
task: Union[Callable, str],
|
|
217
|
+
*parameters: Any,
|
|
218
|
+
**parameters_keyed: Any
|
|
219
|
+
) -> Job
|
|
220
|
+
|
|
221
|
+
def add_job_with_options(
|
|
222
|
+
self,
|
|
223
|
+
options: Optional[Options],
|
|
224
|
+
task: Union[Callable, str],
|
|
225
|
+
*parameters: Any,
|
|
226
|
+
**parameters_keyed: Any
|
|
227
|
+
) -> Job
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- `task`: A `Callable` or `str` representing the task to execute. If a callable, it must be registered with `add_task` first.
|
|
231
|
+
- `options`: Optional `Options` for custom error handling or scheduling behavior. Only available in `add_job_with_options()`.
|
|
232
|
+
- `*parameters`: Positional arguments to pass to the task function.
|
|
233
|
+
- `**parameters_keyed`: Keyword arguments to pass to the task function (Python-specific feature).
|
|
234
|
+
|
|
235
|
+
**Examples:**
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
# Job with both positional and keyword arguments
|
|
239
|
+
job = queuer.add_job(my_task, "arg1", paramKeyed1="test", paramKeyed2=1)
|
|
240
|
+
|
|
241
|
+
# Job with custom options (requires add_job_with_options)
|
|
242
|
+
options = Options(on_error=OnError(max_retries=5))
|
|
243
|
+
job = queuer.add_job_with_options(options, my_task, "arg1", debug=True)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Note:** Keyword arguments (`**parameters_keyed`) are stored separately in the database and enable Python functions to be called with named parameters. This is useful for optional parameters, default values, and improved code clarity. Go jobs continue to use only positional parameters.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## add_jobs
|
|
251
|
+
|
|
252
|
+
The `add_jobs` method allows efficient batch insertion of multiple jobs at once.
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
def add_jobs(self, batch_jobs: List[BatchJob]) -> List[UUID]
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
- `batch_jobs`: A list of `BatchJob` instances to insert.
|
|
259
|
+
|
|
260
|
+
**Example:**
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from model.batch_job import BatchJob
|
|
264
|
+
|
|
265
|
+
batch = [
|
|
266
|
+
BatchJob(task=my_task, parameters=[1, "a"]),
|
|
267
|
+
BatchJob(task=my_task, parameters=[2], parameters_keyed={"name": "b"}),
|
|
268
|
+
BatchJob(task=my_task, parameters_keyed={"id": 3, "name": "c"})
|
|
269
|
+
]
|
|
270
|
+
job_rids = queuer.add_jobs(batch)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
202
275
|
## add_next_interval_func
|
|
203
276
|
|
|
204
277
|
The `add_next_interval_func` method registers a custom function that determines the next execution time for scheduled jobs. This is useful for implementing complex scheduling logic beyond simple fixed intervals.
|
|
@@ -296,19 +369,20 @@ class Schedule:
|
|
|
296
369
|
### Job Management
|
|
297
370
|
|
|
298
371
|
```python
|
|
299
|
-
# Add a
|
|
300
|
-
job = queuer.add_job(my_task, param1, param2)
|
|
372
|
+
# Add a job with both positional and keyword arguments
|
|
373
|
+
job = queuer.add_job(my_task, param1, param2, paramKeyed1="test", paramKeyed2=1)
|
|
301
374
|
|
|
302
375
|
# Add a job with custom options
|
|
303
376
|
from model.options import Options, OnError
|
|
304
377
|
options = Options(on_error=OnError(max_retries=5, timeout=60.0))
|
|
305
|
-
job = queuer.add_job_with_options(options, my_task, param1)
|
|
378
|
+
job = queuer.add_job_with_options(options, my_task, param1, paramKeyed1="test")
|
|
306
379
|
|
|
307
380
|
# Add multiple jobs as a batch
|
|
308
381
|
from model.batch_job import BatchJob
|
|
309
382
|
batch = [
|
|
310
383
|
BatchJob(task=my_task, parameters=[1, "a"]),
|
|
311
|
-
BatchJob(task=my_task, parameters=[2, "b"
|
|
384
|
+
BatchJob(task=my_task, parameters=[2], parameters_keyed={"name": "b"}),
|
|
385
|
+
BatchJob(task=my_task, parameters_keyed={"id": 3, "name": "c"})
|
|
312
386
|
]
|
|
313
387
|
queuer.add_jobs(batch)
|
|
314
388
|
|
|
@@ -335,6 +409,7 @@ worker_jobs = queuer.get_jobs_by_worker_rid(worker.rid)
|
|
|
335
409
|
|
|
336
410
|
# ⭐ Features
|
|
337
411
|
|
|
412
|
+
- **Keyword Arguments Support**: Python functions can use both positional arguments (`*args`) and keyword arguments (`**kwargs`), while maintaining compatibility with Go jobs that use only positional parameters.
|
|
338
413
|
- **Async/Await Support**: Full asyncio integration with threading fallbacks.
|
|
339
414
|
- **PostgreSQL NOTIFY/LISTEN**: Real-time job notifications without polling overhead.
|
|
340
415
|
- **Batch Job Processing**: Insert job batches efficiently using PostgreSQL's `COPY FROM` feature.
|
|
@@ -12,6 +12,7 @@ queuer_listener.py
|
|
|
12
12
|
queuer_master.py
|
|
13
13
|
queuer_next_interval.py
|
|
14
14
|
queuer_task.py
|
|
15
|
+
queuer_worker.py
|
|
15
16
|
./__init__.py
|
|
16
17
|
./_version.py
|
|
17
18
|
./py.typed
|
|
@@ -22,6 +23,7 @@ queuer_task.py
|
|
|
22
23
|
./queuer_master.py
|
|
23
24
|
./queuer_next_interval.py
|
|
24
25
|
./queuer_task.py
|
|
26
|
+
./queuer_worker.py
|
|
25
27
|
./core/__init__.py
|
|
26
28
|
./core/broadcaster.py
|
|
27
29
|
./core/listener.py
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Worker-related methods for the Python queuer implementation.
|
|
3
|
+
Mirrors Go's queuerWorker.go functionality.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from .helper.error import QueuerError
|
|
11
|
+
from .model.worker import Worker, WorkerStatus
|
|
12
|
+
from .queuer_global import QueuerGlobalMixin
|
|
13
|
+
|
|
14
|
+
# Set up logger
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class QueuerWorkerMixin(QueuerGlobalMixin):
|
|
19
|
+
"""
|
|
20
|
+
Mixin class containing worker-related methods for the Queuer.
|
|
21
|
+
This mirrors the worker methods from Go's queuerWorker.go.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
super().__init__()
|
|
26
|
+
|
|
27
|
+
def stop_worker(self, worker_rid: UUID) -> None:
|
|
28
|
+
"""
|
|
29
|
+
StopWorkerGracefully sets the status of the specified worker to 'STOPPED'
|
|
30
|
+
to cancel running jobs when stopping.
|
|
31
|
+
|
|
32
|
+
:param worker_rid: The RID of the worker to stop
|
|
33
|
+
:raises QueuerError: If getting or updating the worker fails
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
worker = self.get_worker(worker_rid)
|
|
37
|
+
if worker is None:
|
|
38
|
+
raise ValueError(f"Worker {worker_rid} not found")
|
|
39
|
+
|
|
40
|
+
worker.status = WorkerStatus.STOPPED
|
|
41
|
+
|
|
42
|
+
worker_updated = self.db_worker.update_worker(worker)
|
|
43
|
+
if not worker_updated:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"Failed to update worker {worker_rid} status to stopped"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Update local worker object if this is the current queuer's worker
|
|
49
|
+
if self.worker is not None and self.worker.rid == worker_rid:
|
|
50
|
+
with self.worker_mutex:
|
|
51
|
+
self.worker = worker_updated
|
|
52
|
+
|
|
53
|
+
except Exception as e:
|
|
54
|
+
raise QueuerError("stopping worker", e)
|
|
55
|
+
|
|
56
|
+
def stop_worker_gracefully(self, worker_rid: UUID) -> None:
|
|
57
|
+
"""
|
|
58
|
+
StopWorkerGracefully sets the worker's status to STOPPING
|
|
59
|
+
to allow it to finish current tasks before stopping.
|
|
60
|
+
|
|
61
|
+
:param worker_rid: The RID of the worker to stop gracefully
|
|
62
|
+
:raises QueuerError: If getting or updating the worker fails
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
worker = self.get_worker(worker_rid)
|
|
66
|
+
if worker is None:
|
|
67
|
+
raise ValueError(f"Worker {worker_rid} not found")
|
|
68
|
+
|
|
69
|
+
worker.status = WorkerStatus.STOPPING
|
|
70
|
+
|
|
71
|
+
worker_updated = self.db_worker.update_worker(worker)
|
|
72
|
+
if not worker_updated:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"Failed to update worker {worker_rid} status to stopping"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Update local worker object if this is the current queuer's worker
|
|
78
|
+
if self.worker is not None and self.worker.rid == worker_rid:
|
|
79
|
+
with self.worker_mutex:
|
|
80
|
+
self.worker = worker_updated
|
|
81
|
+
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise QueuerError("stopping worker gracefully", e)
|
|
84
|
+
|
|
85
|
+
def get_worker(self, worker_rid: UUID) -> Optional[Worker]:
|
|
86
|
+
"""
|
|
87
|
+
Get a worker by its RID.
|
|
88
|
+
|
|
89
|
+
:param worker_rid: The RID of the worker to get
|
|
90
|
+
:returns: The worker if found, None otherwise
|
|
91
|
+
:raises QueuerError: If the database query fails
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
return self.db_worker.select_worker(worker_rid)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
raise QueuerError("getting worker", e)
|
|
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
|
|
File without changes
|
|
File without changes
|