This is an automated email from the ASF dual-hosted git repository.
tbonelee 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 24968cdf47 [ZEPPELIN-6693] Drive NotebookServer heartbeat scheduler
shutdown from ZeppelinServer lifecycle
24968cdf47 is described below
commit 24968cdf471dca93c8d6125e58fcd597eb1ead46
Author: JangAyeon <[email protected]>
AuthorDate: Fri Sep 25 18:34:48 2026 +0900
[ZEPPELIN-6693] Drive NotebookServer heartbeat scheduler shutdown from
ZeppelinServer lifecycle
### What is this PR for?
ZEPPELIN-6092 added a websocket heartbeat scheduler to `NotebookServer`,
torn down by a dedicated JVM shutdown hook that the class registered for
itself. That hook only covers the JVM-exit path:
- When the server is closed inside a running JVM (e.g.
`MiniZeppelinServer.shutDown()` calling `zepServer.close()`), the hook never
runs, so repeated start/stop cycles can leave a scheduler and a hook behind per
instance.
- On SIGTERM, `ZeppelinServer`'s own shutdown hook and this one run in
parallel for the same event.
This PR stops the scheduler from `ZeppelinServer#shutdown`, which already
covers both paths (the JVM shutdown hook and `close()`), and removes the
dedicated hook together with its `removeShutdownHook` / `IllegalStateException`
handling and self-reference guard. The scheduler is stopped after Jetty, so no
new connection can restart it.
### What type of PR is it?
Improvement
### Todos
* [x] - Stop the heartbeat scheduler from `ZeppelinServer#shutdown`
* [x] - Remove the dedicated shutdown hook from `NotebookServer`
* [x] - Add a test that covers the `ZeppelinServer#shutdown` path
* [x] - Add tests for repeated start/stop cycles
### What is the Jira issue?
[ZEPPELIN-6693](https://issues.apache.org/jira/browse/ZEPPELIN-6693)
### How should this be tested?
*
`NotebookServerHeartbeatTest#zeppelinServerShutdownStopsHeartbeatScheduler`:
starts a `MiniZeppelinServer`, starts the heartbeat scheduler, calls
`shutDown()`, and checks the scheduler is shut down. Fails if the
`stopHeartbeatScheduler()` call in `ZeppelinServer#shutdown` is removed.
*
`NotebookServerHeartbeatTest#stopHeartbeatSchedulerAllowsRepeatedStartStopCycles`:
starts and stops the scheduler three times and checks that each scheduler is
shut down and a new one can start.
*
`NotebookServerHeartbeatTest#stopHeartbeatSchedulerIsSafeWhenNeverStarted`:
stopping without a prior start, twice, does not throw.
* Existing `NotebookServerHeartbeatTest` cases still pass.
### Questions:
* Does the license files need to update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No
Closes #5499 from JangAyeon/ZEPPELIN-6693.
Signed-off-by: ChanHo Lee <[email protected]>
---
.../org/apache/zeppelin/server/ZeppelinServer.java | 2 +
.../org/apache/zeppelin/socket/NotebookServer.java | 18 ++-------
.../socket/NotebookServerHeartbeatTest.java | 46 ++++++++++++++++++++++
3 files changed, 51 insertions(+), 15 deletions(-)
diff --git
a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
b/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
index 6dece8a13d..480422b4a5 100644
---
a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
+++
b/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
@@ -359,6 +359,8 @@ public class ZeppelinServer implements AutoCloseable {
jettyWebServer.stop();
}
if (sharedServiceLocator != null) {
+ // Stop after Jetty so no new connection can restart the heartbeat
scheduler.
+
sharedServiceLocator.getService(NotebookServer.class).stopHeartbeatScheduler();
if (!zConf.isRecoveryEnabled()) {
sharedServiceLocator.getService(InterpreterSettingManager.class).close();
}
diff --git
a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
index 41a65e3a4f..d55eb27196 100644
---
a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
+++
b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
@@ -150,7 +150,6 @@ public class NotebookServer implements
AngularObjectRegistryListener,
// Package-private (not private) so NotebookServerHeartbeatTest can observe
scheduler
// lifecycle without exposing it as part of the public API.
ScheduledExecutorService heartbeatScheduler;
- private Thread heartbeatShutdownHook;
private boolean heartbeatInitialized;
// TODO(jl): This will be removed by handling session directly
@@ -287,29 +286,18 @@ public class NotebookServer implements
AngularObjectRegistryListener,
});
heartbeatScheduler.scheduleAtFixedRate(
this::sendHeartbeat, intervalMs, intervalMs, TimeUnit.MILLISECONDS);
- heartbeatShutdownHook = new Thread(this::stopHeartbeatScheduler);
- Runtime.getRuntime().addShutdownHook(heartbeatShutdownHook);
LOGGER.info("Started websocket heartbeat scheduler with interval {} ms",
intervalMs);
}
/**
- * Stops the websocket heartbeat scheduler, if running, and deregisters its
shutdown hook so
- * repeated start/stop cycles do not accumulate hooks. Safe to call multiple
times and safe
- * to call when the scheduler was never started.
+ * Stops the websocket heartbeat scheduler, if running. Safe to call
multiple times and safe
+ * to call when the scheduler was never started; a later connection starts
it again.
*/
- synchronized void stopHeartbeatScheduler() {
+ public synchronized void stopHeartbeatScheduler() {
if (heartbeatScheduler != null) {
heartbeatScheduler.shutdownNow();
heartbeatScheduler = null;
}
- if (heartbeatShutdownHook != null && Thread.currentThread() !=
heartbeatShutdownHook) {
- try {
- Runtime.getRuntime().removeShutdownHook(heartbeatShutdownHook);
- } catch (IllegalStateException e) {
- // JVM is already shutting down; the hook will simply run (as a
harmless no-op).
- }
- heartbeatShutdownHook = null;
- }
heartbeatInitialized = false;
}
diff --git
a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerHeartbeatTest.java
b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerHeartbeatTest.java
index 6f53668261..f932b1fa2d 100644
---
a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerHeartbeatTest.java
+++
b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerHeartbeatTest.java
@@ -19,11 +19,14 @@ package org.apache.zeppelin.socket;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import java.util.concurrent.ScheduledExecutorService;
+import org.apache.zeppelin.MiniZeppelinServer;
import org.apache.zeppelin.conf.ZeppelinConfiguration;
import org.apache.zeppelin.notebook.AuthorizationService;
import org.junit.jupiter.api.AfterEach;
@@ -106,4 +109,47 @@ class NotebookServerHeartbeatTest {
assertNull(server.heartbeatScheduler);
}
+
+ @Test
+ void stopHeartbeatSchedulerAllowsRepeatedStartStopCycles() {
+ NotebookServer server = buildNotebookServer(50L);
+
+ for (int i = 0; i < 3; i++) {
+ server.startHeartbeatScheduler();
+ ScheduledExecutorService scheduler = server.heartbeatScheduler;
+ assertNotNull(scheduler);
+
+ server.stopHeartbeatScheduler();
+
+ assertTrue(scheduler.isShutdown());
+ assertNull(server.heartbeatScheduler);
+ }
+ }
+
+ @Test
+ void stopHeartbeatSchedulerIsSafeWhenNeverStarted() {
+ NotebookServer server = buildNotebookServer(50L);
+
+ assertDoesNotThrow(server::stopHeartbeatScheduler);
+ assertDoesNotThrow(server::stopHeartbeatScheduler);
+ }
+
+ @Test
+ void zeppelinServerShutdownStopsHeartbeatScheduler() throws Exception {
+ MiniZeppelinServer zepServer =
+ new
MiniZeppelinServer(NotebookServerHeartbeatTest.class.getSimpleName());
+ try {
+ zepServer.start();
+ NotebookServer server = zepServer.getService(NotebookServer.class);
+ server.startHeartbeatScheduler();
+ ScheduledExecutorService scheduler = server.heartbeatScheduler;
+ assertNotNull(scheduler);
+
+ zepServer.shutDown();
+
+ assertTrue(scheduler.isShutdown());
+ } finally {
+ zepServer.destroy();
+ }
+ }
}