Skip to content

Commit 206b21e

Browse files
graingertambv
andauthored
bpo-44962: Fix a race in WeakKeyDict, WeakValueDict and WeakSet when two threads attempt to commit the last pending removal (GH-27921)
Fixes: Traceback (most recent call last): File "/home/graingert/projects/asyncio-demo/demo.py", line 36, in <module> sys.exit(main()) File "/home/graingert/projects/asyncio-demo/demo.py", line 30, in main test_all_tasks_threading() File "/home/graingert/projects/asyncio-demo/demo.py", line 24, in test_all_tasks_threading results.append(f.result()) File "/usr/lib/python3.10/concurrent/futures/_base.py", line 438, in result return self.__get_result() File "/usr/lib/python3.10/concurrent/futures/_base.py", line 390, in __get_result raise self._exception File "/usr/lib/python3.10/concurrent/futures/thread.py", line 52, in run result = self.fn(*self.args, **self.kwargs) File "/usr/lib/python3.10/asyncio/runners.py", line 47, in run _cancel_all_tasks(loop) File "/usr/lib/python3.10/asyncio/runners.py", line 56, in _cancel_all_tasks to_cancel = tasks.all_tasks(loop) File "/usr/lib/python3.10/asyncio/tasks.py", line 53, in all_tasks tasks = list(_all_tasks) File "/usr/lib/python3.10/_weakrefset.py", line 60, in __iter__ with _IterationGuard(self): File "/usr/lib/python3.10/_weakrefset.py", line 33, in __exit__ w._commit_removals() File "/usr/lib/python3.10/_weakrefset.py", line 57, in _commit_removals discard(l.pop()) IndexError: pop from empty list Also fixes: Exception ignored in: weakref callback <function WeakKeyDictionary.__init__.<locals>.remove at 0x00007fe82245d2e0> Traceback (most recent call last): File "/usr/lib/pypy3/lib-python/3/weakref.py", line 390, in remove del self.data[k] KeyError: <weakref at 0x00007fe76e8d8180; dead> Exception ignored in: weakref callback <function WeakKeyDictionary.__init__.<locals>.remove at 0x00007fe82245d2e0> Traceback (most recent call last): File "/usr/lib/pypy3/lib-python/3/weakref.py", line 390, in remove del self.data[k] KeyError: <weakref at 0x00007fe76e8d81a0; dead> Exception ignored in: weakref callback <function WeakKeyDictionary.__init__.<locals>.remove at 0x00007fe82245d2e0> Traceback (most recent call last): File "/usr/lib/pypy3/lib-python/3/weakref.py", line 390, in remove del self.data[k] KeyError: <weakref at 0x000056548f1e24a0; dead> See: agronholm/anyio#362 (comment) See also: https://bugs.python.org/issue29519 Co-authored-by: Łukasz Langa <[email protected]>
1 parent 28db1f6 commit 206b21e

File tree

3 files changed

+28
-12
lines changed

3 files changed

+28
-12
lines changed

Lib/_weakrefset.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,14 @@ def _remove(item, selfref=ref(self)):
5151
self.update(data)
5252

5353
def _commit_removals(self):
54-
l = self._pending_removals
54+
pop = self._pending_removals.pop
5555
discard = self.data.discard
56-
while l:
57-
discard(l.pop())
56+
while True:
57+
try:
58+
item = pop()
59+
except IndexError:
60+
return
61+
discard(item)
5862

5963
def __iter__(self):
6064
with _IterationGuard(self):

Lib/weakref.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,17 @@ def remove(wr, selfref=ref(self), _atomic_removal=_remove_dead_weakref):
119119
self.data = {}
120120
self.update(other, **kw)
121121

122-
def _commit_removals(self):
123-
l = self._pending_removals
122+
def _commit_removals(self, _atomic_removal=_remove_dead_weakref):
123+
pop = self._pending_removals.pop
124124
d = self.data
125125
# We shouldn't encounter any KeyError, because this method should
126126
# always be called *before* mutating the dict.
127-
while l:
128-
key = l.pop()
129-
_remove_dead_weakref(d, key)
127+
while True:
128+
try:
129+
key = pop()
130+
except IndexError:
131+
return
132+
_atomic_removal(d, key)
130133

131134
def __getitem__(self, key):
132135
if self._pending_removals:
@@ -370,7 +373,10 @@ def remove(k, selfref=ref(self)):
370373
if self._iterating:
371374
self._pending_removals.append(k)
372375
else:
373-
del self.data[k]
376+
try:
377+
del self.data[k]
378+
except KeyError:
379+
pass
374380
self._remove = remove
375381
# A list of dead weakrefs (keys to be removed)
376382
self._pending_removals = []
@@ -384,11 +390,16 @@ def _commit_removals(self):
384390
# because a dead weakref never compares equal to a live weakref,
385391
# even if they happened to refer to equal objects.
386392
# However, it means keys may already have been removed.
387-
l = self._pending_removals
393+
pop = self._pending_removals.pop
388394
d = self.data
389-
while l:
395+
while True:
396+
try:
397+
key = pop()
398+
except IndexError:
399+
return
400+
390401
try:
391-
del d[l.pop()]
402+
del d[key]
392403
except KeyError:
393404
pass
394405

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a race in WeakKeyDictionary, WeakValueDictionary and WeakSet when two threads attempt to commit the last pending removal. This fixes asyncio.create_task and fixes a data loss in asyncio.run where shutdown_asyncgens is not run

0 commit comments

Comments
 (0)