88import threading
99from multiprocessing import Manager
1010from multiprocessing .managers import SyncManager
11- from typing import Any , Dict , Optional , List
11+ from typing import Any , Dict , Optional
1212
1313from .rp_logger import RunPodLogger
1414
@@ -64,148 +64,52 @@ def __str__(self) -> str:
6464# ---------------------------------------------------------------------------- #
6565# Tracker #
6666# ---------------------------------------------------------------------------- #
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-
17267class JobsProgress :
17368 """Track the state of current jobs in progress using shared memory or thread-safe fallback."""
17469
17570 _instance : Optional ['JobsProgress' ] = None
176- _storage : Optional [_JobStorage ] = None
71+ _manager : Optional [SyncManager ] = None
72+ _shared_data : Optional [Any ] = None
73+ _lock : Optional [Any ] = None
74+ _use_multiprocessing : bool = True
17775
17876 def __new__ (cls ):
17977 if cls ._instance is None :
180- cls ._instance = object .__new__ (cls )
181- cls ._instance ._storage = None
182- return cls ._instance
183-
184- def __init__ (self ):
185- pass
186-
187- def _ensure_initialized (self ):
188- """Lazily initialize storage backend."""
189- if self ._storage is None :
78+ instance = object .__new__ (cls )
79+ # Initialize multiprocessing objects directly like the original
19080 try :
191- self ._storage = _MultiprocessingStorage ()
81+ instance ._manager = Manager ()
82+ instance ._shared_data = instance ._manager .dict ()
83+ instance ._shared_data ['jobs' ] = instance ._manager .list ()
84+ instance ._lock = instance ._manager .Lock ()
85+ instance ._use_multiprocessing = True
19286 log .debug ("JobsProgress | Using multiprocessing for GIL-free operation" )
19387 except Exception as e :
19488 log .warn (f"JobsProgress | Multiprocessing failed ({ e } ), falling back to thread-safe mode" )
195- self ._storage = _ThreadSafeStorage ()
89+ instance ._fallback_jobs = []
90+ instance ._fallback_lock = threading .Lock ()
91+ instance ._use_multiprocessing = False
92+ cls ._instance = instance
93+ return cls ._instance
94+
95+ def __init__ (self ):
96+ pass
19697
19798 def __repr__ (self ) -> str :
19899 return f"<{ self .__class__ .__name__ } >: { self .get_job_list ()} "
199100
200101 def clear (self ) -> None :
201- self ._ensure_initialized ()
202- self ._storage .clear_jobs ()
102+ if self ._use_multiprocessing :
103+ with self ._lock :
104+ self ._shared_data ['jobs' ][:] = []
105+ else :
106+ with self ._fallback_lock :
107+ self ._fallback_jobs .clear ()
203108
204109 def add (self , element : Any ):
205110 """
206111 Adds a Job object to the set.
207112 """
208- self ._ensure_initialized ()
209113 if isinstance (element , str ):
210114 job_dict = {'id' : element }
211115 elif isinstance (element , dict ):
@@ -215,7 +119,16 @@ def add(self, element: Any):
215119 else :
216120 raise TypeError ("Only Job objects can be added to JobsProgress." )
217121
218- self ._storage .add_job (job_dict )
122+ if self ._use_multiprocessing :
123+ with self ._lock :
124+ job_list = self ._shared_data ['jobs' ]
125+ if not any (job ['id' ] == job_dict ['id' ] for job in job_list ):
126+ job_list .append (job_dict )
127+ else :
128+ with self ._fallback_lock :
129+ if not any (job ['id' ] == job_dict ['id' ] for job in self ._fallback_jobs ):
130+ self ._fallback_jobs .append (job_dict )
131+
219132 log .debug (f"JobsProgress | Added job: { job_dict ['id' ]} " )
220133
221134 def get (self , element : Any ) -> Optional [Job ]:
@@ -224,25 +137,31 @@ def get(self, element: Any) -> Optional[Job]:
224137
225138 If the element is a string, searches for Job with that id.
226139 """
227- self ._ensure_initialized ()
228140 if isinstance (element , str ):
229141 search_id = element
230142 elif isinstance (element , Job ):
231143 search_id = element .id
232144 else :
233145 raise TypeError ("Only Job objects can be retrieved from JobsProgress." )
234146
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 )
147+ if self ._use_multiprocessing :
148+ with self ._lock :
149+ for job_dict in self ._shared_data ['jobs' ]:
150+ if job_dict ['id' ] == search_id :
151+ log .debug (f"JobsProgress | Retrieved job: { job_dict ['id' ]} " )
152+ return Job (** job_dict )
153+ else :
154+ with self ._fallback_lock :
155+ for job_dict in self ._fallback_jobs :
156+ if job_dict ['id' ] == search_id :
157+ log .debug (f"JobsProgress | Retrieved job: { job_dict ['id' ]} " )
158+ return Job (** job_dict )
239159 return None
240160
241161 def remove (self , element : Any ):
242162 """
243163 Removes a Job object from the set.
244164 """
245- self ._ensure_initialized ()
246165 if isinstance (element , str ):
247166 job_id = element
248167 elif isinstance (element , dict ):
@@ -252,15 +171,32 @@ def remove(self, element: Any):
252171 else :
253172 raise TypeError ("Only Job objects can be removed from JobsProgress." )
254173
255- self ._storage .remove_job (job_id )
256- log .debug (f"JobsProgress | Removed job: { job_id } " )
174+ if self ._use_multiprocessing :
175+ with self ._lock :
176+ job_list = self ._shared_data ['jobs' ]
177+ for i , job_dict in enumerate (job_list ):
178+ if job_dict ['id' ] == job_id :
179+ del job_list [i ]
180+ log .debug (f"JobsProgress | Removed job: { job_dict ['id' ]} " )
181+ break
182+ else :
183+ with self ._fallback_lock :
184+ for i , job_dict in enumerate (self ._fallback_jobs ):
185+ if job_dict ['id' ] == job_id :
186+ del self ._fallback_jobs [i ]
187+ log .debug (f"JobsProgress | Removed job: { job_dict ['id' ]} " )
188+ break
257189
258190 def get_job_list (self ) -> Optional [str ]:
259191 """
260192 Returns the list of job IDs as comma-separated string.
261193 """
262- self ._ensure_initialized ()
263- job_list = self ._storage .get_all_jobs ()
194+ if self ._use_multiprocessing :
195+ with self ._lock :
196+ job_list = list (self ._shared_data ['jobs' ])
197+ else :
198+ with self ._fallback_lock :
199+ job_list = list (self ._fallback_jobs )
264200
265201 if not job_list :
266202 return None
@@ -272,13 +208,21 @@ def get_job_count(self) -> int:
272208 """
273209 Returns the number of jobs.
274210 """
275- self ._ensure_initialized ()
276- return len (self ._storage .get_all_jobs ())
211+ if self ._use_multiprocessing :
212+ with self ._lock :
213+ return len (self ._shared_data ['jobs' ])
214+ else :
215+ with self ._fallback_lock :
216+ return len (self ._fallback_jobs )
277217
278218 def __iter__ (self ):
279219 """Make the class iterable - returns Job objects"""
280- self ._ensure_initialized ()
281- job_dicts = self ._storage .get_all_jobs ()
220+ if self ._use_multiprocessing :
221+ with self ._lock :
222+ job_dicts = list (self ._shared_data ['jobs' ])
223+ else :
224+ with self ._fallback_lock :
225+ job_dicts = list (self ._fallback_jobs )
282226 return iter (Job (** job_dict ) for job_dict in job_dicts )
283227
284228 def __len__ (self ):
@@ -287,7 +231,6 @@ def __len__(self):
287231
288232 def __contains__ (self , element : Any ) -> bool :
289233 """Support 'in' operator"""
290- self ._ensure_initialized ()
291234 if isinstance (element , str ):
292235 search_id = element
293236 elif isinstance (element , Job ):
@@ -297,4 +240,9 @@ def __contains__(self, element: Any) -> bool:
297240 else :
298241 return False
299242
300- return self ._storage .job_exists (search_id )
243+ if self ._use_multiprocessing :
244+ with self ._lock :
245+ return any (job ['id' ] == search_id for job in self ._shared_data ['jobs' ])
246+ else :
247+ with self ._fallback_lock :
248+ return any (job ['id' ] == search_id for job in self ._fallback_jobs )
0 commit comments