55import os
66import time
77import uuid
8+ import threading
89from multiprocessing import Manager
910from multiprocessing .managers import SyncManager
10- from typing import Any , Dict , Optional
11+ from typing import Any , Dict , Optional , List
1112
1213from .rp_logger import RunPodLogger
1314
@@ -63,45 +64,148 @@ def __str__(self) -> str:
6364# ---------------------------------------------------------------------------- #
6465# Tracker #
6566# ---------------------------------------------------------------------------- #
67+
68+ class _JobStorage :
69+ """Abstract storage backend for jobs."""
70+
71+ def add_job (self , job_dict : Dict [str , Any ]) -> None :
72+ raise NotImplementedError
73+
74+ def remove_job (self , job_id : str ) -> None :
75+ raise NotImplementedError
76+
77+ def get_job (self , job_id : str ) -> Optional [Dict [str , Any ]]:
78+ raise NotImplementedError
79+
80+ def get_all_jobs (self ) -> List [Dict [str , Any ]]:
81+ raise NotImplementedError
82+
83+ def clear_jobs (self ) -> None :
84+ raise NotImplementedError
85+
86+ def job_exists (self , job_id : str ) -> bool :
87+ raise NotImplementedError
88+
89+
90+ class _MultiprocessingStorage (_JobStorage ):
91+ """Multiprocessing-based storage for GIL-free operation."""
92+
93+ def __init__ (self ):
94+ self ._manager = Manager ()
95+ self ._shared_data = self ._manager .dict ()
96+ self ._shared_data ['jobs' ] = self ._manager .list ()
97+ self ._lock = self ._manager .Lock ()
98+
99+ def add_job (self , job_dict : Dict [str , Any ]) -> None :
100+ with self ._lock :
101+ job_list = self ._shared_data ['jobs' ]
102+ if not any (job ['id' ] == job_dict ['id' ] for job in job_list ):
103+ job_list .append (job_dict )
104+
105+ def remove_job (self , job_id : str ) -> None :
106+ with self ._lock :
107+ job_list = self ._shared_data ['jobs' ]
108+ for i , job in enumerate (job_list ):
109+ if job ['id' ] == job_id :
110+ del job_list [i ]
111+ break
112+
113+ def get_job (self , job_id : str ) -> Optional [Dict [str , Any ]]:
114+ with self ._lock :
115+ for job in self ._shared_data ['jobs' ]:
116+ if job ['id' ] == job_id :
117+ return dict (job )
118+ return None
119+
120+ def get_all_jobs (self ) -> List [Dict [str , Any ]]:
121+ with self ._lock :
122+ return [dict (job ) for job in self ._shared_data ['jobs' ]]
123+
124+ def clear_jobs (self ) -> None :
125+ with self ._lock :
126+ self ._shared_data ['jobs' ][:] = []
127+
128+ def job_exists (self , job_id : str ) -> bool :
129+ with self ._lock :
130+ return any (job ['id' ] == job_id for job in self ._shared_data ['jobs' ])
131+
132+
133+ class _ThreadSafeStorage (_JobStorage ):
134+ """Thread-safe storage fallback when multiprocessing fails."""
135+
136+ def __init__ (self ):
137+ self ._jobs : List [Dict [str , Any ]] = []
138+ self ._lock = threading .Lock ()
139+
140+ def add_job (self , job_dict : Dict [str , Any ]) -> None :
141+ with self ._lock :
142+ if not any (job ['id' ] == job_dict ['id' ] for job in self ._jobs ):
143+ self ._jobs .append (job_dict )
144+
145+ def remove_job (self , job_id : str ) -> None :
146+ with self ._lock :
147+ for i , job in enumerate (self ._jobs ):
148+ if job ['id' ] == job_id :
149+ del self ._jobs [i ]
150+ break
151+
152+ def get_job (self , job_id : str ) -> Optional [Dict [str , Any ]]:
153+ with self ._lock :
154+ for job in self ._jobs :
155+ if job ['id' ] == job_id :
156+ return job .copy ()
157+ return None
158+
159+ def get_all_jobs (self ) -> List [Dict [str , Any ]]:
160+ with self ._lock :
161+ return self ._jobs .copy ()
162+
163+ def clear_jobs (self ) -> None :
164+ with self ._lock :
165+ self ._jobs .clear ()
166+
167+ def job_exists (self , job_id : str ) -> bool :
168+ with self ._lock :
169+ return any (job ['id' ] == job_id for job in self ._jobs )
170+
171+
66172class JobsProgress :
67- """Track the state of current jobs in progress using shared memory."""
173+ """Track the state of current jobs in progress using shared memory or thread-safe fallback ."""
68174
69175 _instance : Optional ['JobsProgress' ] = None
70- # Singleton
176+ _storage : Optional [_JobStorage ] = None
177+
71178 def __new__ (cls ):
72179 if cls ._instance is None :
73- cls ._instance = super ().__new__ (cls )
180+ cls ._instance = object .__new__ (cls )
181+ cls ._instance ._storage = None
74182 return cls ._instance
75183
76184 def __init__ (self ):
77- if not hasattr (self , '_initialized' ):
78- self ._manager : Optional [SyncManager ] = None
79- self ._shared_data : Optional [Any ] = None
80- self ._lock : Optional [Any ] = None
81- self ._initialized = True
82-
185+ pass
186+
83187 def _ensure_initialized (self ):
84- """Initialize the multiprocessing manager and shared data structures only when needed."""
85- if self ._manager is None :
86- self ._manager = Manager ()
87- self ._shared_data = self ._manager .dict ()
88- self ._shared_data ['jobs' ] = self ._manager .list ()
89- self ._lock = self ._manager .Lock ()
188+ """Lazily initialize storage backend."""
189+ if self ._storage is None :
190+ try :
191+ self ._storage = _MultiprocessingStorage ()
192+ log .debug ("JobsProgress | Using multiprocessing for GIL-free operation" )
193+ except Exception as e :
194+ log .warn (f"JobsProgress | Multiprocessing failed ({ e } ), falling back to thread-safe mode" )
195+ self ._storage = _ThreadSafeStorage ()
90196
91197 def __repr__ (self ) -> str :
92198 return f"<{ self .__class__ .__name__ } >: { self .get_job_list ()} "
93199
94200 def clear (self ) -> None :
95201 self ._ensure_initialized ()
96- with self ._lock :
97- self ._shared_data ['jobs' ][:] = []
202+ self ._storage .clear_jobs ()
98203
99204 def add (self , element : Any ):
100205 """
101206 Adds a Job object to the set.
102207 """
103208 self ._ensure_initialized ()
104-
105209 if isinstance (element , str ):
106210 job_dict = {'id' : element }
107211 elif isinstance (element , dict ):
@@ -111,16 +215,8 @@ def add(self, element: Any):
111215 else :
112216 raise TypeError ("Only Job objects can be added to JobsProgress." )
113217
114- with self ._lock :
115- # Check if job already exists
116- job_list = self ._shared_data ['jobs' ]
117- for existing_job in job_list :
118- if existing_job ['id' ] == job_dict ['id' ]:
119- return # Job already exists
120-
121- # Add new job
122- job_list .append (job_dict )
123- log .debug (f"JobsProgress | Added job: { job_dict ['id' ]} " )
218+ self ._storage .add_job (job_dict )
219+ log .debug (f"JobsProgress | Added job: { job_dict ['id' ]} " )
124220
125221 def get (self , element : Any ) -> Optional [Job ]:
126222 """
@@ -129,28 +225,24 @@ def get(self, element: Any) -> Optional[Job]:
129225 If the element is a string, searches for Job with that id.
130226 """
131227 self ._ensure_initialized ()
132-
133228 if isinstance (element , str ):
134229 search_id = element
135230 elif isinstance (element , Job ):
136231 search_id = element .id
137232 else :
138233 raise TypeError ("Only Job objects can be retrieved from JobsProgress." )
139234
140- with self ._lock :
141- for job_dict in self ._shared_data ['jobs' ]:
142- if job_dict ['id' ] == search_id :
143- log .debug (f"JobsProgress | Retrieved job: { job_dict ['id' ]} " )
144- return Job (** job_dict )
145-
235+ job_dict = self ._storage .get_job (search_id )
236+ if job_dict :
237+ log .debug (f"JobsProgress | Retrieved job: { job_dict ['id' ]} " )
238+ return Job (** job_dict )
146239 return None
147240
148241 def remove (self , element : Any ):
149242 """
150243 Removes a Job object from the set.
151244 """
152245 self ._ensure_initialized ()
153-
154246 if isinstance (element , str ):
155247 job_id = element
156248 elif isinstance (element , dict ):
@@ -160,23 +252,15 @@ def remove(self, element: Any):
160252 else :
161253 raise TypeError ("Only Job objects can be removed from JobsProgress." )
162254
163- with self ._lock :
164- job_list = self ._shared_data ['jobs' ]
165- for i , job_dict in enumerate (job_list ):
166- if job_dict ['id' ] == job_id :
167- del job_list [i ]
168- log .debug (f"JobsProgress | Removed job: { job_dict ['id' ]} " )
169- break
255+ self ._storage .remove_job (job_id )
256+ log .debug (f"JobsProgress | Removed job: { job_id } " )
170257
171258 def get_job_list (self ) -> Optional [str ]:
172259 """
173260 Returns the list of job IDs as comma-separated string.
174261 """
175- if self ._manager is None :
176- return None
177-
178- with self ._lock :
179- job_list = list (self ._shared_data ['jobs' ])
262+ self ._ensure_initialized ()
263+ job_list = self ._storage .get_all_jobs ()
180264
181265 if not job_list :
182266 return None
@@ -188,22 +272,13 @@ def get_job_count(self) -> int:
188272 """
189273 Returns the number of jobs.
190274 """
191- if self ._manager is None :
192- return 0
193-
194- with self ._lock :
195- return len (self ._shared_data ['jobs' ])
275+ self ._ensure_initialized ()
276+ return len (self ._storage .get_all_jobs ())
196277
197278 def __iter__ (self ):
198279 """Make the class iterable - returns Job objects"""
199- if self ._manager is None :
200- return iter ([])
201-
202- with self ._lock :
203- # Create a snapshot of jobs to avoid holding lock during iteration
204- job_dicts = list (self ._shared_data ['jobs' ])
205-
206- # Return an iterator of Job objects
280+ self ._ensure_initialized ()
281+ job_dicts = self ._storage .get_all_jobs ()
207282 return iter (Job (** job_dict ) for job_dict in job_dicts )
208283
209284 def __len__ (self ):
@@ -212,9 +287,7 @@ def __len__(self):
212287
213288 def __contains__ (self , element : Any ) -> bool :
214289 """Support 'in' operator"""
215- if self ._manager is None :
216- return False
217-
290+ self ._ensure_initialized ()
218291 if isinstance (element , str ):
219292 search_id = element
220293 elif isinstance (element , Job ):
@@ -224,8 +297,4 @@ def __contains__(self, element: Any) -> bool:
224297 else :
225298 return False
226299
227- with self ._lock :
228- for job_dict in self ._shared_data ['jobs' ]:
229- if job_dict ['id' ] == search_id :
230- return True
231- return False
300+ return self ._storage .job_exists (search_id )
0 commit comments