queuerPy 0.21.0__tar.gz → 0.22.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.
Files changed (54) hide show
  1. {queuerpy-0.21.0/queuerPy.egg-info → queuerpy-0.22.0}/PKG-INFO +2 -2
  2. {queuerpy-0.21.0 → queuerpy-0.22.0}/README.md +1 -1
  3. {queuerpy-0.21.0 → queuerpy-0.22.0}/_version.py +1 -1
  4. queuerpy-0.22.0/core/context.py +11 -0
  5. {queuerpy-0.21.0 → queuerpy-0.22.0}/core/runner.py +8 -1
  6. {queuerpy-0.21.0 → queuerpy-0.22.0}/database/db_job.py +112 -66
  7. {queuerpy-0.21.0 → queuerpy-0.22.0}/database/db_worker.py +44 -35
  8. {queuerpy-0.21.0 → queuerpy-0.22.0}/helper/database.py +2 -1
  9. {queuerpy-0.21.0 → queuerpy-0.22.0}/model/options.py +8 -2
  10. {queuerpy-0.21.0 → queuerpy-0.22.0}/pyproject.toml +2 -2
  11. {queuerpy-0.21.0 → queuerpy-0.22.0}/queuer.py +5 -7
  12. {queuerpy-0.21.0 → queuerpy-0.22.0/queuerPy.egg-info}/PKG-INFO +2 -2
  13. {queuerpy-0.21.0 → queuerpy-0.22.0}/queuerPy.egg-info/SOURCES.txt +2 -0
  14. {queuerpy-0.21.0 → queuerpy-0.22.0}/queuer_job.py +23 -3
  15. {queuerpy-0.21.0 → queuerpy-0.22.0}/sql/job.sql +4 -0
  16. {queuerpy-0.21.0 → queuerpy-0.22.0}/LICENSE +0 -0
  17. {queuerpy-0.21.0 → queuerpy-0.22.0}/MANIFEST.in +0 -0
  18. {queuerpy-0.21.0 → queuerpy-0.22.0}/__init__.py +0 -0
  19. {queuerpy-0.21.0 → queuerpy-0.22.0}/core/__init__.py +0 -0
  20. {queuerpy-0.21.0 → queuerpy-0.22.0}/core/broadcaster.py +0 -0
  21. {queuerpy-0.21.0 → queuerpy-0.22.0}/core/listener.py +0 -0
  22. {queuerpy-0.21.0 → queuerpy-0.22.0}/core/retryer.py +0 -0
  23. {queuerpy-0.21.0 → queuerpy-0.22.0}/core/scheduler.py +0 -0
  24. {queuerpy-0.21.0 → queuerpy-0.22.0}/core/ticker.py +0 -0
  25. {queuerpy-0.21.0 → queuerpy-0.22.0}/database/__init__.py +0 -0
  26. {queuerpy-0.21.0 → queuerpy-0.22.0}/database/db_listener.py +0 -0
  27. {queuerpy-0.21.0 → queuerpy-0.22.0}/database/db_master.py +0 -0
  28. {queuerpy-0.21.0 → queuerpy-0.22.0}/helper/__init__.py +0 -0
  29. {queuerpy-0.21.0 → queuerpy-0.22.0}/helper/error.py +0 -0
  30. {queuerpy-0.21.0 → queuerpy-0.22.0}/helper/logging.py +0 -0
  31. {queuerpy-0.21.0 → queuerpy-0.22.0}/helper/sql.py +0 -0
  32. {queuerpy-0.21.0 → queuerpy-0.22.0}/helper/task.py +0 -0
  33. {queuerpy-0.21.0 → queuerpy-0.22.0}/model/__init__.py +0 -0
  34. {queuerpy-0.21.0 → queuerpy-0.22.0}/model/batch_job.py +0 -0
  35. {queuerpy-0.21.0 → queuerpy-0.22.0}/model/connection.py +0 -0
  36. {queuerpy-0.21.0 → queuerpy-0.22.0}/model/job.py +0 -0
  37. {queuerpy-0.21.0 → queuerpy-0.22.0}/model/master.py +0 -0
  38. {queuerpy-0.21.0 → queuerpy-0.22.0}/model/options_on_error.py +0 -0
  39. {queuerpy-0.21.0 → queuerpy-0.22.0}/model/task.py +0 -0
  40. {queuerpy-0.21.0 → queuerpy-0.22.0}/model/worker.py +0 -0
  41. {queuerpy-0.21.0 → queuerpy-0.22.0}/py.typed +0 -0
  42. {queuerpy-0.21.0 → queuerpy-0.22.0}/queuerPy.egg-info/dependency_links.txt +0 -0
  43. {queuerpy-0.21.0 → queuerpy-0.22.0}/queuerPy.egg-info/requires.txt +0 -0
  44. {queuerpy-0.21.0 → queuerpy-0.22.0}/queuerPy.egg-info/top_level.txt +0 -0
  45. {queuerpy-0.21.0 → queuerpy-0.22.0}/queuer_global.py +0 -0
  46. {queuerpy-0.21.0 → queuerpy-0.22.0}/queuer_listener.py +0 -0
  47. {queuerpy-0.21.0 → queuerpy-0.22.0}/queuer_master.py +0 -0
  48. {queuerpy-0.21.0 → queuerpy-0.22.0}/queuer_next_interval.py +0 -0
  49. {queuerpy-0.21.0 → queuerpy-0.22.0}/queuer_task.py +0 -0
  50. {queuerpy-0.21.0 → queuerpy-0.22.0}/queuer_worker.py +0 -0
  51. {queuerpy-0.21.0 → queuerpy-0.22.0}/setup.cfg +0 -0
  52. {queuerpy-0.21.0 → queuerpy-0.22.0}/sql/master.sql +0 -0
  53. {queuerpy-0.21.0 → queuerpy-0.22.0}/sql/notify.sql +0 -0
  54. {queuerpy-0.21.0 → queuerpy-0.22.0}/sql/worker.sql +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: queuerPy
3
- Version: 0.21.0
3
+ Version: 0.22.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
- [![PyPi](https://pythonico.leapcell.app/pypi/queuerPy.svg?style=shields&data=n,v,d)](https://pypi.org/project/queuerPy/)
46
+ [![PyPi](https://img.shields.io/pypi/v/queuerPy.svg)](https://pypi.org/project/queuerPy)
47
47
  [![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)](https://www.python.org/)
48
48
  ![Coverage](https://raw.githubusercontent.com/siherrmann/queuerPy/refs/heads/main/coverage-badge.svg)
49
49
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/siherrmann/queuer/blob/master/LICENSE)
@@ -1,6 +1,6 @@
1
1
  # queuerPy
2
2
 
3
- [![PyPi](https://pythonico.leapcell.app/pypi/queuerPy.svg?style=shields&data=n,v,d)](https://pypi.org/project/queuerPy/)
3
+ [![PyPi](https://img.shields.io/pypi/v/queuerPy.svg)](https://pypi.org/project/queuerPy)
4
4
  [![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)](https://www.python.org/)
5
5
  ![Coverage](https://raw.githubusercontent.com/siherrmann/queuerPy/refs/heads/main/coverage-badge.svg)
6
6
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/siherrmann/queuer/blob/master/LICENSE)
@@ -1,6 +1,6 @@
1
1
  """Version information for queuerPy package."""
2
2
 
3
- __version__ = "0.21.0"
3
+ __version__ = "0.22.0"
4
4
  __version_info__ = tuple(int(part) for part in __version__.split("."))
5
5
 
6
6
  # For compatibility
@@ -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
- with self.db.instance.cursor(row_factory=dict_row) as cur:
190
- cur.execute(
191
- """
192
- SELECT * FROM update_job_initial(%s);
193
- """,
194
- (worker.id,),
195
- )
196
-
197
- jobs: List[Job] = []
198
- for row in cur.fetchall():
199
- jobs.append(Job.from_row(row))
200
-
201
- return jobs
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
- with self.db.instance.cursor(row_factory=dict_row) as cur:
307
- cur.execute(
308
- "SELECT * FROM select_all_jobs(%s, %s, %s);",
309
- (self.encryption_key or "", last_id, entries),
310
- )
311
-
312
- jobs: List[Job] = []
313
- for row in cur.fetchall():
314
- jobs.append(Job.from_row(row))
315
-
316
- return jobs
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
- with self.db.instance.cursor(row_factory=dict_row) as cur:
334
- cur.execute(
335
- "SELECT * FROM select_all_jobs_by_worker_rid(%s, %s, %s, %s);",
336
- (self.encryption_key or "", worker_rid, last_id, entries),
337
- )
338
-
339
- jobs: List[Job] = []
340
- for row in cur.fetchall():
341
- jobs.append(Job.from_row(row))
342
-
343
- return jobs
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
- with self.db.instance.cursor(row_factory=dict_row) as cur:
362
- cur.execute(
363
- "SELECT * FROM select_all_jobs_by_search(%s, %s, %s, %s);",
364
- (self.encryption_key or "", search, last_id, entries),
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
- jobs: List[Job] = []
368
- for row in cur.fetchall():
369
- jobs.append(Job.from_row(row))
377
+ jobs: List[Job] = []
378
+ for row in cur.fetchall():
379
+ jobs.append(Job.from_row(row))
370
380
 
371
- return jobs
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
- with self.db.instance.cursor(row_factory=dict_row) as cur:
445
- cur.execute(
446
- "SELECT * FROM select_all_jobs_from_archive(%s, %s, %s);",
447
- (self.encryption_key or "", last_id, entries),
448
- )
449
-
450
- jobs: List[Job] = []
451
- for row in cur.fetchall():
452
- jobs.append(Job.from_row(row))
453
-
454
- return jobs
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
- with self.db.instance.cursor(row_factory=dict_row) as cur:
472
- cur.execute(
473
- "SELECT * FROM select_all_jobs_from_archive_by_search(%s, %s, %s, %s);",
474
- (self.encryption_key or "", search, last_id, entries),
475
- )
476
-
477
- jobs: List[Job] = []
478
- for row in cur.fetchall():
479
- jobs.append(Job.from_row(row))
480
-
481
- return jobs
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
- with self.db.instance.cursor(row_factory=dict_row) as cur:
199
- cur.execute("SELECT * FROM select_all_workers(%s, %s);", (last_id, entries))
200
-
201
- workers: List[Worker] = []
202
- for row in cur.fetchall():
203
- workers.append(Worker.from_row(row))
204
-
205
- return workers
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
- with self.db.instance.cursor(row_factory=dict_row) as cur:
219
- cur.execute(
220
- "SELECT * FROM select_all_workers_by_search(%s, %s, %s);",
221
- (search, last_id, entries),
222
- )
223
-
224
- workers: List[Worker] = []
225
- for row in cur.fetchall():
226
- workers.append(Worker.from_row(row))
227
-
228
- return workers
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
- with self.db.instance.cursor(row_factory=dict_row) as cur:
239
- cur.execute("SELECT * FROM select_all_connections();")
240
-
241
- connections: List[ConnectionModel] = []
242
- for row in cur.fetchall():
243
- connection = ConnectionModel(
244
- pid=row["output_pid"],
245
- database=row["output_datname"],
246
- username=row["output_usename"],
247
- application_name=row["output_application_name"],
248
- query=row["output_query"],
249
- state=row["output_state"],
250
- )
251
- connections.append(connection)
252
-
253
- return connections
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, schedule: Optional[Schedule] = 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.21.0"
7
+ version = "0.22.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.21.0"
95
+ python_version = "0.22.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
- if self.database:
339
- logger.info("Closing database connection")
340
- try:
341
- self.database.close()
342
- except Exception as e:
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.21.0
3
+ Version: 0.22.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
- [![PyPi](https://pythonico.leapcell.app/pypi/queuerPy.svg?style=shields&data=n,v,d)](https://pypi.org/project/queuerPy/)
46
+ [![PyPi](https://img.shields.io/pypi/v/queuerPy.svg)](https://pypi.org/project/queuerPy)
47
47
  [![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)](https://www.python.org/)
48
48
  ![Coverage](https://raw.githubusercontent.com/siherrmann/queuerPy/refs/heads/main/coverage-badge.svg)
49
49
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/siherrmann/queuer/blob/master/LICENSE)
@@ -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
- if runner:
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