@@ -419,5 +419,137 @@ def __init__(self, version="0.0.0"):
419419 sys .modules .pop ("pkg_resources" , None )
420420
421421
422+ class TestVerboseModeThreadSafety (unittest .TestCase ):
423+ """Tests demonstrating verbose mode thread safety.
424+
425+ The HTTPClient uses threading.local() to store _last_response, ensuring
426+ each thread gets its own response when sharing a client instance.
427+ """
428+
429+ @patch ("requests.get" )
430+ def test_verbose_mode_thread_safe_with_shared_client (self , mock_get ):
431+ """Verify that shared client is thread-safe for verbose mode.
432+
433+ Each thread should see its own response even when sharing the same
434+ HTTPClient instance, thanks to threading.local() storage.
435+ """
436+ import threading
437+
438+ results : dict [str , str | None ] = {
439+ "thread1_ray" : None ,
440+ "thread2_ray" : None ,
441+ }
442+ barrier = threading .Barrier (2 )
443+
444+ def mock_get_side_effect (* args , ** kwargs ):
445+ """Return different cf-ray based on which thread is calling."""
446+ thread_name = threading .current_thread ().name
447+ response = Mock ()
448+ response .ok = True
449+ response .json .return_value = {"thread" : thread_name }
450+ # Each thread gets a unique cf-ray
451+ if "thread1" in thread_name :
452+ response .headers = {"cf-ray" : "ray-thread1" }
453+ else :
454+ response .headers = {"cf-ray" : "ray-thread2" }
455+ response .status_code = 200
456+ return response
457+
458+ mock_get .side_effect = mock_get_side_effect
459+
460+ # Single shared client - now thread-safe!
461+ client = HTTPClient (project_id = "test123" , verbose = True )
462+
463+ def thread1_work ():
464+ client .get ("/test" )
465+ barrier .wait () # Sync with thread2
466+ resp = client .get_last_response ()
467+ assert resp is not None
468+ results ["thread1_ray" ] = resp .headers .get ("cf-ray" )
469+
470+ def thread2_work ():
471+ client .get ("/test" )
472+ barrier .wait () # Sync with thread1
473+ resp = client .get_last_response ()
474+ assert resp is not None
475+ results ["thread2_ray" ] = resp .headers .get ("cf-ray" )
476+
477+ t1 = threading .Thread (target = thread1_work , name = "thread1" )
478+ t2 = threading .Thread (target = thread2_work , name = "thread2" )
479+
480+ t1 .start ()
481+ t2 .start ()
482+ t1 .join ()
483+ t2 .join ()
484+
485+ # With thread-local storage, each thread sees its OWN response
486+ assert (
487+ results ["thread1_ray" ] == "ray-thread1"
488+ ), f"Thread1 should see its own cf-ray, got: { results ['thread1_ray' ]} "
489+ assert (
490+ results ["thread2_ray" ] == "ray-thread2"
491+ ), f"Thread2 should see its own cf-ray, got: { results ['thread2_ray' ]} "
492+
493+ @patch ("requests.get" )
494+ def test_verbose_mode_separate_clients_per_thread (self , mock_get ):
495+ """Verify separate clients per thread also works (alternative pattern).
496+
497+ This test shows that using separate client instances per thread
498+ also provides thread-safe access to response metadata.
499+ """
500+ import threading
501+
502+ results : dict [str , str | None ] = {"thread1_ray" : None , "thread2_ray" : None }
503+ barrier = threading .Barrier (2 )
504+
505+ def mock_get_side_effect (* args , ** kwargs ):
506+ thread_name = threading .current_thread ().name
507+ response = Mock ()
508+ response .ok = True
509+ response .json .return_value = {"thread" : thread_name }
510+ if "thread1" in thread_name :
511+ response .headers = {"cf-ray" : "ray-thread1" }
512+ else :
513+ response .headers = {"cf-ray" : "ray-thread2" }
514+ response .status_code = 200
515+ return response
516+
517+ mock_get .side_effect = mock_get_side_effect
518+
519+ def thread1_work ():
520+ # Each thread creates its own client
521+ client = HTTPClient (project_id = "test123" , verbose = True )
522+ client .get ("/test" )
523+ barrier .wait ()
524+ resp = client .get_last_response ()
525+ assert resp is not None
526+ results ["thread1_ray" ] = resp .headers .get ("cf-ray" )
527+
528+ def thread2_work ():
529+ # Each thread creates its own client
530+ client = HTTPClient (project_id = "test123" , verbose = True )
531+ client .get ("/test" )
532+ barrier .wait ()
533+ resp = client .get_last_response ()
534+ assert resp is not None
535+ results ["thread2_ray" ] = resp .headers .get ("cf-ray" )
536+
537+ t1 = threading .Thread (target = thread1_work , name = "thread1" )
538+ t2 = threading .Thread (target = thread2_work , name = "thread2" )
539+
540+ t1 .start ()
541+ t2 .start ()
542+ t1 .join ()
543+ t2 .join ()
544+
545+ # With separate clients, each thread has its own response
546+ assert (
547+ results ["thread1_ray" ] == "ray-thread1"
548+ ), f"Thread1 should see its own cf-ray, got: { results ['thread1_ray' ]} "
549+ assert (
550+ results ["thread2_ray" ] == "ray-thread2"
551+ ), f"Thread2 should see its own cf-ray, got: { results ['thread2_ray' ]} "
552+
553+
422554if __name__ == "__main__" :
423555 unittest .main ()
0 commit comments