Skip to content

Commit ecbf35f

Browse files
authored
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.
1 parent 01171eb commit ecbf35f

File tree

3 files changed

+75
-6
lines changed

3 files changed

+75
-6
lines changed

Lib/test/test_gc.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,76 @@ def test_get_objects_arguments(self):
822822
self.assertRaises(TypeError, gc.get_objects, "1")
823823
self.assertRaises(TypeError, gc.get_objects, 1.234)
824824

825+
def test_38379(self):
826+
# When a finalizer resurrects objects, stats were reporting them as
827+
# having been collected. This affected both collect()'s return
828+
# value and the dicts returned by get_stats().
829+
N = 100
830+
831+
class A: # simple self-loop
832+
def __init__(self):
833+
self.me = self
834+
835+
class Z(A): # resurrecting __del__
836+
def __del__(self):
837+
zs.append(self)
838+
839+
zs = []
840+
841+
def getstats():
842+
d = gc.get_stats()[-1]
843+
return d['collected'], d['uncollectable']
844+
845+
gc.collect()
846+
gc.disable()
847+
848+
# No problems if just collecting A() instances.
849+
oldc, oldnc = getstats()
850+
for i in range(N):
851+
A()
852+
t = gc.collect()
853+
c, nc = getstats()
854+
self.assertEqual(t, 2*N) # instance object & its dict
855+
self.assertEqual(c - oldc, 2*N)
856+
self.assertEqual(nc - oldnc, 0)
857+
858+
# But Z() is not actually collected.
859+
oldc, oldnc = c, nc
860+
Z()
861+
# Nothing is collected - Z() is merely resurrected.
862+
t = gc.collect()
863+
c, nc = getstats()
864+
#self.assertEqual(t, 2) # before
865+
self.assertEqual(t, 0) # after
866+
#self.assertEqual(c - oldc, 2) # before
867+
self.assertEqual(c - oldc, 0) # after
868+
self.assertEqual(nc - oldnc, 0)
869+
870+
# Unfortunately, a Z() prevents _anything_ from being collected.
871+
# It should be possible to collect the A instances anyway, but
872+
# that will require non-trivial code changes.
873+
oldc, oldnc = c, nc
874+
for i in range(N):
875+
A()
876+
Z()
877+
# Z() prevents anything from being collected.
878+
t = gc.collect()
879+
c, nc = getstats()
880+
#self.assertEqual(t, 2*N + 2) # before
881+
self.assertEqual(t, 0) # after
882+
#self.assertEqual(c - oldc, 2*N + 2) # before
883+
self.assertEqual(c - oldc, 0) # after
884+
self.assertEqual(nc - oldnc, 0)
885+
886+
# But the A() trash is reclaimed on the next run.
887+
oldc, oldnc = c, nc
888+
t = gc.collect()
889+
c, nc = getstats()
890+
self.assertEqual(t, 2*N)
891+
self.assertEqual(c - oldc, 2*N)
892+
self.assertEqual(nc - oldnc, 0)
893+
894+
gc.enable()
825895

826896
class GCCallbackTests(unittest.TestCase):
827897
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 & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,12 +1095,9 @@ collect(struct _gc_runtime_state *state, int generation,
10951095
validate_list(&finalizers, 0);
10961096
validate_list(&unreachable, PREV_MASK_COLLECTING);
10971097

1098-
/* Collect statistics on collectable objects found and print
1099-
* debugging information.
1100-
*/
1101-
for (gc = GC_NEXT(&unreachable); gc != &unreachable; gc = GC_NEXT(gc)) {
1102-
m++;
1103-
if (state->debug & DEBUG_COLLECTABLE) {
1098+
/* Print debugging information. */
1099+
if (state->debug & DEBUG_COLLECTABLE) {
1100+
for (gc = GC_NEXT(&unreachable); gc != &unreachable; gc = GC_NEXT(gc)) {
11041101
debug_cycle("collectable", FROM_GC(gc));
11051102
}
11061103
}
@@ -1122,6 +1119,7 @@ collect(struct _gc_runtime_state *state, int generation,
11221119
* the reference cycles to be broken. It may also cause some objects
11231120
* in finalizers to be freed.
11241121
*/
1122+
m += gc_list_size(&unreachable);
11251123
delete_garbage(state, &unreachable, old);
11261124
}
11271125

0 commit comments

Comments
 (0)