Skip to content

Commit f056212

Browse files
authored
gh-155515: Use GC tracking for HAMT iterators (#155517)
1 parent cd98657 commit f056212

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
@@ -2451,6 +2451,10 @@ hamt_baseiter_tp_clear(PyObject *op)
24512451
{
24522452
PyHamtIterator *it = (PyHamtIterator*)op;
24532453
Py_CLEAR(it->hi_obj);
2454+
/* i_nodes holds borrowed pointers into the tree that hi_obj was keeping
2455+
alive, so the cursor must not be used again. A negative i_level makes
2456+
hamt_iterator_next() report I_END without touching i_nodes. */
2457+
it->hi_iter.i_level = -1;
24542458
return 0;
24552459
}
24562460

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

@@ -2517,6 +2525,7 @@ hamt_baseiter_new(PyTypeObject *type, binaryfunc yield, PyHamtObject *o)
25172525

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

2528+
PyObject_GC_Track(it);
25202529
return (PyObject*)it;
25212530
}
25222531

0 commit comments

Comments
 (0)