This is an automated email from the ASF dual-hosted git repository.
jongyoul pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push:
new 21b845a01d [ZEPPELIN-6561] Restore default SIGINT handler so python
paragraph cancel works under daemon launch
21b845a01d is described below
commit 21b845a01d4d75a32e87dc330760eb60e5a6934b
Author: HwangRock <[email protected]>
AuthorDate: Sun Aug 2 11:50:28 2026 +0900
[ZEPPELIN-6561] Restore default SIGINT handler so python paragraph cancel
works under daemon launch
### What is this PR for?
Cancelling a running `%python` paragraph has no effect when Zeppelin is
started via `zeppelin-daemon.sh`: the user code runs to completion and the
result is recorded as SUCCESS while the job status becomes ABORT.
The cancel plumbing itself works. `cancel()` in `PythonInterpreter` sends
SIGINT to the correct python pid, and the interpreter log shows it. The problem
is signal disposition inheritance: `zeppelin-daemon.sh` starts the server with
`nohup ... &` from a non-interactive shell, so per POSIX the whole process
chain (ZeppelinServer JVM, interpreter JVM, python) inherits SIGINT=SIG_IGN,
and CPython keeps SIGINT ignored instead of installing the KeyboardInterrupt
handler when it starts with [...]
This cannot be fixed in the shell scripts, since POSIX forbids a
non-interactive shell from resetting a signal that was ignored on entry. The
fix restores the default SIGINT handler at the top of `zeppelin_python.py` when
the inherited disposition is SIG_IGN. Starting Zeppelin in the foreground with
`bin/zeppelin.sh` was never affected, which is why cancellation appears to work
in some environments and not in others.
### What type of PR is it?
Bug Fix
### What is the Jira issue?
https://issues.apache.org/jira/browse/ZEPPELIN-6561
### How should this be tested?
* Automated: `testSigintDefaultHandlerRestoredWhenInheritedIgnored` in
`PythonInterpreterTest` launches the interpreter through a shell wrapper that
ignores SIGINT before exec'ing python, reproducing the disposition of a daemon
launch, and asserts the default handler is restored inside the interpreter
process. Without the fix the assertion fails with `Handlers.SIG_IGN`. Unlike
the disabled `testCancelIntp`, it does not depend on timing.
* Manual: start Zeppelin with `bin/zeppelin-daemon.sh start`, run a
`%python` paragraph such as `for i in range(1, 50): print(i); time.sleep(0.5)`,
and cancel it a few seconds in. Before the fix it runs to 49 and stores
SUCCESS. After the fix it stops immediately with a KeyboardInterrupt traceback
and ERROR.
### Screenshots (if appropriate)
#### Before
https://github.com/user-attachments/assets/b9c6f5b4-24f6-4eb9-9019-37dab52526f0
#### After
https://github.com/user-attachments/assets/7b35d549-d97e-45cb-9d95-4ae5c543e0c0
### Questions:
* Does the license files need to update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No
Closes #5346 from HwangRock/ZEPPELIN-6561.
Signed-off-by: Jongyoul Lee <[email protected]>
---
.../src/main/resources/python/zeppelin_python.py | 9 ++++-
.../zeppelin/python/PythonInterpreterTest.java | 43 ++++++++++++++++++++++
2 files changed, 51 insertions(+), 1 deletion(-)
diff --git a/python/src/main/resources/python/zeppelin_python.py
b/python/src/main/resources/python/zeppelin_python.py
index f3f91861c2..83a6137dde 100644
--- a/python/src/main/resources/python/zeppelin_python.py
+++ b/python/src/main/resources/python/zeppelin_python.py
@@ -15,13 +15,20 @@
# limitations under the License.
#
-import os, sys, traceback, json, re
+import os, signal, sys, traceback, json, re
from py4j.java_gateway import java_import, JavaGateway, GatewayClient
from py4j.protocol import Py4JJavaError
import ast
+# When Zeppelin is started via zeppelin-daemon.sh (nohup ... &), this process
+# inherits SIGINT=SIG_IGN and CPython keeps it ignored instead of installing
the
+# KeyboardInterrupt handler, so PythonInterpreter.cancel()'s SIGINT would be a
+# no-op. Restore the default handler to keep paragraph cancellation working.
+if signal.getsignal(signal.SIGINT) == signal.SIG_IGN:
+ signal.signal(signal.SIGINT, signal.default_int_handler)
+
class Logger(object):
def __init__(self):
pass
diff --git
a/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterTest.java
b/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterTest.java
index 28cfadc5b4..7d0ad4f0e2 100644
--- a/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterTest.java
+++ b/python/src/test/java/org/apache/zeppelin/python/PythonInterpreterTest.java
@@ -24,6 +24,7 @@ import org.apache.zeppelin.interpreter.InterpreterContext;
import org.apache.zeppelin.interpreter.InterpreterException;
import org.apache.zeppelin.interpreter.InterpreterGroup;
import org.apache.zeppelin.interpreter.InterpreterResult;
+import org.apache.zeppelin.interpreter.InterpreterResultMessage;
import org.apache.zeppelin.interpreter.LazyOpenInterpreter;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -35,8 +36,12 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
+import java.io.File;
import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Arrays;
import java.util.LinkedList;
+import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
@@ -174,4 +179,42 @@ public class PythonInterpreterTest extends
BasePythonInterpreterTest {
assertTrue(stacktrace.contains("No such file or directory"), stacktrace);
}
}
+
+ @Test
+ public void testSigintDefaultHandlerRestoredWhenInheritedIgnored()
+ throws IOException, InterpreterException {
+ tearDown();
+
+ File wrapper = File.createTempFile("python-sigint-wrapper", ".sh");
+ wrapper.deleteOnExit();
+ Files.write(wrapper.toPath(), Arrays.asList(
+ "#!/bin/sh",
+ "trap '' INT",
+ "exec python \"$@\""));
+ wrapper.setExecutable(true);
+
+ intpGroup = new InterpreterGroup();
+
+ Properties properties = new Properties();
+ properties.setProperty("zeppelin.python", wrapper.getAbsolutePath());
+ properties.setProperty("zeppelin.python.useIPython", "false");
+ properties.setProperty("zeppelin.python.gatewayserver_address",
"127.0.0.1");
+
+ interpreter = new LazyOpenInterpreter(new PythonInterpreter(properties));
+
+ intpGroup.put("note", new LinkedList<Interpreter>());
+ intpGroup.get("note").add(interpreter);
+ interpreter.setInterpreterGroup(intpGroup);
+
+ InterpreterContext.set(getInterpreterContext());
+
+ InterpreterContext context = getInterpreterContext();
+ InterpreterResult result = interpreter.interpret(
+ "import signal\nprint(signal.getsignal(signal.SIGINT))", context);
+ assertEquals(InterpreterResult.Code.SUCCESS, result.code());
+ List<InterpreterResultMessage> interpreterResultMessages =
+ context.out.toInterpreterResultMessage();
+ String output = interpreterResultMessages.get(0).getData();
+ assertTrue(output.contains("default_int_handler"), output);
+ }
}