Skip to content

Commit cae9916

Browse files
committed
gh-157841: Synchronize functools.partial state access
Acquire coherent strong-reference snapshots under the partial object lock for calls, repr, reduce, and nested-partial construction. Publish replacement state atomically, release displaced references after publication, and retire replaced instance dictionaries through QSBR. Keep free-threaded vectorcall dispatch immutable after construction. Add concurrent state/coherence coverage, placeholder-aware flattening tests, and regressions for reentrant keyword merging and finalizers.
1 parent 738d055 commit cae9916

3 files changed

Lines changed: 336 additions & 61 deletions

File tree

‎Lib/test/test_free_threading/test_functools.py‎

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import random
22
import unittest
33

4-
from functools import lru_cache
4+
from functools import Placeholder, lru_cache, partial
55
from threading import Barrier, Thread
66

77
from test.support import threading_helper
@@ -71,5 +71,144 @@ def test_reentrant_cache_clear_bounded(self):
7171
self._test_reentrant_cache_clear(maxsize=128)
7272

7373

74+
def partial_target(x, y=0, *extra, z=0):
75+
return x, y, z, True
76+
77+
78+
class PartialCallable:
79+
def __call__(self, x, y=0, *extra, z=0):
80+
return x, y, z, False
81+
82+
83+
@threading_helper.requires_working_threading()
84+
class TestPartial(unittest.TestCase):
85+
def _race_setstate(self, p, read, *, placeholders=False, readers=4):
86+
iterations = 2000
87+
barrier = Barrier(readers + 2)
88+
errors = []
89+
callable_object = PartialCallable()
90+
91+
def reader():
92+
try:
93+
barrier.wait()
94+
for _ in range(iterations):
95+
read(p)
96+
except Exception as exc:
97+
errors.append(exc)
98+
99+
def writer(offset):
100+
try:
101+
barrier.wait()
102+
for i in range(iterations):
103+
value = 10**12 + offset * iterations + i
104+
if placeholders and i % 2:
105+
args = (Placeholder, value)
106+
else:
107+
args = (value, value)
108+
func = partial_target if i % 2 else callable_object
109+
p.__setstate__((func, args, {"z": value}, {"tag": value}))
110+
except Exception as exc:
111+
errors.append(exc)
112+
113+
threads = [Thread(target=reader) for _ in range(readers)]
114+
threads += [Thread(target=writer, args=(i,)) for i in range(2)]
115+
with threading_helper.start_threads(threads):
116+
pass
117+
self.assertEqual(errors, [])
118+
119+
def _partial(self):
120+
p = partial(PartialCallable(), 0, 0, z=0)
121+
p.tag = 0
122+
return p
123+
124+
def test_concurrent_setstate(self):
125+
p = self._partial()
126+
self._race_setstate(p, None, readers=0)
127+
x, y, z, marker = p()
128+
self.assertEqual((x, x), (y, z))
129+
self.assertEqual(marker, bool(y % 2))
130+
131+
def test_concurrent_setstate_and_repr(self):
132+
self._race_setstate(self._partial(), repr)
133+
134+
def test_concurrent_setstate_and_call(self):
135+
def read(p):
136+
x, y, z, marker = p()
137+
self.assertEqual((x, x), (y, z))
138+
self.assertEqual(marker, bool(y % 2))
139+
140+
self._race_setstate(self._partial(), read)
141+
142+
def test_concurrent_setstate_and_placeholders(self):
143+
def read(p):
144+
x, y, z, marker = p(99)
145+
self.assertEqual(y, z)
146+
self.assertIn(x, (99, y))
147+
self.assertEqual(marker, bool(y % 2))
148+
149+
self._race_setstate(self._partial(), read, placeholders=True)
150+
151+
def test_concurrent_setstate_and_reduce(self):
152+
def read(p):
153+
constructor_args, state = p.__reduce__()[1:]
154+
func, args, kw, instance_dict = state
155+
self.assertIs(constructor_args[0], func)
156+
self.assertEqual(func is partial_target, bool(args[0] % 2))
157+
self.assertEqual((args[0], args[0]), (args[1], kw["z"]))
158+
self.assertEqual(args[0], instance_dict["tag"])
159+
160+
self._race_setstate(self._partial(), read)
161+
162+
def test_concurrent_setstate_and_flatten(self):
163+
p = partial(partial_target, 0, 0, z=0)
164+
iterations = 2000
165+
barrier = Barrier(5)
166+
errors = []
167+
168+
def writer():
169+
try:
170+
barrier.wait()
171+
for i in range(iterations):
172+
value = 10**12 + i
173+
args = (Placeholder, value) if i % 2 else (value, value)
174+
p.__setstate__((partial_target, args, {"z": value}, None))
175+
except Exception as exc:
176+
errors.append(exc)
177+
178+
def reader():
179+
try:
180+
barrier.wait()
181+
for _ in range(iterations):
182+
q = partial(p, 99)
183+
x, y, z, marker = q()
184+
self.assertIs(q.func, partial_target)
185+
self.assertEqual(y, z)
186+
self.assertIn(x, (99, y))
187+
self.assertTrue(marker)
188+
except Exception as exc:
189+
errors.append(exc)
190+
191+
threads = [Thread(target=reader) for _ in range(4)]
192+
threads.append(Thread(target=writer))
193+
with threading_helper.start_threads(threads):
194+
pass
195+
self.assertEqual(errors, [])
196+
197+
def test_concurrent_setstate_and_members(self):
198+
def read(p):
199+
self.assertTrue(callable(p.func))
200+
self.assertEqual(len(p.args), 2)
201+
self.assertIn("z", p.keywords)
202+
203+
self._race_setstate(self._partial(), read)
204+
205+
def test_concurrent_setstate_and_instance_dict(self):
206+
def read(p):
207+
self.assertIsInstance(p.tag, int)
208+
self.assertIn("tag", p.__dict__)
209+
210+
self._race_setstate(self._partial(), read)
211+
212+
74213
if __name__ == "__main__":
75214
unittest.main()

‎Lib/test/test_functools.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,46 @@ class TestPartialC(TestPartial, unittest.TestCase):
619619
module = c_functools
620620
partial = c_functools.partial
621621

622+
def test_call_snapshot_reentrancy(self):
623+
PH = self.module.Placeholder
624+
p = self.partial(capture, PH, 2, k=0)
625+
626+
class EvilKey(str):
627+
armed = True
628+
__hash__ = str.__hash__
629+
630+
def __eq__(self, other):
631+
if self.armed:
632+
self.armed = False
633+
p.__setstate__((capture, (PH, PH, 3), {}, None))
634+
return str.__eq__(self, other)
635+
636+
key = EvilKey("k")
637+
# The slot wrapper exercises tp_call, including its keyword merge.
638+
self.assertEqual(p.__call__(1, **{key: 1}), ((1, 2), {"k": 1}))
639+
self.assertFalse(key.armed)
640+
self.assertEqual(p(10, 20), ((10, 20, 3), {}))
641+
642+
def test_setstate_finalizer_reentrancy(self):
643+
p = self.partial(capture)
644+
seen = []
645+
646+
class OldCallable:
647+
def __call__(self, *args, **kw):
648+
pass
649+
650+
def __del__(self):
651+
seen.append((p.func, p.args, p.keywords.copy(),
652+
p.__dict__.copy()))
653+
p.__setstate__((capture, (3,), {"a": 3}, {"tag": 3}))
654+
655+
p.__setstate__((OldCallable(), (1,), {"a": 1}, {"tag": 1}))
656+
p.__setstate__((capture, (2,), {"a": 2}, {"tag": 2}))
657+
gc.collect()
658+
self.assertEqual(seen, [(capture, (2,), {"a": 2}, {"tag": 2})])
659+
self.assertEqual(p(), ((3,), {"a": 3}))
660+
self.assertEqual(p.tag, 3)
661+
622662
def test_attributes_unwritable(self):
623663
# attributes should not be writable
624664
p = self.partial(capture, 1, 2, a=10, b=20)

0 commit comments

Comments
 (0)