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);
+  }
 }

Reply via email to