Skip to content

Commit 6d13dc5

Browse files
hanlee55nascheme
andauthored
[3.13] gh-155515: Use GC tracking for HAMT iterators (GH-155517) (#155918)
Co-authored-by: Neil Schemenauer <nas-github@arctrix.com>
1 parent b0a181d commit 6d13dc5

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
@@ -1096,6 +1096,31 @@ def test_hamt_gc_2(self):
10961096

10971097
self.assertIsNone(ref())
10981098

1099+
def test_hamt_gc_3(self):
1100+
# gh-154535: the iterators must be tracked by the GC, otherwise a
1101+
# cycle running through one is never collected and the HAMT it
1102+
# holds -- and everything in it -- leaks.
1103+
A = HashKey(100, 'A')
1104+
1105+
container = []
1106+
h = hamt()
1107+
h = h.set(A, container)
1108+
1109+
hi = h.items()
1110+
self.assertTrue(gc.is_tracked(hi))
1111+
1112+
# Close the cycle: hi -> h -> container -> hi.
1113+
container.append(hi)
1114+
ref = weakref.ref(h)
1115+
1116+
del h, hi, container
1117+
1118+
gc.collect()
1119+
gc.collect()
1120+
gc.collect()
1121+
1122+
self.assertIsNone(ref())
1123+
10991124
def test_hamt_in_1(self):
11001125
A = HashKey(100, 'A')
11011126
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
@@ -2487,6 +2487,10 @@ static int
24872487
hamt_baseiter_tp_clear(PyHamtIterator *it)
24882488
{
24892489
Py_CLEAR(it->hi_obj);
2490+
/* i_nodes holds borrowed pointers into the tree that hi_obj was keeping
2491+
alive, so the cursor must not be used again. A negative i_level makes
2492+
hamt_iterator_next() report I_END without touching i_nodes. */
2493+
it->hi_iter.i_level = -1;
24902494
return 0;
24912495
}
24922496

@@ -2530,6 +2534,10 @@ hamt_baseiter_tp_iternext(PyHamtIterator *it)
25302534
static Py_ssize_t
25312535
hamt_baseiter_tp_len(PyHamtIterator *it)
25322536
{
2537+
if (it->hi_obj == NULL) {
2538+
/* tp_clear() ran on this iterator. */
2539+
return 0;
2540+
}
25332541
return it->hi_obj->h_count;
25342542
}
25352543

@@ -2550,6 +2558,7 @@ hamt_baseiter_new(PyTypeObject *type, binaryfunc yield, PyHamtObject *o)
25502558

25512559
hamt_iterator_init(&it->hi_iter, o->h_root);
25522560

2561+
PyObject_GC_Track(it);
25532562
return (PyObject*)it;
25542563
}
25552564

0 commit comments

Comments
 (0)