@@ -416,6 +416,52 @@ async def notmuch():
416416 self .assertEqual (t .get_name (), '123456789' )
417417 self .loop .run_until_complete (t )
418418
419+ # The free-threaded GC publishes allocation counts in batches, so the
420+ # collection cannot be scheduled to run inside get_name().
421+ @support .requires_gil_enabled ("needs precise GC scheduling" )
422+ def test_task_get_name_reentered_during_formatting (self ):
423+ # gh-158159: formatting the lazy default name can run the GC, and a
424+ # finalizer may call get_name() on the same task, replacing the name
425+ # the outer call is still formatting.
426+ loop = self .loop
427+ observed = {}
428+ keep = []
429+ task = None
430+
431+ class Reenter :
432+ def __init__ (self ):
433+ self .cycle = self
434+
435+ def __del__ (self ):
436+ observed ['inner' ] = task .get_name ()
437+ # Reuse the memory of a counter that was freed too early.
438+ keep .extend (10 ** 9 + i for i in range (8 ))
439+
440+ async def notmuch ():
441+ pass
442+
443+ async def scenario ():
444+ gc .collect ()
445+ Reenter ()
446+ old_threshold = gc .get_threshold ()
447+ gc .set_threshold (1 )
448+ try :
449+ scratch = [[0 ], [0 ], [0 ]] # schedule a collection
450+ observed ['outer' ] = task .get_name ()
451+ finally :
452+ gc .set_threshold (* old_threshold )
453+ del scratch
454+
455+ # Names are only freeable once the counter is past the small ints.
456+ for _ in range (1100 ):
457+ loop .run_until_complete (self .new_task (loop , notmuch (), name = None ))
458+ task = self .new_task (loop , scenario (), name = None )
459+ loop .run_until_complete (task )
460+
461+ self .assertRegex (observed ['outer' ], r'^Task-\d+$' )
462+ self .assertEqual (observed ['outer' ], observed ['inner' ])
463+ self .assertEqual (task .get_name (), observed ['outer' ])
464+
419465 def test_task_repr_name_not_str (self ):
420466 async def notmuch ():
421467 return 123
0 commit comments