77import time
88from abc import ABC , abstractmethod
99from types import FrameType
10- from typing import Optional , Dict , List , Type , Tuple
10+ from typing import Optional , Dict , List , Tuple , Type , cast
1111
1212import psutil
1313
1414from psij import InvalidJobException , SubmitException , Launcher
1515from psij import Job , JobSpec , JobExecutorConfig , JobState , JobStatus
1616from psij import JobExecutor
17+ from psij .utils import SingletonThread
1718
1819logger = logging .getLogger (__name__ )
1920
@@ -22,7 +23,14 @@ def _handle_sigchld(signum: int, frame: Optional[FrameType]) -> None:
2223 _ProcessReaper .get_instance ()._handle_sigchld ()
2324
2425
25- _REAPER_SLEEP_TIME = 0.2
26+ if threading .current_thread () != threading .main_thread ():
27+ logger .warning ('The psij module is being imported from a non-main thread. This prevents the'
28+ 'use of signals in the local executor, which will slow things down a bit.' )
29+ else :
30+ signal .signal (signal .SIGCHLD , _handle_sigchld )
31+
32+
33+ _REAPER_SLEEP_TIME = 0.1
2634
2735
2836class _ProcessEntry (ABC ):
@@ -110,18 +118,11 @@ def _get_env(spec: JobSpec) -> Optional[Dict[str, str]]:
110118 return spec .environment
111119
112120
113- class _ProcessReaper (threading .Thread ):
114- _instance : Optional ['_ProcessReaper' ] = None
115- _lock = threading .RLock ()
121+ class _ProcessReaper (SingletonThread ):
116122
117123 @classmethod
118124 def get_instance (cls : Type ['_ProcessReaper' ]) -> '_ProcessReaper' :
119- with cls ._lock :
120- if cls ._instance is None :
121- cls ._instance = _ProcessReaper ()
122- cls ._instance .start ()
123- signal .signal (signal .SIGCHLD , _handle_sigchld )
124- return cls ._instance
125+ return cast ('_ProcessReaper' , super ().get_instance ())
125126
126127 def __init__ (self ) -> None :
127128 super ().__init__ (name = 'Local Executor Process Reaper' , daemon = True )
@@ -198,8 +199,15 @@ class LocalJobExecutor(JobExecutor):
198199 This job executor is intended to be used when there is no resource manager, only
199200 the operating system. Or when there is a resource manager, but it should be ignored.
200201
201- Limitations: in Linux, attached jobs always appear to complete with a zero exit code regardless
202+ Limitations:
203+ - In Linux, attached jobs always appear to complete with a zero exit code regardless
202204 of the actual exit code.
205+ - Instantiation of a local executor from both parent process and a `fork()`-ed process
206+ is not guaranteed to work. In general, using `fork()` and multi-threading in Linux is unsafe,
207+ as suggested by the `fork()` man page. While PSI/J attempts to minimize problems that can
208+ arise when `fork()` is combined with threads (which are used by PSI/J), no guarantees can be
209+ made and the chances of unexpected behavior are high. Please do not use PSI/J with `fork()`.
210+ If you do, please be mindful that support for using PSI/J with `fork()` will be limited.
203211 """
204212
205213 def __init__ (self , url : Optional [str ] = None ,
0 commit comments