1
1
import json
2
2
import os
3
3
import sys
4
-
5
- # --- MULTIPROCESSING CHANGES ---
4
+ import threading
6
5
from multiprocessing import Manager
7
- from multiprocessing import RLock
8
6
from pathlib import Path
9
7
10
8
from geophires_x import GEOPHIRESv3 as geophires
16
14
17
15
18
16
class GeophiresXClient :
19
- # --- LAZY-LOADED, PROCESS-SAFE SINGLETONS ---
20
- # Define class-level placeholders. These will be shared across all instances.
17
+ # --- Class-level shared resources ---
18
+ # These will be initialized lazily and shared across all instances and processes .
21
19
_manager = None
22
20
_cache = None
23
- _lock = RLock () # Use a process-safe re-entrant lock
21
+ _lock = None # This will be a process-safe RLock from the manager.
22
+
23
+ # A standard threading lock to make the one-time initialization thread-safe.
24
+ _init_lock = threading .Lock ()
24
25
25
26
def __init__ (self , enable_caching = True , logger_name = None ):
26
27
if logger_name is None :
@@ -29,33 +30,39 @@ def __init__(self, enable_caching=True, logger_name=None):
29
30
self ._logger = _get_logger (logger_name = logger_name )
30
31
self ._enable_caching = enable_caching
31
32
32
- # This method will safely initialize the shared manager and cache
33
- # only when the first client instance is created.
34
- self ._initialize_shared_resources ()
33
+ # Lazy-initialize shared resources if they haven't been already.
34
+ # This approach is safe to call from multiple threads/processes.
35
+ if GeophiresXClient ._manager is None :
36
+ self ._initialize_shared_resources ()
35
37
36
38
@classmethod
37
39
def _initialize_shared_resources (cls ):
38
40
"""
39
- Initializes the multiprocessing Manager and shared cache dictionary.
40
- This method is designed to be called safely by multiple processes,
41
- ensuring the manager is only started once.
41
+ Initializes the multiprocessing Manager and shared resources (cache, lock)
42
+ in a thread-safe and process-safe manner.
42
43
"""
43
- with cls ._lock :
44
+ # Use a thread-safe lock to ensure this block only ever runs once
45
+ # across all threads in the main process.
46
+ with cls ._init_lock :
47
+ # The double-check locking pattern ensures we don't try to
48
+ # re-initialize if another thread finished while we were waiting.
44
49
if cls ._manager is None :
45
- # This code is now protected. It won't run on module import.
46
- # It runs only when the first GeophiresXClient is instantiated.
47
50
cls ._manager = Manager ()
48
51
cls ._cache = cls ._manager .dict ()
52
+ cls ._lock = cls ._manager .RLock () # The Manager now creates the lock.
49
53
50
54
def get_geophires_result (self , input_params : GeophiresInputParameters ) -> GeophiresXResult :
51
- # Use the class-level lock to protect access to the shared cache
52
- # and the non-reentrant GEOPHIRES core.
55
+ """
56
+ Calculates a GEOPHIRES result in a thread-safe and process-safe manner.
57
+ """
58
+ # Use the process-safe lock from the manager to make the check-then-act
59
+ # operation on the cache fully atomic across multiple processes.
53
60
with GeophiresXClient ._lock :
54
61
cache_key = hash (input_params )
55
62
if self ._enable_caching and cache_key in GeophiresXClient ._cache :
56
63
return GeophiresXClient ._cache [cache_key ]
57
64
58
- # ... (The rest of your logic remains the same)
65
+ # --- This section is now guaranteed to run only once per unique input ---
59
66
stash_cwd = Path .cwd ()
60
67
stash_sys_argv = sys .argv
61
68
@@ -67,20 +74,21 @@ def get_geophires_result(self, input_params: GeophiresInputParameters) -> Geophi
67
74
except SystemExit :
68
75
raise RuntimeError ('GEOPHIRES exited without giving a reason' ) from None
69
76
finally :
77
+ # Ensure global state is restored even if geophires.main() fails
70
78
sys .argv = stash_sys_argv
71
79
os .chdir (stash_cwd )
72
80
73
81
self ._logger .info (f'GEOPHIRES-X output file: { input_params .get_output_file_path ()} ' )
74
82
75
83
result = GeophiresXResult (input_params .get_output_file_path ())
76
84
if self ._enable_caching :
77
- GeophiresXClient ._cache [cache_key ] = result
85
+ self ._cache [cache_key ] = result
78
86
79
87
return result
80
88
81
89
82
90
if __name__ == '__main__' :
83
- # This block is safe, as it's protected from being run on import .
91
+ # This block remains for direct testing of the script .
84
92
client = GeophiresXClient ()
85
93
log = _get_logger ()
86
94
0 commit comments