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.
Files changed (78) hide show
  1. queuemgr/__init__.py +86 -0
  2. queuemgr/async_process_manager.py +472 -0
  3. queuemgr/async_simple_api.py +323 -0
  4. queuemgr/async_simple_queue.py +371 -0
  5. queuemgr/constants.py +70 -0
  6. queuemgr/core/__init__.py +6 -0
  7. queuemgr/core/exceptions.py +63 -0
  8. queuemgr/core/ipc.py +38 -0
  9. queuemgr/core/ipc_manager.py +54 -0
  10. queuemgr/core/ipc_operations.py +351 -0
  11. queuemgr/core/registry.py +266 -0
  12. queuemgr/core/types.py +75 -0
  13. queuemgr/examples/__init__.py +6 -0
  14. queuemgr/examples/async_fastapi_example.py +330 -0
  15. queuemgr/examples/async_web_example.py +301 -0
  16. queuemgr/examples/data_analyzer.py +178 -0
  17. queuemgr/examples/error_handling_job.py +135 -0
  18. queuemgr/examples/full_app_example.py +158 -0
  19. queuemgr/examples/integration_examples/cli_integration.py +320 -0
  20. queuemgr/examples/integration_examples/flask_api.py +209 -0
  21. queuemgr/examples/integration_examples/flask_integration.py +16 -0
  22. queuemgr/examples/integration_examples/flask_routes.py +109 -0
  23. queuemgr/examples/integration_examples/systemd_integration.py +286 -0
  24. queuemgr/examples/jobs/__init__.py +14 -0
  25. queuemgr/examples/jobs/api_call_job.py +82 -0
  26. queuemgr/examples/jobs/data_processing_job.py +72 -0
  27. queuemgr/examples/jobs/file_operation_job.py +77 -0
  28. queuemgr/examples/large_data_generator.py +234 -0
  29. queuemgr/examples/large_result_job.py +109 -0
  30. queuemgr/examples/mcp_adapter_example.py +520 -0
  31. queuemgr/examples/proc_manager_example.py +150 -0
  32. queuemgr/examples/progress_job.py +137 -0
  33. queuemgr/examples/registry_example.py +161 -0
  34. queuemgr/examples/result_job.py +358 -0
  35. queuemgr/examples/service_example.py +380 -0
  36. queuemgr/examples/simple_job.py +125 -0
  37. queuemgr/examples/simple_manager_example.py +132 -0
  38. queuemgr/examples/simple_mcp_example.py +195 -0
  39. queuemgr/examples/working_mcp_server.py +258 -0
  40. queuemgr/exceptions.py +165 -0
  41. queuemgr/jobs/__init__.py +6 -0
  42. queuemgr/jobs/base.py +15 -0
  43. queuemgr/jobs/base_core.py +311 -0
  44. queuemgr/jobs/exceptions.py +47 -0
  45. queuemgr/proc_api.py +297 -0
  46. queuemgr/proc_config.py +22 -0
  47. queuemgr/proc_ipc.py +103 -0
  48. queuemgr/proc_manager.py +16 -0
  49. queuemgr/proc_manager_core.py +406 -0
  50. queuemgr/process_commands.py +59 -0
  51. queuemgr/process_config.py +23 -0
  52. queuemgr/process_context.py +38 -0
  53. queuemgr/process_core.py +367 -0
  54. queuemgr/process_manager.py +17 -0
  55. queuemgr/queue/__init__.py +6 -0
  56. queuemgr/queue/exceptions.py +58 -0
  57. queuemgr/queue/job_queue.py +457 -0
  58. queuemgr/simple_api.py +281 -0
  59. queuemgr-1.0.7.dist-info/METADATA +148 -0
  60. queuemgr-1.0.7.dist-info/RECORD +78 -0
  61. queuemgr-1.0.7.dist-info/WHEEL +5 -0
  62. queuemgr-1.0.7.dist-info/entry_points.txt +4 -0
  63. queuemgr-1.0.7.dist-info/licenses/LICENSE +21 -0
  64. queuemgr-1.0.7.dist-info/top_level.txt +2 -0
  65. tests/__init__.py +6 -0
  66. tests/core/test_ipc.py +269 -0
  67. tests/core/test_registry.py +360 -0
  68. tests/core/test_types.py +191 -0
  69. tests/jobs/test_base.py +20 -0
  70. tests/jobs/test_base_initialization.py +133 -0
  71. tests/jobs/test_base_job_loop.py +216 -0
  72. tests/jobs/test_base_process_control.py +163 -0
  73. tests/queue/test_job_queue.py +15 -0
  74. tests/queue/test_job_queue_basic.py +181 -0
  75. tests/queue/test_job_queue_list_jobs.py +92 -0
  76. tests/queue/test_job_queue_operations.py +252 -0
  77. tests/queue/test_job_queue_per_type_limits.py +331 -0
  78. 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()