From 99d916fa5850d2b44cf591167769e50775c2656d Mon Sep 17 00:00:00 2001 From: SpencerPark Date: Sun, 30 Sep 2018 22:53:43 -0400 Subject: [PATCH] Support interrupting the kernel. See #33 --- .../io/github/spencerpark/ijava/IJava.java | 2 +- .../github/spencerpark/ijava/JavaKernel.java | 19 +++++++-- .../ijava/execution/CodeEvaluator.java | 19 +++++++-- .../EvaluationInterruptedException.java | 19 +++++++++ .../execution/IJavaExecutionControl.java | 39 ++++++++++++++++--- 5 files changed, 84 insertions(+), 14 deletions(-) create mode 100644 src/main/java/io/github/spencerpark/ijava/execution/EvaluationInterruptedException.java diff --git a/src/main/java/io/github/spencerpark/ijava/IJava.java b/src/main/java/io/github/spencerpark/ijava/IJava.java index 0482689..5cfbb7a 100644 --- a/src/main/java/io/github/spencerpark/ijava/IJava.java +++ b/src/main/java/io/github/spencerpark/ijava/IJava.java @@ -96,7 +96,7 @@ public class IJava { String contents = new String(Files.readAllBytes(connectionFile)); - JupyterSocket.JUPYTER_LOGGER.setLevel(Level.WARNING); + JupyterSocket.JUPYTER_LOGGER.setLevel(Level.ALL); KernelConnectionProperties connProps = KernelConnectionProperties.parse(contents); JupyterConnection connection = new JupyterConnection(connProps); diff --git a/src/main/java/io/github/spencerpark/ijava/JavaKernel.java b/src/main/java/io/github/spencerpark/ijava/JavaKernel.java index d315be1..9ab2f39 100644 --- a/src/main/java/io/github/spencerpark/ijava/JavaKernel.java +++ b/src/main/java/io/github/spencerpark/ijava/JavaKernel.java @@ -28,7 +28,6 @@ import io.github.spencerpark.jupyter.kernel.BaseKernel; import io.github.spencerpark.jupyter.kernel.LanguageInfo; import io.github.spencerpark.jupyter.kernel.ReplacementOptions; import io.github.spencerpark.jupyter.kernel.display.DisplayData; -import io.github.spencerpark.jupyter.kernel.magic.*; import io.github.spencerpark.jupyter.kernel.magic.registry.Magics; import io.github.spencerpark.jupyter.kernel.util.CharPredicate; import io.github.spencerpark.jupyter.kernel.util.GlobFinder; @@ -38,10 +37,7 @@ import io.github.spencerpark.jupyter.messages.Header; import jdk.jshell.*; import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Base64; import java.util.LinkedList; import java.util.List; import java.util.stream.Collectors; @@ -171,6 +167,8 @@ public class JavaKernel extends BaseKernel { return formatEvalException((EvalException) e); } else if (e instanceof EvaluationTimeoutException) { return formatEvaluationTimeoutException((EvaluationTimeoutException) e); + } else if (e instanceof EvaluationInterruptedException) { + return formatEvaluationInterruptedException((EvaluationInterruptedException) e); } else { fmt.addAll(super.formatError(e)); } @@ -245,6 +243,14 @@ public class JavaKernel extends BaseKernel { return fmt; } + private List formatEvaluationInterruptedException(EvaluationInterruptedException e) { + List fmt = new ArrayList<>(this.errorStyler.primaryLines(e.getSource())); + + fmt.add(this.errorStyler.secondary("Evaluation interrupted.")); + + return fmt; + } + public Object evalRaw(String expr) throws Exception { expr = this.magicsTransformer.transformMagics(expr); @@ -338,4 +344,9 @@ public class JavaKernel extends BaseKernel { public void onShutdown(boolean isRestarting) { this.evaluator.shutdown(); } + + @Override + public void interrupt() { + this.evaluator.interrupt(); + } } diff --git a/src/main/java/io/github/spencerpark/ijava/execution/CodeEvaluator.java b/src/main/java/io/github/spencerpark/ijava/execution/CodeEvaluator.java index b6a2d09..fdd8178 100644 --- a/src/main/java/io/github/spencerpark/ijava/execution/CodeEvaluator.java +++ b/src/main/java/io/github/spencerpark/ijava/execution/CodeEvaluator.java @@ -101,8 +101,17 @@ public class CodeEvaluator { if (event.causeSnippet() == null) { JShellException e = event.exception(); if (e != null) { - if (e instanceof EvalException && IJavaExecutionControl.EXECUTION_TIMEOUT_NAME.equals(((EvalException) e).getExceptionClassName())) - throw new EvaluationTimeoutException(executionControl.getTimeoutDuration(), executionControl.getTimeoutUnit(), code.trim()); + if (e instanceof EvalException) { + switch (((EvalException) e).getExceptionClassName()) { + case IJavaExecutionControl.EXECUTION_TIMEOUT_NAME: + throw new EvaluationTimeoutException(executionControl.getTimeoutDuration(), executionControl.getTimeoutUnit(), code.trim()); + case IJavaExecutionControl.EXECUTION_INTERRUPTED_NAME: + throw new EvaluationInterruptedException(code.trim()); + default: + throw e; + } + } + throw e; } @@ -136,7 +145,11 @@ public class CodeEvaluator { } public void interrupt() { - this.shell.stop(); + IJavaExecutionControl executionControl = + this.executionControlProvider.getRegisteredControlByID(this.executionControlID); + + if (executionControl != null) + executionControl.interrupt(); } public void shutdown() { diff --git a/src/main/java/io/github/spencerpark/ijava/execution/EvaluationInterruptedException.java b/src/main/java/io/github/spencerpark/ijava/execution/EvaluationInterruptedException.java new file mode 100644 index 0000000..a1382fa --- /dev/null +++ b/src/main/java/io/github/spencerpark/ijava/execution/EvaluationInterruptedException.java @@ -0,0 +1,19 @@ +package io.github.spencerpark.ijava.execution; + +public class EvaluationInterruptedException extends Exception { + private final String source; + + public EvaluationInterruptedException(String source) { + this.source = source; + } + + public String getSource() { + return source; + } + + @Override + public String getMessage() { + return String.format("Evaluator was interrupted while executing: '%s'", + this.source); + } +} diff --git a/src/main/java/io/github/spencerpark/ijava/execution/IJavaExecutionControl.java b/src/main/java/io/github/spencerpark/ijava/execution/IJavaExecutionControl.java index 26377fc..7d462d1 100644 --- a/src/main/java/io/github/spencerpark/ijava/execution/IJavaExecutionControl.java +++ b/src/main/java/io/github/spencerpark/ijava/execution/IJavaExecutionControl.java @@ -41,10 +41,18 @@ import java.util.concurrent.atomic.AtomicInteger; public class IJavaExecutionControl extends DirectExecutionControl { /** * A special "class name" for a {@link jdk.jshell.spi.ExecutionControl.UserException} such that it may be - * identified after serialization into an {@link jdk.jshell.EvalException} via {@link EvalException#getExceptionClassName()}. + * identified after serialization into an {@link jdk.jshell.EvalException} via {@link + * EvalException#getExceptionClassName()}. */ public static final String EXECUTION_TIMEOUT_NAME = "Execution Timeout"; // Has spaces to not collide with a class name + /** + * A special "class name" for a {@link jdk.jshell.spi.ExecutionControl.UserException} such that it may be + * identified after serialization into an {@link jdk.jshell.EvalException} via {@link + * EvalException#getExceptionClassName()} + */ + public static final String EXECUTION_INTERRUPTED_NAME = "Execution Interrupted"; + private static final Object NULL = new Object(); private static final AtomicInteger EXECUTOR_THREAD_ID = new AtomicInteger(0); @@ -54,6 +62,7 @@ public class IJavaExecutionControl extends DirectExecutionControl { private final long timeoutTime; private final TimeUnit timeoutUnit; + private final ConcurrentMap> running = new ConcurrentHashMap<>(); private final Map results = new ConcurrentHashMap<>(); public IJavaExecutionControl() { @@ -81,17 +90,28 @@ public class IJavaExecutionControl extends DirectExecutionControl { return result == NULL ? null : result; } - private Object execute(Method doitMethod) throws TimeoutException, Exception { + private Object execute(String key, Method doitMethod) throws TimeoutException, Exception { Future runningTask = this.executor.submit(() -> doitMethod.invoke(null)); + this.running.put(key, runningTask); + try { if (this.timeoutTime > 0) return runningTask.get(this.timeoutTime, this.timeoutUnit); return runningTask.get(); } catch (CancellationException e) { - // If canceled this means that stop() was invoked in which case the protocol is to - // throw an ExecutionControl.StoppedException. - throw new StoppedException(); + // If canceled this means that stop() or interrupt() was invoked. + if (this.executor.isShutdown()) + // If the executor is shutdown, the situation is the former in which + // case the protocol is to throw an ExecutionControl.StoppedException. + throw new StoppedException(); + else + // The execution was purposely interrupted. + throw new UserException( + "Execution interrupted.", + EXECUTION_INTERRUPTED_NAME, + new StackTraceElement[]{} // The trace is irrelevant because it is in the kernel space not the user space so leave it blank. + ); } catch (ExecutionException e) { // The execution threw an exception. The actual exception is the cause of the ExecutionException. Throwable cause = e.getCause(); @@ -110,6 +130,8 @@ public class IJavaExecutionControl extends DirectExecutionControl { EXECUTION_TIMEOUT_NAME, new StackTraceElement[]{} // The trace is irrelevant because it is in the kernel space not the user space so leave it blank. ); + } finally { + this.running.remove(key, runningTask); } } @@ -125,12 +147,17 @@ public class IJavaExecutionControl extends DirectExecutionControl { */ @Override protected String invoke(Method doitMethod) throws Exception { - Object value = this.execute(doitMethod); String id = UUID.randomUUID().toString(); + Object value = this.execute(id, doitMethod); this.results.put(id, value); return id; } + public void interrupt() { + this.running.forEach((id, future) -> + future.cancel(true)); + } + @Override public void stop() throws EngineTerminationException, InternalException { this.executor.shutdownNow();