queuerPy 0.20.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.
- {queuerpy-0.20.0/queuerPy.egg-info → queuerpy-0.22.0}/PKG-INFO +3 -2
- {queuerpy-0.20.0 → queuerpy-0.22.0}/README.md +2 -1
- {queuerpy-0.20.0 → queuerpy-0.22.0}/_version.py +1 -1
- queuerpy-0.22.0/core/context.py +11 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/core/runner.py +135 -66
- {queuerpy-0.20.0 → queuerpy-0.22.0}/database/db_job.py +112 -66
- {queuerpy-0.20.0 → queuerpy-0.22.0}/database/db_worker.py +44 -35
- {queuerpy-0.20.0 → queuerpy-0.22.0}/helper/database.py +2 -1
- {queuerpy-0.20.0 → queuerpy-0.22.0}/model/options.py +8 -2
- {queuerpy-0.20.0 → queuerpy-0.22.0}/pyproject.toml +2 -2
- {queuerpy-0.20.0 → queuerpy-0.22.0}/queuer.py +36 -7
- {queuerpy-0.20.0 → queuerpy-0.22.0/queuerPy.egg-info}/PKG-INFO +3 -2
- {queuerpy-0.20.0 → queuerpy-0.22.0}/queuerPy.egg-info/SOURCES.txt +2 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/queuer_global.py +5 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/queuer_job.py +28 -5
- {queuerpy-0.20.0 → queuerpy-0.22.0}/sql/job.sql +4 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/LICENSE +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/MANIFEST.in +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/__init__.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/core/__init__.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/core/broadcaster.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/core/listener.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/core/retryer.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/core/scheduler.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/core/ticker.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/database/__init__.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/database/db_listener.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/database/db_master.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/helper/__init__.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/helper/error.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/helper/logging.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/helper/sql.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/helper/task.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/model/__init__.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/model/batch_job.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/model/connection.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/model/job.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/model/master.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/model/options_on_error.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/model/task.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/model/worker.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/py.typed +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/queuerPy.egg-info/dependency_links.txt +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/queuerPy.egg-info/requires.txt +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/queuerPy.egg-info/top_level.txt +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/queuer_listener.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/queuer_master.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/queuer_next_interval.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/queuer_task.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/queuer_worker.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/setup.cfg +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/sql/master.sql +0 -0
- {queuerpy-0.20.0 → queuerpy-0.22.0}/sql/notify.sql +0 -0
- {queuerpy-0.20.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.
|
|
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
|
-
[](https://pypi.org/project/queuerPy)
|
|
47
47
|
[](https://www.python.org/)
|
|
48
48
|

|
|
49
49
|
[](https://github.com/siherrmann/queuer/blob/master/LICENSE)
|
|
@@ -454,6 +454,7 @@ worker_jobs = queuer.get_jobs_by_worker_rid(worker.rid)
|
|
|
454
454
|
|
|
455
455
|
- **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.
|
|
456
456
|
- **Async/Await Support**: Full asyncio integration with threading fallbacks.
|
|
457
|
+
- **Multiprocessing pool**: Usage of a multiprocessing pool to reuse memory so global variables can be reused after first load.
|
|
457
458
|
- **PostgreSQL NOTIFY/LISTEN**: Real-time job notifications without polling overhead.
|
|
458
459
|
- **Batch Job Processing**: Insert job batches efficiently using PostgreSQL's `COPY FROM` feature.
|
|
459
460
|
- **Panic Recovery**: Automatic recovery for all running jobs in case of unexpected failures.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# queuerPy
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/queuerPy)
|
|
4
4
|
[](https://www.python.org/)
|
|
5
5
|

|
|
6
6
|
[](https://github.com/siherrmann/queuer/blob/master/LICENSE)
|
|
@@ -411,6 +411,7 @@ worker_jobs = queuer.get_jobs_by_worker_rid(worker.rid)
|
|
|
411
411
|
|
|
412
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.
|
|
413
413
|
- **Async/Await Support**: Full asyncio integration with threading fallbacks.
|
|
414
|
+
- **Multiprocessing pool**: Usage of a multiprocessing pool to reuse memory so global variables can be reused after first load.
|
|
414
415
|
- **PostgreSQL NOTIFY/LISTEN**: Real-time job notifications without polling overhead.
|
|
415
416
|
- **Batch Job Processing**: Insert job batches efficiently using PostgreSQL's `COPY FROM` feature.
|
|
416
417
|
- **Panic Recovery**: Automatic recovery for all running jobs in case of unexpected failures.
|
|
@@ -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)
|
|
@@ -15,115 +15,177 @@ from ..helper.logging import get_logger
|
|
|
15
15
|
logger = get_logger(__name__)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class Runner
|
|
18
|
+
class Runner:
|
|
19
19
|
"""
|
|
20
20
|
The core execution environment. Manages the execution of a single task
|
|
21
|
-
in a
|
|
21
|
+
in a multiprocessing pool for efficient process reuse.
|
|
22
|
+
|
|
23
|
+
Supports cancellation with automatic periodic checking:
|
|
24
|
+
- Async tasks are monitored every 0.1s and can be cancelled mid-execution
|
|
25
|
+
- Sync tasks can only be cancelled before they start (Python limitation)
|
|
22
26
|
|
|
23
27
|
:param task: The synchronous or asynchronous function to execute.
|
|
24
28
|
:param args: Arguments to pass to the task function.
|
|
25
|
-
:param
|
|
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).
|
|
31
|
+
:param kwargs: Keyword arguments to pass to the task function.
|
|
26
32
|
"""
|
|
27
33
|
|
|
28
34
|
def __init__(
|
|
29
35
|
self,
|
|
30
36
|
task: Callable[..., Any],
|
|
31
37
|
*args: Any,
|
|
38
|
+
pool: Any,
|
|
39
|
+
setup_func: Optional[Callable[[], None]] = None,
|
|
32
40
|
**kwargs: Any,
|
|
33
41
|
):
|
|
34
42
|
"""
|
|
35
|
-
Initializes the Runner
|
|
43
|
+
Initializes the Runner.
|
|
36
44
|
|
|
37
45
|
:param task: The synchronous or asynchronous function to execute.
|
|
38
46
|
:param args: Arguments to pass to the task function.
|
|
47
|
+
:param pool: Multiprocessing.Pool for process reuse (required).
|
|
48
|
+
:param kwargs: Keyword arguments to pass to the task function.
|
|
39
49
|
"""
|
|
40
|
-
super().__init__(name=task.__name__, daemon=True)
|
|
41
50
|
self.task = task
|
|
42
51
|
self.args = args
|
|
43
52
|
self.kwargs = kwargs
|
|
44
53
|
self.is_async = inspect.iscoroutinefunction(task)
|
|
45
|
-
self.
|
|
54
|
+
self.name = task.__name__
|
|
55
|
+
self._pool = pool
|
|
56
|
+
self._setup_func = setup_func
|
|
57
|
+
self._async_result: Optional[Any] = None
|
|
58
|
+
# Use Manager Event for cross-process sharing (pickleable)
|
|
59
|
+
manager = multiprocessing.Manager()
|
|
60
|
+
self._cancel_event = manager.Event()
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def pid(self) -> Optional[int]:
|
|
64
|
+
"""Process ID (not available in pool mode)."""
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def daemon(self) -> bool:
|
|
69
|
+
"""Daemon status (always True for pool workers)."""
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def exitcode(self) -> Optional[int]:
|
|
74
|
+
"""Exit code (not available in pool mode)."""
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def _execute_in_pool_worker(
|
|
79
|
+
task: Callable[..., Any],
|
|
80
|
+
args: tuple,
|
|
81
|
+
kwargs: dict,
|
|
82
|
+
is_async: bool,
|
|
83
|
+
cancel_event: Any,
|
|
84
|
+
setup_func: Optional[Callable[[], None]] = None,
|
|
85
|
+
) -> Any:
|
|
86
|
+
"""
|
|
87
|
+
Static method to execute tasks in pool workers.
|
|
88
|
+
Handles both sync and async tasks, returns result directly.
|
|
89
|
+
Checks for cancellation before and during execution.
|
|
90
|
+
"""
|
|
91
|
+
# Check if task was cancelled before execution
|
|
92
|
+
if cancel_event.is_set():
|
|
93
|
+
raise asyncio.CancelledError("Task was cancelled before execution")
|
|
46
94
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
self.start()
|
|
95
|
+
if setup_func is not None:
|
|
96
|
+
setup_func()
|
|
50
97
|
|
|
51
|
-
|
|
98
|
+
if is_async:
|
|
99
|
+
# For async tasks, monitor cancellation during execution
|
|
100
|
+
result = asyncio.run(
|
|
101
|
+
Runner._run_async_with_cancellation(task, args, kwargs, cancel_event)
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
# For sync tasks, just execute (can't interrupt mid-execution easily)
|
|
105
|
+
result = task(*args, **kwargs)
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
async def _run_async_with_cancellation(
|
|
110
|
+
task: Callable[..., Any],
|
|
111
|
+
args: tuple,
|
|
112
|
+
kwargs: dict,
|
|
113
|
+
cancel_event: Any,
|
|
114
|
+
) -> Any:
|
|
52
115
|
"""
|
|
53
|
-
|
|
54
|
-
|
|
116
|
+
Run async task with periodic cancellation checking.
|
|
117
|
+
Monitors cancel_event and cancels the task if it's set.
|
|
55
118
|
"""
|
|
119
|
+
# Create the task
|
|
120
|
+
task_coro = task(*args, **kwargs)
|
|
121
|
+
task_obj = asyncio.create_task(task_coro)
|
|
122
|
+
|
|
123
|
+
# Monitor for cancellation every second
|
|
124
|
+
async def monitor_cancellation():
|
|
125
|
+
while not task_obj.done():
|
|
126
|
+
if cancel_event.is_set():
|
|
127
|
+
task_obj.cancel()
|
|
128
|
+
break
|
|
129
|
+
await asyncio.sleep(1)
|
|
130
|
+
|
|
131
|
+
monitor = asyncio.create_task(monitor_cancellation())
|
|
56
132
|
try:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
except BrokenPipeError as e:
|
|
65
|
-
pass
|
|
66
|
-
except Exception as e:
|
|
67
|
-
logger.debug(traceback.format_exc())
|
|
133
|
+
result = await task_obj
|
|
134
|
+
monitor.cancel()
|
|
135
|
+
return result
|
|
136
|
+
except asyncio.CancelledError:
|
|
137
|
+
monitor.cancel()
|
|
138
|
+
raise asyncio.CancelledError("Task was cancelled during execution")
|
|
139
|
+
finally:
|
|
68
140
|
try:
|
|
69
|
-
|
|
70
|
-
except
|
|
141
|
+
await monitor
|
|
142
|
+
except asyncio.CancelledError:
|
|
71
143
|
pass
|
|
72
144
|
|
|
145
|
+
def go(self) -> None:
|
|
146
|
+
"""Starts the Runner in the background using the pool."""
|
|
147
|
+
self._async_result = self._pool.apply_async(
|
|
148
|
+
Runner._execute_in_pool_worker,
|
|
149
|
+
(self.task, self.args, self.kwargs, self.is_async, self._cancel_event, self._setup_func),
|
|
150
|
+
)
|
|
151
|
+
|
|
73
152
|
def get_results(self, timeout: Optional[float] = None) -> Any:
|
|
74
153
|
"""
|
|
75
|
-
Waits for the runner
|
|
154
|
+
Waits for the runner to complete and returns the result.
|
|
76
155
|
|
|
77
156
|
:param timeout: Time in seconds to wait for the result. If None, waits indefinitely.
|
|
78
157
|
:returns: The result returned by the executed task function.
|
|
79
158
|
:raises TimeoutError: If the result is not available within the specified timeout.
|
|
80
|
-
:raises
|
|
159
|
+
:raises asyncio.CancelledError: If the task was cancelled.
|
|
160
|
+
:raises Exception: If the task execution failed.
|
|
81
161
|
"""
|
|
82
162
|
logger.debug(f"Runner.get_results() called for {self.name}, timeout={timeout}")
|
|
163
|
+
assert (
|
|
164
|
+
self._async_result is not None
|
|
165
|
+
), "go() must be called before get_results()"
|
|
83
166
|
try:
|
|
84
|
-
self.
|
|
85
|
-
|
|
86
|
-
if self.is_alive():
|
|
87
|
-
raise TimeoutError(f"runner timed out after {timeout} seconds.")
|
|
88
|
-
|
|
89
|
-
if self._result_queue.empty():
|
|
90
|
-
if self.exitcode == 0:
|
|
91
|
-
return None
|
|
92
|
-
else:
|
|
93
|
-
raise Exception(f"runner failed with code {self.exitcode}")
|
|
94
|
-
|
|
95
|
-
try:
|
|
96
|
-
result = self._result_queue.get_nowait()
|
|
97
|
-
except Exception as e:
|
|
98
|
-
raise
|
|
99
|
-
|
|
100
|
-
if isinstance(result, Exception):
|
|
101
|
-
raise result
|
|
102
|
-
|
|
167
|
+
result = self._async_result.get(timeout=timeout)
|
|
103
168
|
return result
|
|
104
|
-
except
|
|
105
|
-
raise
|
|
169
|
+
except multiprocessing.TimeoutError:
|
|
170
|
+
raise TimeoutError(f"runner timed out after {timeout} seconds.")
|
|
171
|
+
except asyncio.CancelledError:
|
|
172
|
+
logger.debug(f"Runner {self.name} was cancelled")
|
|
173
|
+
raise
|
|
106
174
|
|
|
107
175
|
def cancel(self) -> bool:
|
|
108
176
|
"""
|
|
109
|
-
Attempts to cancel the running
|
|
177
|
+
Attempts to cancel the running task by setting the cancellation event.
|
|
110
178
|
|
|
111
|
-
:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if not self.is_alive():
|
|
116
|
-
logger.debug(f"Runner {self.name}: Already finished")
|
|
117
|
-
return True
|
|
179
|
+
Cancellation behavior:
|
|
180
|
+
- Async tasks: Checked every second during execution, task will be cancelled
|
|
181
|
+
- Sync tasks: Only checked before execution starts (cannot interrupt mid-execution)
|
|
182
|
+
- Queued tasks: Cancelled before they begin executing
|
|
118
183
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
except Exception as e:
|
|
125
|
-
logger.warning(f"Error cancelling runner {self.name}: {e}")
|
|
126
|
-
return False
|
|
184
|
+
:returns: True if cancellation signal was sent successfully.
|
|
185
|
+
"""
|
|
186
|
+
logger.debug(f"Cancelling runner {self.name}")
|
|
187
|
+
self._cancel_event.set()
|
|
188
|
+
return True
|
|
127
189
|
|
|
128
190
|
|
|
129
191
|
class SmallRunner(threading.Thread):
|
|
@@ -253,7 +315,11 @@ class SmallRunner(threading.Thread):
|
|
|
253
315
|
|
|
254
316
|
|
|
255
317
|
def go_func(
|
|
256
|
-
func: Callable[..., Any],
|
|
318
|
+
func: Callable[..., Any],
|
|
319
|
+
use_mp: bool = True,
|
|
320
|
+
*args: Any,
|
|
321
|
+
pool: Any = None,
|
|
322
|
+
**kwargs: Any,
|
|
257
323
|
) -> Union[Runner, SmallRunner]:
|
|
258
324
|
"""
|
|
259
325
|
Go-like async function execution factory. Selects the appropriate runner.
|
|
@@ -262,11 +328,14 @@ def go_func(
|
|
|
262
328
|
:param use_mp: If True (default), uses the multiprocessing Runner (isolated).
|
|
263
329
|
If False, uses the threading SmallRunner (shared state).
|
|
264
330
|
:param args: Positional arguments for the function.
|
|
331
|
+
:param pool: Multiprocessing.Pool for process reuse (required if use_mp=True).
|
|
265
332
|
:param kwargs: Keyword arguments for the function.
|
|
266
333
|
:returns: A running Runner or SmallRunner instance.
|
|
267
334
|
"""
|
|
268
335
|
if use_mp:
|
|
269
|
-
|
|
336
|
+
if pool is None:
|
|
337
|
+
raise ValueError("pool is required when use_mp=True")
|
|
338
|
+
runner = Runner(func, *args, pool=pool, **kwargs)
|
|
270
339
|
else:
|
|
271
340
|
runner = SmallRunner(func, *args, **kwargs)
|
|
272
341
|
|
|
@@ -186,19 +186,22 @@ class JobDBHandler:
|
|
|
186
186
|
if self.db.instance is None:
|
|
187
187
|
raise ValueError("Database connection is not established")
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
189
|
+
try:
|
|
190
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
191
|
+
cur.execute(
|
|
192
|
+
"""
|
|
193
|
+
SELECT * FROM update_job_initial(%s);
|
|
194
|
+
""",
|
|
195
|
+
(worker.id,),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
jobs: List[Job] = []
|
|
199
|
+
for row in cur.fetchall():
|
|
200
|
+
jobs.append(Job.from_row(row))
|
|
201
|
+
|
|
202
|
+
return jobs
|
|
203
|
+
except Exception as e:
|
|
204
|
+
raise QueuerError("update jobs initial", e)
|
|
202
205
|
|
|
203
206
|
def update_job_final(self, job: Job) -> Job:
|
|
204
207
|
"""
|
|
@@ -303,17 +306,20 @@ class JobDBHandler:
|
|
|
303
306
|
if self.db.instance is None:
|
|
304
307
|
raise ValueError("Database connection is not established")
|
|
305
308
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
309
|
+
try:
|
|
310
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
311
|
+
cur.execute(
|
|
312
|
+
"SELECT * FROM select_all_jobs(%s, %s, %s);",
|
|
313
|
+
(self.encryption_key or "", last_id, entries),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
jobs: List[Job] = []
|
|
317
|
+
for row in cur.fetchall():
|
|
318
|
+
jobs.append(Job.from_row(row))
|
|
319
|
+
|
|
320
|
+
return jobs
|
|
321
|
+
except Exception as e:
|
|
322
|
+
raise QueuerError("select all jobs", e)
|
|
317
323
|
|
|
318
324
|
def select_all_jobs_by_worker_rid(
|
|
319
325
|
self, worker_rid: UUID, last_id: int = 0, entries: int = 100
|
|
@@ -330,17 +336,20 @@ class JobDBHandler:
|
|
|
330
336
|
if self.db.instance is None:
|
|
331
337
|
raise ValueError("Database connection is not established")
|
|
332
338
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
339
|
+
try:
|
|
340
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
341
|
+
cur.execute(
|
|
342
|
+
"SELECT * FROM select_all_jobs_by_worker_rid(%s, %s, %s, %s);",
|
|
343
|
+
(self.encryption_key or "", str(worker_rid), last_id, entries),
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
jobs: List[Job] = []
|
|
347
|
+
for row in cur.fetchall():
|
|
348
|
+
jobs.append(Job.from_row(row))
|
|
349
|
+
|
|
350
|
+
return jobs
|
|
351
|
+
except Exception as e:
|
|
352
|
+
raise QueuerError("select all jobs by worker rid", e)
|
|
344
353
|
|
|
345
354
|
def select_all_jobs_by_search(
|
|
346
355
|
self, search: str, last_id: int = 0, entries: int = 100
|
|
@@ -358,17 +367,48 @@ class JobDBHandler:
|
|
|
358
367
|
if self.db.instance is None:
|
|
359
368
|
raise ValueError("Database connection is not established")
|
|
360
369
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
370
|
+
try:
|
|
371
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
372
|
+
cur.execute(
|
|
373
|
+
"SELECT * FROM select_all_jobs_by_search(%s, %s, %s, %s);",
|
|
374
|
+
(self.encryption_key or "", search, last_id, entries),
|
|
375
|
+
)
|
|
366
376
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
377
|
+
jobs: List[Job] = []
|
|
378
|
+
for row in cur.fetchall():
|
|
379
|
+
jobs.append(Job.from_row(row))
|
|
370
380
|
|
|
371
|
-
|
|
381
|
+
return jobs
|
|
382
|
+
except Exception as e:
|
|
383
|
+
raise QueuerError("select all jobs by search", e)
|
|
384
|
+
|
|
385
|
+
def select_jobs_without_worker(
|
|
386
|
+
self, last_id: int = 0, entries: int = 100
|
|
387
|
+
) -> List[Job]:
|
|
388
|
+
"""
|
|
389
|
+
Select all jobs that are not assigned to any worker.
|
|
390
|
+
|
|
391
|
+
:param last_id: Last job ID from previous page.
|
|
392
|
+
:param entries: Number of entries to retrieve.
|
|
393
|
+
:return: List of Job instances.
|
|
394
|
+
"""
|
|
395
|
+
if self.db.instance is None:
|
|
396
|
+
raise ValueError("Database connection is not established")
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
400
|
+
cur.execute(
|
|
401
|
+
"SELECT * FROM select_jobs_without_worker(%s, %s, %s);",
|
|
402
|
+
(self.encryption_key or "", last_id, entries),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
jobs: List[Job] = []
|
|
406
|
+
for row in cur.fetchall():
|
|
407
|
+
jobs.append(Job.from_row(row))
|
|
408
|
+
|
|
409
|
+
return jobs
|
|
410
|
+
except Exception as e:
|
|
411
|
+
raise QueuerError("select jobs without worker", e)
|
|
372
412
|
|
|
373
413
|
def add_retention_archive(self, days: int) -> None:
|
|
374
414
|
"""
|
|
@@ -441,17 +481,20 @@ class JobDBHandler:
|
|
|
441
481
|
if self.db.instance is None:
|
|
442
482
|
raise ValueError("Database connection is not established")
|
|
443
483
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
484
|
+
try:
|
|
485
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
486
|
+
cur.execute(
|
|
487
|
+
"SELECT * FROM select_all_jobs_from_archive(%s, %s, %s);",
|
|
488
|
+
(self.encryption_key or "", last_id, entries),
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
jobs: List[Job] = []
|
|
492
|
+
for row in cur.fetchall():
|
|
493
|
+
jobs.append(Job.from_row(row))
|
|
494
|
+
|
|
495
|
+
return jobs
|
|
496
|
+
except Exception as e:
|
|
497
|
+
raise QueuerError("select all jobs from archive", e)
|
|
455
498
|
|
|
456
499
|
def select_all_jobs_from_archive_by_search(
|
|
457
500
|
self, search: str, last_id: int = 0, entries: int = 100
|
|
@@ -468,14 +511,17 @@ class JobDBHandler:
|
|
|
468
511
|
if self.db.instance is None:
|
|
469
512
|
raise ValueError("Database connection is not established")
|
|
470
513
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
514
|
+
try:
|
|
515
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
516
|
+
cur.execute(
|
|
517
|
+
"SELECT * FROM select_all_jobs_from_archive_by_search(%s, %s, %s, %s);",
|
|
518
|
+
(self.encryption_key or "", search, last_id, entries),
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
jobs: List[Job] = []
|
|
522
|
+
for row in cur.fetchall():
|
|
523
|
+
jobs.append(Job.from_row(row))
|
|
524
|
+
|
|
525
|
+
return jobs
|
|
526
|
+
except Exception as e:
|
|
527
|
+
raise QueuerError("select all jobs from archive by search", e)
|
|
@@ -195,14 +195,17 @@ class WorkerDBHandler:
|
|
|
195
195
|
if self.db.instance is None:
|
|
196
196
|
raise ValueError("Database connection is not established")
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
198
|
+
try:
|
|
199
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
200
|
+
cur.execute("SELECT * FROM select_all_workers(%s, %s);", (last_id, entries))
|
|
201
|
+
|
|
202
|
+
workers: List[Worker] = []
|
|
203
|
+
for row in cur.fetchall():
|
|
204
|
+
workers.append(Worker.from_row(row))
|
|
205
|
+
|
|
206
|
+
return workers
|
|
207
|
+
except Exception as e:
|
|
208
|
+
raise QueuerError("select all workers", e)
|
|
206
209
|
|
|
207
210
|
def select_all_workers_by_search(
|
|
208
211
|
self, search: str, last_id: int = 0, entries: int = 100
|
|
@@ -215,17 +218,20 @@ class WorkerDBHandler:
|
|
|
215
218
|
if self.db.instance is None:
|
|
216
219
|
raise ValueError("Database connection is not established")
|
|
217
220
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
221
|
+
try:
|
|
222
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
223
|
+
cur.execute(
|
|
224
|
+
"SELECT * FROM select_all_workers_by_search(%s, %s, %s);",
|
|
225
|
+
(search, last_id, entries),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
workers: List[Worker] = []
|
|
229
|
+
for row in cur.fetchall():
|
|
230
|
+
workers.append(Worker.from_row(row))
|
|
231
|
+
|
|
232
|
+
return workers
|
|
233
|
+
except Exception as e:
|
|
234
|
+
raise QueuerError("select all workers by search", e)
|
|
229
235
|
|
|
230
236
|
def select_all_connections(self) -> List[ConnectionModel]:
|
|
231
237
|
"""
|
|
@@ -235,19 +241,22 @@ class WorkerDBHandler:
|
|
|
235
241
|
if self.db.instance is None:
|
|
236
242
|
raise ValueError("Database connection is not established")
|
|
237
243
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
244
|
+
try:
|
|
245
|
+
with self.db.instance.cursor(row_factory=dict_row) as cur:
|
|
246
|
+
cur.execute("SELECT * FROM select_all_connections();")
|
|
247
|
+
|
|
248
|
+
connections: List[ConnectionModel] = []
|
|
249
|
+
for row in cur.fetchall():
|
|
250
|
+
connection = ConnectionModel(
|
|
251
|
+
pid=row["output_pid"],
|
|
252
|
+
database=row["output_datname"],
|
|
253
|
+
username=row["output_usename"],
|
|
254
|
+
application_name=row["output_application_name"],
|
|
255
|
+
query=row["output_query"],
|
|
256
|
+
state=row["output_state"],
|
|
257
|
+
)
|
|
258
|
+
connections.append(connection)
|
|
259
|
+
|
|
260
|
+
return connections
|
|
261
|
+
except Exception as e:
|
|
262
|
+
raise QueuerError("select all connections", e)
|
|
@@ -88,8 +88,9 @@ class DatabaseConfiguration:
|
|
|
88
88
|
f"user={self.username} "
|
|
89
89
|
f"password={self.password} "
|
|
90
90
|
f"sslmode={self.sslmode} "
|
|
91
|
+
f"connect_timeout=10 "
|
|
91
92
|
f"application_name=queuer "
|
|
92
|
-
f"options='-c search_path={self.schema}'"
|
|
93
|
+
f"options='-c search_path={self.schema} -c statement_timeout=30000'"
|
|
93
94
|
)
|
|
94
95
|
|
|
95
96
|
|
|
@@ -78,6 +78,7 @@ class Options:
|
|
|
78
78
|
|
|
79
79
|
on_error: Optional[OnError] = None
|
|
80
80
|
schedule: Optional[Schedule] = None
|
|
81
|
+
parent_rid: Optional[str] = None
|
|
81
82
|
|
|
82
83
|
def is_valid(self) -> bool:
|
|
83
84
|
"""
|
|
@@ -100,6 +101,7 @@ class Options:
|
|
|
100
101
|
return {
|
|
101
102
|
"on_error": self.on_error.to_dict() if self.on_error else None,
|
|
102
103
|
"schedule": self.schedule.to_dict() if self.schedule else None,
|
|
104
|
+
"parent_rid": self.parent_rid,
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
@classmethod
|
|
@@ -115,11 +117,15 @@ class Options:
|
|
|
115
117
|
options.on_error = OnError.from_dict(data["on_error"])
|
|
116
118
|
if data.get("schedule"):
|
|
117
119
|
options.schedule = Schedule.from_dict(data["schedule"])
|
|
120
|
+
if data.get("parent_rid"):
|
|
121
|
+
options.parent_rid = data["parent_rid"]
|
|
118
122
|
return options
|
|
119
123
|
|
|
120
124
|
|
|
121
125
|
def new_options(
|
|
122
|
-
on_error: Optional[OnError] = None,
|
|
126
|
+
on_error: Optional[OnError] = None,
|
|
127
|
+
schedule: Optional[Schedule] = None,
|
|
128
|
+
parent_rid: Optional[str] = None,
|
|
123
129
|
) -> Options:
|
|
124
130
|
"""
|
|
125
131
|
Create new options. Mirrors Go's pattern.
|
|
@@ -128,4 +134,4 @@ def new_options(
|
|
|
128
134
|
:param schedule: Schedule options.
|
|
129
135
|
:return: Options instance.
|
|
130
136
|
"""
|
|
131
|
-
return Options(on_error=on_error, schedule=schedule)
|
|
137
|
+
return Options(on_error=on_error, schedule=schedule, parent_rid=parent_rid)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "queuerPy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.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.
|
|
95
|
+
python_version = "0.22.0"
|
|
96
96
|
warn_return_any = true
|
|
97
97
|
warn_unused_configs = true
|
|
98
98
|
disallow_untyped_defs = true
|
|
@@ -6,6 +6,7 @@ Mirrors the Go Queuer struct with Python async patterns.
|
|
|
6
6
|
# Standard library imports
|
|
7
7
|
import asyncio
|
|
8
8
|
import json
|
|
9
|
+
import multiprocessing
|
|
9
10
|
import os
|
|
10
11
|
import threading
|
|
11
12
|
import time
|
|
@@ -121,6 +122,9 @@ class Queuer(
|
|
|
121
122
|
# Active runners (jobs currently executing)
|
|
122
123
|
self.active_runners: Dict[UUID, Runner] = {}
|
|
123
124
|
|
|
125
|
+
# Set pool size based on max_concurrency
|
|
126
|
+
self._pool_size = max_concurrency if max_concurrency > 0 else 4
|
|
127
|
+
|
|
124
128
|
# Database listeners (will be initialized in start())
|
|
125
129
|
self.job_db_listener: Optional[QueuerListener] = None
|
|
126
130
|
self.job_archive_db_listener: Optional[QueuerListener] = None
|
|
@@ -170,6 +174,17 @@ class Queuer(
|
|
|
170
174
|
self._stopped.clear()
|
|
171
175
|
self._cancel_event = asyncio.Event()
|
|
172
176
|
|
|
177
|
+
# Create process pool for job execution
|
|
178
|
+
try:
|
|
179
|
+
self._process_pool = multiprocessing.Pool(
|
|
180
|
+
processes=self._pool_size,
|
|
181
|
+
maxtasksperchild=100,
|
|
182
|
+
)
|
|
183
|
+
logger.info(f"Created process pool with {self._pool_size} workers")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.error(f"Failed to create process pool: {e}")
|
|
186
|
+
raise
|
|
187
|
+
|
|
173
188
|
# Set up database listeners
|
|
174
189
|
try:
|
|
175
190
|
self.job_db_listener = new_queuer_db_listener(
|
|
@@ -293,9 +308,24 @@ class Queuer(
|
|
|
293
308
|
|
|
294
309
|
# Cancel all active runners
|
|
295
310
|
for runner_id, runner in list(self.active_runners.items()):
|
|
296
|
-
runner.cancel()
|
|
311
|
+
cancelled = runner.cancel()
|
|
312
|
+
if not cancelled:
|
|
313
|
+
# Pool-based runner, need to restart pool
|
|
314
|
+
logger.warning("Pool-based runner cannot be individually cancelled")
|
|
297
315
|
del self.active_runners[runner_id]
|
|
298
316
|
|
|
317
|
+
# Clean up process pool
|
|
318
|
+
if self._process_pool:
|
|
319
|
+
logger.info("Closing process pool")
|
|
320
|
+
try:
|
|
321
|
+
self._process_pool.close()
|
|
322
|
+
# Force terminate if workers didn't finish gracefully
|
|
323
|
+
self._process_pool.terminate()
|
|
324
|
+
self._process_pool.join()
|
|
325
|
+
self._process_pool = None
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.warning(f"Error closing process pool: {e}")
|
|
328
|
+
|
|
299
329
|
# Cancel the context to stop the queuer
|
|
300
330
|
if self._cancel_event:
|
|
301
331
|
self._cancel_event.set()
|
|
@@ -304,12 +334,11 @@ class Queuer(
|
|
|
304
334
|
time.sleep(0.1)
|
|
305
335
|
|
|
306
336
|
# Close database connection
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
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}")
|
|
313
342
|
|
|
314
343
|
logger.info("Queuer stopped")
|
|
315
344
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: queuerPy
|
|
3
|
-
Version: 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
|
-
[](https://pypi.org/project/queuerPy)
|
|
47
47
|
[](https://www.python.org/)
|
|
48
48
|

|
|
49
49
|
[](https://github.com/siherrmann/queuer/blob/master/LICENSE)
|
|
@@ -454,6 +454,7 @@ worker_jobs = queuer.get_jobs_by_worker_rid(worker.rid)
|
|
|
454
454
|
|
|
455
455
|
- **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.
|
|
456
456
|
- **Async/Await Support**: Full asyncio integration with threading fallbacks.
|
|
457
|
+
- **Multiprocessing pool**: Usage of a multiprocessing pool to reuse memory so global variables can be reused after first load.
|
|
457
458
|
- **PostgreSQL NOTIFY/LISTEN**: Real-time job notifications without polling overhead.
|
|
458
459
|
- **Batch Job Processing**: Insert job batches efficiently using PostgreSQL's `COPY FROM` feature.
|
|
459
460
|
- **Panic Recovery**: Automatic recovery for all running jobs in case of unexpected failures.
|
|
@@ -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
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import multiprocessing
|
|
1
2
|
import threading
|
|
2
3
|
from typing import Any, Callable, Dict, Optional
|
|
3
4
|
|
|
@@ -45,6 +46,10 @@ class QueuerGlobalMixin:
|
|
|
45
46
|
self.tasks: Dict[str, Task] = {}
|
|
46
47
|
self.next_interval_funcs: Dict[str, Callable[..., Any]] = {}
|
|
47
48
|
|
|
49
|
+
# Process pool for runner reuse (initialized in start())
|
|
50
|
+
self._process_pool: Optional[Any] = None
|
|
51
|
+
self._pool_size: int = 4 # Default, will be set based on max_concurrency
|
|
52
|
+
|
|
48
53
|
def initialise(
|
|
49
54
|
self,
|
|
50
55
|
db_config: DatabaseConfiguration,
|
|
@@ -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(
|
|
@@ -110,6 +130,7 @@ class QueuerJobMixin(QueuerGlobalMixin):
|
|
|
110
130
|
runner = Runner(
|
|
111
131
|
task=self._wait_for_job_added_inner,
|
|
112
132
|
args=(timeout_seconds,),
|
|
133
|
+
pool=self._process_pool,
|
|
113
134
|
kwargs={},
|
|
114
135
|
)
|
|
115
136
|
runner.go()
|
|
@@ -421,7 +442,10 @@ class QueuerJobMixin(QueuerGlobalMixin):
|
|
|
421
442
|
try:
|
|
422
443
|
parameters = getattr(job, "parameters", [])
|
|
423
444
|
parameters_keyed = getattr(job, "parameters_keyed", {})
|
|
424
|
-
|
|
445
|
+
setup_func = partial(_setup_job_context, job.rid)
|
|
446
|
+
runner = Runner(
|
|
447
|
+
task, *parameters, pool=self._process_pool, setup_func=setup_func, **parameters_keyed
|
|
448
|
+
)
|
|
425
449
|
|
|
426
450
|
logger.info(f"Created runner for job {job.rid}")
|
|
427
451
|
|
|
@@ -436,8 +460,7 @@ class QueuerJobMixin(QueuerGlobalMixin):
|
|
|
436
460
|
return [results], False, None
|
|
437
461
|
|
|
438
462
|
except asyncio.CancelledError:
|
|
439
|
-
|
|
440
|
-
runner.cancel()
|
|
463
|
+
runner.cancel()
|
|
441
464
|
return [], True, None
|
|
442
465
|
except Exception as e:
|
|
443
466
|
logger.error(f"Process runner task {job.task_name} failed: {e}")
|
|
@@ -507,9 +530,9 @@ class QueuerJobMixin(QueuerGlobalMixin):
|
|
|
507
530
|
"""
|
|
508
531
|
try:
|
|
509
532
|
# Create a Runner process for the async job
|
|
510
|
-
runner = Runner(self._run_job, job)
|
|
533
|
+
runner = Runner(self._run_job, job, pool=self._process_pool)
|
|
511
534
|
runner.go()
|
|
512
|
-
logger.info(f"Scheduled job {job.rid} started
|
|
535
|
+
logger.info(f"Scheduled job {job.rid} started")
|
|
513
536
|
|
|
514
537
|
except Exception as e:
|
|
515
538
|
logger.error(f"Error executing scheduled job {job.rid}: {e}")
|
|
@@ -57,6 +57,10 @@ BEGIN
|
|
|
57
57
|
ON job
|
|
58
58
|
USING HASH ((options->'schedule'->>'next_interval'));
|
|
59
59
|
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_job_parent_rid
|
|
61
|
+
ON job
|
|
62
|
+
USING BTREE ((options->>'parent_rid'));
|
|
63
|
+
|
|
60
64
|
CREATE INDEX IF NOT EXISTS idx_job_worker_id ON job (worker_id);
|
|
61
65
|
CREATE INDEX IF NOT EXISTS idx_job_worker_rid ON job (worker_rid);
|
|
62
66
|
CREATE INDEX IF NOT EXISTS idx_job_status ON job (status);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|