Skip to content

Commit 04350d9

Browse files
[3.14] gh-155515: Use GC tracking for HAMT iterators (GH-155517) (#155698)
Co-authored-by: Neil Schemenauer <nas-github@arctrix.com>
1 parent b104b74 commit 04350d9

3 files changed

Lines changed: 38 additions & 0 deletions

File tree

Lib/test/test_context.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,6 +1282,31 @@ def test_hamt_gc_2(self):
12821282

12831283
self.assertIsNone(ref())
12841284

1285+
def test_hamt_gc_3(self):
1286+
# gh-154535: the iterators must be tracked by the GC, otherwise a
1287+
# cycle running through one is never collected and the HAMT it
1288+
# holds -- and everything in it -- leaks.
1289+
A = HashKey(100, 'A')
1290+
1291+
container = []
1292+
h = hamt()
1293+
h = h.set(A, container)
1294+
1295+
hi = h.items()
1296+
self.assertTrue(gc.is_tracked(hi))
1297+
1298+
# Close the cycle: hi -> h -> container -> hi.
1299+
container.append(hi)
1300+
ref = weakref.ref(h)
1301+
1302+
del h, hi, container
1303+
1304+
gc.collect()
1305+
gc.collect()
1306+
gc.collect()
1307+
1308+
self.assertIsNone(ref())
1309+
12851310
def test_hamt_in_1(self):
12861311
A = HashKey(100, 'A')
12871312
AA = HashKey(100, 'A')
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Track the internal HAMT iterators, which back iteration over a
2+
:class:`contextvars.Context`, with the garbage collector. A reference cycle
3+
running through such an iterator was never collected, leaking the whole
4+
context it iterated over.

Python/hamt.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2450,6 +2450,10 @@ hamt_baseiter_tp_clear(PyObject *op)
24502450
{
24512451
PyHamtIterator *it = (PyHamtIterator*)op;
24522452
Py_CLEAR(it->hi_obj);
2453+
/* i_nodes holds borrowed pointers into the tree that hi_obj was keeping
2454+
alive, so the cursor must not be used again. A negative i_level makes
2455+
hamt_iterator_next() report I_END without touching i_nodes. */
2456+
it->hi_iter.i_level = -1;
24532457
return 0;
24542458
}
24552459

@@ -2496,6 +2500,10 @@ static Py_ssize_t
24962500
hamt_baseiter_tp_len(PyObject *op)
24972501
{
24982502
PyHamtIterator *it = (PyHamtIterator*)op;
2503+
if (it->hi_obj == NULL) {
2504+
/* tp_clear() ran on this iterator. */
2505+
return 0;
2506+
}
24992507
return it->hi_obj->h_count;
25002508
}
25012509

@@ -2516,6 +2524,7 @@ hamt_baseiter_new(PyTypeObject *type, binaryfunc yield, PyHamtObject *o)
25162524

25172525
hamt_iterator_init(&it->hi_iter, o->h_root);
25182526

2527+
PyObject_GC_Track(it);
25192528
return (PyObject*)it;
25202529
}
25212530

0 commit comments

Comments
 (0)