Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ public void bind(KernelConnectionProperties connProps) {
ShellReplyEnvironment env = connection.prepareReplyEnv(this, message);
try {
handler.handle(env, message);
} catch (Exception e) {
} catch (Throwable e) {
// last-resort guard: nothing a handler throws may kill the channel loop
logger.warn("Unhandled exception handling {}. {} - {}",
message.getHeader().getType().getName(),
e.getClass().getSimpleName(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,10 @@ protected synchronized void handleExecuteRequest(ShellReplyEnvironment env, Mess
}

env.defer().reply(new ExecuteReply(count, Collections.emptyMap()));
} catch (Exception e) {
} catch (Throwable e) {
// Throwable, not Exception: an escaping Error (OutOfMemoryError, LinkageError,
// AssertionError from non-JShell evaluators, ...) would otherwise kill the
// shell channel loop and permanently hang the kernel
ErrorReply error = ErrorReply.of(e);
error.setExecutionCount(count);
env.publish(PublishError.of(e, this::formatError));
Expand All @@ -439,7 +442,7 @@ protected void handleInspectRequest(ShellReplyEnvironment env, Message<InspectRe
try {
DisplayData inspection = this.inspect(request.getCode(), request.getCursorPos(), request.getDetailLevel() > 0);
env.reply(new InspectReply(inspection != null, DisplayData.emptyIfNull(inspection)));
} catch (Exception e) {
} catch (Throwable e) {
env.replyError(InspectReply.MESSAGE_TYPE.error(), ErrorReply.of(e));
}
}
Expand All @@ -453,7 +456,7 @@ protected void handleCompleteRequest(ShellReplyEnvironment env, Message<Complete
env.reply(new CompleteReply(Collections.emptyList(), request.getCursorPos(), request.getCursorPos(), Collections.emptyMap()));
else
env.reply(new CompleteReply(options.getReplacements(), options.getSourceStart(), options.getSourceEnd(), Collections.emptyMap()));
} catch (Exception e) {
} catch (Throwable e) {
env.replyError(CompleteReply.MESSAGE_TYPE.error(), ErrorReply.of(e));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@

@FunctionalInterface
public interface ErrorFormatter {
List<String> format(Exception e);
List<String> format(Throwable t);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
public class PublishError implements ContentType<PublishError> {
public static final MessageType<PublishError> MESSAGE_TYPE = MessageType.PUBLISH_ERROR;

public static PublishError of(Exception exception, ErrorFormatter formatter) {
public static PublishError of(Throwable exception, ErrorFormatter formatter) {
String name = exception.getClass().getSimpleName();
String msg = exception.getLocalizedMessage();
List<String> stacktrace = formatter.format(exception);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public MessageType<Object> getRequestType() {
return MessageType.UNKNOWN;
}

public static ErrorReply of(Exception exception) {
public static ErrorReply of(Throwable exception) {
String name = exception.getClass().getSimpleName();
String msg = exception.getLocalizedMessage();
List<String> stacktrace = Arrays.stream(exception.getStackTrace())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package org.dflib.jjava.jupyter.kernel;

import org.dflib.jjava.jupyter.channels.ShellReplyEnvironment;
import org.dflib.jjava.jupyter.kernel.comm.CommManager;
import org.dflib.jjava.jupyter.kernel.display.DisplayData;
import org.dflib.jjava.jupyter.kernel.display.Renderer;
import org.dflib.jjava.jupyter.kernel.magic.MagicTranspiler;
import org.dflib.jjava.jupyter.kernel.magic.MagicsRegistry;
import org.dflib.jjava.jupyter.kernel.magic.MagicsResolver;
import org.dflib.jjava.jupyter.kernel.util.StringStyler;
import org.dflib.jjava.jupyter.messages.Message;
import org.dflib.jjava.jupyter.messages.MessageType;
import org.dflib.jjava.jupyter.messages.publish.PublishError;
import org.dflib.jjava.jupyter.messages.reply.ErrorReply;
import org.dflib.jjava.jupyter.messages.request.ExecuteRequest;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* The execute handler must convert anything thrown by evaluation — Errors
* included, not just Exceptions — into a published error and an error reply.
* An escaping Throwable would kill the shell channel loop and permanently
* hang the kernel (see gh-132).
*/
public class BaseKernelErrorHandlingTest {

@Test
public void errorFromEvalProducesErrorReply() {
assertErrorHandled(new AssertionError("boom"), "AssertionError");
}

@Test
public void linkageErrorFromEvalProducesErrorReply() {
assertErrorHandled(new NoClassDefFoundError("com/example/Gone"), "NoClassDefFoundError");
}

@Test
public void exceptionFromEvalProducesErrorReply() {
assertErrorHandled(new RuntimeException("boom"), "RuntimeException");
}

private void assertErrorHandled(Throwable thrown, String expectedName) {
ThrowingKernel kernel = new ThrowingKernel(thrown);
CapturingEnv env = new CapturingEnv();
Message<ExecuteRequest> message = new Message<>(null, MessageType.EXECUTE_REQUEST,
new ExecuteRequest("1 + 1", false, false, Collections.emptyMap(), false, false));

kernel.handleExecuteRequest(env, message);
env.resolveDeferrals();

List<Object> published = contentsOf(env.published);
List<Object> replied = contentsOf(env.replied);

PublishError publishedError = (PublishError) published.stream()
.filter(c -> c instanceof PublishError)
.findFirst()
.orElseThrow(() -> new AssertionError("no error published, got: " + published));
assertEquals(expectedName, publishedError.getErrorName());

ErrorReply reply = (ErrorReply) replied.stream()
.filter(c -> c instanceof ErrorReply)
.findFirst()
.orElseThrow(() -> new AssertionError("no error reply sent, got: " + replied));
assertEquals(expectedName, reply.getErrorName());
assertTrue(reply.getErrorMessage().contains("boom") || reply.getErrorMessage().contains("Gone"));
}

private static List<Object> contentsOf(List<Message<?>> messages) {
List<Object> contents = new ArrayList<>();
for (Message<?> message : messages) {
contents.add(message.getContent());
}
return contents;
}

private static class ThrowingKernel extends BaseKernel {

private final Throwable toThrow;

ThrowingKernel(Throwable toThrow) {
super("test", "0",
new LanguageInfo.Builder("test").build(),
Collections.emptyList(),
null,
new JupyterIO(StandardCharsets.UTF_8),
new CommManager(),
new Renderer(),
new MagicsResolver("%", "%%", new MagicTranspiler()),
new MagicsRegistry(),
false,
new StringStyler.Builder().build());
this.toThrow = toThrow;
}

@Override
protected Object doEval(String source) {
if (toThrow instanceof RuntimeException) {
throw (RuntimeException) toThrow;
}
if (toThrow instanceof Error) {
throw (Error) toThrow;
}
throw new IllegalStateException(toThrow);
}

@Override
public DisplayData inspect(String code, int at, boolean extraDetail) {
return null;
}

@Override
public ReplacementOptions complete(String code, int at) {
return null;
}

@Override
public String isComplete(String code) {
return IS_COMPLETE_MAYBE;
}
}

private static class CapturingEnv extends ShellReplyEnvironment {

final List<Message<?>> published = new ArrayList<>();
final List<Message<?>> replied = new ArrayList<>();

CapturingEnv() {
super(null, null, null, null);
}

@Override
public void publish(Message<?> msg) {
published.add(msg);
}

@Override
public void reply(Message<?> msg) {
replied.add(msg);
}

@Override
public ShellReplyEnvironment defer() {
// captures send immediately, so defer-then-send collapses to send
return this;
}
}
}
Loading