queuerPy 0.20.0__tar.gz → 0.21.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.21.0}/PKG-INFO +2 -1
- {queuerpy-0.20.0 → queuerpy-0.21.0}/README.md +1 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/_version.py +1 -1
- {queuerpy-0.20.0 → queuerpy-0.21.0}/core/runner.py +129 -67
- {queuerpy-0.20.0 → queuerpy-0.21.0}/pyproject.toml +2 -2
- {queuerpy-0.20.0 → queuerpy-0.21.0}/queuer.py +32 -1
- {queuerpy-0.20.0 → queuerpy-0.21.0/queuerPy.egg-info}/PKG-INFO +2 -1
- {queuerpy-0.20.0 → queuerpy-0.21.0}/queuer_global.py +5 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/queuer_job.py +6 -3
- {queuerpy-0.20.0 → queuerpy-0.21.0}/LICENSE +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/MANIFEST.in +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/__init__.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/core/__init__.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/core/broadcaster.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/core/listener.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/core/retryer.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/core/scheduler.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/core/ticker.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/database/__init__.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/database/db_job.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/database/db_listener.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/database/db_master.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/database/db_worker.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/helper/__init__.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/helper/database.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/helper/error.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/helper/logging.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/helper/sql.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/helper/task.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/model/__init__.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/model/batch_job.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/model/connection.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/model/job.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/model/master.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/model/options.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/model/options_on_error.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/model/task.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/model/worker.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/py.typed +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/queuerPy.egg-info/SOURCES.txt +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/queuerPy.egg-info/dependency_links.txt +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/queuerPy.egg-info/requires.txt +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/queuerPy.egg-info/top_level.txt +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/queuer_listener.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/queuer_master.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/queuer_next_interval.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/queuer_task.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/queuer_worker.py +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/setup.cfg +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/sql/job.sql +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/sql/master.sql +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.0}/sql/notify.sql +0 -0
- {queuerpy-0.20.0 → queuerpy-0.21.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.21.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>
|
|
@@ -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.
|
|
@@ -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.
|
|
@@ -15,115 +15,170 @@ 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 kwargs: Keyword arguments to pass to the task function.
|
|
26
31
|
"""
|
|
27
32
|
|
|
28
33
|
def __init__(
|
|
29
34
|
self,
|
|
30
35
|
task: Callable[..., Any],
|
|
31
36
|
*args: Any,
|
|
37
|
+
pool: Any,
|
|
32
38
|
**kwargs: Any,
|
|
33
39
|
):
|
|
34
40
|
"""
|
|
35
|
-
Initializes the Runner
|
|
41
|
+
Initializes the Runner.
|
|
36
42
|
|
|
37
43
|
:param task: The synchronous or asynchronous function to execute.
|
|
38
44
|
:param args: Arguments to pass to the task function.
|
|
45
|
+
:param pool: Multiprocessing.Pool for process reuse (required).
|
|
46
|
+
:param kwargs: Keyword arguments to pass to the task function.
|
|
39
47
|
"""
|
|
40
|
-
super().__init__(name=task.__name__, daemon=True)
|
|
41
48
|
self.task = task
|
|
42
49
|
self.args = args
|
|
43
50
|
self.kwargs = kwargs
|
|
44
51
|
self.is_async = inspect.iscoroutinefunction(task)
|
|
45
|
-
self.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
self.name = task.__name__
|
|
53
|
+
self._pool = pool
|
|
54
|
+
self._async_result: Optional[Any] = None
|
|
55
|
+
# Use Manager Event for cross-process sharing (pickleable)
|
|
56
|
+
manager = multiprocessing.Manager()
|
|
57
|
+
self._cancel_event = manager.Event()
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def pid(self) -> Optional[int]:
|
|
61
|
+
"""Process ID (not available in pool mode)."""
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def daemon(self) -> bool:
|
|
66
|
+
"""Daemon status (always True for pool workers)."""
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def exitcode(self) -> Optional[int]:
|
|
71
|
+
"""Exit code (not available in pool mode)."""
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _execute_in_pool_worker(
|
|
76
|
+
task: Callable[..., Any],
|
|
77
|
+
args: tuple,
|
|
78
|
+
kwargs: dict,
|
|
79
|
+
is_async: bool,
|
|
80
|
+
cancel_event: Any,
|
|
81
|
+
) -> Any:
|
|
82
|
+
"""
|
|
83
|
+
Static method to execute tasks in pool workers.
|
|
84
|
+
Handles both sync and async tasks, returns result directly.
|
|
85
|
+
Checks for cancellation before and during execution.
|
|
86
|
+
"""
|
|
87
|
+
# Check if task was cancelled before execution
|
|
88
|
+
if cancel_event.is_set():
|
|
89
|
+
raise asyncio.CancelledError("Task was cancelled before execution")
|
|
90
|
+
|
|
91
|
+
if is_async:
|
|
92
|
+
# For async tasks, monitor cancellation during execution
|
|
93
|
+
result = asyncio.run(
|
|
94
|
+
Runner._run_async_with_cancellation(task, args, kwargs, cancel_event)
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
# For sync tasks, just execute (can't interrupt mid-execution easily)
|
|
98
|
+
result = task(*args, **kwargs)
|
|
99
|
+
return result
|
|
50
100
|
|
|
51
|
-
|
|
101
|
+
@staticmethod
|
|
102
|
+
async def _run_async_with_cancellation(
|
|
103
|
+
task: Callable[..., Any],
|
|
104
|
+
args: tuple,
|
|
105
|
+
kwargs: dict,
|
|
106
|
+
cancel_event: Any,
|
|
107
|
+
) -> Any:
|
|
52
108
|
"""
|
|
53
|
-
|
|
54
|
-
|
|
109
|
+
Run async task with periodic cancellation checking.
|
|
110
|
+
Monitors cancel_event and cancels the task if it's set.
|
|
55
111
|
"""
|
|
112
|
+
# Create the task
|
|
113
|
+
task_coro = task(*args, **kwargs)
|
|
114
|
+
task_obj = asyncio.create_task(task_coro)
|
|
115
|
+
|
|
116
|
+
# Monitor for cancellation every second
|
|
117
|
+
async def monitor_cancellation():
|
|
118
|
+
while not task_obj.done():
|
|
119
|
+
if cancel_event.is_set():
|
|
120
|
+
task_obj.cancel()
|
|
121
|
+
break
|
|
122
|
+
await asyncio.sleep(1)
|
|
123
|
+
|
|
124
|
+
monitor = asyncio.create_task(monitor_cancellation())
|
|
56
125
|
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())
|
|
126
|
+
result = await task_obj
|
|
127
|
+
monitor.cancel()
|
|
128
|
+
return result
|
|
129
|
+
except asyncio.CancelledError:
|
|
130
|
+
monitor.cancel()
|
|
131
|
+
raise asyncio.CancelledError("Task was cancelled during execution")
|
|
132
|
+
finally:
|
|
68
133
|
try:
|
|
69
|
-
|
|
70
|
-
except
|
|
134
|
+
await monitor
|
|
135
|
+
except asyncio.CancelledError:
|
|
71
136
|
pass
|
|
72
137
|
|
|
138
|
+
def go(self) -> None:
|
|
139
|
+
"""Starts the Runner in the background using the pool."""
|
|
140
|
+
self._async_result = self._pool.apply_async(
|
|
141
|
+
Runner._execute_in_pool_worker,
|
|
142
|
+
(self.task, self.args, self.kwargs, self.is_async, self._cancel_event),
|
|
143
|
+
)
|
|
144
|
+
|
|
73
145
|
def get_results(self, timeout: Optional[float] = None) -> Any:
|
|
74
146
|
"""
|
|
75
|
-
Waits for the runner
|
|
147
|
+
Waits for the runner to complete and returns the result.
|
|
76
148
|
|
|
77
149
|
:param timeout: Time in seconds to wait for the result. If None, waits indefinitely.
|
|
78
150
|
:returns: The result returned by the executed task function.
|
|
79
151
|
:raises TimeoutError: If the result is not available within the specified timeout.
|
|
80
|
-
:raises
|
|
152
|
+
:raises asyncio.CancelledError: If the task was cancelled.
|
|
153
|
+
:raises Exception: If the task execution failed.
|
|
81
154
|
"""
|
|
82
155
|
logger.debug(f"Runner.get_results() called for {self.name}, timeout={timeout}")
|
|
156
|
+
assert (
|
|
157
|
+
self._async_result is not None
|
|
158
|
+
), "go() must be called before get_results()"
|
|
83
159
|
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
|
-
|
|
160
|
+
result = self._async_result.get(timeout=timeout)
|
|
103
161
|
return result
|
|
104
|
-
except
|
|
105
|
-
raise
|
|
162
|
+
except multiprocessing.TimeoutError:
|
|
163
|
+
raise TimeoutError(f"runner timed out after {timeout} seconds.")
|
|
164
|
+
except asyncio.CancelledError:
|
|
165
|
+
logger.debug(f"Runner {self.name} was cancelled")
|
|
166
|
+
raise
|
|
106
167
|
|
|
107
168
|
def cancel(self) -> bool:
|
|
108
169
|
"""
|
|
109
|
-
Attempts to cancel the running
|
|
170
|
+
Attempts to cancel the running task by setting the cancellation event.
|
|
110
171
|
|
|
111
|
-
:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if not self.is_alive():
|
|
116
|
-
logger.debug(f"Runner {self.name}: Already finished")
|
|
117
|
-
return True
|
|
172
|
+
Cancellation behavior:
|
|
173
|
+
- Async tasks: Checked every second during execution, task will be cancelled
|
|
174
|
+
- Sync tasks: Only checked before execution starts (cannot interrupt mid-execution)
|
|
175
|
+
- Queued tasks: Cancelled before they begin executing
|
|
118
176
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
except Exception as e:
|
|
125
|
-
logger.warning(f"Error cancelling runner {self.name}: {e}")
|
|
126
|
-
return False
|
|
177
|
+
:returns: True if cancellation signal was sent successfully.
|
|
178
|
+
"""
|
|
179
|
+
logger.debug(f"Cancelling runner {self.name}")
|
|
180
|
+
self._cancel_event.set()
|
|
181
|
+
return True
|
|
127
182
|
|
|
128
183
|
|
|
129
184
|
class SmallRunner(threading.Thread):
|
|
@@ -253,7 +308,11 @@ class SmallRunner(threading.Thread):
|
|
|
253
308
|
|
|
254
309
|
|
|
255
310
|
def go_func(
|
|
256
|
-
func: Callable[..., Any],
|
|
311
|
+
func: Callable[..., Any],
|
|
312
|
+
use_mp: bool = True,
|
|
313
|
+
*args: Any,
|
|
314
|
+
pool: Any = None,
|
|
315
|
+
**kwargs: Any,
|
|
257
316
|
) -> Union[Runner, SmallRunner]:
|
|
258
317
|
"""
|
|
259
318
|
Go-like async function execution factory. Selects the appropriate runner.
|
|
@@ -262,11 +321,14 @@ def go_func(
|
|
|
262
321
|
:param use_mp: If True (default), uses the multiprocessing Runner (isolated).
|
|
263
322
|
If False, uses the threading SmallRunner (shared state).
|
|
264
323
|
:param args: Positional arguments for the function.
|
|
324
|
+
:param pool: Multiprocessing.Pool for process reuse (required if use_mp=True).
|
|
265
325
|
:param kwargs: Keyword arguments for the function.
|
|
266
326
|
:returns: A running Runner or SmallRunner instance.
|
|
267
327
|
"""
|
|
268
328
|
if use_mp:
|
|
269
|
-
|
|
329
|
+
if pool is None:
|
|
330
|
+
raise ValueError("pool is required when use_mp=True")
|
|
331
|
+
runner = Runner(func, *args, pool=pool, **kwargs)
|
|
270
332
|
else:
|
|
271
333
|
runner = SmallRunner(func, *args, **kwargs)
|
|
272
334
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "queuerPy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.21.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.21.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,25 @@ 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
|
+
self._process_pool.join(timeout=5.0)
|
|
323
|
+
# Force terminate if workers didn't finish gracefully
|
|
324
|
+
self._process_pool.terminate()
|
|
325
|
+
self._process_pool.join()
|
|
326
|
+
self._process_pool = None
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logger.warning(f"Error closing process pool: {e}")
|
|
329
|
+
|
|
299
330
|
# Cancel the context to stop the queuer
|
|
300
331
|
if self._cancel_event:
|
|
301
332
|
self._cancel_event.set()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: queuerPy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.21.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>
|
|
@@ -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,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,
|
|
@@ -110,6 +110,7 @@ class QueuerJobMixin(QueuerGlobalMixin):
|
|
|
110
110
|
runner = Runner(
|
|
111
111
|
task=self._wait_for_job_added_inner,
|
|
112
112
|
args=(timeout_seconds,),
|
|
113
|
+
pool=self._process_pool,
|
|
113
114
|
kwargs={},
|
|
114
115
|
)
|
|
115
116
|
runner.go()
|
|
@@ -421,7 +422,9 @@ class QueuerJobMixin(QueuerGlobalMixin):
|
|
|
421
422
|
try:
|
|
422
423
|
parameters = getattr(job, "parameters", [])
|
|
423
424
|
parameters_keyed = getattr(job, "parameters_keyed", {})
|
|
424
|
-
runner = Runner(
|
|
425
|
+
runner = Runner(
|
|
426
|
+
task, *parameters, pool=self._process_pool, **parameters_keyed
|
|
427
|
+
)
|
|
425
428
|
|
|
426
429
|
logger.info(f"Created runner for job {job.rid}")
|
|
427
430
|
|
|
@@ -507,9 +510,9 @@ class QueuerJobMixin(QueuerGlobalMixin):
|
|
|
507
510
|
"""
|
|
508
511
|
try:
|
|
509
512
|
# Create a Runner process for the async job
|
|
510
|
-
runner = Runner(self._run_job, job)
|
|
513
|
+
runner = Runner(self._run_job, job, pool=self._process_pool)
|
|
511
514
|
runner.go()
|
|
512
|
-
logger.info(f"Scheduled job {job.rid} started
|
|
515
|
+
logger.info(f"Scheduled job {job.rid} started")
|
|
513
516
|
|
|
514
517
|
except Exception as e:
|
|
515
518
|
logger.error(f"Error executing scheduled job {job.rid}: {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
|