Skip to content

Commit ae6bcf1

Browse files
committed
[GR-78917] Use reference queue signal thread.
PullRequest: graalpython/4810
2 parents 4131a53 + 9597666 commit ae6bcf1

11 files changed

Lines changed: 939 additions & 156 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/*
2+
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package com.oracle.graal.python.test.builtin.objects.cext;
42+
43+
import static org.junit.Assert.assertFalse;
44+
import static org.junit.Assert.assertNotNull;
45+
import static org.junit.Assert.assertNull;
46+
import static org.junit.Assert.assertSame;
47+
import static org.junit.Assert.assertTrue;
48+
49+
import java.lang.ref.Reference;
50+
import java.lang.ref.ReferenceQueue;
51+
import java.lang.ref.WeakReference;
52+
import java.util.concurrent.CountDownLatch;
53+
import java.util.concurrent.TimeUnit;
54+
import java.util.concurrent.atomic.AtomicReference;
55+
56+
import org.junit.Test;
57+
58+
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.ReferenceQueueCoordinator;
59+
60+
public class ReferenceQueueCoordinatorTests {
61+
62+
@Test
63+
public void watcherHandsOffExactlyOneReference() {
64+
ReferenceQueueCoordinator coordinator = startedCoordinator();
65+
Reference<?> reference = new WeakReference<>(new Object());
66+
67+
assertTrue(coordinator.beginWatcherRemove());
68+
Reference<?> handoff = coordinator.endWatcherRemove(reference);
69+
70+
assertSame(reference, coordinator.beginWatcherDrain(handoff));
71+
coordinator.finishDrain();
72+
73+
assertTrue(coordinator.beginWatcherRemove());
74+
assertNull(coordinator.endWatcherRemove(null));
75+
stop(coordinator);
76+
}
77+
78+
@Test
79+
public void deferredWatcherDrainRetainsHandoff() {
80+
ReferenceQueueCoordinator coordinator = startedCoordinator();
81+
Reference<?> reference = new WeakReference<>(new Object());
82+
83+
assertTrue(coordinator.beginWatcherRemove());
84+
Reference<?> handoff = coordinator.endWatcherRemove(reference);
85+
86+
// A caller that cannot drain yet must be able to leave the handoff pending and retry later.
87+
assertSame(reference, coordinator.getPendingReference());
88+
assertSame(reference, coordinator.beginWatcherDrain(handoff));
89+
coordinator.finishDrain();
90+
stop(coordinator);
91+
}
92+
93+
@Test
94+
public void forcedDrainSupersedesQueuedHandoff() {
95+
ReferenceQueueCoordinator coordinator = startedCoordinator();
96+
Reference<?> reference = new WeakReference<>(new Object());
97+
98+
assertTrue(coordinator.beginWatcherRemove());
99+
Reference<?> handoff = coordinator.endWatcherRemove(reference);
100+
101+
assertSame(reference, coordinator.beginForcedDrain(null));
102+
assertNull(coordinator.beginWatcherDrain(handoff));
103+
coordinator.finishDrain();
104+
stop(coordinator);
105+
}
106+
107+
@Test
108+
public void forcedDrainWithoutHandoffIsStillAcquired() {
109+
ReferenceQueueCoordinator coordinator = startedCoordinator();
110+
111+
assertNotNull(coordinator.beginForcedDrain(null));
112+
coordinator.finishDrain();
113+
stop(coordinator);
114+
}
115+
116+
@Test
117+
public void disabledCoordinatorPreservesHandoffUntilReenabled() {
118+
ReferenceQueueCoordinator coordinator = startedCoordinator();
119+
Reference<?> reference = new WeakReference<>(new Object());
120+
121+
assertTrue(coordinator.beginWatcherRemove());
122+
Reference<?> handoff = coordinator.endWatcherRemove(reference);
123+
124+
assertTrue(coordinator.disable(null));
125+
assertNull(coordinator.beginWatcherDrain(handoff));
126+
assertNull(coordinator.beginForcedDrain(null));
127+
128+
coordinator.enable();
129+
assertSame(reference, coordinator.beginWatcherDrain(handoff));
130+
coordinator.finishDrain();
131+
stop(coordinator);
132+
}
133+
134+
@Test
135+
public void disabledCoordinatorResumesWatcherWhenReenabled() {
136+
ReferenceQueueCoordinator coordinator = startedCoordinator();
137+
138+
assertTrue(coordinator.disable(null));
139+
coordinator.enable();
140+
assertTrue(coordinator.beginWatcherRemove());
141+
assertNull(coordinator.endWatcherRemove(null));
142+
stop(coordinator);
143+
}
144+
145+
@Test
146+
public void stopInvalidatesPendingHandoff() {
147+
ReferenceQueueCoordinator coordinator = startedCoordinator();
148+
Reference<?> reference = new WeakReference<>(new Object());
149+
150+
assertTrue(coordinator.beginWatcherRemove());
151+
Reference<?> handoff = coordinator.endWatcherRemove(reference);
152+
coordinator.stop();
153+
154+
assertFalse(coordinator.isActive());
155+
assertNull(coordinator.beginWatcherDrain(handoff));
156+
}
157+
158+
@Test
159+
public void blockingRemoveIsInterruptibleForStop() throws InterruptedException {
160+
ReferenceQueueCoordinator coordinator = new ReferenceQueueCoordinator();
161+
ReferenceQueue<Object> queue = new ReferenceQueue<>();
162+
CountDownLatch removing = new CountDownLatch(1);
163+
AtomicReference<Reference<?>> result = new AtomicReference<>();
164+
Thread watcher = new Thread(() -> {
165+
if (coordinator.beginWatcherRemove()) {
166+
removing.countDown();
167+
Reference<?> reference = coordinator.blockingRemove(queue);
168+
result.set(reference);
169+
coordinator.endWatcherRemove(reference);
170+
}
171+
coordinator.stopped();
172+
});
173+
coordinator.start(watcher);
174+
175+
assertTrue(removing.await(1, TimeUnit.SECONDS));
176+
coordinator.stop();
177+
178+
assertFalse(watcher.isAlive());
179+
assertNull(result.get());
180+
assertFalse(coordinator.isActive());
181+
}
182+
183+
private static ReferenceQueueCoordinator startedCoordinator() {
184+
ReferenceQueueCoordinator coordinator = new ReferenceQueueCoordinator();
185+
coordinator.start(new Thread(() -> {
186+
}));
187+
assertTrue(coordinator.isActive());
188+
return coordinator;
189+
}
190+
191+
private static void stop(ReferenceQueueCoordinator coordinator) {
192+
coordinator.stop();
193+
assertFalse(coordinator.isActive());
194+
}
195+
}

‎graalpython/com.oracle.graal.python/src/com/oracle/graal/python/PythonLanguage.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,14 @@ protected void finalizeContext(PythonContext context) {
433433
boundaryCallDataMap.size();
434434
}
435435

436+
@Override
437+
protected void disposeContext(PythonContext context) {
438+
if (context.getCApiContext() != null) {
439+
context.getCApiContext().dispose();
440+
}
441+
super.disposeContext(context);
442+
}
443+
436444
@Override
437445
protected boolean areOptionsCompatible(OptionValues firstOptions, OptionValues newOptions) {
438446
return PythonOptions.areOptionsCompatible(firstOptions, newOptions);

‎graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/GcModuleBuiltins.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ static long javaCollect(Node inliningTarget, GilNode gil) {
211211
}
212212
// collect some weak references now
213213
PythonContext.triggerAsyncActions(inliningTarget);
214-
CApiTransitions.pollReferenceQueue();
214+
CApiTransitions.pollReferenceQueueIfPending(PythonContext.get(null));
215215
/*
216216
* CPython's GC returns the number of collected cycles. This is not something we can
217217
* determine, but to return some useful info to the Python program, we return the amount

‎graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextBuiltins.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,7 @@ public Object execute(Object[] arguments) {
698698
if (retNode != null) {
699699
result = retNode.execute(result);
700700
}
701-
CApiTransitions.maybeGCALot();
701+
CApiTransitions.maybeGCALot(this);
702702
return result;
703703
} catch (Throwable t) {
704704
throw checkThrowableBeforeNative(t, "CApiBuiltin", self.name);
@@ -1044,7 +1044,7 @@ static void GraalPyPrivate_TriggerGC(long delay) {
10441044
// Restore interrupt status
10451045
Thread.currentThread().interrupt();
10461046
}
1047-
CApiTransitions.pollReferenceQueue();
1047+
CApiTransitions.pollReferenceQueueIfPending(PythonContext.get(null));
10481048
PythonContext.triggerAsyncActions(EncapsulatingNodeReference.getCurrent().get());
10491049
}
10501050

0 commit comments

Comments
 (0)