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.
Files changed (68) hide show
  1. queuemgr/__init__.py +13 -0
  2. queuemgr/constants.py +70 -0
  3. queuemgr/core/__init__.py +6 -0
  4. queuemgr/core/exceptions.py +61 -0
  5. queuemgr/core/ipc.py +38 -0
  6. queuemgr/core/ipc_manager.py +54 -0
  7. queuemgr/core/ipc_operations.py +352 -0
  8. queuemgr/core/registry.py +263 -0
  9. queuemgr/core/types.py +75 -0
  10. queuemgr/examples/__init__.py +6 -0
  11. queuemgr/examples/data_analyzer.py +188 -0
  12. queuemgr/examples/error_handling_job.py +135 -0
  13. queuemgr/examples/full_app_example.py +155 -0
  14. queuemgr/examples/integration_examples/cli_integration.py +319 -0
  15. queuemgr/examples/integration_examples/flask_api.py +206 -0
  16. queuemgr/examples/integration_examples/flask_integration.py +16 -0
  17. queuemgr/examples/integration_examples/flask_routes.py +109 -0
  18. queuemgr/examples/integration_examples/systemd_integration.py +278 -0
  19. queuemgr/examples/jobs/__init__.py +14 -0
  20. queuemgr/examples/jobs/api_call_job.py +82 -0
  21. queuemgr/examples/jobs/data_processing_job.py +72 -0
  22. queuemgr/examples/jobs/file_operation_job.py +78 -0
  23. queuemgr/examples/large_data_generator.py +234 -0
  24. queuemgr/examples/large_result_job.py +114 -0
  25. queuemgr/examples/proc_manager_example.py +150 -0
  26. queuemgr/examples/progress_job.py +137 -0
  27. queuemgr/examples/registry_example.py +161 -0
  28. queuemgr/examples/result_job.py +351 -0
  29. queuemgr/examples/service_example.py +380 -0
  30. queuemgr/examples/simple_job.py +125 -0
  31. queuemgr/examples/simple_manager_example.py +132 -0
  32. queuemgr/exceptions.py +160 -0
  33. queuemgr/jobs/__init__.py +6 -0
  34. queuemgr/jobs/base.py +15 -0
  35. queuemgr/jobs/base_core.py +304 -0
  36. queuemgr/jobs/exceptions.py +47 -0
  37. queuemgr/proc_api.py +297 -0
  38. queuemgr/proc_config.py +22 -0
  39. queuemgr/proc_ipc.py +105 -0
  40. queuemgr/proc_manager.py +16 -0
  41. queuemgr/proc_manager_core.py +399 -0
  42. queuemgr/process_commands.py +57 -0
  43. queuemgr/process_config.py +20 -0
  44. queuemgr/process_context.py +38 -0
  45. queuemgr/process_core.py +335 -0
  46. queuemgr/process_manager.py +17 -0
  47. queuemgr/queue/__init__.py +6 -0
  48. queuemgr/queue/exceptions.py +58 -0
  49. queuemgr/queue/job_queue.py +336 -0
  50. queuemgr/simple_api.py +281 -0
  51. queuemgr-1.0.0.dist-info/METADATA +148 -0
  52. queuemgr-1.0.0.dist-info/RECORD +68 -0
  53. queuemgr-1.0.0.dist-info/WHEEL +5 -0
  54. queuemgr-1.0.0.dist-info/entry_points.txt +4 -0
  55. queuemgr-1.0.0.dist-info/licenses/LICENSE +21 -0
  56. queuemgr-1.0.0.dist-info/top_level.txt +2 -0
  57. tests/__init__.py +6 -0
  58. tests/core/test_ipc.py +269 -0
  59. tests/core/test_registry.py +344 -0
  60. tests/core/test_types.py +191 -0
  61. tests/jobs/test_base.py +20 -0
  62. tests/jobs/test_base_initialization.py +133 -0
  63. tests/jobs/test_base_job_loop.py +216 -0
  64. tests/jobs/test_base_process_control.py +163 -0
  65. tests/queue/test_job_queue.py +18 -0
  66. tests/queue/test_job_queue_basic.py +181 -0
  67. tests/queue/test_job_queue_operations.py +242 -0
  68. 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,6 @@
1
+ """
2
+ Core components for the queue manager system.
3
+
4
+ Author: Vasiliy Zdanovskiy
5
+ email: vasilyvz@gmail.com
6
+ """
@@ -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