queuemgr 1.0.0__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 +13 -0
- queuemgr/constants.py +70 -0
- queuemgr/core/__init__.py +6 -0
- queuemgr/core/exceptions.py +61 -0
- queuemgr/core/ipc.py +38 -0
- queuemgr/core/ipc_manager.py +54 -0
- queuemgr/core/ipc_operations.py +352 -0
- queuemgr/core/registry.py +263 -0
- queuemgr/core/types.py +75 -0
- queuemgr/examples/__init__.py +6 -0
- queuemgr/examples/data_analyzer.py +188 -0
- queuemgr/examples/error_handling_job.py +135 -0
- queuemgr/examples/full_app_example.py +155 -0
- queuemgr/examples/integration_examples/cli_integration.py +319 -0
- queuemgr/examples/integration_examples/flask_api.py +206 -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 +278 -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 +78 -0
- queuemgr/examples/large_data_generator.py +234 -0
- queuemgr/examples/large_result_job.py +114 -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 +351 -0
- queuemgr/examples/service_example.py +380 -0
- queuemgr/examples/simple_job.py +125 -0
- queuemgr/examples/simple_manager_example.py +132 -0
- queuemgr/exceptions.py +160 -0
- queuemgr/jobs/__init__.py +6 -0
- queuemgr/jobs/base.py +15 -0
- queuemgr/jobs/base_core.py +304 -0
- queuemgr/jobs/exceptions.py +47 -0
- queuemgr/proc_api.py +297 -0
- queuemgr/proc_config.py +22 -0
- queuemgr/proc_ipc.py +105 -0
- queuemgr/proc_manager.py +16 -0
- queuemgr/proc_manager_core.py +399 -0
- queuemgr/process_commands.py +57 -0
- queuemgr/process_config.py +20 -0
- queuemgr/process_context.py +38 -0
- queuemgr/process_core.py +335 -0
- queuemgr/process_manager.py +17 -0
- queuemgr/queue/__init__.py +6 -0
- queuemgr/queue/exceptions.py +58 -0
- queuemgr/queue/job_queue.py +336 -0
- queuemgr/simple_api.py +281 -0
- queuemgr-1.0.0.dist-info/METADATA +148 -0
- queuemgr-1.0.0.dist-info/RECORD +68 -0
- queuemgr-1.0.0.dist-info/WHEEL +5 -0
- queuemgr-1.0.0.dist-info/entry_points.txt +4 -0
- queuemgr-1.0.0.dist-info/licenses/LICENSE +21 -0
- queuemgr-1.0.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +6 -0
- tests/core/test_ipc.py +269 -0
- tests/core/test_registry.py +344 -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 +18 -0
- tests/queue/test_job_queue_basic.py +181 -0
- tests/queue/test_job_queue_operations.py +242 -0
- tests/test_exceptions.py +184 -0
queuemgr/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
Author: Vasiliy Zdanovskiy
|
|
8
|
+
email: vasilyvz@gmail.com
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "1.0.1"
|
|
12
|
+
__author__ = "Vasiliy Zdanovskiy"
|
|
13
|
+
__email__ = "vasilyvz@gmail.com"
|
queuemgr/constants.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Global constants and configuration keys for the queue manager system.
|
|
3
|
+
|
|
4
|
+
Author: Vasiliy Zdanovskiy
|
|
5
|
+
email: vasilyvz@gmail.com
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Final
|
|
9
|
+
|
|
10
|
+
# Default timeout values (in seconds)
|
|
11
|
+
DEFAULT_STOP_TIMEOUT: Final[float] = 10.0
|
|
12
|
+
DEFAULT_SHUTDOWN_TIMEOUT: Final[float] = 30.0
|
|
13
|
+
DEFAULT_TERMINATE_TIMEOUT: Final[float] = 5.0
|
|
14
|
+
|
|
15
|
+
# Progress limits
|
|
16
|
+
MIN_PROGRESS: Final[int] = 0
|
|
17
|
+
MAX_PROGRESS: Final[int] = 100
|
|
18
|
+
|
|
19
|
+
# Registry configuration
|
|
20
|
+
DEFAULT_REGISTRY_PATH: Final[str] = "runtime/registry.jsonl"
|
|
21
|
+
REGISTRY_ENCODING: Final[str] = "utf-8"
|
|
22
|
+
|
|
23
|
+
# Process configuration
|
|
24
|
+
DEFAULT_PROCESS_NAME_PREFIX: Final[str] = "Job-"
|
|
25
|
+
DEFAULT_JOB_LOOP_DELAY: Final[float] = 0.1
|
|
26
|
+
|
|
27
|
+
# File system configuration
|
|
28
|
+
DEFAULT_RUNTIME_DIR: Final[str] = "runtime"
|
|
29
|
+
DEFAULT_EXAMPLES_DIR: Final[str] = "examples"
|
|
30
|
+
|
|
31
|
+
# Error messages
|
|
32
|
+
ERROR_JOB_NOT_FOUND: Final[str] = "Job with ID '{job_id}' not found"
|
|
33
|
+
ERROR_JOB_ALREADY_EXISTS: Final[str] = "Job with ID '{job_id}' already exists"
|
|
34
|
+
ERROR_INVALID_JOB_STATE: Final[str] = (
|
|
35
|
+
"Cannot {operation} job '{job_id}' in state '{state}'"
|
|
36
|
+
)
|
|
37
|
+
ERROR_JOB_EXECUTION_FAILED: Final[str] = "Job '{job_id}' execution failed"
|
|
38
|
+
ERROR_REGISTRY_OPERATION_FAILED: Final[str] = "Registry operation failed: {message}"
|
|
39
|
+
ERROR_PROCESS_CONTROL_FAILED: Final[str] = (
|
|
40
|
+
"Process control error for job '{job_id}' during {operation}"
|
|
41
|
+
)
|
|
42
|
+
ERROR_VALIDATION_FAILED: Final[str] = "Validation error for field '{field}': {reason}"
|
|
43
|
+
ERROR_OPERATION_TIMEOUT: Final[str] = (
|
|
44
|
+
"Operation '{operation}' timed out after {timeout} seconds"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Status descriptions
|
|
48
|
+
DESCRIPTION_JOB_CREATED: Final[str] = "Job created"
|
|
49
|
+
DESCRIPTION_JOB_STARTED: Final[str] = "Job started"
|
|
50
|
+
DESCRIPTION_JOB_STOPPED: Final[str] = "Job stopped by user"
|
|
51
|
+
DESCRIPTION_JOB_DELETED: Final[str] = "Job deleted"
|
|
52
|
+
DESCRIPTION_JOB_COMPLETED: Final[str] = "Job completed successfully"
|
|
53
|
+
DESCRIPTION_JOB_FAILED: Final[str] = "Job failed: {error}"
|
|
54
|
+
DESCRIPTION_JOB_INTERRUPTED: Final[str] = "Job interrupted"
|
|
55
|
+
|
|
56
|
+
# Configuration keys
|
|
57
|
+
CONFIG_REGISTRY_PATH: Final[str] = "registry_path"
|
|
58
|
+
CONFIG_MAX_CONCURRENT_JOBS: Final[str] = "max_concurrent_jobs"
|
|
59
|
+
CONFIG_DEFAULT_TIMEOUT: Final[str] = "default_timeout"
|
|
60
|
+
CONFIG_LOG_LEVEL: Final[str] = "log_level"
|
|
61
|
+
CONFIG_LOG_FORMAT: Final[str] = "log_format"
|
|
62
|
+
|
|
63
|
+
# Logging configuration
|
|
64
|
+
DEFAULT_LOG_LEVEL: Final[str] = "INFO"
|
|
65
|
+
DEFAULT_LOG_FORMAT: Final[str] = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
66
|
+
|
|
67
|
+
# Version information
|
|
68
|
+
VERSION: Final[str] = "0.1.0"
|
|
69
|
+
AUTHOR: Final[str] = "Vasiliy Zdanovskiy"
|
|
70
|
+
EMAIL: Final[str] = "vasilyvz@gmail.com"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core exceptions for IPC and registry operations.
|
|
3
|
+
|
|
4
|
+
This module contains specific exceptions for core functionality.
|
|
5
|
+
|
|
6
|
+
Author: Vasiliy Zdanovskiy
|
|
7
|
+
email: vasilyvz@gmail.com
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ..exceptions import QueueManagerError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IPCError(QueueManagerError):
|
|
14
|
+
"""Raised when IPC operations fail."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, operation: str, message: str) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Initialize IPCError.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
operation: The IPC operation that failed.
|
|
22
|
+
message: Error message.
|
|
23
|
+
"""
|
|
24
|
+
self.operation = operation
|
|
25
|
+
super().__init__(f"IPC error during {operation}: {message}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RegistryError(QueueManagerError):
|
|
29
|
+
"""Raised when registry operations fail."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, message: str, original_error: Exception = None) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Initialize RegistryError.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
message: Error message.
|
|
37
|
+
original_error: The original exception that caused the failure.
|
|
38
|
+
"""
|
|
39
|
+
self.original_error = original_error
|
|
40
|
+
super().__init__(f"Registry error: {message}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ProcessControlError(QueueManagerError):
|
|
44
|
+
"""Raised when process control operations fail."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, job_id: str, operation: str, original_error: Exception = None) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Initialize ProcessControlError.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
job_id: The job ID.
|
|
52
|
+
operation: The operation that failed.
|
|
53
|
+
original_error: The original exception that caused the failure.
|
|
54
|
+
"""
|
|
55
|
+
self.job_id = job_id
|
|
56
|
+
self.operation = operation
|
|
57
|
+
self.original_error = original_error
|
|
58
|
+
error_msg = f"Process control error for job '{job_id}' during {operation}"
|
|
59
|
+
if original_error:
|
|
60
|
+
error_msg += f": {original_error}"
|
|
61
|
+
super().__init__(error_msg)
|
queuemgr/core/ipc.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Inter-process communication for Queue Manager.
|
|
3
|
+
|
|
4
|
+
This module provides IPC functionality for job state management,
|
|
5
|
+
commands, and progress tracking using multiprocessing.
|
|
6
|
+
|
|
7
|
+
Author: Vasiliy Zdanovskiy
|
|
8
|
+
email: vasilyvz@gmail.com
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Import all functionality from separate modules
|
|
12
|
+
from .ipc_manager import get_manager, create_job_shared_state
|
|
13
|
+
from .ipc_operations import (
|
|
14
|
+
with_job_lock,
|
|
15
|
+
update_job_state,
|
|
16
|
+
read_job_state,
|
|
17
|
+
set_command,
|
|
18
|
+
get_command,
|
|
19
|
+
clear_command,
|
|
20
|
+
set_status,
|
|
21
|
+
set_progress,
|
|
22
|
+
get_progress
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Re-export for backward compatibility
|
|
26
|
+
__all__ = [
|
|
27
|
+
'get_manager',
|
|
28
|
+
'create_job_shared_state',
|
|
29
|
+
'with_job_lock',
|
|
30
|
+
'update_job_state',
|
|
31
|
+
'read_job_state',
|
|
32
|
+
'set_command',
|
|
33
|
+
'get_command',
|
|
34
|
+
'clear_command',
|
|
35
|
+
'set_status',
|
|
36
|
+
'set_progress',
|
|
37
|
+
'get_progress'
|
|
38
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IPC manager for creating and managing shared state.
|
|
3
|
+
|
|
4
|
+
This module contains the manager creation and shared state
|
|
5
|
+
management functionality.
|
|
6
|
+
|
|
7
|
+
Author: Vasiliy Zdanovskiy
|
|
8
|
+
email: vasilyvz@gmail.com
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from multiprocessing import Manager
|
|
12
|
+
from typing import Dict, Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_manager() -> Dict[str, Any]:
|
|
16
|
+
"""
|
|
17
|
+
Return a process-shared Manager instance for the queue runtime.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Manager: A multiprocessing Manager instance for creating shared
|
|
21
|
+
objects.
|
|
22
|
+
"""
|
|
23
|
+
return Manager()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_job_shared_state(manager: Dict[str, Any]) -> Dict[str, Any]:
|
|
27
|
+
"""
|
|
28
|
+
Create and return shared variables for a job.
|
|
29
|
+
|
|
30
|
+
Creates shared state including status, command, progress, description,
|
|
31
|
+
result, and mutex. All shared variables are thread/process safe and can
|
|
32
|
+
be accessed from multiple processes.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
manager: Multiprocessing Manager instance.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Dict containing shared state variables:
|
|
39
|
+
- status: Shared integer for job status
|
|
40
|
+
- command: Shared integer for job command
|
|
41
|
+
- progress: Shared integer for job progress (0-100)
|
|
42
|
+
- description: Shared string for job description
|
|
43
|
+
- result: Shared value for job result
|
|
44
|
+
- lock: Shared mutex for thread safety
|
|
45
|
+
"""
|
|
46
|
+
shared_state = {
|
|
47
|
+
"status": manager.Value("i", 0), # JobStatus enum value
|
|
48
|
+
"command": manager.Value("i", 0), # JobCommand enum value
|
|
49
|
+
"progress": manager.Value("i", 0), # 0-100
|
|
50
|
+
"description": manager.Value("c", b""), # UTF-8 encoded string
|
|
51
|
+
"result": manager.Value("O", None), # Any Python object
|
|
52
|
+
"lock": manager.Lock(), # Mutex for thread safety
|
|
53
|
+
}
|
|
54
|
+
return shared_state
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IPC operations for job state management.
|
|
3
|
+
|
|
4
|
+
This module contains the IPC operations for managing job state,
|
|
5
|
+
commands, and progress.
|
|
6
|
+
|
|
7
|
+
Author: Vasiliy Zdanovskiy
|
|
8
|
+
email: vasilyvz@gmail.com
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from typing import Dict, Any, Generator, Optional, Union, List
|
|
13
|
+
from .types import JobCommand, JobStatus
|
|
14
|
+
from .exceptions import IPCError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@contextmanager
|
|
18
|
+
def with_job_lock(shared_state: Dict[str, Any]) -> Generator[None, None, None]:
|
|
19
|
+
"""
|
|
20
|
+
Context manager for acquiring the job's mutex for consistent updates/reads.
|
|
21
|
+
|
|
22
|
+
Ensures atomic access to multiple shared fields by acquiring the job's lock
|
|
23
|
+
before performing operations and releasing it when done.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
shared_state: Dictionary containing shared state variables.
|
|
27
|
+
|
|
28
|
+
Yields:
|
|
29
|
+
None: Context manager yields control to the caller.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
ValueError: If shared_state doesn't contain a 'lock' key.
|
|
33
|
+
"""
|
|
34
|
+
lock = shared_state.get("lock")
|
|
35
|
+
if lock is None:
|
|
36
|
+
raise ValueError("Shared state must contain a 'lock' key")
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
lock.acquire()
|
|
40
|
+
yield
|
|
41
|
+
finally:
|
|
42
|
+
try:
|
|
43
|
+
lock.release()
|
|
44
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
45
|
+
# Process is shutting down, ignore
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def update_job_state(
|
|
50
|
+
shared_state: Dict[str, Any],
|
|
51
|
+
status: JobStatus = None,
|
|
52
|
+
command: JobCommand = None,
|
|
53
|
+
progress: int = None,
|
|
54
|
+
description: str = None,
|
|
55
|
+
result: Optional[Union[str, int, float, bool, Dict[str, Any], List[Any]]] = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Update job state variables atomically.
|
|
59
|
+
|
|
60
|
+
Updates multiple shared state variables in a single atomic operation
|
|
61
|
+
to ensure consistency across processes.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
shared_state: Dictionary containing shared state variables.
|
|
65
|
+
status: New job status (optional).
|
|
66
|
+
command: New job command (optional).
|
|
67
|
+
progress: New job progress 0-100 (optional).
|
|
68
|
+
description: New job description (optional).
|
|
69
|
+
result: New job result (optional).
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValueError: If shared_state doesn't contain required keys.
|
|
73
|
+
ValueError: If progress is not between 0 and 100.
|
|
74
|
+
"""
|
|
75
|
+
lock = shared_state.get("lock")
|
|
76
|
+
if lock is None:
|
|
77
|
+
raise ValueError("Shared state must contain a 'lock' key")
|
|
78
|
+
|
|
79
|
+
if progress is not None and not (0 <= progress <= 100):
|
|
80
|
+
raise ValueError("Progress must be between 0 and 100")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
lock.acquire()
|
|
84
|
+
|
|
85
|
+
if status is not None:
|
|
86
|
+
try:
|
|
87
|
+
shared_state["status"].value = status.value
|
|
88
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
89
|
+
# Process is shutting down, ignore
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
if command is not None:
|
|
93
|
+
try:
|
|
94
|
+
shared_state["command"].value = command.value
|
|
95
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
96
|
+
# Process is shutting down, ignore
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
if progress is not None:
|
|
100
|
+
try:
|
|
101
|
+
shared_state["progress"].value = progress
|
|
102
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
103
|
+
# Process is shutting down, ignore
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
if description is not None:
|
|
107
|
+
try:
|
|
108
|
+
shared_state["description"].value = description.encode("utf-8")
|
|
109
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
110
|
+
# Process is shutting down, ignore
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
if result is not None:
|
|
114
|
+
try:
|
|
115
|
+
shared_state["result"].value = result
|
|
116
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
117
|
+
# Process is shutting down, ignore
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
finally:
|
|
121
|
+
try:
|
|
122
|
+
lock.release()
|
|
123
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
124
|
+
# Process is shutting down, ignore
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def read_job_state(shared_state: Dict[str, Any]) -> Dict[str, Any]:
|
|
129
|
+
"""
|
|
130
|
+
Read all job state variables atomically.
|
|
131
|
+
|
|
132
|
+
Reads all shared state variables in a single atomic operation
|
|
133
|
+
to ensure consistency across processes.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
shared_state: Dictionary containing shared state variables.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Dictionary containing current job state:
|
|
140
|
+
- status: Current job status
|
|
141
|
+
- command: Current job command
|
|
142
|
+
- progress: Current job progress (0-100)
|
|
143
|
+
- description: Current job description
|
|
144
|
+
- result: Current job result
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
ValueError: If shared_state doesn't contain required keys.
|
|
148
|
+
"""
|
|
149
|
+
lock = shared_state.get("lock")
|
|
150
|
+
if lock is None:
|
|
151
|
+
raise ValueError("Shared state must contain a 'lock' key")
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
lock.acquire()
|
|
155
|
+
try:
|
|
156
|
+
return {
|
|
157
|
+
"status": JobStatus(shared_state["status"].value),
|
|
158
|
+
"command": JobCommand(shared_state["command"].value),
|
|
159
|
+
"progress": shared_state["progress"].value,
|
|
160
|
+
"description": (
|
|
161
|
+
shared_state["description"].value.decode("utf-8")
|
|
162
|
+
if shared_state["description"].value
|
|
163
|
+
else ""
|
|
164
|
+
),
|
|
165
|
+
"result": shared_state["result"].value,
|
|
166
|
+
}
|
|
167
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
168
|
+
# Process is shutting down, return default state
|
|
169
|
+
return {
|
|
170
|
+
"status": JobStatus.PENDING,
|
|
171
|
+
"command": JobCommand.NONE,
|
|
172
|
+
"progress": 0,
|
|
173
|
+
"description": "",
|
|
174
|
+
"result": None,
|
|
175
|
+
}
|
|
176
|
+
finally:
|
|
177
|
+
try:
|
|
178
|
+
lock.release()
|
|
179
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
180
|
+
# Process is shutting down, ignore
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def set_command(shared_state: Dict[str, Any], command: JobCommand) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Set the job command.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
shared_state: Dictionary containing shared state variables.
|
|
190
|
+
command: Command to set.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
ValueError: If shared_state doesn't contain required keys.
|
|
194
|
+
"""
|
|
195
|
+
lock = shared_state.get("lock")
|
|
196
|
+
if lock is None:
|
|
197
|
+
raise ValueError("Shared state must contain a 'lock' key")
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
lock.acquire()
|
|
201
|
+
try:
|
|
202
|
+
shared_state["command"].value = command.value
|
|
203
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
204
|
+
# Process is shutting down, ignore
|
|
205
|
+
pass
|
|
206
|
+
finally:
|
|
207
|
+
try:
|
|
208
|
+
lock.release()
|
|
209
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
210
|
+
# Process is shutting down, ignore
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def get_command(shared_state: Dict[str, Any]) -> JobCommand:
|
|
215
|
+
"""
|
|
216
|
+
Get the current command for the job.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
shared_state: Dictionary containing shared state variables.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Current job command.
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
ValueError: If shared_state doesn't contain required keys.
|
|
226
|
+
"""
|
|
227
|
+
lock = shared_state.get("lock")
|
|
228
|
+
if lock is None:
|
|
229
|
+
raise ValueError("Shared state must contain a 'lock' key")
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
lock.acquire()
|
|
233
|
+
try:
|
|
234
|
+
return JobCommand(shared_state["command"].value)
|
|
235
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
236
|
+
# Process is shutting down, return NONE command
|
|
237
|
+
return JobCommand.NONE
|
|
238
|
+
finally:
|
|
239
|
+
try:
|
|
240
|
+
lock.release()
|
|
241
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
242
|
+
# Process is shutting down, ignore
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def clear_command(shared_state: Dict[str, Any]) -> None:
|
|
247
|
+
"""
|
|
248
|
+
Clear the job command (set to NONE).
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
shared_state: Dictionary containing shared state variables.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
ValueError: If shared_state doesn't contain required keys.
|
|
255
|
+
"""
|
|
256
|
+
set_command(shared_state, JobCommand.NONE)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def set_status(shared_state: Dict[str, Any], status: JobStatus) -> None:
|
|
260
|
+
"""
|
|
261
|
+
Set the job status.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
shared_state: Dictionary containing shared state variables.
|
|
265
|
+
status: Status to set.
|
|
266
|
+
|
|
267
|
+
Raises:
|
|
268
|
+
ValueError: If shared_state doesn't contain required keys.
|
|
269
|
+
"""
|
|
270
|
+
lock = shared_state.get("lock")
|
|
271
|
+
if lock is None:
|
|
272
|
+
raise ValueError("Shared state must contain a 'lock' key")
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
lock.acquire()
|
|
276
|
+
try:
|
|
277
|
+
shared_state["status"].value = status.value
|
|
278
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
279
|
+
# Process is shutting down, ignore
|
|
280
|
+
pass
|
|
281
|
+
finally:
|
|
282
|
+
try:
|
|
283
|
+
lock.release()
|
|
284
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
285
|
+
# Process is shutting down, ignore
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def set_progress(shared_state: Dict[str, Any], progress: int) -> None:
|
|
290
|
+
"""
|
|
291
|
+
Set the job progress.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
shared_state: Dictionary containing shared state variables.
|
|
295
|
+
progress: Progress value (0-100).
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
ValueError: If shared_state doesn't contain required keys.
|
|
299
|
+
ValueError: If progress is not between 0 and 100.
|
|
300
|
+
"""
|
|
301
|
+
if not (0 <= progress <= 100):
|
|
302
|
+
raise ValueError("Progress must be between 0 and 100")
|
|
303
|
+
|
|
304
|
+
lock = shared_state.get("lock")
|
|
305
|
+
if lock is None:
|
|
306
|
+
raise ValueError("Shared state must contain a 'lock' key")
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
lock.acquire()
|
|
310
|
+
try:
|
|
311
|
+
shared_state["progress"].value = progress
|
|
312
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
313
|
+
# Process is shutting down, ignore
|
|
314
|
+
pass
|
|
315
|
+
finally:
|
|
316
|
+
try:
|
|
317
|
+
lock.release()
|
|
318
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
319
|
+
# Process is shutting down, ignore
|
|
320
|
+
pass
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def get_progress(shared_state: Dict[str, Any]) -> int:
|
|
324
|
+
"""
|
|
325
|
+
Get job progress with minimal lock time.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
shared_state: Dictionary containing shared state variables.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Current job progress (0-100).
|
|
332
|
+
|
|
333
|
+
Raises:
|
|
334
|
+
ValueError: If shared_state doesn't contain required keys.
|
|
335
|
+
"""
|
|
336
|
+
lock = shared_state.get("lock")
|
|
337
|
+
if lock is None:
|
|
338
|
+
raise ValueError("Shared state must contain a 'lock' key")
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
lock.acquire()
|
|
342
|
+
try:
|
|
343
|
+
return int(shared_state["progress"].value)
|
|
344
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
345
|
+
# Process is shutting down, return default progress
|
|
346
|
+
return 0
|
|
347
|
+
finally:
|
|
348
|
+
try:
|
|
349
|
+
lock.release()
|
|
350
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
351
|
+
# Process is shutting down, ignore
|
|
352
|
+
pass
|