queuemgr 1.0.7__py3-none-any.whl
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.
- queuemgr/__init__.py +86 -0
- queuemgr/async_process_manager.py +472 -0
- queuemgr/async_simple_api.py +323 -0
- queuemgr/async_simple_queue.py +371 -0
- queuemgr/constants.py +70 -0
- queuemgr/core/__init__.py +6 -0
- queuemgr/core/exceptions.py +63 -0
- queuemgr/core/ipc.py +38 -0
- queuemgr/core/ipc_manager.py +54 -0
- queuemgr/core/ipc_operations.py +351 -0
- queuemgr/core/registry.py +266 -0
- queuemgr/core/types.py +75 -0
- queuemgr/examples/__init__.py +6 -0
- queuemgr/examples/async_fastapi_example.py +330 -0
- queuemgr/examples/async_web_example.py +301 -0
- queuemgr/examples/data_analyzer.py +178 -0
- queuemgr/examples/error_handling_job.py +135 -0
- queuemgr/examples/full_app_example.py +158 -0
- queuemgr/examples/integration_examples/cli_integration.py +320 -0
- queuemgr/examples/integration_examples/flask_api.py +209 -0
- queuemgr/examples/integration_examples/flask_integration.py +16 -0
- queuemgr/examples/integration_examples/flask_routes.py +109 -0
- queuemgr/examples/integration_examples/systemd_integration.py +286 -0
- queuemgr/examples/jobs/__init__.py +14 -0
- queuemgr/examples/jobs/api_call_job.py +82 -0
- queuemgr/examples/jobs/data_processing_job.py +72 -0
- queuemgr/examples/jobs/file_operation_job.py +77 -0
- queuemgr/examples/large_data_generator.py +234 -0
- queuemgr/examples/large_result_job.py +109 -0
- queuemgr/examples/mcp_adapter_example.py +520 -0
- queuemgr/examples/proc_manager_example.py +150 -0
- queuemgr/examples/progress_job.py +137 -0
- queuemgr/examples/registry_example.py +161 -0
- queuemgr/examples/result_job.py +358 -0
- queuemgr/examples/service_example.py +380 -0
- queuemgr/examples/simple_job.py +125 -0
- queuemgr/examples/simple_manager_example.py +132 -0
- queuemgr/examples/simple_mcp_example.py +195 -0
- queuemgr/examples/working_mcp_server.py +258 -0
- queuemgr/exceptions.py +165 -0
- queuemgr/jobs/__init__.py +6 -0
- queuemgr/jobs/base.py +15 -0
- queuemgr/jobs/base_core.py +311 -0
- queuemgr/jobs/exceptions.py +47 -0
- queuemgr/proc_api.py +297 -0
- queuemgr/proc_config.py +22 -0
- queuemgr/proc_ipc.py +103 -0
- queuemgr/proc_manager.py +16 -0
- queuemgr/proc_manager_core.py +406 -0
- queuemgr/process_commands.py +59 -0
- queuemgr/process_config.py +23 -0
- queuemgr/process_context.py +38 -0
- queuemgr/process_core.py +367 -0
- queuemgr/process_manager.py +17 -0
- queuemgr/queue/__init__.py +6 -0
- queuemgr/queue/exceptions.py +58 -0
- queuemgr/queue/job_queue.py +457 -0
- queuemgr/simple_api.py +281 -0
- queuemgr-1.0.7.dist-info/METADATA +148 -0
- queuemgr-1.0.7.dist-info/RECORD +78 -0
- queuemgr-1.0.7.dist-info/WHEEL +5 -0
- queuemgr-1.0.7.dist-info/entry_points.txt +4 -0
- queuemgr-1.0.7.dist-info/licenses/LICENSE +21 -0
- queuemgr-1.0.7.dist-info/top_level.txt +2 -0
- tests/__init__.py +6 -0
- tests/core/test_ipc.py +269 -0
- tests/core/test_registry.py +360 -0
- tests/core/test_types.py +191 -0
- tests/jobs/test_base.py +20 -0
- tests/jobs/test_base_initialization.py +133 -0
- tests/jobs/test_base_job_loop.py +216 -0
- tests/jobs/test_base_process_control.py +163 -0
- tests/queue/test_job_queue.py +15 -0
- tests/queue/test_job_queue_basic.py +181 -0
- tests/queue/test_job_queue_list_jobs.py +92 -0
- tests/queue/test_job_queue_operations.py +252 -0
- tests/queue/test_job_queue_per_type_limits.py +331 -0
- tests/test_exceptions.py +184 -0
queuemgr/__init__.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Queue Manager - Full-featured job queue system with multiprocessing support for Linux.
|
|
3
|
+
|
|
4
|
+
A production-ready job queue system with automatic process management,
|
|
5
|
+
real-time monitoring, systemd integration, and multiple interfaces (CLI, web).
|
|
6
|
+
|
|
7
|
+
Supports both synchronous and asyncio-based applications.
|
|
8
|
+
|
|
9
|
+
Author: Vasiliy Zdanovskiy
|
|
10
|
+
email: vasilyvz@gmail.com
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__version__ = "1.0.7"
|
|
14
|
+
__author__ = "Vasiliy Zdanovskiy"
|
|
15
|
+
__email__ = "vasilyvz@gmail.com"
|
|
16
|
+
|
|
17
|
+
# Import core components
|
|
18
|
+
from .jobs.base import QueueJobBase
|
|
19
|
+
from .exceptions import (
|
|
20
|
+
QueueManagerError,
|
|
21
|
+
JobNotFoundError,
|
|
22
|
+
JobAlreadyExistsError,
|
|
23
|
+
InvalidJobStateError,
|
|
24
|
+
JobExecutionError,
|
|
25
|
+
RegistryError,
|
|
26
|
+
ProcessControlError,
|
|
27
|
+
ValidationError,
|
|
28
|
+
TimeoutError,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Import synchronous API
|
|
32
|
+
from .simple_api import QueueSystem
|
|
33
|
+
|
|
34
|
+
# Import asyncio API
|
|
35
|
+
from .async_simple_api import (
|
|
36
|
+
AsyncQueueSystem,
|
|
37
|
+
async_queue_system_context,
|
|
38
|
+
get_global_async_queue,
|
|
39
|
+
shutdown_global_async_queue,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Import simplified asyncio API
|
|
43
|
+
from .async_simple_queue import (
|
|
44
|
+
AsyncSimpleQueue,
|
|
45
|
+
async_simple_queue_context,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Import process managers
|
|
49
|
+
from .process_manager import ProcessManager
|
|
50
|
+
from .async_process_manager import AsyncProcessManager
|
|
51
|
+
|
|
52
|
+
# Import queue components
|
|
53
|
+
from .queue.job_queue import JobQueue
|
|
54
|
+
|
|
55
|
+
# Export main components
|
|
56
|
+
__all__ = [
|
|
57
|
+
# Version info
|
|
58
|
+
"__version__",
|
|
59
|
+
"__author__",
|
|
60
|
+
"__email__",
|
|
61
|
+
# Core components
|
|
62
|
+
"QueueJobBase",
|
|
63
|
+
# Exceptions
|
|
64
|
+
"QueueManagerError",
|
|
65
|
+
"JobNotFoundError",
|
|
66
|
+
"JobAlreadyExistsError",
|
|
67
|
+
"InvalidJobStateError",
|
|
68
|
+
"JobExecutionError",
|
|
69
|
+
"RegistryError",
|
|
70
|
+
"ProcessControlError",
|
|
71
|
+
"ValidationError",
|
|
72
|
+
"TimeoutError",
|
|
73
|
+
# Synchronous API
|
|
74
|
+
"QueueSystem",
|
|
75
|
+
"ProcessManager",
|
|
76
|
+
"JobQueue",
|
|
77
|
+
# AsyncIO API
|
|
78
|
+
"AsyncQueueSystem",
|
|
79
|
+
"AsyncProcessManager",
|
|
80
|
+
"async_queue_system_context",
|
|
81
|
+
"get_global_async_queue",
|
|
82
|
+
"shutdown_global_async_queue",
|
|
83
|
+
# Simplified AsyncIO API
|
|
84
|
+
"AsyncSimpleQueue",
|
|
85
|
+
"async_simple_queue_context",
|
|
86
|
+
]
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsyncIO-compatible ProcessManager for queuemgr.
|
|
3
|
+
|
|
4
|
+
This module provides asyncio-compatible versions of ProcessManager
|
|
5
|
+
that work correctly in asyncio applications and web servers.
|
|
6
|
+
|
|
7
|
+
Author: Vasiliy Zdanovskiy
|
|
8
|
+
email: vasilyvz@gmail.com
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import logging
|
|
13
|
+
import signal
|
|
14
|
+
import time
|
|
15
|
+
from multiprocessing import Process, Queue, Event
|
|
16
|
+
from queue import Empty
|
|
17
|
+
from typing import Dict, Any, Optional, Callable
|
|
18
|
+
from contextlib import asynccontextmanager
|
|
19
|
+
|
|
20
|
+
from .queue.job_queue import JobQueue
|
|
21
|
+
from .core.registry import JsonlRegistry
|
|
22
|
+
from queuemgr.core.exceptions import ProcessControlError
|
|
23
|
+
from .process_config import ProcessManagerConfig
|
|
24
|
+
from .process_commands import process_command
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("queuemgr.async_process_manager")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AsyncProcessManager:
|
|
31
|
+
"""
|
|
32
|
+
AsyncIO-compatible process manager for the queue system.
|
|
33
|
+
|
|
34
|
+
Manages the entire queue system in a separate process with automatic
|
|
35
|
+
cleanup and graceful shutdown, designed to work with asyncio applications.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, config: Optional[ProcessManagerConfig] = None):
|
|
39
|
+
"""
|
|
40
|
+
Initialize the async process manager.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
config: Configuration for the process manager.
|
|
44
|
+
"""
|
|
45
|
+
self.config = config or ProcessManagerConfig()
|
|
46
|
+
self._process: Optional[Process] = None
|
|
47
|
+
self._control_queue: Optional[Queue] = None
|
|
48
|
+
self._response_queue: Optional[Queue] = None
|
|
49
|
+
self._shutdown_event: Optional[Event] = None
|
|
50
|
+
self._is_running = False
|
|
51
|
+
self._shutdown_callback: Optional[Callable] = None
|
|
52
|
+
|
|
53
|
+
async def start(self) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Start the process manager in a separate process.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ProcessControlError: If the manager is already running or fails to start.
|
|
59
|
+
"""
|
|
60
|
+
if self._is_running:
|
|
61
|
+
raise ProcessControlError(
|
|
62
|
+
"manager", "start", "Process manager is already running"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Create communication queues
|
|
66
|
+
self._control_queue = Queue()
|
|
67
|
+
self._response_queue = Queue()
|
|
68
|
+
self._shutdown_event = Event()
|
|
69
|
+
|
|
70
|
+
# Start the manager process
|
|
71
|
+
self._process = Process(
|
|
72
|
+
target=self._manager_process,
|
|
73
|
+
name="AsyncQueueManager",
|
|
74
|
+
args=(
|
|
75
|
+
self._control_queue,
|
|
76
|
+
self._response_queue,
|
|
77
|
+
self._shutdown_event,
|
|
78
|
+
self.config,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
self._process.start()
|
|
82
|
+
|
|
83
|
+
# Wait for initialization with asyncio timeout
|
|
84
|
+
try:
|
|
85
|
+
# Use asyncio.wait_for for timeout handling
|
|
86
|
+
response = await asyncio.wait_for(self._get_response_async(), timeout=10.0)
|
|
87
|
+
if response.get("status") != "ready":
|
|
88
|
+
raise ProcessControlError(
|
|
89
|
+
"manager", "start", f"Manager failed to initialize: {response}"
|
|
90
|
+
)
|
|
91
|
+
except asyncio.TimeoutError:
|
|
92
|
+
await self.stop()
|
|
93
|
+
raise ProcessControlError(
|
|
94
|
+
"manager", "start", "Manager initialization timed out"
|
|
95
|
+
)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
await self.stop()
|
|
98
|
+
raise ProcessControlError(
|
|
99
|
+
"manager", "start", f"Failed to start manager: {e}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
self._is_running = True
|
|
103
|
+
|
|
104
|
+
async def stop(self, timeout: Optional[float] = None) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Stop the process manager and all running jobs.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
timeout: Maximum time to wait for graceful shutdown.
|
|
110
|
+
"""
|
|
111
|
+
if not self._is_running:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
timeout = timeout or self.config.shutdown_timeout
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
# Send shutdown command
|
|
118
|
+
if self._control_queue:
|
|
119
|
+
self._control_queue.put({"command": "shutdown"})
|
|
120
|
+
|
|
121
|
+
# Wait for graceful shutdown with asyncio
|
|
122
|
+
if self._process:
|
|
123
|
+
await asyncio.wait_for(
|
|
124
|
+
self._wait_for_process_shutdown(timeout), timeout=timeout
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
except asyncio.TimeoutError:
|
|
128
|
+
# Force terminate if still running
|
|
129
|
+
if self._process and self._process.is_alive():
|
|
130
|
+
self._process.terminate()
|
|
131
|
+
await asyncio.sleep(0.1) # Brief wait
|
|
132
|
+
if self._process.is_alive():
|
|
133
|
+
self._process.kill()
|
|
134
|
+
await asyncio.sleep(0.1)
|
|
135
|
+
|
|
136
|
+
except Exception:
|
|
137
|
+
# Force cleanup
|
|
138
|
+
if self._process and self._process.is_alive():
|
|
139
|
+
self._process.terminate()
|
|
140
|
+
await asyncio.sleep(0.1)
|
|
141
|
+
if self._process.is_alive():
|
|
142
|
+
self._process.kill()
|
|
143
|
+
|
|
144
|
+
finally:
|
|
145
|
+
self._is_running = False
|
|
146
|
+
self._process = None
|
|
147
|
+
self._control_queue = None
|
|
148
|
+
self._response_queue = None
|
|
149
|
+
self._shutdown_event = None
|
|
150
|
+
|
|
151
|
+
async def _wait_for_process_shutdown(self, timeout: float) -> None:
|
|
152
|
+
"""Wait for process shutdown with asyncio."""
|
|
153
|
+
start_time = time.time()
|
|
154
|
+
while self._process and self._process.is_alive():
|
|
155
|
+
if time.time() - start_time > timeout:
|
|
156
|
+
break
|
|
157
|
+
await asyncio.sleep(0.1)
|
|
158
|
+
|
|
159
|
+
async def _get_response_async(self) -> Dict[str, Any]:
|
|
160
|
+
"""Get response from queue asynchronously."""
|
|
161
|
+
loop = asyncio.get_event_loop()
|
|
162
|
+
|
|
163
|
+
def get_response():
|
|
164
|
+
try:
|
|
165
|
+
return self._response_queue.get(timeout=0.1)
|
|
166
|
+
except Exception:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
# Poll the queue with short timeouts to avoid blocking
|
|
170
|
+
for _ in range(100): # 10 seconds total
|
|
171
|
+
result = await loop.run_in_executor(None, get_response)
|
|
172
|
+
if result is not None:
|
|
173
|
+
return result
|
|
174
|
+
await asyncio.sleep(0.1)
|
|
175
|
+
|
|
176
|
+
raise asyncio.TimeoutError("No response received")
|
|
177
|
+
|
|
178
|
+
def is_running(self) -> bool:
|
|
179
|
+
"""Check if the manager is running."""
|
|
180
|
+
return (
|
|
181
|
+
self._is_running and self._process is not None and self._process.is_alive()
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
async def add_job(
|
|
185
|
+
self, job_class: type, job_id: str, params: Dict[str, Any]
|
|
186
|
+
) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Add a job to the queue.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
job_class: Job class to instantiate.
|
|
192
|
+
job_id: Unique job identifier.
|
|
193
|
+
params: Job parameters.
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
ProcessControlError: If the manager is not running or command fails.
|
|
197
|
+
"""
|
|
198
|
+
if not self.is_running():
|
|
199
|
+
raise ProcessControlError("manager", "add_job", "Manager is not running")
|
|
200
|
+
|
|
201
|
+
await self._send_command_async(
|
|
202
|
+
"add_job", {"job_class": job_class, "job_id": job_id, "params": params}
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
async def start_job(self, job_id: str) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Start a job.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
job_id: Job identifier.
|
|
211
|
+
|
|
212
|
+
Raises:
|
|
213
|
+
ProcessControlError: If the manager is not running or command fails.
|
|
214
|
+
"""
|
|
215
|
+
if not self.is_running():
|
|
216
|
+
raise ProcessControlError("manager", "start_job", "Manager is not running")
|
|
217
|
+
|
|
218
|
+
await self._send_command_async("start_job", {"job_id": job_id})
|
|
219
|
+
|
|
220
|
+
async def stop_job(self, job_id: str) -> None:
|
|
221
|
+
"""
|
|
222
|
+
Stop a job.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
job_id: Job identifier.
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
ProcessControlError: If the manager is not running or command fails.
|
|
229
|
+
"""
|
|
230
|
+
if not self.is_running():
|
|
231
|
+
raise ProcessControlError("manager", "stop_job", "Manager is not running")
|
|
232
|
+
|
|
233
|
+
await self._send_command_async("stop_job", {"job_id": job_id})
|
|
234
|
+
|
|
235
|
+
async def delete_job(self, job_id: str, force: bool = False) -> None:
|
|
236
|
+
"""
|
|
237
|
+
Delete a job.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
job_id: Job identifier.
|
|
241
|
+
force: Force deletion even if job is running.
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
ProcessControlError: If the manager is not running or command fails.
|
|
245
|
+
"""
|
|
246
|
+
if not self.is_running():
|
|
247
|
+
raise ProcessControlError("manager", "delete_job", "Manager is not running")
|
|
248
|
+
|
|
249
|
+
await self._send_command_async("delete_job", {"job_id": job_id, "force": force})
|
|
250
|
+
|
|
251
|
+
async def get_job_status(self, job_id: str) -> Dict[str, Any]:
|
|
252
|
+
"""
|
|
253
|
+
Get job status.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
job_id: Job identifier.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Job status information.
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
ProcessControlError: If the manager is not running or command fails.
|
|
263
|
+
"""
|
|
264
|
+
if not self.is_running():
|
|
265
|
+
raise ProcessControlError(
|
|
266
|
+
"manager", "get_job_status", "Manager is not running"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return await self._send_command_async("get_job_status", {"job_id": job_id})
|
|
270
|
+
|
|
271
|
+
async def list_jobs(self) -> list:
|
|
272
|
+
"""
|
|
273
|
+
List all jobs.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
List of job information.
|
|
277
|
+
|
|
278
|
+
Raises:
|
|
279
|
+
ProcessControlError: If the manager is not running or command fails.
|
|
280
|
+
"""
|
|
281
|
+
if not self.is_running():
|
|
282
|
+
raise ProcessControlError("manager", "list_jobs", "Manager is not running")
|
|
283
|
+
|
|
284
|
+
return await self._send_command_async("list_jobs", {})
|
|
285
|
+
|
|
286
|
+
async def _send_command_async(
|
|
287
|
+
self, command: str, params: Dict[str, Any]
|
|
288
|
+
) -> Dict[str, Any]:
|
|
289
|
+
"""
|
|
290
|
+
Send a command to the manager process and wait for response asynchronously.
|
|
291
|
+
"""
|
|
292
|
+
try:
|
|
293
|
+
if self._control_queue and self._response_queue:
|
|
294
|
+
# Send command in executor to avoid blocking
|
|
295
|
+
loop = asyncio.get_event_loop()
|
|
296
|
+
|
|
297
|
+
def send_command():
|
|
298
|
+
self._control_queue.put({"command": command, "params": params})
|
|
299
|
+
|
|
300
|
+
await loop.run_in_executor(None, send_command)
|
|
301
|
+
|
|
302
|
+
# Get response with timeout
|
|
303
|
+
try:
|
|
304
|
+
response = await asyncio.wait_for(
|
|
305
|
+
self._get_response_async(), timeout=30.0
|
|
306
|
+
)
|
|
307
|
+
except asyncio.TimeoutError:
|
|
308
|
+
logger.warning(
|
|
309
|
+
"Async manager command '%s' timed out after %.1fs",
|
|
310
|
+
command,
|
|
311
|
+
30.0,
|
|
312
|
+
)
|
|
313
|
+
raise ProcessControlError(
|
|
314
|
+
"manager", command, "Command timed out waiting for response"
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
raise ProcessControlError("manager", command, "Queues not initialized")
|
|
318
|
+
|
|
319
|
+
if response.get("status") == "error":
|
|
320
|
+
error_message = response.get("error", "Unknown error")
|
|
321
|
+
logger.error(
|
|
322
|
+
"Async manager command '%s' failed inside manager: %s",
|
|
323
|
+
command,
|
|
324
|
+
error_message,
|
|
325
|
+
)
|
|
326
|
+
raise ProcessControlError("manager", command, error_message)
|
|
327
|
+
|
|
328
|
+
return response.get("result")
|
|
329
|
+
|
|
330
|
+
except asyncio.TimeoutError:
|
|
331
|
+
logger.warning(
|
|
332
|
+
"Async manager command '%s' exceeded response timeout", command
|
|
333
|
+
)
|
|
334
|
+
raise ProcessControlError(
|
|
335
|
+
"manager", command, "Command timed out waiting for response"
|
|
336
|
+
)
|
|
337
|
+
except Exception as e:
|
|
338
|
+
logger.error(
|
|
339
|
+
"Async manager command '%s' failed unexpectedly: %s", command, e
|
|
340
|
+
)
|
|
341
|
+
raise ProcessControlError("manager", command, f"Command failed: {e}")
|
|
342
|
+
|
|
343
|
+
@staticmethod
|
|
344
|
+
def _manager_process(
|
|
345
|
+
control_queue: Queue,
|
|
346
|
+
response_queue: Queue,
|
|
347
|
+
shutdown_event: Event,
|
|
348
|
+
config: ProcessManagerConfig,
|
|
349
|
+
) -> None:
|
|
350
|
+
"""
|
|
351
|
+
Main process function for the manager.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
control_queue: Queue for receiving commands.
|
|
355
|
+
response_queue: Queue for sending responses.
|
|
356
|
+
shutdown_event: Event for shutdown signaling.
|
|
357
|
+
config: Manager configuration.
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
# Set up signal handlers for graceful shutdown
|
|
361
|
+
def signal_handler(signum, frame):
|
|
362
|
+
"""
|
|
363
|
+
Handle OS signals for graceful shutdown.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
signum: Signal number.
|
|
367
|
+
frame: Current stack frame.
|
|
368
|
+
"""
|
|
369
|
+
shutdown_event.set()
|
|
370
|
+
|
|
371
|
+
# Only register signals in the main thread
|
|
372
|
+
try:
|
|
373
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
374
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
375
|
+
except ValueError:
|
|
376
|
+
# Signals can only be registered in the main thread
|
|
377
|
+
# This is expected in subprocesses, so we ignore the error
|
|
378
|
+
pass
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
# Initialize the queue system
|
|
382
|
+
registry = JsonlRegistry(config.registry_path)
|
|
383
|
+
job_queue = JobQueue(
|
|
384
|
+
registry,
|
|
385
|
+
max_queue_size=config.max_queue_size,
|
|
386
|
+
per_job_type_limits=config.per_job_type_limits,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Signal that we're ready
|
|
390
|
+
response_queue.put({"status": "ready"})
|
|
391
|
+
|
|
392
|
+
# Main command loop
|
|
393
|
+
cleanup_timer = time.time()
|
|
394
|
+
|
|
395
|
+
while not shutdown_event.is_set():
|
|
396
|
+
try:
|
|
397
|
+
command_data: Optional[Dict[str, Any]] = None
|
|
398
|
+
try:
|
|
399
|
+
command_data = control_queue.get(timeout=1.0)
|
|
400
|
+
except Empty:
|
|
401
|
+
# No commands were received during this window.
|
|
402
|
+
pass
|
|
403
|
+
|
|
404
|
+
if command_data:
|
|
405
|
+
command = command_data.get("command")
|
|
406
|
+
params = command_data.get("params", {})
|
|
407
|
+
|
|
408
|
+
if command == "shutdown":
|
|
409
|
+
break
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
result = process_command(job_queue, command, params)
|
|
413
|
+
except (
|
|
414
|
+
Exception
|
|
415
|
+
) as command_error: # pylint: disable=broad-except
|
|
416
|
+
error_message = (
|
|
417
|
+
f"[manager] Command '{command}' failed: {command_error}"
|
|
418
|
+
)
|
|
419
|
+
logger.exception(
|
|
420
|
+
"Async queue manager failed to process command '%s'",
|
|
421
|
+
command,
|
|
422
|
+
)
|
|
423
|
+
response_queue.put(
|
|
424
|
+
{"status": "error", "error": error_message}
|
|
425
|
+
)
|
|
426
|
+
else:
|
|
427
|
+
response_queue.put({"status": "success", "result": result})
|
|
428
|
+
|
|
429
|
+
# Periodic cleanup
|
|
430
|
+
if time.time() - cleanup_timer > config.cleanup_interval:
|
|
431
|
+
job_queue.cleanup_completed_jobs()
|
|
432
|
+
cleanup_timer = time.time()
|
|
433
|
+
|
|
434
|
+
except Exception as loop_error: # pylint: disable=broad-except
|
|
435
|
+
error_message = f"[manager] Command loop failed: {loop_error}"
|
|
436
|
+
logger.exception("Async queue manager loop failure")
|
|
437
|
+
response_queue.put({"status": "error", "error": error_message})
|
|
438
|
+
|
|
439
|
+
# Graceful shutdown
|
|
440
|
+
job_queue.shutdown()
|
|
441
|
+
|
|
442
|
+
except Exception as e:
|
|
443
|
+
response_queue.put(
|
|
444
|
+
{"status": "error", "error": f"Manager initialization failed: {e}"}
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@asynccontextmanager
|
|
449
|
+
async def async_queue_system(
|
|
450
|
+
registry_path: str = "queuemgr_registry.jsonl",
|
|
451
|
+
shutdown_timeout: float = 30.0,
|
|
452
|
+
):
|
|
453
|
+
"""
|
|
454
|
+
AsyncIO-compatible context manager for the queue system.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
registry_path: Path to the registry file.
|
|
458
|
+
shutdown_timeout: Timeout for graceful shutdown.
|
|
459
|
+
|
|
460
|
+
Yields:
|
|
461
|
+
AsyncProcessManager: The async process manager instance.
|
|
462
|
+
"""
|
|
463
|
+
config = ProcessManagerConfig(
|
|
464
|
+
registry_path=registry_path, shutdown_timeout=shutdown_timeout
|
|
465
|
+
)
|
|
466
|
+
manager = AsyncProcessManager(config)
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
await manager.start()
|
|
470
|
+
yield manager
|
|
471
|
+
finally:
|
|
472
|
+
await manager.stop()
|