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 d6bdd8b6ea [ZEPPELIN-6574] Add read-only REST API for interpreter 
process status
d6bdd8b6ea is described below

commit d6bdd8b6eaef8123daaa37834d1f75259e4c522a
Author: HyeonUk Kang <[email protected]>
AuthorDate: Thu Sep 10 01:30:06 2026 +0900

    [ZEPPELIN-6574] Add read-only REST API for interpreter process status
    
    ### What is this PR for?
    
    Zeppelin server owns interpreter processes but exposes no API to see which 
ones are running. This adds a read-only snapshot endpoint:
    
    ```json
    GET /api/interpreter/status
    
    [{
      "settingId": "spark", "settingName": "spark", "groupId": 
"spark-shared_process",
      "numSessions": 2, "started": true, "host": "127.0.0.1", "port": 51037,
      "startTime": "2026-08-09 14:20:11", "uptimeSeconds": 3184, 
"errorMessage": null
    }]
    ```
    
    Review point:
    
    - **No remote probe.** Built from in-memory server state only, so a stuck 
interpreter cannot block the call. `started` means a process handle exists, not 
that the process is reachable. A bounded liveness probe is deliberately left to 
ZEPPELIN-6576.
    
    First sub-task of ZEPPELIN-6568 (server-side status and idle lifecycle for 
interpreter processes); the remaining sub-tasks build on this.
    
    ### What type of PR is it?
    Feature
    
    ### Todos
    * [x] - Add `InterpreterProcessStatus`, a snapshot DTO built from a 
`ManagedInterpreterGroup`
    * [x] - Add `InterpreterSettingManager#getInterpreterProcessStatuses` to 
aggregate all running groups
    * [x] - Add `RemoteInterpreterProcess#getStartTimeMs` so uptime is computed 
without a remote call
    * [x] - Add the `GET /api/interpreter/status` endpoint
    * [x] - Add unit and REST tests
    
    ### What is the Jira issue?
    * [ZEPPELIN-6574](https://issues.apache.org/jira/browse/ZEPPELIN-6574)
    
    ### How should this be tested?
    
    Included tests, both passing on current master:
    
    - `InterpreterSettingManagerTest#testGetInterpreterProcessStatuses` - 
aggregation is empty before any group exists; after a session is created it 
reports the setting name, session count, and `started=false` / `port=-1` for an 
unlaunched process. (13/13)
    - `InterpreterRestApiTest#testGetInterpreterProcessStatus` - endpoint 
returns a JSON array. (12/12)
    
    Manually: run a paragraph, then `curl -u <admin> 
http://localhost:8080/api/interpreter/status` and confirm the interpreter 
appears with `started: true`. Restarting it from the setting page resets 
`uptimeSeconds`.
    
    ### Screenshots (if appropriate)
    
    N/A - REST only.
    
    ### Questions:
    * Does the license files need to update? No.
    * Is there breaking changes for older versions? No - additive and read-only.
    * Does this needs documentation? Can add a REST API doc section here if 
preferred, otherwise once the sibling sub-tasks land.
    
    
    Closes #5403 from hyunw9/ZEPPELIN-6574.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 .../interpreter/InterpreterProcessStatus.java      | 111 +++++++++++++++++++++
 .../interpreter/InterpreterSettingManager.java     |  11 ++
 .../interpreter/ManagedInterpreterGroup.java       |   2 +-
 .../remote/RemoteInterpreterManagedProcess.java    |   6 +-
 .../remote/RemoteInterpreterProcess.java           |  18 +++-
 .../apache/zeppelin/rest/InterpreterRestApi.java   |  11 ++
 .../interpreter/InterpreterSettingManagerTest.java |  25 +++++
 .../zeppelin/rest/InterpreterRestApiTest.java      |  11 ++
 8 files changed, 189 insertions(+), 6 deletions(-)

diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/InterpreterProcessStatus.java
 
b/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/InterpreterProcessStatus.java
new file mode 100644
index 0000000000..7d311aa35b
--- /dev/null
+++ 
b/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/InterpreterProcessStatus.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.zeppelin.interpreter;
+
+import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess;
+
+/**
+ * Point-in-time status snapshot of a single interpreter process as seen by 
the Zeppelin server.
+ * Built purely from in-memory server state without contacting the process, so 
{@code started}
+ * reflects whether a process handle exists, not whether the process is 
currently reachable.
+ * Reachability is intentionally out of scope here to keep the read path 
non-blocking.
+ *
+ * <p>Every value below must be readable without leaving the JVM. In 
particular do not call
+ * {@code isRunning()}, {@code isAlive()} or {@code getErrorMessage()} on the 
process from here:
+ * those are failure-path diagnostics that contact the container runtime on 
some launchers, so
+ * calling them would let a slow or unreachable runtime block this endpoint.
+ *
+ * <p>{@code started} and {@code launching} are read one after the other 
rather than under the
+ * lock that guards a launch, so this is a best-effort view of a group that is 
starting up: the
+ * pair can straddle the moment a launch finishes. What it does buy is that 
the window in which
+ * a handle carries no {@code host} or {@code port} yet is reported as such 
instead of looking
+ * like a fully started process.
+ */
+public class InterpreterProcessStatus {
+  private final String settingId;
+  private final String settingName;
+  private final String groupId;
+  private final int numSessions;
+  private final boolean launching;
+  private final boolean started;
+  private String host;
+  private int port = -1;
+  private String startTime;
+  private long attachedForSeconds;
+
+  public InterpreterProcessStatus(ManagedInterpreterGroup group) {
+    InterpreterSetting setting = group.getInterpreterSetting();
+    this.settingId = setting.getId();
+    this.settingName = setting.getName();
+    this.groupId = group.getId();
+    this.numSessions = group.getSessionNum();
+    this.launching = group.isLaunchingInterpreterProcess();
+    RemoteInterpreterProcess process = group.getInterpreterProcess();
+    this.started = process != null;
+    if (started) {
+      this.host = process.getHost();
+      this.port = process.getPort();
+      this.startTime = process.getStartTime();
+      this.attachedForSeconds = (System.currentTimeMillis() - 
process.getStartTimeMs()) / 1000;
+    }
+  }
+
+  public String getSettingId() {
+    return settingId;
+  }
+
+  public String getSettingName() {
+    return settingName;
+  }
+
+  public String getGroupId() {
+    return groupId;
+  }
+
+  public int getNumSessions() {
+    return numSessions;
+  }
+
+  /**
+   * @return whether a process is currently being launched for this group, in 
which case
+   *         {@code host} and {@code port} may not be filled in yet even when 
{@code started}
+   */
+  public boolean isLaunching() {
+    return launching;
+  }
+
+  public boolean isStarted() {
+    return started;
+  }
+
+  public String getHost() {
+    return host;
+  }
+
+  public int getPort() {
+    return port;
+  }
+
+  public String getStartTime() {
+    return startTime;
+  }
+
+  public long getAttachedForSeconds() {
+    return attachedForSeconds;
+  }
+}
diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java
 
b/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java
index 8b4c2fe55b..d0cff37f2b 100644
--- 
a/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java
+++ 
b/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java
@@ -711,6 +711,17 @@ public class InterpreterSettingManager implements 
NoteEventListener {
     return interpreterGroups;
   }
 
+  /**
+   * Snapshot the status of every running interpreter group. Uses in-memory 
state only
+   */
+  public List<InterpreterProcessStatus> getInterpreterProcessStatuses() {
+    List<InterpreterProcessStatus> statuses = new ArrayList<>();
+    for (ManagedInterpreterGroup group : getAllInterpreterGroup()) {
+      statuses.add(new InterpreterProcessStatus(group));
+    }
+    return statuses;
+  }
+
   // TODO(zjffdu) Current approach is not optimized. we have to iterate all 
interpreter settings.
   public void removeInterpreterGroup(String intpGroupId) {
     for (InterpreterSetting interpreterSetting : interpreterSettings.values()) 
{
diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java
 
b/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java
index 3a2f78af89..3a8b14ee81 100644
--- 
a/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java
+++ 
b/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/ManagedInterpreterGroup.java
@@ -41,7 +41,7 @@ public class ManagedInterpreterGroup extends InterpreterGroup 
{
   private static final Logger LOGGER = 
LoggerFactory.getLogger(ManagedInterpreterGroup.class);
 
   private InterpreterSetting interpreterSetting;
-  private RemoteInterpreterProcess remoteInterpreterProcess; // attached 
remote interpreter process
+  private volatile RemoteInterpreterProcess remoteInterpreterProcess;
   private Object interpreterProcessCreationLock = new Object();
   private final ZeppelinConfiguration zConf;
   private volatile long lastUsedTimeInMillis = System.currentTimeMillis();
diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java
 
b/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java
index 02cedb322f..d9c33e4005 100644
--- 
a/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java
+++ 
b/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterManagedProcess.java
@@ -32,14 +32,14 @@ public abstract class RemoteInterpreterManagedProcess 
extends RemoteInterpreterP
 
   private final String interpreterPortRange;
 
-  private String host = null;
-  private int port = -1;
+  private volatile String host = null;
+  private volatile int port = -1;
   private final String interpreterDir;
   private final String localRepoDir;
   private final String interpreterSettingName;
   private final String interpreterGroupId;
   private final boolean isUserImpersonated;
-  private String errorMessage;
+  private volatile String errorMessage;
 
   private Map<String, String> env;
 
diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java
 
b/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java
index e994439c89..89c27a418c 100644
--- 
a/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java
+++ 
b/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterProcess.java
@@ -45,7 +45,8 @@ public abstract class RemoteInterpreterProcess implements 
InterpreterClient, Aut
   protected String intpEventServerHost;
   protected int intpEventServerPort;
   private PooledRemoteClient<Client> remoteClient;
-  private String startTime;
+  private final long startTimeMs;
+  private final String startTime;
 
   public RemoteInterpreterProcess(int connectTimeout,
                                   int connectionPoolSize,
@@ -54,7 +55,8 @@ public abstract class RemoteInterpreterProcess implements 
InterpreterClient, Aut
     this.connectTimeout = connectTimeout;
     this.intpEventServerHost = intpEventServerHost;
     this.intpEventServerPort = intpEventServerPort;
-    this.startTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new 
Date());
+    this.startTimeMs = System.currentTimeMillis();
+    this.startTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new 
Date(startTimeMs));
     this.remoteClient = new PooledRemoteClient<>(() -> {
       TSocket transport = new TSocket(getHost(), getPort());
       try {
@@ -71,9 +73,21 @@ public abstract class RemoteInterpreterProcess implements 
InterpreterClient, Aut
     return connectTimeout;
   }
 
+  /**
+   * When the server created this object, formatted for display. This is not 
necessarily when the
+   * interpreter itself started: {@link RemoteInterpreterRunningProcess} is 
constructed fresh when
+   * the server recovers a process that outlived it, and when it attaches to 
an interpreter that
+   * was already running, so on those paths the stamp is the moment of 
attachment.
+   *
+   * @return the creation instant of this object as {@code yyyy-MM-dd HH:mm:ss}
+   */
   public String getStartTime() {
     return startTime;
   }
+  
+  public long getStartTimeMs() {
+    return startTimeMs;
+  }
 
   @Override
   public void close() {
diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/InterpreterRestApi.java
 
b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/InterpreterRestApi.java
index 3b9d754e91..163fad9614 100644
--- 
a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/InterpreterRestApi.java
+++ 
b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/InterpreterRestApi.java
@@ -102,6 +102,17 @@ public class InterpreterRestApi extends AbstractRestApi {
     return new JsonResponse<>(Status.OK, "", 
interpreterSettingManager.get()).build();
   }
 
+  /**
+   * List the runtime status of all running interpreter processes.
+   */
+  @GET
+  @Path("status")
+  @ZeppelinApi
+  public Response getInterpreterProcessStatus() {
+    return new JsonResponse<>(Status.OK, "",
+        interpreterSettingManager.getInterpreterProcessStatuses()).build();
+  }
+
   /**
    * Get a setting.
    */
diff --git 
a/zeppelin-server/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerTest.java
 
b/zeppelin-server/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerTest.java
index 95e126fedf..ab489c278c 100644
--- 
a/zeppelin-server/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerTest.java
+++ 
b/zeppelin-server/src/test/java/org/apache/zeppelin/interpreter/InterpreterSettingManagerTest.java
@@ -40,7 +40,9 @@ import java.util.List;
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 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.junit.jupiter.api.Assertions.fail;
 import static org.mockito.Mockito.mock;
@@ -262,6 +264,29 @@ class InterpreterSettingManagerTest extends 
AbstractInterpreterTest {
     assertEquals(0, interpreterSetting.getAllInterpreterGroups().size());
   }
 
+  @Test
+  void testGetInterpreterProcessStatuses() throws InterpreterException {
+    // no interpreter group has been created yet
+    
assertTrue(interpreterSettingManager.getInterpreterProcessStatuses().isEmpty());
+
+    InterpreterSetting interpreterSetting = 
interpreterSettingManager.getByName("test");
+    interpreterSetting.getOption().setPerUser("shared");
+    interpreterSetting.getOption().setPerNote("shared");
+    interpreterSetting.getOrCreateSession("user1", note1Id);
+
+    List<InterpreterProcessStatus> statuses =
+        interpreterSettingManager.getInterpreterProcessStatuses();
+    assertEquals(1, statuses.size());
+    InterpreterProcessStatus status = statuses.get(0);
+    assertEquals("test", status.getSettingName());
+    assertEquals(1, status.getNumSessions());
+    // process starts lazily on first interpret, so it is not started at this 
point
+    assertFalse(status.isStarted());
+    assertFalse(status.isLaunching());
+    assertNull(status.getHost());
+    assertEquals(-1, status.getPort());
+  }
+
   @Test
   void testRestartPerUserIsolated() throws InterpreterException {
     InterpreterSetting interpreterSetting = 
interpreterSettingManager.getByName("test");
diff --git 
a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/InterpreterRestApiTest.java
 
b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/InterpreterRestApiTest.java
index 19435b3211..66d266b703 100644
--- 
a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/InterpreterRestApiTest.java
+++ 
b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/InterpreterRestApiTest.java
@@ -106,6 +106,17 @@ class InterpreterRestApiTest extends AbstractTestRestApi {
     get.close();
   }
 
+  @Test
+  void testGetInterpreterProcessStatus() throws IOException {
+    // when
+    CloseableHttpResponse get = httpGet("/interpreter/status");
+    // then
+    assertThat(get, isAllowed());
+    JsonArray body = 
getArrayBodyFieldFromResponse(EntityUtils.toString(get.getEntity(), 
StandardCharsets.UTF_8));
+    assertNotNull(body);
+    get.close();
+  }
+
   @Test
   void testGetNonExistInterpreterSetting() throws IOException {
     // when

Reply via email to