[Python-checkins] bpo-38379: don't claim objects are collected when they aren't (#16658)

Tim Peters webhook-mailer at python.org
Wed Oct 9 13:37:42 EDT 2019


https://github.com/python/cpython/commit/ecbf35f9335b0420cb8adfda6f299d6747a16515
commit: ecbf35f9335b0420cb8adfda6f299d6747a16515
branch: master
author: Tim Peters <tim.peters at gmail.com>
committer: GitHub <noreply at github.com>
date: 2019-10-09T12:37:30-05:00
summary:

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.

files:
A Misc/NEWS.d/next/Core and Builtins/2019-10-09-16-50-52.bpo-38379.oz5qZx.rst
M Lib/test/test_gc.py
M Modules/gcmodule.c

diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py
index 8215390cb7089..f52db1eab169c 100644
--- a/Lib/test/test_gc.py
+++ b/Lib/test/test_gc.py
@@ -822,6 +822,76 @@ def test_get_objects_arguments(self):
         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 0000000000000..82dcb525dd49d
--- /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 2b47abae1ae4d..766f8e0c67fa8 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);
     }
 



More information about the Python-checkins mailing list