From ecbf35f9335b0420cb8adfda6f299d6747a16515 Mon Sep 17 00:00:00 2001 From: Tim Peters Date: Wed, 9 Oct 2019 12:37:30 -0500 Subject: bpo-38379: don't claim objects are collected when they aren't (#16658) * bpo-38379: when a finalizer resurrects an object, nothing is actually collected in this run of gc. Change the stats to relect that truth. --- Lib/test/test_gc.py | 70 ++++++++++++++++++++++ .../2019-10-09-16-50-52.bpo-38379.oz5qZx.rst | 1 + Modules/gcmodule.c | 10 ++-- 3 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2019-10-09-16-50-52.bpo-38379.oz5qZx.rst diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index 8215390cb7..f52db1eab1 100644 --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -822,6 +822,76 @@ class GCTests(unittest.TestCase): self.assertRaises(TypeError, gc.get_objects, "1") self.assertRaises(TypeError, gc.get_objects, 1.234) + def test_38379(self): + # When a finalizer resurrects objects, stats were reporting them as + # having been collected. This affected both collect()'s return + # value and the dicts returned by get_stats(). + N = 100 + + class A: # simple self-loop + def __init__(self): + self.me = self + + class Z(A): # resurrecting __del__ + def __del__(self): + zs.append(self) + + zs = [] + + def getstats(): + d = gc.get_stats()[-1] + return d['collected'], d['uncollectable'] + + gc.collect() + gc.disable() + + # No problems if just collecting A() instances. + oldc, oldnc = getstats() + for i in range(N): + A() + t = gc.collect() + c, nc = getstats() + self.assertEqual(t, 2*N) # instance object & its dict + self.assertEqual(c - oldc, 2*N) + self.assertEqual(nc - oldnc, 0) + + # But Z() is not actually collected. + oldc, oldnc = c, nc + Z() + # Nothing is collected - Z() is merely resurrected. + t = gc.collect() + c, nc = getstats() + #self.assertEqual(t, 2) # before + self.assertEqual(t, 0) # after + #self.assertEqual(c - oldc, 2) # before + self.assertEqual(c - oldc, 0) # after + self.assertEqual(nc - oldnc, 0) + + # Unfortunately, a Z() prevents _anything_ from being collected. + # It should be possible to collect the A instances anyway, but + # that will require non-trivial code changes. + oldc, oldnc = c, nc + for i in range(N): + A() + Z() + # Z() prevents anything from being collected. + t = gc.collect() + c, nc = getstats() + #self.assertEqual(t, 2*N + 2) # before + self.assertEqual(t, 0) # after + #self.assertEqual(c - oldc, 2*N + 2) # before + self.assertEqual(c - oldc, 0) # after + self.assertEqual(nc - oldnc, 0) + + # But the A() trash is reclaimed on the next run. + oldc, oldnc = c, nc + t = gc.collect() + c, nc = getstats() + self.assertEqual(t, 2*N) + self.assertEqual(c - oldc, 2*N) + self.assertEqual(nc - oldnc, 0) + + gc.enable() class GCCallbackTests(unittest.TestCase): def setUp(self): diff --git a/Misc/NEWS.d/next/Core and Builtins/2019-10-09-16-50-52.bpo-38379.oz5qZx.rst b/Misc/NEWS.d/next/Core and Builtins/2019-10-09-16-50-52.bpo-38379.oz5qZx.rst new file mode 100644 index 0000000000..82dcb525dd --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2019-10-09-16-50-52.bpo-38379.oz5qZx.rst @@ -0,0 +1 @@ +When cyclic garbage collection (gc) runs finalizers that resurrect unreachable objects, the current gc run ends, without collecting any cyclic trash. However, the statistics reported by ``collect()`` and ``get_stats()`` claimed that all cyclic trash found was collected, and that the resurrected objects were collected. Changed the stats to report that none were collected. diff --git a/Modules/gcmodule.c b/Modules/gcmodule.c index 2b47abae1a..766f8e0c67 100644 --- a/Modules/gcmodule.c +++ b/Modules/gcmodule.c @@ -1095,12 +1095,9 @@ collect(struct _gc_runtime_state *state, int generation, validate_list(&finalizers, 0); validate_list(&unreachable, PREV_MASK_COLLECTING); - /* Collect statistics on collectable objects found and print - * debugging information. - */ - for (gc = GC_NEXT(&unreachable); gc != &unreachable; gc = GC_NEXT(gc)) { - m++; - if (state->debug & DEBUG_COLLECTABLE) { + /* Print debugging information. */ + if (state->debug & DEBUG_COLLECTABLE) { + for (gc = GC_NEXT(&unreachable); gc != &unreachable; gc = GC_NEXT(gc)) { debug_cycle("collectable", FROM_GC(gc)); } } @@ -1122,6 +1119,7 @@ collect(struct _gc_runtime_state *state, int generation, * the reference cycles to be broken. It may also cause some objects * in finalizers to be freed. */ + m += gc_list_size(&unreachable); delete_garbage(state, &unreachable, old); } -- cgit v1.2.1