Skip to content

Commit a081e93

Browse files
pablogsalmiss-islington
authored andcommitted
[3.7] bpo-38379: don't claim objects are collected when they aren't (GH-16658) (GH-16685)
* [bpo-38379](https://bugs.python.org/issue38379): when a finalizer resurrects an object, nothing is actually collected in this run of gc. Change the stats to relect that truth.. (cherry picked from commit ecbf35f) Co-authored-by: Tim Peters <tim.peters@gmail.com> https://bugs.python.org/issue38379 Automerge-Triggered-By: @pablogsal
1 parent 598bfa4 commit a081e93

File tree

3 files changed

+76
-7
lines changed

3 files changed

+76
-7
lines changed

Lib/test/test_gc.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,77 @@ def test_freeze(self):
755755
gc.unfreeze()
756756
self.assertEqual(gc.get_freeze_count(), 0)
757757

758+
def test_38379(self):
759+
# When a finalizer resurrects objects, stats were reporting them as
760+
# having been collected. This affected both collect()'s return
761+
# value and the dicts returned by get_stats().
762+
N = 100
763+
764+
class A: # simple self-loop
765+
def __init__(self):
766+
self.me = self
767+
768+
class Z(A): # resurrecting __del__
769+
def __del__(self):
770+
zs.append(self)
771+
772+
zs = []
773+
774+
def getstats():
775+
d = gc.get_stats()[-1]
776+
return d['collected'], d['uncollectable']
777+
778+
gc.collect()
779+
gc.disable()
780+
781+
# No problems if just collecting A() instances.
782+
oldc, oldnc = getstats()
783+
for i in range(N):
784+
A()
785+
t = gc.collect()
786+
c, nc = getstats()
787+
self.assertEqual(t, 2*N) # instance object & its dict
788+
self.assertEqual(c - oldc, 2*N)
789+
self.assertEqual(nc - oldnc, 0)
790+
791+
# But Z() is not actually collected.
792+
oldc, oldnc = c, nc
793+
Z()
794+
# Nothing is collected - Z() is merely resurrected.
795+
t = gc.collect()
796+
c, nc = getstats()
797+
#self.assertEqual(t, 2) # before
798+
self.assertEqual(t, 0) # after
799+
#self.assertEqual(c - oldc, 2) # before
800+
self.assertEqual(c - oldc, 0) # after
801+
self.assertEqual(nc - oldnc, 0)
802+
803+
# Unfortunately, a Z() prevents _anything_ from being collected.
804+
# It should be possible to collect the A instances anyway, but
805+
# that will require non-trivial code changes.
806+
oldc, oldnc = c, nc
807+
for i in range(N):
808+
A()
809+
Z()
810+
# Z() prevents anything from being collected.
811+
t = gc.collect()
812+
c, nc = getstats()
813+
#self.assertEqual(t, 2*N + 2) # before
814+
self.assertEqual(t, 0) # after
815+
#self.assertEqual(c - oldc, 2*N + 2) # before
816+
self.assertEqual(c - oldc, 0) # after
817+
self.assertEqual(nc - oldnc, 0)
818+
819+
# But the A() trash is reclaimed on the next run.
820+
oldc, oldnc = c, nc
821+
t = gc.collect()
822+
c, nc = getstats()
823+
self.assertEqual(t, 2*N)
824+
self.assertEqual(c - oldc, 2*N)
825+
self.assertEqual(nc - oldnc, 0)
826+
827+
gc.enable()
828+
758829

759830
class GCCallbackTests(unittest.TestCase):
760831
def setUp(self):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

Modules/gcmodule.c

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -887,13 +887,9 @@ collect(int generation, Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable,
887887
*/
888888
move_legacy_finalizer_reachable(&finalizers);
889889

890-
/* Collect statistics on collectable objects found and print
891-
* debugging information.
892-
*/
893-
for (gc = unreachable.gc.gc_next; gc != &unreachable;
894-
gc = gc->gc.gc_next) {
895-
m++;
896-
if (_PyRuntime.gc.debug & DEBUG_COLLECTABLE) {
890+
/* Print debugging information. */
891+
if (_PyRuntime.gc.debug & DEBUG_COLLECTABLE) {
892+
for (gc = unreachable.gc.gc_next; gc != &unreachable; gc = gc->gc.gc_next) {
897893
debug_cycle("collectable", FROM_GC(gc));
898894
}
899895
}
@@ -913,6 +909,7 @@ collect(int generation, Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable,
913909
* the reference cycles to be broken. It may also cause some objects
914910
* in finalizers to be freed.
915911
*/
912+
m += gc_list_size(&unreachable);
916913
delete_garbage(&unreachable, old);
917914
}
918915

0 commit comments

Comments
 (0)