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 f473ef9745 log and object cleanup was not working as expected, fixes
#2796 (#7553)
f473ef9745 is described below
commit f473ef9745deb6d3b8053ef00d62c7fe091fb674
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Jul 17 13:49:57 2026 +0200
log and object cleanup was not working as expected, fixes #2796 (#7553)
---
.../org/apache/hop/core/logging/HopLogStore.java | 14 ++
.../apache/hop/core/logging/LoggingBufferTest.java | 92 +++++++++
.../modules/ROOT/pages/hop-server/index.adoc | 62 +++++-
.../java/org/apache/hop/www/GetStatusServlet.java | 16 +-
.../org/apache/hop/www/HopServerSingleton.java | 107 +++++++++--
.../apache/hop/www/jaxrs/HopServerResource.java | 9 +-
.../org/apache/hop/www/HopServerSingletonTest.java | 209 +++++++++++++++++++++
7 files changed, 480 insertions(+), 29 deletions(-)
diff --git a/core/src/main/java/org/apache/hop/core/logging/HopLogStore.java
b/core/src/main/java/org/apache/hop/core/logging/HopLogStore.java
index 04f2992824..ffac3de533 100644
--- a/core/src/main/java/org/apache/hop/core/logging/HopLogStore.java
+++ b/core/src/main/java/org/apache/hop/core/logging/HopLogStore.java
@@ -39,6 +39,9 @@ public class HopLogStore {
private Timer logCleanerTimer;
+ /** The log line timeout the cleaner is running with, 0 or lower means:
never clean up lines. */
+ private int maxLogTimeoutMinutes;
+
private static AtomicBoolean initialized = new AtomicBoolean(false);
private static ILogChannelFactory logChannelFactory = new
LogChannelFactory();
@@ -72,6 +75,7 @@ public class HopLogStore {
}
public void replaceLogCleaner(final int maxLogTimeoutMinutes) {
+ this.maxLogTimeoutMinutes = maxLogTimeoutMinutes;
ExecutorUtil.cleanup(logCleanerTimer);
logCleanerTimer = new Timer(true);
@@ -207,6 +211,16 @@ public class HopLogStore {
return getInstance().appender;
}
+ /**
+ * The log line timeout the log cleaner is actually running with, which is
not necessarily the one
+ * that was asked for: a timeout of zero or lower makes it fall back to the
default.
+ *
+ * @return the timeout in minutes, 0 or lower means that log lines are never
cleaned up
+ */
+ public static int getMaxLogTimeoutMinutes() {
+ return getInstance().maxLogTimeoutMinutes;
+ }
+
/**
* Discard all the lines for the specified log channel id AND all the
children.
*
diff --git
a/core/src/test/java/org/apache/hop/core/logging/LoggingBufferTest.java
b/core/src/test/java/org/apache/hop/core/logging/LoggingBufferTest.java
index bcb65ef9f5..86c4dc1469 100644
--- a/core/src/test/java/org/apache/hop/core/logging/LoggingBufferTest.java
+++ b/core/src/test/java/org/apache/hop/core/logging/LoggingBufferTest.java
@@ -18,10 +18,13 @@
package org.apache.hop.core.logging;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
/** Unit test for {@link LoggingBuffer} */
@@ -98,6 +101,95 @@ class LoggingBufferTest {
assertEquals(20, loggingBuffer.size());
}
+ /**
+ * The maximum number of lines is kept while lines are added, dropping the
oldest ones. This is
+ * what the max_log_lines setting of a server is meant to limit.
+ */
+ @Test
+ void maxNrLinesDropsTheOldestLines() {
+ LoggingBuffer loggingBuffer = new LoggingBuffer(10);
+
+ for (int i = 0; i < 25; i++) {
+ loggingBuffer.addLogggingEvent(event("line " + i, i));
+ }
+
+ assertEquals(10, loggingBuffer.size(), "No more than the maximum number of
lines is kept");
+ List<String> kept = messagesOf(loggingBuffer);
+ assertTrue(kept.contains("line 24"), "The newest line is kept");
+ assertFalse(kept.contains("line 14"), "The oldest lines are dropped");
+ }
+
+ /** A maximum of zero lines means: no limit, which is what a server defaults
to. */
+ @Test
+ void maxNrLinesOfZeroKeepsEveryLine() {
+ LoggingBuffer loggingBuffer = new LoggingBuffer(0);
+
+ for (int i = 0; i < 100; i++) {
+ loggingBuffer.addLogggingEvent(event("line " + i, i));
+ }
+
+ assertEquals(100, loggingBuffer.size(), "Without a limit every line is
kept");
+ }
+
+ /**
+ * The two limits are independent of each other. Lines are dropped as soon
as there are too many
+ * of them, no matter how recent they are.
+ */
+ @Test
+ void maxNrLinesIsReachedBeforeAnyLineTimesOut() {
+ LoggingBuffer loggingBuffer = new LoggingBuffer(5);
+
+ // Ten lines that all carry the same, recent, timestamp.
+ long now = System.currentTimeMillis();
+ for (int i = 0; i < 10; i++) {
+ loggingBuffer.addLogggingEvent(event("line " + i, now));
+ }
+
+ assertEquals(5, loggingBuffer.size(), "The line limit applies to lines
that did not time out");
+
+ // Nothing times out yet, the lines are all recent.
+ loggingBuffer.removeBufferLinesBefore(now - 60000L);
+ assertEquals(5, loggingBuffer.size());
+ }
+
+ /** And the other way around: lines time out while there is still plenty of
room in the buffer. */
+ @Test
+ void linesTimeOutBeforeMaxNrLinesIsReached() {
+ LoggingBuffer loggingBuffer = new LoggingBuffer(1000);
+
+ long now = System.currentTimeMillis();
+ for (int i = 0; i < 10; i++) {
+ loggingBuffer.addLogggingEvent(event("old line " + i, now - 120000L));
+ }
+ for (int i = 0; i < 10; i++) {
+ loggingBuffer.addLogggingEvent(event("new line " + i, now));
+ }
+ assertEquals(20, loggingBuffer.size(), "The line limit is nowhere near
reached");
+
+ loggingBuffer.removeBufferLinesBefore(now - 60000L);
+
+ assertEquals(10, loggingBuffer.size(), "Only the lines that timed out are
removed");
+ assertTrue(messagesOf(loggingBuffer).contains("new line 0"), "Recent lines
are kept");
+ }
+
+ private static HopLoggingEvent event(String message, long timeStamp) {
+ HopLoggingEvent event = new HopLoggingEvent();
+ // The two argument constructor takes a subject, only this one takes a
message.
+ event.setMessage(new LogMessage(message, "a-log-channel", LogLevel.BASIC));
+ event.setTimeStamp(timeStamp);
+ return event;
+ }
+
+ /** The messages still in the buffer, to check which lines were kept and
which were dropped. */
+ private static List<String> messagesOf(LoggingBuffer loggingBuffer) {
+ List<HopLoggingEvent> events =
+ loggingBuffer.getLogBufferFromTo(
+ (List<String>) null, true, 0, loggingBuffer.getLastBufferLineNr());
+ return events.stream()
+ .map(event -> ((LogMessage) event.getMessage()).getMessage())
+ .collect(Collectors.toList());
+ }
+
@Test
void testRemoveChannelFromBuffer() {
String logChannelId = "1";
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 15ccfc759a..9ea54d5c1d 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
@@ -245,17 +245,21 @@ The syntax of this configuration file is straightforward.
Hostname and port are
<joining>true</joining>
<!-- The maximum number of log lines kept in memory by the server.
- The default is 0 which means: keep all lines
+ Leave this at 0 or leave the element out to use the
HOP_MAX_LOG_SIZE_IN_LINES variable,
+ which keeps all log lines by default.
-->
<max_log_lines>0</max_log_lines>
<!-- The time (in minutes) it takes for a log line to be cleaned up in
memory.
- The default is 0 which means: never clean up log lines
+ Leave this at 0 or leave the element out to use the
HOP_MAX_LOG_TIMEOUT_IN_MINUTES variable,
+ which cleans up log lines after 1440 minutes (one day) by default.
-->
<max_log_timeout_minutes>1440</max_log_timeout_minutes>
- <!-- The time (in minutes) it takes for a pipeline or workflow execution to
be removed from the server status.
- The default is 0 which means: never clean executions
+ <!-- The time (in minutes) it takes for a finished or stopped pipeline or
workflow execution to be
+ removed from the server status.
+ Leave this at 0 or leave the element out to use the
HOP_SERVER_OBJECT_TIMEOUT_MINUTES
+ variable, which removes executions after 1440 minutes (one day) by
default.
-->
<object_timeout_minutes>1440</object_timeout_minutes>
@@ -281,6 +285,56 @@ The syntax of this configuration file is straightforward.
Hostname and port are
</hop-server-config>
----
+==== Cleanup settings and where they come from
+
+`max_log_lines`, `max_log_timeout_minutes` and `object_timeout_minutes` keep
the memory use of a
+long running server in check. Each of them is resolved separately, and the
first of these that
+supplies a value wins:
+
+. the configuration file, when the element is set to a value larger than 0
+. the matching Java system property, for example
`-DHOP_SERVER_OBJECT_TIMEOUT_MINUTES=60`
+. the variable in `hop-config.json`
+. the built-in default
+
+[cols="1,1,1", options="header"]
+|===
+|Configuration element |Variable |Default
+
+|`max_log_lines`
+|`HOP_MAX_LOG_SIZE_IN_LINES`
+|`0`: every log line is kept
+
+|`max_log_timeout_minutes`
+|`HOP_MAX_LOG_TIMEOUT_IN_MINUTES`
+|`1440`: log lines are cleaned up after one day
+
+|`object_timeout_minutes`
+|`HOP_SERVER_OBJECT_TIMEOUT_MINUTES`
+|`1440`: finished and stopped executions are removed from the server status
after one day
+|===
+
+A server that is started without a configuration file, or without these
elements, therefore keeps
+all of its log lines, cleans them up after a day, and removes finished
executions from its status
+after a day. The `Configuration details` section of the status page reports
the values the server
+is actually running with.
+
+WARNING: The default keeps *every* log line of *every* execution for a day. A
server that runs a lot
+of short executions can run out of memory that way. Set `max_log_lines` to
limit this.
+
+[NOTE]
+====
+These three settings are read from Java system properties, not from the
environment of the process.
+Setting an environment variable on a container, for example `-e
HOP_MAX_LOG_SIZE_IN_LINES=5000`,
+therefore has no effect: the value from `hop-config.json` is used instead. To
override them in a
+container, pass them as system properties through `HOP_OPTIONS`:
+
+[source,shell]
+docker run -e HOP_OPTIONS="-Xmx2048m -DHOP_MAX_LOG_SIZE_IN_LINES=5000" ...
+
+Mind that `HOP_OPTIONS` replaces the default `-Xmx2048m`, so repeat the memory
settings you need.
+Alternatively, set the elements in the server configuration file, which takes
precedence over both.
+====
+
Example startup commands with a configuration file are: +
[tabs]
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 ab46bb3574..fcc7a3b55b 100644
--- a/engine/src/main/java/org/apache/hop/www/GetStatusServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/GetStatusServlet.java
@@ -35,6 +35,7 @@ import org.apache.hop.core.Const;
import org.apache.hop.core.annotations.HopServerServlet;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.json.HopJson;
+import org.apache.hop.core.logging.HopLogStore;
import org.apache.hop.core.xml.XmlHandler;
import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.pipeline.PipelineMeta;
@@ -662,11 +663,12 @@ public class GetStatusServlet extends BaseHttpServlet
implements IHopServerPlugi
HopServerConfig serverConfig = getPipelineMap().getHopServerConfig();
if (serverConfig != null) {
String maxLines = "";
- if (serverConfig.getMaxLogLines() == 0) {
+ if (HopLogStore.getAppender().getMaxNrLines() == 0) {
maxLines = BaseMessages.getString(PKG, CONST_NO_LIMIT);
} else {
maxLines =
- serverConfig.getMaxLogLines() + BaseMessages.getString(PKG,
"GetStatusServlet.Lines");
+ HopLogStore.getAppender().getMaxNrLines()
+ + BaseMessages.getString(PKG, "GetStatusServlet.Lines");
}
out.print(
CONST_TABLE_ROW
@@ -678,11 +680,11 @@ public class GetStatusServlet extends BaseHttpServlet
implements IHopServerPlugi
// The max age of log lines
//
String maxAge = "";
- if (serverConfig.getMaxLogTimeoutMinutes() == 0) {
+ if (HopLogStore.getMaxLogTimeoutMinutes() == 0) {
maxAge = BaseMessages.getString(PKG, CONST_NO_LIMIT);
} else {
maxAge =
- serverConfig.getMaxLogTimeoutMinutes()
+ HopLogStore.getMaxLogTimeoutMinutes()
+ BaseMessages.getString(PKG, "GetStatusServlet.Minutes");
}
out.print(
@@ -694,13 +696,13 @@ public class GetStatusServlet extends BaseHttpServlet
implements IHopServerPlugi
// The max age of stale objects
//
+ int objectTimeoutMinutes =
HopServerSingleton.determineObjectTimeoutMinutes(serverConfig);
String maxObjAge = "";
- if (serverConfig.getObjectTimeoutMinutes() == 0) {
+ if (objectTimeoutMinutes == 0) {
maxObjAge = BaseMessages.getString(PKG, CONST_NO_LIMIT);
} else {
maxObjAge =
- serverConfig.getObjectTimeoutMinutes()
- + BaseMessages.getString(PKG, "GetStatusServlet.Minutes");
+ objectTimeoutMinutes + BaseMessages.getString(PKG,
"GetStatusServlet.Minutes");
}
out.print(
CONST_TABLE_ROW
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 8df83f13d7..aae1cdb346 100644
--- a/engine/src/main/java/org/apache/hop/www/HopServerSingleton.java
+++ b/engine/src/main/java/org/apache/hop/www/HopServerSingleton.java
@@ -33,7 +33,6 @@ import org.apache.hop.core.logging.LoggingObjectType;
import org.apache.hop.core.logging.LoggingRegistry;
import org.apache.hop.core.logging.SimpleLoggingObject;
import org.apache.hop.core.util.EnvUtil;
-import org.apache.hop.core.util.ExecutorUtil;
import org.apache.hop.core.util.Utils;
import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.pipeline.PipelineMeta;
@@ -64,7 +63,7 @@ public class HopServerSingleton {
private HopServerSingleton(HopServerConfig config) throws HopException {
HopEnvironment.init();
- HopLogStore.init();
+ configureLogStore(config);
this.log = new LogChannel("HopServer");
pipelineMap = new PipelineMap();
@@ -90,24 +89,103 @@ public class HopServerSingleton {
}
}
+ /** How often the server looks for stale objects to purge, in milliseconds.
*/
+ private static final long PURGE_INTERVAL_MS = 20000L;
+
+ /** How long a stale object is kept when neither the configuration nor the
variable says so. */
+ private static final int DEFAULT_OBJECT_TIMEOUT_MINUTES = 24 * 60;
+
+ /**
+ * Hand the log line limits of the server over to the log store. Without
this the max_log_lines
+ * and max_log_timeout_minutes of the server configuration are read and
reported, but never
+ * applied, and the log store keeps running with its own defaults. See issue
#2796.
+ *
+ * <p>A limit of zero or lower keeps the default of the log store, the way
it has always behaved
+ * for a server that does not configure one.
+ */
+ static void configureLogStore(final HopServerConfig config) {
+ // Both limits are resolved separately on purpose. Handing the log store
the two configured
+ // values as they are would make it take both from the configuration as
soon as one of them is
+ // set, so configuring only a line limit would leave the log lines without
a timeout.
+ //
+ HopLogStore.init(
+ determineMaxLogLines(config),
+ determineMaxLogTimeoutMinutes(config),
+ EnvUtil.getSystemProperty(Const.HOP_REDIRECT_STDOUT,
"N").equalsIgnoreCase("Y"),
+ EnvUtil.getSystemProperty(Const.HOP_REDIRECT_STDERR,
"N").equalsIgnoreCase("Y"));
+ }
+
+ /**
+ * The number of log lines a server actually keeps: the configuration file
takes precedence over
+ * the variable, which in turn takes precedence over the default.
+ *
+ * @param config the configuration of the server
+ * @return the number of lines, 0 or lower means that every line is kept
+ */
+ public static int determineMaxLogLines(final HopServerConfig config) {
+ if (config.getMaxLogLines() > 0) {
+ return config.getMaxLogLines();
+ }
+ return Const.toInt(
+ EnvUtil.getSystemProperty(Const.HOP_MAX_LOG_SIZE_IN_LINES),
Const.MAX_NR_LOG_LINES);
+ }
+
+ /**
+ * The age a server actually lets its log lines reach: the configuration
file takes precedence
+ * over the variable, which in turn takes precedence over the default.
+ *
+ * @param config the configuration of the server
+ * @return the timeout in minutes, 0 or lower means that log lines are never
cleaned up
+ */
+ public static int determineMaxLogTimeoutMinutes(final HopServerConfig
config) {
+ if (config.getMaxLogTimeoutMinutes() > 0) {
+ return config.getMaxLogTimeoutMinutes();
+ }
+ return Const.toInt(
+ EnvUtil.getSystemProperty(Const.HOP_MAX_LOG_TIMEOUT_IN_MINUTES),
+ Const.MAX_LOG_LINE_TIMEOUT_MINUTES);
+ }
+
public static void installPurgeTimer(
final HopServerConfig config,
final ILogChannel log,
final PipelineMap pipelineMap,
final WorkflowMap workflowMap) {
+ installPurgeTimer(config, log, pipelineMap, workflowMap,
PURGE_INTERVAL_MS);
+ }
- final int objectTimeout;
- String systemTimeout =
EnvUtil.getSystemProperty(Const.HOP_SERVER_OBJECT_TIMEOUT_MINUTES, null);
-
- // The value specified in XML takes precedence over the environment
variable!
- //
+ /**
+ * The timeout a server actually purges stale objects with: the
configuration file takes
+ * precedence over the variable, which in turn takes precedence over the
default of one day. A
+ * server that configures nothing still purges, so this never resolves to
zero unless it was asked
+ * for.
+ *
+ * @param config the configuration of the server
+ * @return the timeout in minutes, 0 or lower means that objects are never
purged
+ */
+ public static int determineObjectTimeoutMinutes(final HopServerConfig
config) {
if (config.getObjectTimeoutMinutes() > 0) {
- objectTimeout = config.getObjectTimeoutMinutes();
- } else if (!Utils.isEmpty(systemTimeout)) {
- objectTimeout = Const.toInt(systemTimeout, 1440);
- } else {
- objectTimeout = 24 * 60; // 1440 : default is a one day time-out
+ return config.getObjectTimeoutMinutes();
+ }
+ String systemTimeout =
EnvUtil.getSystemProperty(Const.HOP_SERVER_OBJECT_TIMEOUT_MINUTES, null);
+ if (!Utils.isEmpty(systemTimeout)) {
+ return Const.toInt(systemTimeout, DEFAULT_OBJECT_TIMEOUT_MINUTES);
}
+ return DEFAULT_OBJECT_TIMEOUT_MINUTES;
+ }
+
+ /**
+ * @param purgeIntervalMs how often to look for stale objects, so that a
test does not have to
+ * wait for the interval the server itself uses
+ */
+ static void installPurgeTimer(
+ final HopServerConfig config,
+ final ILogChannel log,
+ final PipelineMap pipelineMap,
+ final WorkflowMap workflowMap,
+ final long purgeIntervalMs) {
+
+ final int objectTimeout = determineObjectTimeoutMinutes(config);
// If we need to time out finished or idle objects, we should create a
timer
// in the background to clean
@@ -203,7 +281,6 @@ public class HopServerSingleton {
+ " from "
+ workflow.getExecutionStartDate());
}
- ExecutorUtil.cleanup(timer, 1);
}
}
@@ -214,9 +291,9 @@ public class HopServerSingleton {
}
};
- // Search for stale objects every 20 seconds:
+ // Search for stale objects at every interval:
//
- timer.schedule(timerTask, 20000, 20000);
+ timer.schedule(timerTask, purgeIntervalMs, purgeIntervalMs);
}
}
diff --git
a/engine/src/main/java/org/apache/hop/www/jaxrs/HopServerResource.java
b/engine/src/main/java/org/apache/hop/www/jaxrs/HopServerResource.java
index 647c81668e..903dbf10a0 100644
--- a/engine/src/main/java/org/apache/hop/www/jaxrs/HopServerResource.java
+++ b/engine/src/main/java/org/apache/hop/www/jaxrs/HopServerResource.java
@@ -23,6 +23,7 @@ import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.List;
+import org.apache.hop.core.logging.HopLogStore;
import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.engine.IPipelineEngine;
import org.apache.hop.workflow.WorkflowMeta;
@@ -88,9 +89,11 @@ public class HopServerResource {
HopServerConfig serverConfig =
HopServerSingleton.getInstance().getPipelineMap().getHopServerConfig();
List<NVPair> list = new ArrayList<>();
- list.add(new NVPair("maxLogLines", "" + serverConfig.getMaxLogLines()));
- list.add(new NVPair("maxLogLinesAge", "" +
serverConfig.getMaxLogTimeoutMinutes()));
- list.add(new NVPair("maxObjectsAge", "" +
serverConfig.getObjectTimeoutMinutes()));
+ list.add(new NVPair("maxLogLines", "" +
HopLogStore.getAppender().getMaxNrLines()));
+ list.add(new NVPair("maxLogLinesAge", "" +
HopLogStore.getMaxLogTimeoutMinutes()));
+ list.add(
+ new NVPair(
+ "maxObjectsAge", "" +
HopServerSingleton.determineObjectTimeoutMinutes(serverConfig)));
list.add(new NVPair("configFile", "" + serverConfig.getFilename()));
return list;
}
diff --git
a/engine/src/test/java/org/apache/hop/www/HopServerSingletonTest.java
b/engine/src/test/java/org/apache/hop/www/HopServerSingletonTest.java
index 36fd9a8f48..a9556ad7bc 100644
--- a/engine/src/test/java/org/apache/hop/www/HopServerSingletonTest.java
+++ b/engine/src/test/java/org/apache/hop/www/HopServerSingletonTest.java
@@ -17,15 +17,25 @@
package org.apache.hop.www;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.contains;
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 java.util.Date;
+import java.util.function.BooleanSupplier;
import org.apache.hop.core.Const;
+import org.apache.hop.core.logging.HopLogStore;
import org.apache.hop.core.logging.ILogChannel;
import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engine.IPipelineEngine;
+import org.apache.hop.workflow.WorkflowConfiguration;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.apache.hop.workflow.engine.IWorkflowEngine;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -55,4 +65,203 @@ class HopServerSingletonTest {
verify(log).logBasic(contains("Installing timer"));
}
+
+ /**
+ * The log line limits of the server configuration have to reach the log
store, or they are read
+ * and reported but never applied. See issue #2796.
+ */
+ @Test
+ void logStoreUsesTheConfiguredLogLimits() {
+ HopServerConfig config = new HopServerConfig();
+ config.setMaxLogLines(123);
+ config.setMaxLogTimeoutMinutes(7);
+
+ HopServerSingleton.configureLogStore(config);
+
+ assertEquals(123, HopLogStore.getAppender().getMaxNrLines());
+ assertEquals(7, HopLogStore.getMaxLogTimeoutMinutes());
+ }
+
+ /**
+ * The two log limits are configured independently: setting one of them may
not silently drop the
+ * other back to "no limit". See issue #2796.
+ */
+ @Test
+ void configuringOnlyTheLineLimitKeepsTheLogTimeout() {
+ HopLogStore.init();
+ int defaultMaxLogTimeoutMinutes = HopLogStore.getMaxLogTimeoutMinutes();
+
+ HopServerConfig config = new HopServerConfig();
+ config.setMaxLogLines(1000);
+
+ HopServerSingleton.configureLogStore(config);
+
+ assertEquals(1000, HopLogStore.getAppender().getMaxNrLines());
+ assertEquals(
+ defaultMaxLogTimeoutMinutes,
+ HopLogStore.getMaxLogTimeoutMinutes(),
+ "Log lines should still time out when only a line limit is
configured");
+ }
+
+ /** The variable is used for the log limits when the configuration file does
not say otherwise. */
+ @Test
+ void logLimitsUseTheVariablesWithoutConfiguration() {
+ System.setProperty(Const.HOP_MAX_LOG_SIZE_IN_LINES, "321");
+ System.setProperty(Const.HOP_MAX_LOG_TIMEOUT_IN_MINUTES, "77");
+ try {
+ HopServerConfig config = new HopServerConfig();
+
+ assertEquals(321, HopServerSingleton.determineMaxLogLines(config));
+ assertEquals(77,
HopServerSingleton.determineMaxLogTimeoutMinutes(config));
+
+ // ... and the configuration file wins over the variable.
+ config.setMaxLogLines(10);
+ config.setMaxLogTimeoutMinutes(20);
+ assertEquals(10, HopServerSingleton.determineMaxLogLines(config));
+ assertEquals(20,
HopServerSingleton.determineMaxLogTimeoutMinutes(config));
+ } finally {
+ System.clearProperty(Const.HOP_MAX_LOG_SIZE_IN_LINES);
+ System.clearProperty(Const.HOP_MAX_LOG_TIMEOUT_IN_MINUTES);
+ }
+ }
+
+ /**
+ * A server that configures no limits keeps the defaults of the log store,
the way it behaved
+ * before the configured limits were passed on.
+ */
+ @Test
+ void logStoreKeepsItsDefaultsWithoutConfiguredLogLimits() {
+ HopLogStore.init();
+ int defaultMaxLogLines = HopLogStore.getAppender().getMaxNrLines();
+ int defaultMaxLogTimeoutMinutes = HopLogStore.getMaxLogTimeoutMinutes();
+
+ HopServerSingleton.configureLogStore(new HopServerConfig());
+
+ assertEquals(defaultMaxLogLines,
HopLogStore.getAppender().getMaxNrLines());
+ assertEquals(defaultMaxLogTimeoutMinutes,
HopLogStore.getMaxLogTimeoutMinutes());
+ }
+
+ /**
+ * A server that configures no object timeout still purges after a day. The
status page reports
+ * this value, so it may not claim that stale objects are never cleaned up.
See issue #2796.
+ */
+ @Test
+ void objectTimeoutFallsBackToADayWithoutConfigurationOrVariable() {
+ System.clearProperty(Const.HOP_SERVER_OBJECT_TIMEOUT_MINUTES);
+
+ assertEquals(1440, HopServerSingleton.determineObjectTimeoutMinutes(new
HopServerConfig()));
+ }
+
+ /** The variable is used when the configuration file does not say otherwise.
*/
+ @Test
+ void objectTimeoutUsesTheVariableWithoutConfiguration() {
+ System.setProperty(Const.HOP_SERVER_OBJECT_TIMEOUT_MINUTES, "60");
+ try {
+ assertEquals(60, HopServerSingleton.determineObjectTimeoutMinutes(new
HopServerConfig()));
+ } finally {
+ System.clearProperty(Const.HOP_SERVER_OBJECT_TIMEOUT_MINUTES);
+ }
+ }
+
+ /** The configuration file takes precedence over the variable. */
+ @Test
+ void objectTimeoutFromConfigurationBeatsTheVariable() {
+ System.setProperty(Const.HOP_SERVER_OBJECT_TIMEOUT_MINUTES, "60");
+ try {
+ HopServerConfig config = new HopServerConfig();
+ config.setObjectTimeoutMinutes(15);
+
+ assertEquals(15,
HopServerSingleton.determineObjectTimeoutMinutes(config));
+ } finally {
+ System.clearProperty(Const.HOP_SERVER_OBJECT_TIMEOUT_MINUTES);
+ }
+ }
+
+ /** A pipeline that ran longer ago than the object timeout is purged. */
+ @Test
+ void purgeTimerRemovesStalePipeline() throws Exception {
+ PipelineMap pipelineMap = new PipelineMap();
+ pipelineMap.addPipeline("stale", "p1", finishedPipeline(minutesAgo(5)),
null);
+
+ installPurgeTimer(pipelineMap, new WorkflowMap());
+
+ waitUntil(() -> pipelineMap.getPipelineObjects().isEmpty());
+ assertTrue(pipelineMap.getPipelineObjects().isEmpty(), "The stale pipeline
should be purged");
+ }
+
+ /** A pipeline that ran more recently than the object timeout is kept. */
+ @Test
+ void purgeTimerKeepsRecentPipeline() throws Exception {
+ PipelineMap pipelineMap = new PipelineMap();
+ pipelineMap.addPipeline("recent", "p1", finishedPipeline(new Date()),
null);
+
+ installPurgeTimer(pipelineMap, new WorkflowMap());
+ Thread.sleep(500);
+
+ assertEquals(1, pipelineMap.getPipelineObjects().size(), "The recent
pipeline should be kept");
+ }
+
+ /**
+ * The server has to keep purging for as long as it runs. A finished
workflow that is not stale
+ * enough to be purged used to stop the timer, after which nothing was ever
cleaned up again and
+ * the memory of the server kept growing. See issue #2796.
+ */
+ @Test
+ void purgeTimerKeepsPurgingAfterSeeingAFinishedWorkflow() throws Exception {
+ PipelineMap pipelineMap = new PipelineMap();
+ WorkflowMap workflowMap = new WorkflowMap();
+
+ // A workflow that finished just now, so it is not stale enough to be
purged.
+ workflowMap.addWorkflow(
+ "recent", "w1", finishedWorkflow(new Date()),
mock(WorkflowConfiguration.class));
+
+ installPurgeTimer(pipelineMap, workflowMap);
+
+ // Give the timer the time to run over the workflow at least once.
+ Thread.sleep(500);
+
+ // A pipeline that is stale enough to be purged shows up afterwards.
+ pipelineMap.addPipeline("stale", "p1", finishedPipeline(minutesAgo(5)),
null);
+
+ waitUntil(() -> pipelineMap.getPipelineObjects().isEmpty());
+ assertTrue(
+ pipelineMap.getPipelineObjects().isEmpty(),
+ "The stale pipeline should still be purged after a finished workflow
was seen");
+ }
+
+ private void installPurgeTimer(PipelineMap pipelineMap, WorkflowMap
workflowMap) {
+ HopServerConfig config = mock(HopServerConfig.class);
+ when(config.getObjectTimeoutMinutes()).thenReturn(1);
+ System.clearProperty(Const.HOP_SERVER_OBJECT_TIMEOUT_MINUTES);
+
+ HopServerSingleton.installPurgeTimer(
+ config, mock(ILogChannel.class), pipelineMap, workflowMap, 50L);
+ }
+
+ private static Date minutesAgo(int minutes) {
+ return new Date(System.currentTimeMillis() - minutes * 60_000L);
+ }
+
+ private IPipelineEngine<PipelineMeta> finishedPipeline(Date
executionStartDate) {
+ IPipelineEngine<PipelineMeta> pipeline = mock(IPipelineEngine.class);
+ when(pipeline.isFinished()).thenReturn(true);
+ when(pipeline.getExecutionStartDate()).thenReturn(executionStartDate);
+ when(pipeline.getLogChannelId()).thenReturn("pipeline-log-channel");
+ return pipeline;
+ }
+
+ private IWorkflowEngine<WorkflowMeta> finishedWorkflow(Date
executionStartDate) {
+ IWorkflowEngine<WorkflowMeta> workflow = mock(IWorkflowEngine.class);
+ when(workflow.isFinished()).thenReturn(true);
+ when(workflow.getExecutionStartDate()).thenReturn(executionStartDate);
+ when(workflow.getLogChannelId()).thenReturn("workflow-log-channel");
+ return workflow;
+ }
+
+ private void waitUntil(BooleanSupplier condition) throws
InterruptedException {
+ long deadline = System.currentTimeMillis() + 5000L;
+ while (System.currentTimeMillis() < deadline && !condition.getAsBoolean())
{
+ Thread.sleep(50);
+ }
+ }
}