@@ -48,6 +48,28 @@ def _run_periodic():
4848 t .join ()
4949
5050
51+ def test_periodic_awake_after_stop_raises_not_hangs ():
52+ """Regression: awake() after a completed stop() used to block forever.
53+
54+ The native awake() cleared _served and then waited on it, but the worker
55+ thread had already set _served one last time during its cleanup and was
56+ gone — nothing would set _served again. The wait happened with the GIL
57+ released, so not even a Python-level SIGALRM handler could interrupt it;
58+ the only escape was SIGKILL.
59+
60+ After the fix, awake() rechecks _stopping under the awake mutex (with
61+ stop() acquiring the same mutex) and raises RuntimeError instead of
62+ blocking.
63+ """
64+ t = periodic .PeriodicThread (60.0 , lambda : None )
65+ t .start ()
66+ t .stop ()
67+ t .join () # fully drained
68+
69+ with pytest .raises (RuntimeError ):
70+ t .awake ()
71+
72+
5173def test_periodic_error ():
5274 x = {"OK" : False }
5375
@@ -351,7 +373,11 @@ def _get_native_thread_name():
351373 # Use pthread_getname_np
352374 pthread = ctypes .CDLL ("/usr/lib/libpthread.dylib" )
353375 pthread_getname_np = pthread .pthread_getname_np
354- pthread_getname_np .argtypes = [ctypes .c_void_p , ctypes .c_char_p , ctypes .c_size_t ]
376+ pthread_getname_np .argtypes = [
377+ ctypes .c_void_p ,
378+ ctypes .c_char_p ,
379+ ctypes .c_size_t ,
380+ ]
355381 pthread_getname_np .restype = ctypes .c_int
356382
357383 # Get current thread handle
@@ -464,14 +490,20 @@ def _capture_native_name():
464490 t1 .join ()
465491
466492 if native_name [0 ] is not None :
467- assert native_name [0 ] == "ShortName" , f"Expected 'ShortName', got '{ native_name [0 ]} '"
493+ assert native_name [0 ] == "ShortName" , (
494+ f"Expected 'ShortName', got '{ native_name [0 ]} '"
495+ )
468496
469497 # Test 2: Long name with module:class format
470498 # On Linux (15 char limit), should keep "StackCollectorThread" -> truncated to "StackCollectorT"
471499 # On macOS (63 char limit), should keep the full class name "StackCollectorThread"
472500 thread_started .clear ()
473501 native_name [0 ] = None
474- t2 = periodic .PeriodicThread (0.1 , _capture_native_name , name = "ddtrace.profiling.collector:StackCollectorThread" )
502+ t2 = periodic .PeriodicThread (
503+ 0.1 ,
504+ _capture_native_name ,
505+ name = "ddtrace.profiling.collector:StackCollectorThread" ,
506+ )
475507 t2 .start ()
476508 thread_started .wait ()
477509 t2 .stop ()
@@ -480,7 +512,9 @@ def _capture_native_name():
480512 if native_name [0 ] is not None :
481513 if system == "Linux" :
482514 # Linux truncates to 15 characters
483- assert native_name [0 ] == "StackCollectorT" , f"Expected 'StackCollectorT' on Linux, got '{ native_name [0 ]} '"
515+ assert native_name [0 ] == "StackCollectorT" , (
516+ f"Expected 'StackCollectorT' on Linux, got '{ native_name [0 ]} '"
517+ )
484518 elif system == "Darwin" :
485519 # macOS can fit the full class name
486520 assert native_name [0 ].endswith ("StackCollectorThread" ), (
@@ -490,7 +524,9 @@ def _capture_native_name():
490524 # Test 3: Long name without colon (should truncate from start)
491525 thread_started .clear ()
492526 native_name [0 ] = None
493- t3 = periodic .PeriodicThread (0.1 , _capture_native_name , name = "VeryLongThreadNameWithoutColonSeparator" )
527+ t3 = periodic .PeriodicThread (
528+ 0.1 , _capture_native_name , name = "VeryLongThreadNameWithoutColonSeparator"
529+ )
494530 t3 .start ()
495531 thread_started .wait ()
496532 t3 .stop ()
@@ -499,7 +535,9 @@ def _capture_native_name():
499535 if native_name [0 ] is not None :
500536 if system == "Linux" :
501537 # Should truncate from the start to 15 characters
502- assert native_name [0 ] == "VeryLongThreadN" , f"Expected 'VeryLongThreadN' on Linux, got '{ native_name [0 ]} '"
538+ assert native_name [0 ] == "VeryLongThreadN" , (
539+ f"Expected 'VeryLongThreadN' on Linux, got '{ native_name [0 ]} '"
540+ )
503541 elif system == "Darwin" :
504542 # macOS limit is 63, name is 41 chars, should fit fully
505543 assert native_name [0 ] == "VeryLongThreadNameWithoutColonSeparator" , (
@@ -510,7 +548,9 @@ def _capture_native_name():
510548 # "module:ClassNameFit" on Linux should become "ClassNameFit" (12 chars, fits)
511549 thread_started .clear ()
512550 native_name [0 ] = None
513- t4 = periodic .PeriodicThread (0.1 , _capture_native_name , name = "some.module:ClassNameFit" )
551+ t4 = periodic .PeriodicThread (
552+ 0.1 , _capture_native_name , name = "some.module:ClassNameFit"
553+ )
514554 t4 .start ()
515555 thread_started .wait ()
516556 t4 .stop ()
0 commit comments