@@ -164,31 +164,35 @@ def test_concurrent_downloads_serialized(self, nc_app):
164164 os .chdir (tmpdir )
165165 try :
166166 test_url = "https://raw.githubusercontent.com/cloud-py-api/nc_py_api/refs/heads/main/LICENSE.txt"
167- models = {test_url : {"save_path" : "concurrent_test.txt" }}
168167
169- # Track download events
170- download_events = []
171- download_lock = threading .Lock ()
168+ # Track lock acquisition events
169+ lock_events = []
170+ lock_events_lock = threading .Lock ()
172171
173- # Patch niquests.get to track when downloads actually happen
174- original_get = __import__ ( "niquests" ). get
172+ # Patch SoftFileLock to track when locks are acquired/released
173+ from filelock import SoftFileLock as OriginalSoftFileLock
175174
176- def tracked_get (* args , ** kwargs ):
177- with download_lock :
178- download_events .append (("start" , threading .current_thread ().name , time .time ()))
179- result = original_get (* args , ** kwargs )
180- with download_lock :
181- download_events .append (("end" , threading .current_thread ().name , time .time ()))
182- return result
175+ class TrackedSoftFileLock (OriginalSoftFileLock ):
176+ def acquire (self , * args , ** kwargs ):
177+ result = super ().acquire (* args , ** kwargs )
178+ with lock_events_lock :
179+ lock_events .append (("acquire" , threading .current_thread ().name , time .time ()))
180+ return result
183181
184- with patch ("niquests.get" , side_effect = tracked_get ):
182+ def release (self , * args , ** kwargs ):
183+ with lock_events_lock :
184+ lock_events .append (("release" , threading .current_thread ().name , time .time ()))
185+ return super ().release (* args , ** kwargs )
186+
187+ with patch ("nc_py_api.ex_app.integration_fastapi.SoftFileLock" , TrackedSoftFileLock ):
185188 # Simulate two pods trying to download simultaneously
186189 results = []
187190 errors = []
188191
189192 def download_in_thread (thread_name ):
190193 try :
191- fetch_models_task (nc_app , models .copy (), 0 )
194+ models = {test_url : {"save_path" : "concurrent_test.txt" }}
195+ fetch_models_task (nc_app , models , 0 )
192196 results .append (thread_name )
193197 except Exception as e :
194198 errors .append ((thread_name , e ))
@@ -207,21 +211,18 @@ def download_in_thread(thread_name):
207211 assert len (results ) + len (errors ) == 2 , f"Not all threads completed: { results } , { errors } "
208212 assert len (errors ) == 0 , f"Threads failed with errors: { errors } "
209213
210- # Check that downloads were serialized (not concurrent)
211- start_events = [e for e in download_events if e [0 ] == "start" ]
212- end_events = [e for e in download_events if e [0 ] == "end" ]
213-
214- if len (start_events ) == 2 :
215- # If two downloads happened, they should be sequential
216- first_end_time = end_events [0 ][2 ]
217- second_start_time = start_events [1 ][2 ]
218- # Second download should start after first ends (serialized)
219- assert second_start_time >= first_end_time , "Downloads happened concurrently instead of serially"
220- elif len (start_events ) == 1 :
221- # Only one download happened - the other thread reused the file (also valid)
222- pass
223- else :
224- pytest .fail (f"Unexpected number of download events: { download_events } " )
214+ # Check that lock acquisitions were serialized (not concurrent)
215+ acquire_events = [e for e in lock_events if e [0 ] == "acquire" ]
216+ release_events = [e for e in lock_events if e [0 ] == "release" ]
217+
218+ assert len (acquire_events ) == 2 , f"Expected 2 lock acquisitions, got { len (acquire_events )} "
219+ assert len (release_events ) == 2 , f"Expected 2 lock releases, got { len (release_events )} "
220+
221+ # Verify locks were acquired serially: first must release before second acquires
222+ first_release_time = release_events [0 ][2 ]
223+ second_acquire_time = acquire_events [1 ][2 ]
224+ assert second_acquire_time >= first_release_time , \
225+ f"Second thread acquired lock before first released: { lock_events } "
225226
226227 # Verify file was downloaded successfully
227228 assert Path ("concurrent_test.txt" ).exists ()
@@ -355,31 +356,35 @@ def test_concurrent_snapshot_downloads_serialized(self, nc_app):
355356 try :
356357 model_name = "hf-internal-testing/tiny-random-gpt2"
357358 cache_dir = Path (tmpdir ) / "hf_cache"
358- models = {model_name : {"cache_dir" : str (cache_dir ), "max_workers" : 1 }}
359359
360- # Track when snapshot_download is called
361- from huggingface_hub import snapshot_download
362- download_events = []
363- download_lock = threading .Lock ()
360+ # Track lock acquisition events
361+ lock_events = []
362+ lock_events_lock = threading .Lock ()
364363
365- original_snapshot_download = snapshot_download
364+ # Patch SoftFileLock to track when locks are acquired/released
365+ from filelock import SoftFileLock as OriginalSoftFileLock
366366
367- def tracked_snapshot_download (* args , ** kwargs ):
368- with download_lock :
369- download_events .append (("start" , threading .current_thread ().name , time .time ()))
370- result = original_snapshot_download (* args , ** kwargs )
371- with download_lock :
372- download_events .append (("end" , threading .current_thread ().name , time .time ()))
373- return result
367+ class TrackedSoftFileLock (OriginalSoftFileLock ):
368+ def acquire (self , * args , ** kwargs ):
369+ result = super ().acquire (* args , ** kwargs )
370+ with lock_events_lock :
371+ lock_events .append (("acquire" , threading .current_thread ().name , time .time ()))
372+ return result
374373
375- with patch ("nc_py_api.ex_app.integration_fastapi.snapshot_download" , side_effect = tracked_snapshot_download ):
374+ def release (self , * args , ** kwargs ):
375+ with lock_events_lock :
376+ lock_events .append (("release" , threading .current_thread ().name , time .time ()))
377+ return super ().release (* args , ** kwargs )
378+
379+ with patch ("nc_py_api.ex_app.integration_fastapi.SoftFileLock" , TrackedSoftFileLock ):
376380 # Simulate two pods trying to download simultaneously
377381 results = []
378382 errors = []
379383
380384 def download_in_thread (thread_name ):
381385 try :
382- fetch_models_task (nc_app , {model_name : {"cache_dir" : str (cache_dir ), "max_workers" : 1 }}, 0 )
386+ models = {model_name : {"cache_dir" : str (cache_dir ), "max_workers" : 1 }}
387+ fetch_models_task (nc_app , models , 0 )
383388 results .append (thread_name )
384389 except Exception as e :
385390 errors .append ((thread_name , e ))
@@ -398,21 +403,18 @@ def download_in_thread(thread_name):
398403 assert len (results ) + len (errors ) == 2 , f"Not all threads completed: { results } , { errors } "
399404 assert len (errors ) == 0 , f"Threads failed with errors: { errors } "
400405
401- # Check that downloads were serialized (not concurrent)
402- start_events = [e for e in download_events if e [0 ] == "start" ]
403- end_events = [e for e in download_events if e [0 ] == "end" ]
404-
405- if len (start_events ) == 2 :
406- # If two downloads happened, they should be sequential
407- first_end_time = end_events [0 ][2 ]
408- second_start_time = start_events [1 ][2 ]
409- # Second download should start after first ends (serialized)
410- assert second_start_time >= first_end_time , "Downloads happened concurrently instead of serially"
411- elif len (start_events ) == 1 :
412- # Only one download happened - the other thread reused cached model (also valid)
413- pass
414- else :
415- pytest .fail (f"Unexpected number of download events: { download_events } " )
406+ # Check that lock acquisitions were serialized (not concurrent)
407+ acquire_events = [e for e in lock_events if e [0 ] == "acquire" ]
408+ release_events = [e for e in lock_events if e [0 ] == "release" ]
409+
410+ assert len (acquire_events ) == 2 , f"Expected 2 lock acquisitions, got { len (acquire_events )} "
411+ assert len (release_events ) == 2 , f"Expected 2 lock releases, got { len (release_events )} "
412+
413+ # Verify locks were acquired serially: first must release before second acquires
414+ first_release_time = release_events [0 ][2 ]
415+ second_acquire_time = acquire_events [1 ][2 ]
416+ assert second_acquire_time >= first_release_time , \
417+ f"Second thread acquired lock before first released: { lock_events } "
416418
417419 # Verify model was downloaded successfully
418420 assert cache_dir .exists ()
0 commit comments