This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 02b72d78af add graceful shutdown to hop server, fixes #6403 (#7480)
02b72d78af is described below
commit 02b72d78af3f03839a0cdf4aeba3cc2fae8c6064
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Jul 10 16:27:50 2026 +0200
add graceful shutdown to hop server, fixes #6403 (#7480)
---
.../modules/ROOT/pages/docker-container.adoc | 4 +
.../modules/ROOT/pages/hop-server/index.adoc | 34 +++++++
.../java/org/apache/hop/www/AddExportServlet.java | 3 +
.../org/apache/hop/www/AddPipelineServlet.java | 3 +
.../org/apache/hop/www/AddWorkflowServlet.java | 3 +
.../java/org/apache/hop/www/BaseHttpServlet.java | 21 ++++
.../java/org/apache/hop/www/BodyHttpServlet.java | 5 +
.../java/org/apache/hop/www/GetStatusServlet.java | 4 +-
.../main/java/org/apache/hop/www/HopServer.java | 87 ++++++++++++++++
.../org/apache/hop/www/HopServerSingleton.java | 19 ++++
.../java/org/apache/hop/www/HopServerStatus.java | 6 ++
.../hop/www/PrepareExecutionPipelineServlet.java | 3 +
.../hop/www/StartExecutionPipelineServlet.java | 3 +
.../org/apache/hop/www/StartPipelineServlet.java | 3 +
.../org/apache/hop/www/StartWorkflowServlet.java | 3 +
.../org/apache/hop/www/GracefulShutdownTest.java | 109 +++++++++++++++++++++
.../org/apache/hop/www/HopServerStatusTest.java | 15 +++
.../org/apache/hop/www/ShutdownServletTest.java | 2 +-
18 files changed, 325 insertions(+), 2 deletions(-)
diff --git a/docs/hop-tech-manual/modules/ROOT/pages/docker-container.adoc
b/docs/hop-tech-manual/modules/ROOT/pages/docker-container.adoc
index 0a9ad3580a..49bd5d46e6 100644
--- a/docs/hop-tech-manual/modules/ROOT/pages/docker-container.adoc
+++ b/docs/hop-tech-manual/modules/ROOT/pages/docker-container.adoc
@@ -188,6 +188,10 @@ Below are the variables you can use for a **long-lived**
container, running Hop
| `8080`
| The port the server will listen to.
+|```HOP_SERVER_SHUTDOWN_TIMEOUT```
+| `0`
+| The maximum number of seconds to wait for running pipelines and workflows to
finish when the server shuts down (for example on a `SIGTERM` from the
container runtime). `0` shuts down immediately.
+
|```HOP_SERVER_AUTH```
| `true`
| Disable/Enable the authentication used for hop server
diff --git a/docs/hop-user-manual/modules/ROOT/pages/hop-server/index.adoc
b/docs/hop-user-manual/modules/ROOT/pages/hop-server/index.adoc
index d49a33a60a..15ccfc759a 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-server/index.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-server/index.adoc
@@ -138,6 +138,11 @@ This is provided for by the 'projects' plugin.
|
|Specify the ID of the pipeline or workflow to query
+|-swt
+|--shutdown-timeout
+|Default: 0 +
+The maximum number of seconds to wait for running pipelines and workflows to
finish when the server shuts down (0 shuts down immediately). Can also be set
with the `HOP_SERVER_SHUTDOWN_TIMEOUT` environment variable.
+
|===
=====
@@ -459,6 +464,35 @@ Linux, macOS::
--
====
+==== Graceful shutdown
+
+Whichever way a shutdown is triggered (`CTRL-C`, `SIGTERM` or the `/shutdown`
servlet), Hop Server shuts down gracefully:
+
+* it immediately stops accepting new work: requests that add or start
pipelines, workflows or packages are refused with an HTTP `503 Service
Unavailable`, while status requests keep working,
+* the server status reports `Shutting down` (the status XML/JSON contains
`shutting_down=Y`), which is useful as a Kubernetes readiness signal,
+* it waits for the running pipelines and workflows to finish before the
process exits.
+
+By default the server waits `0` seconds, i.e. it shuts down immediately.
+Use the `--shutdown-timeout` option (or the `HOP_SERVER_SHUTDOWN_TIMEOUT`
environment variable) to allow running executions to finish within a maximum
number of seconds.
+When the timeout elapses while executions are still running, the server shuts
down anyway.
+
+[tabs]
+====
+Windows::
++
+--
+[source,shell]
+hop-server.bat 127.0.0.1 8080 --shutdown-timeout 120
+--
+
+Linux, macOS::
++
+--
+[source,shell]
+ ./hop-server.sh 127.0.0.1 8080 --shutdown-timeout 120
+--
+====
+
== Verify startup
Starting a Hop Server on the local machine e.g. on port 8081 will only take 1
or 2 seconds.
diff --git a/engine/src/main/java/org/apache/hop/www/AddExportServlet.java
b/engine/src/main/java/org/apache/hop/www/AddExportServlet.java
index f1fb64a4a7..fcd9220c71 100644
--- a/engine/src/main/java/org/apache/hop/www/AddExportServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/AddExportServlet.java
@@ -83,6 +83,9 @@ public class AddExportServlet extends BaseHttpServlet
implements IHopServerPlugi
if (isJettyMode() && !request.getRequestURI().startsWith(CONTEXT_PATH)) {
return;
}
+ if (refuseIfShuttingDown(response)) {
+ return;
+ }
if (log.isDebug()) {
logDebug("Addition of export requested");
diff --git a/engine/src/main/java/org/apache/hop/www/AddPipelineServlet.java
b/engine/src/main/java/org/apache/hop/www/AddPipelineServlet.java
index 988599f3fa..008cc348f4 100644
--- a/engine/src/main/java/org/apache/hop/www/AddPipelineServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/AddPipelineServlet.java
@@ -66,6 +66,9 @@ public class AddPipelineServlet extends BaseHttpServlet
implements IHopServerPlu
if (isJettyMode() && !request.getRequestURI().startsWith(CONTEXT_PATH)) {
return;
}
+ if (refuseIfShuttingDown(response)) {
+ return;
+ }
if (log.isDebug()) {
logDebug("Addition of pipeline requested");
diff --git a/engine/src/main/java/org/apache/hop/www/AddWorkflowServlet.java
b/engine/src/main/java/org/apache/hop/www/AddWorkflowServlet.java
index 12ceb0ebd2..f835721b53 100644
--- a/engine/src/main/java/org/apache/hop/www/AddWorkflowServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/AddWorkflowServlet.java
@@ -66,6 +66,9 @@ public class AddWorkflowServlet extends BaseHttpServlet
implements IHopServerPlu
if (isJettyMode() && !request.getRequestURI().startsWith(CONTEXT_PATH)) {
return;
}
+ if (refuseIfShuttingDown(response)) {
+ return;
+ }
if (log.isDebug()) {
logDebug("Addition of workflow requested");
diff --git a/engine/src/main/java/org/apache/hop/www/BaseHttpServlet.java
b/engine/src/main/java/org/apache/hop/www/BaseHttpServlet.java
index 97fb89a4ea..85650eee28 100644
--- a/engine/src/main/java/org/apache/hop/www/BaseHttpServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/BaseHttpServlet.java
@@ -224,6 +224,27 @@ public class BaseHttpServlet extends HttpServlet {
}
}
+ /**
+ * Refuse a request with HTTP 503 (Service Unavailable) while the server is
performing a graceful
+ * shutdown. Servlets that accept new work (register/add/start/execute
pipelines and workflows)
+ * should call this at the top of their handler and return immediately when
it returns true.
+ * Status and reporting servlets do not call it and keep working during
shutdown.
+ *
+ * @return true when the server is shutting down and an error response has
been sent; the caller
+ * must return immediately without performing any work.
+ */
+ protected boolean refuseIfShuttingDown(HttpServletResponse response) {
+ if (!HopServerSingleton.isServerShuttingDown()) {
+ return false;
+ }
+ logBasic("Refused a request: the Hop server is shutting down.");
+ sendSafeError(
+ response,
+ HttpServletResponse.SC_SERVICE_UNAVAILABLE,
+ "The Hop server is shutting down and is not accepting new work.");
+ return true;
+ }
+
/**
* Log server-side and return a {@link WebResult} error to the client in the
requested XML or JSON
* shape. Use {@code writer} when the response writer is already acquired
for this request;
diff --git a/engine/src/main/java/org/apache/hop/www/BodyHttpServlet.java
b/engine/src/main/java/org/apache/hop/www/BodyHttpServlet.java
index e1a2d3721c..6035d7c002 100644
--- a/engine/src/main/java/org/apache/hop/www/BodyHttpServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/BodyHttpServlet.java
@@ -48,6 +48,11 @@ public abstract class BodyHttpServlet extends
BaseHttpServlet implements IHopSer
return;
}
+ // Register/add servlets accept new work, so refuse them while the server
is shutting down.
+ if (refuseIfShuttingDown(response)) {
+ return;
+ }
+
if (log.isDebug()) {
logDebug(messages.getString("Log.Execute"));
}
diff --git a/engine/src/main/java/org/apache/hop/www/GetStatusServlet.java
b/engine/src/main/java/org/apache/hop/www/GetStatusServlet.java
index 8f91b29576..ab46bb3574 100644
--- a/engine/src/main/java/org/apache/hop/www/GetStatusServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/GetStatusServlet.java
@@ -113,8 +113,10 @@ public class GetStatusServlet extends BaseHttpServlet
implements IHopServerPlugi
if (useXml || useJson) {
try {
+ boolean shuttingDown = HopServerSingleton.isServerShuttingDown();
HopServerStatus serverStatus = new HopServerStatus();
- serverStatus.setStatusDescription("Online");
+ serverStatus.setStatusDescription(shuttingDown ? "Shutting down" :
"Online");
+ serverStatus.setShuttingDown(shuttingDown);
getSystemInfo(serverStatus);
diff --git a/engine/src/main/java/org/apache/hop/www/HopServer.java
b/engine/src/main/java/org/apache/hop/www/HopServer.java
index 08365612d2..504ae34487 100644
--- a/engine/src/main/java/org/apache/hop/www/HopServer.java
+++ b/engine/src/main/java/org/apache/hop/www/HopServer.java
@@ -56,8 +56,12 @@ import
org.apache.hop.metadata.serializer.json.JsonMetadataProvider;
import org.apache.hop.metadata.serializer.multi.MultiMetadataProvider;
import org.apache.hop.metadata.util.HopMetadataInstance;
import org.apache.hop.metadata.util.HopMetadataUtil;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engine.IPipelineEngine;
import org.apache.hop.pipeline.transform.TransformStatus;
import org.apache.hop.server.HopServerMeta;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.apache.hop.workflow.engine.IWorkflowEngine;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import picocli.CommandLine;
@@ -135,6 +139,15 @@ public class HopServer implements Runnable,
IHasHopMetadataProvider, IHopCommand
description = "Does the Hop web server have authentication enabled")
private Boolean enableAuth;
+ @CommandLine.Option(
+ names = {"-swt", "--shutdown-timeout"},
+ description =
+ "The maximum number of seconds to wait for running pipelines and
workflows to finish "
+ + "when the server shuts down. 0 (the default) shuts down
immediately without "
+ + "waiting. Can also be set with the HOP_SERVER_SHUTDOWN_TIMEOUT
environment variable.",
+ defaultValue = "${env:HOP_SERVER_SHUTDOWN_TIMEOUT:-0}")
+ private long shutdownTimeout;
+
private WebServer webServer;
private HopServerConfig config;
private boolean allOK;
@@ -267,6 +280,10 @@ public class HopServer implements Runnable,
IHasHopMetadataProvider, IHopCommand
/** Gracefully shut down the running Hop web server. */
public void shutdown() {
if (webServer != null) {
+ // From now on the server refuses new work (adding/running pipelines,
workflows, ...) but
+ // keeps serving status requests.
+ HopServerSingleton.setServerShuttingDown(true);
+
// Right before the Hop server will shut down
try {
ExtensionPointHandler.callExtensionPoint(
@@ -277,10 +294,80 @@ public class HopServer implements Runnable,
IHasHopMetadataProvider, IHopCommand
log.logError("Error calling extension point HopServerShutdown", e);
}
+ // Wait for running pipelines and workflows to finish, up to the
configured timeout.
+ awaitRunningExecutions();
+
webServer.stop();
}
}
+ /**
+ * Wait until all running pipelines and workflows on this server have
finished, or until the
+ * configured shutdown timeout (in seconds) has elapsed. A timeout of 0 (the
default) shuts down
+ * immediately without waiting.
+ */
+ private void awaitRunningExecutions() {
+ if (shutdownTimeout <= 0) {
+ log.logBasic(
+ "Shutdown timeout is 0; stopping the server immediately without
waiting for running "
+ + "pipelines and workflows to finish. Set --shutdown-timeout (or
"
+ + "HOP_SERVER_SHUTDOWN_TIMEOUT) to a number of seconds to wait
for them.");
+ return;
+ }
+
+ HopServerSingleton singleton = HopServerSingleton.getInstance();
+ PipelineMap pipelineMap = singleton.getPipelineMap();
+ WorkflowMap workflowMap = singleton.getWorkflowMap();
+
+ long deadline = System.currentTimeMillis() + shutdownTimeout * 1000L;
+ log.logBasic(
+ "Waiting up to "
+ + shutdownTimeout
+ + " seconds for running pipelines and workflows to finish before
shutting down.");
+
+ int running;
+ while ((running = countRunningExecutions(pipelineMap, workflowMap)) > 0
+ && System.currentTimeMillis() < deadline) {
+ log.logBasic(running + " pipeline(s)/workflow(s) still running, waiting
for completion...");
+ try {
+ Thread.sleep(5000L);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+
+ running = countRunningExecutions(pipelineMap, workflowMap);
+ if (running > 0) {
+ log.logBasic(
+ "Shutdown wait time elapsed while "
+ + running
+ + " pipeline(s)/workflow(s) are still running; stopping the
server now.");
+ } else {
+ log.logBasic("All pipelines and workflows have finished; stopping the
server.");
+ }
+ }
+
+ /**
+ * @return the number of pipelines and workflows that are currently running
on this server
+ */
+ private int countRunningExecutions(PipelineMap pipelineMap, WorkflowMap
workflowMap) {
+ int running = 0;
+ for (HopServerObjectEntry entry : pipelineMap.getPipelineObjects()) {
+ IPipelineEngine<PipelineMeta> pipeline = pipelineMap.getPipeline(entry);
+ if (pipeline != null && !pipeline.isFinished() && !pipeline.isStopped())
{
+ running++;
+ }
+ }
+ for (HopServerObjectEntry entry : workflowMap.getWorkflowObjects()) {
+ IWorkflowEngine<WorkflowMeta> workflow = workflowMap.getWorkflow(entry);
+ if (workflow != null && !workflow.isFinished() && !workflow.isStopped())
{
+ running++;
+ }
+ }
+ return running;
+ }
+
/** Virtual-machine shutdown hook that stops the Hop server gracefully (e.g.
on Ctrl-C). */
private class ShutdownHook extends Thread {
@Override
diff --git a/engine/src/main/java/org/apache/hop/www/HopServerSingleton.java
b/engine/src/main/java/org/apache/hop/www/HopServerSingleton.java
index db3ea4deec..8df83f13d7 100644
--- a/engine/src/main/java/org/apache/hop/www/HopServerSingleton.java
+++ b/engine/src/main/java/org/apache/hop/www/HopServerSingleton.java
@@ -55,6 +55,13 @@ public class HopServerSingleton {
private PipelineMap pipelineMap;
private WorkflowMap workflowMap;
+ /**
+ * Set to true once a graceful shutdown has been initiated. While the server
is shutting down it
+ * refuses new work (adding/running pipelines, workflows, ...) but keeps
serving status requests.
+ * It is static because there is a single Hop server per JVM.
+ */
+ private static final AtomicBoolean shuttingDown = new AtomicBoolean(false);
+
private HopServerSingleton(HopServerConfig config) throws HopException {
HopEnvironment.init();
HopLogStore.init();
@@ -239,6 +246,18 @@ public class HopServerSingleton {
}
}
+ /**
+ * @return true when a graceful shutdown of the server has been initiated.
Safe to call from
+ * servlets at any time.
+ */
+ public static boolean isServerShuttingDown() {
+ return shuttingDown.get();
+ }
+
+ public static void setServerShuttingDown(boolean value) {
+ shuttingDown.set(value);
+ }
+
public PipelineMap getPipelineMap() {
return pipelineMap;
}
diff --git a/engine/src/main/java/org/apache/hop/www/HopServerStatus.java
b/engine/src/main/java/org/apache/hop/www/HopServerStatus.java
index 4cc5fd49ce..a3e03877c5 100644
--- a/engine/src/main/java/org/apache/hop/www/HopServerStatus.java
+++ b/engine/src/main/java/org/apache/hop/www/HopServerStatus.java
@@ -34,6 +34,10 @@ public class HopServerStatus {
@Getter @Setter private String statusDescription;
@Getter @Setter private String errorDescription;
+
+ /** True when the server has started a graceful shutdown and is refusing new
work. */
+ @Getter @Setter private boolean shuttingDown;
+
@Getter @Setter private List<HopServerPipelineStatus> pipelineStatusList;
@Getter @Setter private List<HopServerWorkflowStatus> workflowStatusList;
@Getter @Setter private long memoryFree;
@@ -77,6 +81,7 @@ public class HopServerStatus {
xml.append("<" + XML_TAG + ">").append(Const.CR);
xml.append(XmlHandler.addTagValue("statusdesc", statusDescription));
+ xml.append(XmlHandler.addTagValue("shutting_down", shuttingDown));
xml.append(XmlHandler.addTagValue("memory_free", memoryFree));
xml.append(XmlHandler.addTagValue("memory_total", memoryTotal));
@@ -113,6 +118,7 @@ public class HopServerStatus {
public HopServerStatus(Node statusNode) throws HopException {
this();
statusDescription = XmlHandler.getTagValue(statusNode, "statusdesc");
+ shuttingDown = "Y".equalsIgnoreCase(XmlHandler.getTagValue(statusNode,
"shutting_down"));
memoryFree = Const.toLong(XmlHandler.getTagValue(statusNode,
"memory_free"), -1L);
memoryTotal = Const.toLong(XmlHandler.getTagValue(statusNode,
"memory_total"), -1L);
diff --git
a/engine/src/main/java/org/apache/hop/www/PrepareExecutionPipelineServlet.java
b/engine/src/main/java/org/apache/hop/www/PrepareExecutionPipelineServlet.java
index e4dd239cbc..d9616c96f7 100644
---
a/engine/src/main/java/org/apache/hop/www/PrepareExecutionPipelineServlet.java
+++
b/engine/src/main/java/org/apache/hop/www/PrepareExecutionPipelineServlet.java
@@ -62,6 +62,9 @@ public class PrepareExecutionPipelineServlet extends
BaseHttpServlet implements
if (isJettyMode() && !request.getContextPath().startsWith(CONTEXT_PATH)) {
return;
}
+ if (refuseIfShuttingDown(response)) {
+ return;
+ }
if (log.isDebug()) {
logDebug(
diff --git
a/engine/src/main/java/org/apache/hop/www/StartExecutionPipelineServlet.java
b/engine/src/main/java/org/apache/hop/www/StartExecutionPipelineServlet.java
index 3c6611f96d..4d9b1a4f00 100644
--- a/engine/src/main/java/org/apache/hop/www/StartExecutionPipelineServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/StartExecutionPipelineServlet.java
@@ -56,6 +56,9 @@ public class StartExecutionPipelineServlet extends
BaseHttpServlet implements IH
if (isJettyMode() && !request.getContextPath().startsWith(CONTEXT_PATH)) {
return;
}
+ if (refuseIfShuttingDown(response)) {
+ return;
+ }
if (log.isDebug()) {
logDebug("Start execution of pipeline requested");
diff --git a/engine/src/main/java/org/apache/hop/www/StartPipelineServlet.java
b/engine/src/main/java/org/apache/hop/www/StartPipelineServlet.java
index bf980f5e4f..d222cca473 100644
--- a/engine/src/main/java/org/apache/hop/www/StartPipelineServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/StartPipelineServlet.java
@@ -62,6 +62,9 @@ public class StartPipelineServlet extends BaseHttpServlet
implements IHopServerP
if (isJettyMode() && !request.getContextPath().startsWith(CONTEXT_PATH)) {
return;
}
+ if (refuseIfShuttingDown(response)) {
+ return;
+ }
if (log.isDebug()) {
logDebug(BaseMessages.getString(PKG,
"StartPipelineServlet.Log.PipelineStartRequested"));
diff --git a/engine/src/main/java/org/apache/hop/www/StartWorkflowServlet.java
b/engine/src/main/java/org/apache/hop/www/StartWorkflowServlet.java
index f2005b08b8..cf0a9e50ed 100644
--- a/engine/src/main/java/org/apache/hop/www/StartWorkflowServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/StartWorkflowServlet.java
@@ -64,6 +64,9 @@ public class StartWorkflowServlet extends BaseHttpServlet
implements IHopServerP
if (isJettyMode() && !request.getContextPath().startsWith(CONTEXT_PATH)) {
return;
}
+ if (refuseIfShuttingDown(response)) {
+ return;
+ }
if (log.isDebug()) {
logDebug(BaseMessages.getString(PKG,
"StartWorkflowServlet.Log.StartWorkflowRequested"));
diff --git a/engine/src/test/java/org/apache/hop/www/GracefulShutdownTest.java
b/engine/src/test/java/org/apache/hop/www/GracefulShutdownTest.java
new file mode 100644
index 0000000000..15304dd528
--- /dev/null
+++ b/engine/src/test/java/org/apache/hop/www/GracefulShutdownTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.hop.www;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Collections;
+import org.apache.hop.core.logging.HopLogStore;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests the graceful-shutdown behaviour: refusing new work and reporting the
shutting-down state.
+ */
+class GracefulShutdownTest {
+
+ @BeforeEach
+ void setup() {
+ HopLogStore.init();
+ HopServerSingleton.setServerShuttingDown(false);
+ }
+
+ @AfterEach
+ void tearDown() {
+ HopServerSingleton.setServerShuttingDown(false);
+ }
+
+ @Test
+ void workServletIsRefusedWithServiceUnavailableWhileShuttingDown() throws
Exception {
+ HopServerSingleton.setServerShuttingDown(true);
+
+ StartPipelineServlet servlet = new
StartPipelineServlet(mock(PipelineMap.class));
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+
when(request.getContextPath()).thenReturn(StartPipelineServlet.CONTEXT_PATH);
+
+ servlet.doGet(request, response);
+
+ verify(response).sendError(eq(HttpServletResponse.SC_SERVICE_UNAVAILABLE),
anyString());
+ }
+
+ @Test
+ void workServletIsNotRefusedWhenNotShuttingDown() throws Exception {
+ // Not shutting down: the refusal path must not fire (the servlet is free
to do its own work).
+ assertFalse(HopServerSingleton.isServerShuttingDown());
+
+ AddPipelineServlet servlet = new
AddPipelineServlet(mock(PipelineMap.class));
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ // Not the right URI in jetty mode, so the servlet returns early without
doing any real work.
+ when(request.getRequestURI()).thenReturn("/some/other/path");
+
+ servlet.doGet(request, response);
+
+ verify(response, never())
+ .sendError(eq(HttpServletResponse.SC_SERVICE_UNAVAILABLE),
anyString());
+ }
+
+ @Test
+ void statusServletReportsShuttingDownState() throws Exception {
+ HopServerSingleton.setServerShuttingDown(true);
+
+ PipelineMap pipelineMap = mock(PipelineMap.class);
+ when(pipelineMap.getPipelineObjects()).thenReturn(Collections.emptyList());
+ WorkflowMap workflowMap = mock(WorkflowMap.class);
+ when(workflowMap.getWorkflowObjects()).thenReturn(Collections.emptyList());
+
+ GetStatusServlet servlet = new GetStatusServlet(pipelineMap, workflowMap);
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ StringWriter out = new StringWriter();
+ when(response.getWriter()).thenReturn(new PrintWriter(out));
+ when(request.getContextPath()).thenReturn(GetStatusServlet.CONTEXT_PATH);
+ when(request.getParameter("xml")).thenReturn("Y");
+
+ servlet.doGet(request, response);
+
+ String xml = out.toString();
+ assertTrue(xml.contains("Shutting down"), xml);
+ assertTrue(xml.contains("<shutting_down>Y</shutting_down>"), xml);
+ }
+}
diff --git a/engine/src/test/java/org/apache/hop/www/HopServerStatusTest.java
b/engine/src/test/java/org/apache/hop/www/HopServerStatusTest.java
index 081c0e63b0..723871e200 100644
--- a/engine/src/test/java/org/apache/hop/www/HopServerStatusTest.java
+++ b/engine/src/test/java/org/apache/hop/www/HopServerStatusTest.java
@@ -18,6 +18,7 @@
package org.apache.hop.www;
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.assertTrue;
@@ -52,4 +53,18 @@ class HopServerStatusTest {
assertTrue(xml.contains("pipeline_status_list"));
assertTrue(xml.contains("workflow_status_list"));
}
+
+ @Test
+ void shuttingDownRoundTripsThroughXml() throws HopException {
+ HopServerStatus status = new HopServerStatus("Shutting down");
+ status.setShuttingDown(true);
+
+ HopServerStatus parsed = HopServerStatus.fromXml(status.getXml());
+ assertTrue(parsed.isShuttingDown());
+ assertEquals("Shutting down", parsed.getStatusDescription());
+
+ // Default is false and it round-trips as false as well.
+ HopServerStatus online = HopServerStatus.fromXml(new
HopServerStatus("Online").getXml());
+ assertFalse(online.isShuttingDown());
+ }
}
diff --git a/engine/src/test/java/org/apache/hop/www/ShutdownServletTest.java
b/engine/src/test/java/org/apache/hop/www/ShutdownServletTest.java
index c0c40988bd..ba9131c5ef 100644
--- a/engine/src/test/java/org/apache/hop/www/ShutdownServletTest.java
+++ b/engine/src/test/java/org/apache/hop/www/ShutdownServletTest.java
@@ -56,7 +56,7 @@ class ShutdownServletTest {
// The servlet responds immediately and schedules the shutdown on a short
delayed timer.
verify(response).setStatus(HttpServletResponse.SC_OK);
- verify(server, timeout(6000)).shutdown();
+ verify(server, timeout(15000)).shutdown();
}
@Test