Skip to content

Commit 2e65067

Browse files
committed
[GR-31943] Serialize interrupts and cancels of a thread in NativeCallInterrupt
PullRequest: truffleruby/2709
2 parents 4be53f4 + 9ad9fa4 commit 2e65067

File tree

12 files changed

+126
-32
lines changed

12 files changed

+126
-32
lines changed

doc/contributor/debugging.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ You've got many options for debugging the implementation of TruffleRuby.
55
If you want to debug an application running on TruffleRuby, and don't suspect
66
any bug in TruffleRuby itself, use GraalVM [tools](../user/tools.md) as usual.
77

8+
Also see [user debugging docs](../user/debugging.md).
9+
810
## Print Debugging
911

1012
`System.err.println(...);` in Java and `p` in Ruby is a great first tool to use for

doc/user/debugging.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,12 @@ When debugging you may want to see this information.
4040
An option to show more information is `--backtraces-interleave-java=true`, which shows you the Java methods involved in executing each Ruby method.
4141

4242
When you are interoperating with other languages, including C extensions, backtraces for Java exceptions may be missing information, as the Java frames are gone by the time Ruby has a chance to format them into a backtrace.
43+
44+
## Printing Subprocesses
45+
46+
You can print all subprocesses created by TruffleRuby using the option `--log-subprocess`.
47+
48+
```bash
49+
$ ruby --log-subprocess -e '`ls .`'
50+
[ruby] INFO: spawn: ls .
51+
```

src/main/c/rubysignal/src/rubysignal.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
#include <pthread.h>
33
#include <signal.h>
44

5+
_Static_assert(sizeof(pthread_t) == sizeof(jlong), "Expected sizeof(pthread_t) == sizeof(jlong)");
6+
57
static void empty_handler(int sig) {
68
}
79

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,6 @@ public class RubyContext {
164164
public RubyContext(RubyLanguage language, TruffleLanguage.Env env) {
165165
Metrics.printTime("before-context-constructor");
166166

167-
this.logger = env.getLogger("");
168167
this.language = language;
169168
this.callStack = new CallStackManager(language, this);
170169
setEnv(env);
@@ -392,6 +391,7 @@ private void setEnv(Env env) {
392391
this.env = env;
393392
this.outStream = printStreamFor(env.out());
394393
this.errStream = printStreamFor(env.err());
394+
this.logger = env.getLogger("");
395395
this.hasOtherPublicLanguages = computeHasOtherPublicLanguages(env);
396396
this.preInitializing = env.isPreInitialization();
397397
}

src/main/java/org/truffleruby/core/thread/NativeCallInterrupter.java

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
package org.truffleruby.core.thread;
1111

12+
import com.oracle.truffle.api.CompilerDirectives;
1213
import com.oracle.truffle.api.TruffleSafepoint;
1314
import org.truffleruby.signal.LibRubySignal;
1415

@@ -19,7 +20,7 @@ class NativeCallInterrupter implements TruffleSafepoint.Interrupter {
1920

2021
private final Timer timer;
2122
private final long threadID;
22-
private volatile Task currentTask = null;
23+
private Task currentTask = null;
2324

2425
NativeCallInterrupter(Timer timer, long threadID) {
2526
this.timer = timer;
@@ -28,16 +29,25 @@ class NativeCallInterrupter implements TruffleSafepoint.Interrupter {
2829

2930
@Override
3031
public void interrupt(Thread thread) {
31-
final Task task = new Task(threadID);
32-
currentTask = task;
33-
timer.schedule(task, 0, Task.PERIOD);
32+
synchronized (this) {
33+
final Task previousTask = this.currentTask;
34+
if (previousTask != null) {
35+
previousTask.cancel();
36+
}
37+
38+
final Task task = new Task(threadID);
39+
this.currentTask = task;
40+
timer.schedule(task, 0, Task.PERIOD);
41+
}
3442
}
3543

3644
@Override
3745
public void resetInterrupted() {
38-
Task task = currentTask;
39-
if (task != null) {
40-
task.cancel();
46+
synchronized (this) {
47+
final Task task = this.currentTask;
48+
if (task != null) {
49+
task.cancel();
50+
}
4151
}
4252
}
4353

@@ -49,18 +59,39 @@ static class Task extends TimerTask {
4959

5060
private final long threadID;
5161
private int executed = 0;
62+
private boolean cancelled = false;
5263

5364
Task(long threadID) {
5465
this.threadID = threadID;
5566
}
5667

68+
@Override
69+
public boolean cancel() {
70+
// Ensure this task does not run later when this method returns.
71+
// If it was running at the time of cancel(), wait for that.
72+
synchronized (this) {
73+
cancelled = true;
74+
return super.cancel();
75+
}
76+
}
77+
5778
@Override
5879
public void run() {
59-
if (executed < MAX_EXECUTIONS) {
60-
executed++;
61-
LibRubySignal.sendSIGVTALRMToThread(threadID);
62-
} else {
63-
cancel();
80+
synchronized (this) {
81+
if (cancelled) {
82+
return;
83+
}
84+
85+
if (executed < MAX_EXECUTIONS) {
86+
executed++;
87+
int result = LibRubySignal.sendSIGVTALRMToThread(threadID);
88+
if (result != 0) {
89+
throw CompilerDirectives.shouldNotReachHere(
90+
String.format("pthread_kill(%x, SIGVTALRM) failed with result=%d", threadID, result));
91+
}
92+
} else {
93+
cancel();
94+
}
6495
}
6596
}
6697
}

src/main/java/org/truffleruby/core/thread/ThreadManager.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -295,21 +295,23 @@ private void threadMain(RubyThread thread, Node currentNode, Supplier<Object> ta
295295
final Object result = task.get();
296296
setThreadValue(thread, result);
297297
// Handlers in the same order as in FiberManager
298+
// Each catch must either setThreadValue() (before rethrowing) or setException()
298299
} catch (KillException e) {
299300
setThreadValue(thread, Nil.INSTANCE);
300301
} catch (ThreadDeath e) { // Context#close(true)
302+
setThreadValue(thread, Nil.INSTANCE);
301303
throw e;
302304
} catch (RaiseException e) {
303305
setException(thread, e.getException(), currentNode);
304306
} catch (DynamicReturnException e) {
305307
setException(thread, context.getCoreExceptions().unexpectedReturn(currentNode), currentNode);
306308
} catch (ExitException e) {
307-
rethrowOnMainThread(currentNode, e);
308309
setThreadValue(thread, Nil.INSTANCE);
310+
rethrowOnMainThread(currentNode, e);
309311
} catch (Throwable e) {
310312
final RuntimeException runtimeException = printInternalError(e);
311-
rethrowOnMainThread(currentNode, runtimeException);
312313
setThreadValue(thread, Nil.INSTANCE);
314+
rethrowOnMainThread(currentNode, runtimeException);
313315
} finally {
314316
assert thread.value != null || thread.exception != null;
315317
cleanupKillOtherFibers(thread);
@@ -344,18 +346,18 @@ private void setThreadValue(RubyThread thread, Object value) {
344346
}
345347

346348
private void setException(RubyThread thread, RubyException exception, Node currentNode) {
347-
// A Thread is always shared (Thread.list)
348-
SharedObjects.propagate(language, thread, exception);
349-
350349
// We materialize the backtrace eagerly here, as the exception escapes the thread and needs
351350
// to capture the backtrace from this thread.
352351
final RaiseException truffleException = exception.backtrace.getRaiseException();
353352
if (truffleException != null) {
354353
TruffleStackTrace.fillIn(truffleException);
355354
}
356355

357-
final RubyThread mainThread = context.getThreadManager().getRootThread();
356+
// A Thread is always shared (Thread.list)
357+
SharedObjects.propagate(language, thread, exception);
358+
thread.exception = exception;
358359

360+
final RubyThread mainThread = context.getThreadManager().getRootThread();
359361
if (thread != mainThread) {
360362
final boolean isSystemExit = exception instanceof RubySystemExit;
361363

@@ -372,7 +374,6 @@ private void setException(RubyThread thread, RubyException exception, Node curre
372374
.raiseInThread(language, context, mainThread, exception, currentNode);
373375
}
374376
}
375-
thread.exception = exception;
376377
}
377378

378379
// Share the Ruby Thread before it can be accessed concurrently, and before it is added to Thread.list

src/main/java/org/truffleruby/debug/TruffleDebugNodes.java

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -330,36 +330,53 @@ protected int getCacheLimit() {
330330

331331
}
332332

333-
@CoreMethod(names = "log_config", onSingleton = true, required = 1)
334-
public abstract static class LogConfigNode extends CoreMethodArrayArgumentsNode {
333+
@CoreMethod(names = "log_warning", onSingleton = true, required = 1)
334+
public abstract static class LogWarningNode extends CoreMethodArrayArgumentsNode {
335335

336336
@Specialization
337-
protected Object logConfig(Object value,
337+
protected Object logWarning(Object value,
338+
@Cached ToJavaStringNode toJavaStringNode) {
339+
warning(toJavaStringNode.executeToJavaString(value));
340+
return nil;
341+
}
342+
343+
@TruffleBoundary
344+
static void warning(String message) {
345+
RubyLanguage.LOGGER.warning(message);
346+
}
347+
348+
}
349+
350+
@CoreMethod(names = "log_info", onSingleton = true, required = 1)
351+
public abstract static class LogInfoNode extends CoreMethodArrayArgumentsNode {
352+
353+
@Specialization
354+
protected Object logInfo(Object value,
338355
@Cached ToJavaStringNode toJavaStringNode) {
339356
config(toJavaStringNode.executeToJavaString(value));
340357
return nil;
341358
}
342359

343360
@TruffleBoundary
344361
static void config(String message) {
345-
RubyLanguage.LOGGER.config(message);
362+
RubyLanguage.LOGGER.info(message);
346363
}
347364

348365
}
349366

350-
@CoreMethod(names = "log_warning", onSingleton = true, required = 1)
351-
public abstract static class LogWarningNode extends CoreMethodArrayArgumentsNode {
367+
@CoreMethod(names = "log_config", onSingleton = true, required = 1)
368+
public abstract static class LogConfigNode extends CoreMethodArrayArgumentsNode {
352369

353370
@Specialization
354-
protected Object logWarning(Object value,
371+
protected Object logConfig(Object value,
355372
@Cached ToJavaStringNode toJavaStringNode) {
356-
warning(toJavaStringNode.executeToJavaString(value));
373+
config(toJavaStringNode.executeToJavaString(value));
357374
return nil;
358375
}
359376

360377
@TruffleBoundary
361-
static void warning(String message) {
362-
RubyLanguage.LOGGER.warning(message);
378+
static void config(String message) {
379+
RubyLanguage.LOGGER.config(message);
363380
}
364381

365382
}

src/main/java/org/truffleruby/language/TruffleBootNodes.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,8 @@ protected Object getOption(Object optionName,
319319
if (descriptor == null) {
320320
throw new RaiseException(
321321
getContext(),
322-
coreExceptions().nameError("option not defined", nil, optionNameString, this));
322+
coreExceptions()
323+
.nameError("option not defined: " + optionNameString, nil, optionNameString, this));
323324
}
324325

325326
Object value = getContext().getOptions().fromDescriptor(descriptor);

src/main/java/org/truffleruby/options/Options.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ public class Options {
7171
public final boolean HASHING_DETERMINISTIC;
7272
/** --fiber-leave-context=true */
7373
public final boolean FIBER_LEAVE_CONTEXT;
74+
/** --log-subprocess=false */
75+
public final boolean LOG_SUBPROCESS;
7476
/** --exceptions-store-java=false */
7577
public final boolean EXCEPTIONS_STORE_JAVA;
7678
/** --exceptions-print-java=false */
@@ -215,6 +217,7 @@ public Options(Env env, OptionValues options, LanguageOptions languageOptions) {
215217
PATCHING = options.get(OptionsCatalog.PATCHING_KEY);
216218
HASHING_DETERMINISTIC = options.get(OptionsCatalog.HASHING_DETERMINISTIC_KEY);
217219
FIBER_LEAVE_CONTEXT = options.get(OptionsCatalog.FIBER_LEAVE_CONTEXT_KEY);
220+
LOG_SUBPROCESS = options.get(OptionsCatalog.LOG_SUBPROCESS_KEY);
218221
EXCEPTIONS_STORE_JAVA = options.get(OptionsCatalog.EXCEPTIONS_STORE_JAVA_KEY);
219222
EXCEPTIONS_PRINT_JAVA = options.get(OptionsCatalog.EXCEPTIONS_PRINT_JAVA_KEY);
220223
EXCEPTIONS_PRINT_UNCAUGHT_JAVA = options.get(OptionsCatalog.EXCEPTIONS_PRINT_UNCAUGHT_JAVA_KEY);
@@ -326,6 +329,8 @@ public Object fromDescriptor(OptionDescriptor descriptor) {
326329
return HASHING_DETERMINISTIC;
327330
case "ruby.fiber-leave-context":
328331
return FIBER_LEAVE_CONTEXT;
332+
case "ruby.log-subprocess":
333+
return LOG_SUBPROCESS;
329334
case "ruby.exceptions-store-java":
330335
return EXCEPTIONS_STORE_JAVA;
331336
case "ruby.exceptions-print-java":

src/main/ruby/truffleruby/core/truffle/process_operations.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,8 @@ def convert_env_value(value)
350350

351351
def spawn_setup(alter_process)
352352
env = @options.delete(:unsetenv_others) ? {} : ENV.to_hash
353-
if add_to_env = @options.delete(:env)
353+
@add_to_env = add_to_env = @options.delete(:env)
354+
if add_to_env
354355
add_to_env.each do |key, value|
355356
if value
356357
env[key] = value
@@ -415,6 +416,7 @@ def redirect_file_descriptor(from, to, alter_process)
415416
end
416417

417418
def spawn
419+
log_command 'spawn'
418420
pid = posix_spawnp @command, @argv, @env_array, @options
419421
# Check if the command exists *after* invoking posix_spawn so we have a pid
420422
if not resolve_in_path(@command)
@@ -497,6 +499,7 @@ def posix_spawnp(command, args, env_array, options)
497499
end
498500

499501
def exec
502+
log_command('exec')
500503
# exec validates the command only if it searches in $PATH
501504
if should_search_path?(@command)
502505
if resolved = resolve_in_path(@command)
@@ -516,6 +519,17 @@ def exec
516519
raise SystemCallError.new('execve() should not return', 0)
517520
end
518521

522+
Truffle::Boot.delay do
523+
LOG_SUBPROCESS = Truffle::Boot.get_option 'log-subprocess'
524+
end
525+
526+
def log_command(type)
527+
if LOG_SUBPROCESS
528+
env = (@add_to_env || {}).map { |k,v| "#{k}=#{v}" }.join(' ')
529+
Truffle::Debug.log_info "#{type}: #{env}#{' ' unless env.empty?}#{@argv.join(' ')}"
530+
end
531+
end
532+
519533
def should_use_shell?(command)
520534
command.b.match?(SHELL_META_CHAR_PATTERN)
521535
end

0 commit comments

Comments
 (0)