Skip to content

Commit b947a6d

Browse files
committed
[GR-7405] [GR-7408] Signal handlers can now be run without triggering multi-threading
PullRequest: truffleruby/2439
2 parents 5a015e2 + a6e3d0b commit b947a6d

File tree

15 files changed

+201
-116
lines changed

15 files changed

+201
-116
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ New features:
44

55
* Access to local variables of the interactive Binding via language bindings is now supported: `context.getBindings("ruby").putMember("my_var", 42);` (#2030).
66
* `VALUE`s in C extensions now expose the Ruby object when viewed in the debugger, as long as they have not been converted to native values.
7+
* Signal handlers can now be run without triggering multi-threading.
78

89
Bug fixes:
910

mx.truffleruby/mx_truffleruby.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def contents(self, result):
105105
ruby_options = [
106106
'--experimental-options',
107107
'--building-core-cexts',
108+
'--platform-native-interrupt=false', # no librubysignal in the ruby home yet
108109
'--launcher=' + result,
109110
'--disable-gems',
110111
'--disable-rubyopt',
@@ -267,6 +268,6 @@ def verify_ci(args):
267268
'ruby_testdownstream_hello': [ruby_testdownstream_hello, ''],
268269
'ruby_testdownstream_sulong': [ruby_testdownstream_sulong, ''],
269270
'ruby_spotbugs': [ruby_spotbugs, ''],
270-
'verify-ci' : [verify_ci, '[options]'],
271+
'verify-ci': [verify_ci, '[options]'],
271272
'ruby_jacoco_args': [ruby_jacoco_args, ''],
272273
})

mx.truffleruby/suite.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
"mxversion": "5.281.0",
33
"name": "truffleruby",
44

5-
"imports" : {
6-
"suites" : [
5+
"imports": {
6+
"suites": [
77
{
88
"name": "regex",
99
"subdir": True,
1010
"version": "8c1abd10ad2992580462d7629db316177a256f82",
1111
"urls": [
12-
{"url": "https://github.com/oracle/graal.git", "kind" : "git"},
13-
{"url": "https://curio.ssw.jku.at/nexus/content/repositories/snapshots", "kind" : "binary"},
12+
{"url": "https://github.com/oracle/graal.git", "kind": "git"},
13+
{"url": "https://curio.ssw.jku.at/nexus/content/repositories/snapshots", "kind": "binary"},
1414
]
1515
},
1616
{
@@ -172,9 +172,20 @@
172172
"license": ["EPL-2.0"],
173173
},
174174

175+
"org.truffleruby.rubysignal": {
176+
"dir": "src/main/c/rubysignal",
177+
"native": "shared_lib",
178+
"deliverable": "rubysignal",
179+
"buildDependencies": [
180+
"org.truffleruby", # for the generated JNI header file
181+
],
182+
"cflags": ["-g", "-Wall", "-Werror"],
183+
},
184+
175185
"org.truffleruby": {
176186
"dir": "src/main",
177187
"sourceDirs": ["java"],
188+
"jniHeaders": True,
178189
"dependencies": [
179190
"truffleruby:TRUFFLERUBY-ANNOTATIONS",
180191
"truffleruby:TRUFFLERUBY-SHARED",
@@ -446,6 +457,7 @@
446457
"file:lib/cext/*.rb",
447458
"dependency:org.truffleruby.cext/src/main/c/truffleposix/<lib:truffleposix>",
448459
"dependency:org.truffleruby.cext/src/main/c/cext/<lib:truffleruby>",
460+
"dependency:org.truffleruby.rubysignal",
449461
],
450462
"lib/cext/include/": [
451463
"file:lib/cext/include/ccan",
@@ -513,14 +525,21 @@
513525
"distDependencies": [
514526
"TRUFFLERUBY",
515527
"TRUFFLERUBY-SERVICES",
528+
"TRUFFLERUBY_GRAALVM_SUPPORT",
516529
],
530+
"javaProperties": {
531+
"org.graalvm.language.ruby.home": "<path:TRUFFLERUBY_GRAALVM_SUPPORT>"
532+
},
517533
"license": ["EPL-2.0"],
518534
},
519535

520536
"TRUFFLERUBY-TCK": {
521537
"testDistribution": True,
522538
"dependencies": ["org.truffleruby.tck"],
523539
"distDependencies": ["truffle:TRUFFLE_TCK"],
540+
"javaProperties": {
541+
"org.graalvm.language.ruby.home": "<path:TRUFFLERUBY_GRAALVM_SUPPORT>"
542+
},
524543
"license": ["EPL-2.0"],
525544
},
526545
},

spec/ruby/core/signal/trap_spec.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,21 @@
5252
Process.kill(:HUP, Process.pid).should == 1
5353
end
5454

55+
it "can register a new handler after :IGNORE" do
56+
Signal.trap :HUP, :IGNORE
57+
Process.kill(:HUP, Process.pid).should == 1
58+
59+
done = false
60+
Signal.trap(:HUP) do
61+
ScratchPad.record :block_trap
62+
done = true
63+
end
64+
65+
Process.kill(:HUP, Process.pid).should == 1
66+
Thread.pass until done
67+
ScratchPad.recorded.should == :block_trap
68+
end
69+
5570
it "ignores the signal when passed nil" do
5671
Signal.trap :HUP, nil
5772
Signal.trap(:HUP, @saved_trap).should be_nil

spec/ruby/optional/capi/thread_spec.rb

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,13 @@ def call_capi_rb_thread_wakeup
102102
end
103103

104104
describe "rb_thread_call_without_gvl" do
105-
it "runs a C function with the global lock unlocked" do
105+
it "runs a C function with the global lock unlocked and can be woken by Thread#wakeup" do
106106
thr = Thread.new do
107107
@t.rb_thread_call_without_gvl
108108
end
109109

110110
# Wait until it's blocking...
111-
Thread.pass while thr.status and thr.status != "sleep"
111+
Thread.pass until thr.stop?
112112

113113
# The thread status is set to sleep by rb_thread_call_without_gvl(),
114114
# but the thread might not be in the blocking read(2) yet, so wait a bit.
@@ -121,6 +121,40 @@ def call_capi_rb_thread_wakeup
121121
thr.value.should be_true
122122
end
123123

124+
platform_is_not :windows do
125+
it "runs a C function with the global lock unlocked and can be woken by a signal" do
126+
# Ruby signal handlers run on the main thread, so we need to reverse roles here and have a thread interrupt us
127+
thr = Thread.current
128+
thr.should == Thread.main
129+
130+
going_to_block = false
131+
interrupter = Thread.new do
132+
# Wait until it's blocking...
133+
Thread.pass until going_to_block and thr.stop?
134+
135+
# The thread status is set to sleep by rb_thread_call_without_gvl(),
136+
# but the thread might not be in the blocking read(2) yet, so wait a bit.
137+
sleep 0.1
138+
139+
# Wake it up by sending a signal
140+
done = false
141+
prev_handler = Signal.trap(:HUP) { done = true }
142+
begin
143+
Process.kill :HUP, Process.pid
144+
sleep 0.001 until done
145+
ensure
146+
Signal.trap(:HUP, prev_handler)
147+
end
148+
end
149+
150+
going_to_block = true
151+
# Make sure it stopped and we got a proper value
152+
@t.rb_thread_call_without_gvl.should be_true
153+
154+
interrupter.join
155+
end
156+
end
157+
124158
guard -> { platform_is :mingw and ruby_version_is ""..."2.7" } do
125159
it "runs a C function with the global lock unlocked and unlocks IO with the generic RUBY_UBF_IO" do
126160
thr = Thread.new do
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#include "org_truffleruby_signal_LibRubySignal.h"
2+
#include <pthread.h>
3+
#include <signal.h>
4+
5+
static void empty_handler(int sig) {
6+
}
7+
8+
JNIEXPORT jint JNICALL Java_org_truffleruby_signal_LibRubySignal_setupSIGVTALRMEmptySignalHandler(JNIEnv *env, jclass clazz) {
9+
struct sigaction action = {
10+
.sa_flags = 0, /* flags = 0 is intended as we want no SA_RESTART so we can interrupt blocking syscalls */
11+
.sa_handler = empty_handler,
12+
};
13+
return sigaction(SIGVTALRM, &action, NULL);
14+
}
15+
16+
JNIEXPORT jlong JNICALL Java_org_truffleruby_signal_LibRubySignal_threadID(JNIEnv *env, jclass clazz) {
17+
pthread_t pthread_id = pthread_self();
18+
return (jlong) pthread_id;
19+
}
20+
21+
JNIEXPORT jint JNICALL Java_org_truffleruby_signal_LibRubySignal_sendSIGVTALRMToThread(JNIEnv *env, jclass clazz, jlong threadID) {
22+
pthread_t pthread_id = (pthread_t) threadID;
23+
return pthread_kill(pthread_id, SIGVTALRM);
24+
}

src/main/java/org/truffleruby/RubyContext.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ public RubyContext(RubyLanguage language, TruffleLanguage.Env env) {
214214

215215
Metrics.printTime("before-thread-manager");
216216
threadManager = new ThreadManager(this, language);
217-
threadManager.initialize(truffleNFIPlatform, nativeConfiguration);
217+
threadManager.initialize();
218218
threadManager.initializeMainThread(Thread.currentThread());
219219
Metrics.printTime("after-thread-manager");
220220

@@ -300,7 +300,7 @@ protected boolean patch(Env newEnv) {
300300
this.truffleNFIPlatform = createNativePlatform();
301301
encodingManager.initializeDefaultEncodings(truffleNFIPlatform, nativeConfiguration);
302302

303-
threadManager.initialize(truffleNFIPlatform, nativeConfiguration);
303+
threadManager.initialize();
304304
threadManager.restartMainThread(Thread.currentThread());
305305

306306
Metrics.printTime("before-rehash");

src/main/java/org/truffleruby/core/VMPrimitiveNodes.java

Lines changed: 5 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,12 @@
8080

8181
import com.oracle.truffle.api.CompilerDirectives;
8282
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
83-
import com.oracle.truffle.api.TruffleContext;
8483
import com.oracle.truffle.api.dsl.Cached;
8584
import com.oracle.truffle.api.dsl.Specialization;
8685
import com.oracle.truffle.api.frame.VirtualFrame;
8786
import com.oracle.truffle.api.profiles.BranchProfile;
8887
import com.oracle.truffle.api.profiles.ConditionProfile;
8988

90-
import sun.misc.Signal;
9189
import sun.misc.SignalHandler;
9290

9391
@CoreModule(value = "VMPrimitives", isClass = true)
@@ -260,48 +258,16 @@ protected boolean watchSignalProc(Object signalString, RubyProc action,
260258
}
261259

262260
final String signalName = libSignalString.getJavaString(signalString);
263-
return registerHandler(signalName, signal -> {
264-
if (context.getOptions().SINGLE_THREADED) {
265-
warnRestoreAndRaise(context, signalName, signal, "create a thread");
266-
return;
267-
}
268261

262+
return registerHandler(signalName, signal -> {
269263
final RubyThread rootThread = context.getThreadManager().getRootThread();
270-
271-
// Workaround: we need to register with Truffle (which means going multithreaded),
272-
// so that NFI can get its context to call pthread_kill() (GR-7405).
273-
final TruffleContext truffleContext = context.getEnv().getContext();
274-
final Object prev;
275-
try {
276-
prev = truffleContext.enter(this);
277-
} catch (IllegalStateException e) { // Multi threaded access denied from Truffle
278-
warnRestoreAndRaise(context, signalName, signal, "attach a thread");
279-
return;
280-
}
281-
try {
282-
context.getSafepointManager().pauseAllThreadsAndExecuteFromNonRubyThread(
283-
"Handling of signal " + signal,
284-
SafepointPredicate.currentFiberOfThread(context, rootThread),
285-
(rubyThread, currentNode) -> ProcOperations.rootCall(action));
286-
} finally {
287-
truffleContext.leave(this, prev);
288-
}
264+
context.getSafepointManager().pauseAllThreadsAndExecuteFromNonRubyThread(
265+
"Handling of signal " + signal,
266+
SafepointPredicate.currentFiberOfThread(context, rootThread),
267+
(rubyThread, currentNode) -> ProcOperations.rootCall(action));
289268
});
290269
}
291270

292-
private static void warnRestoreAndRaise(RubyContext context, String signalName, Signal signal, String failure) {
293-
// Not in a context, so we cannot use TruffleLogger
294-
context.getEnvErrStream().println(
295-
"[ruby] SEVERE: signal " + signal + " caught but can't " + failure +
296-
" to handle it so restoring the default handler and re-raising the signal");
297-
Signals.restoreDefaultHandler(signalName);
298-
try {
299-
Signal.raise(signal);
300-
} catch (IllegalArgumentException illegalArgumentException) {
301-
illegalArgumentException.printStackTrace(context.getEnvErrStream());
302-
}
303-
}
304-
305271
@TruffleBoundary
306272
private boolean restoreDefaultHandler(String signalName) {
307273
if (getContext().getOptions().EMBEDDED) {

src/main/java/org/truffleruby/core/module/ModuleNodes.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,8 @@ protected Object appendFeatures(RubyModule features, RubyModule target,
394394
public abstract static class GeneratedReaderNode extends AlwaysInlinedMethodNode {
395395

396396
@Specialization(limit = "getCacheLimit()")
397-
protected Object reader(Frame frame, RubyDynamicObject self, Object[] args, Object block, RootCallTarget target,
397+
protected Object reader(
398+
Frame callerFrame, RubyDynamicObject self, Object[] args, Object block, RootCallTarget target,
398399
@CachedLibrary("self") DynamicObjectLibrary objectLibrary) {
399400
// Or a subclass of RubyRootNode with an extra field?
400401
final String ivarName = RubyRootNode.of(target).getSharedMethodInfo().getNotes();
@@ -404,7 +405,7 @@ protected Object reader(Frame frame, RubyDynamicObject self, Object[] args, Obje
404405
}
405406

406407
@Specialization(guards = "!isRubyDynamicObject(self)")
407-
protected Object notObject(Frame frame, Object self, Object[] args, Object block, RootCallTarget target) {
408+
protected Object notObject(Frame callerFrame, Object self, Object[] args, Object block, RootCallTarget target) {
408409
return nil;
409410
}
410411

@@ -417,7 +418,8 @@ protected int getCacheLimit() {
417418
public abstract static class GeneratedWriterNode extends AlwaysInlinedMethodNode {
418419

419420
@Specialization(guards = "!rubyLibrary.isFrozen(self)")
420-
protected Object writer(Frame frame, RubyDynamicObject self, Object[] args, Object block, RootCallTarget target,
421+
protected Object writer(
422+
Frame callerFrame, RubyDynamicObject self, Object[] args, Object block, RootCallTarget target,
421423
@CachedLibrary(limit = "getRubyLibraryCacheLimit()") RubyLibrary rubyLibrary,
422424
@Cached WriteObjectFieldNode writeObjectFieldNode) {
423425
final String ivarName = RubyRootNode.of(target).getSharedMethodInfo().getNotes();
@@ -429,7 +431,7 @@ protected Object writer(Frame frame, RubyDynamicObject self, Object[] args, Obje
429431
}
430432

431433
@Specialization(guards = "rubyLibrary.isFrozen(self)")
432-
protected Object frozen(Frame frame, Object self, Object[] args, Object block, RootCallTarget target,
434+
protected Object frozen(Frame callerFrame, Object self, Object[] args, Object block, RootCallTarget target,
433435
@CachedLibrary(limit = "getRubyLibraryCacheLimit()") RubyLibrary rubyLibrary,
434436
@CachedContext(RubyLanguage.class) RubyContext context) {
435437
throw new RaiseException(context, context.getCoreExceptions().frozenError(self, this));

0 commit comments

Comments
 (0)