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 267be5906f Add some extra logging and timeout options, fixes #7452 
(#7454)
267be5906f is described below

commit 267be5906fcf4630cc3f36067892fd26b925a26e
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Wed Jul 8 14:30:25 2026 +0200

    Add some extra logging and timeout options, fixes #7452 (#7454)
---
 core/src/main/java/org/apache/hop/core/Const.java  |  30 ++++
 .../org/apache/hop/core/database/Database.java     |  65 ++++++++
 .../hop/core/database/DatabaseTimeoutTest.java     | 175 +++++++++++++++++++++
 .../modules/ROOT/pages/database/databases.adoc     |  20 +++
 .../modules/ROOT/pages/variables.adoc              |   8 +
 5 files changed, 298 insertions(+)

diff --git a/core/src/main/java/org/apache/hop/core/Const.java 
b/core/src/main/java/org/apache/hop/core/Const.java
index 9ab20b8003..a1d5db35df 100644
--- a/core/src/main/java/org/apache/hop/core/Const.java
+++ b/core/src/main/java/org/apache/hop/core/Const.java
@@ -176,6 +176,36 @@ public class Const {
       description = "A comma separated list pointing to folders with JDBC 
drivers to add.")
   public static final String HOP_SHARED_JDBC_FOLDERS = 
"HOP_SHARED_JDBC_FOLDERS";
 
+  /**
+   * Default login/connection timeout (in seconds) applied when opening a JDBC 
database connection.
+   * Maps to {@link java.sql.DriverManager#setLoginTimeout(int)} and bounds 
how long Hop waits while
+   * establishing a connection. Defaults to 30 seconds; set to 0 to impose no 
timeout (a stalled
+   * connect can then block indefinitely). Note the setting is advisory: 
individual JDBC drivers may
+   * ignore it. It can still be overridden per connection via driver-specific 
connection options.
+   */
+  @Variable(
+      value = "30",
+      description =
+          "Default login/connection timeout in seconds when opening a JDBC 
database connection (DriverManager.setLoginTimeout). Defaults to 30; set to 0 
to impose no timeout. Advisory: some JDBC drivers ignore it.")
+  public static final String HOP_DATABASE_CONNECTION_TIMEOUT = 
"HOP_DATABASE_CONNECTION_TIMEOUT";
+
+  /**
+   * Default network/socket timeout (in seconds) applied to a JDBC database 
connection after it has
+   * been opened. Maps to {@link
+   * java.sql.Connection#setNetworkTimeout(java.util.concurrent.Executor, 
int)} and bounds how long
+   * a driver waits for a database request to complete. A value of 0 (default) 
means no timeout is
+   * imposed. WARNING: when set, this applies to every request on the 
connection, so any single
+   * query, read or bulk operation that runs longer than the timeout is 
aborted and the connection
+   * is closed - use a value large enough for your longest-running SQL. 
Drivers that do not support
+   * the operation are ignored (logged at detailed level). This guards reads 
on an established
+   * connection; the initial handshake read still relies on the driver's own 
socket-timeout option.
+   */
+  @Variable(
+      value = "0",
+      description =
+          "Default network/socket timeout in seconds applied to a JDBC 
connection after it is opened (Connection.setNetworkTimeout). 0 (default) means 
no timeout is imposed. WARNING: when set, any single query/read running longer 
than this is aborted - use a value large enough for your longest-running SQL. 
Unsupported drivers are ignored.")
+  public static final String HOP_DATABASE_SOCKET_TIMEOUT = 
"HOP_DATABASE_SOCKET_TIMEOUT";
+
   /**
    * Per-run override for the engine-compatibility gate in {@code 
Pipeline.prepareExecution} /
    * {@code Workflow.startExecution}. Set to 'Y' to run a pipeline or workflow 
that contains
diff --git a/core/src/main/java/org/apache/hop/core/database/Database.java 
b/core/src/main/java/org/apache/hop/core/database/Database.java
index 73001b73fc..0eaf15c009 100644
--- a/core/src/main/java/org/apache/hop/core/database/Database.java
+++ b/core/src/main/java/org/apache/hop/core/database/Database.java
@@ -430,6 +430,10 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
         PluginRegistry.getInstance()
             .getPlugin(DatabasePluginType.class, databaseMeta.getIDatabase());
 
+    if (log.isDetailed()) {
+      log.logDetailed("Loading JDBC driver class '" + classname + "'");
+    }
+
     try {
       synchronized (DriverManager.class) {
         ClassLoader classLoader = 
PluginRegistry.getInstance().getClassLoader(plugin);
@@ -495,6 +499,14 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
 
       Properties properties = databaseMeta.getConnectionProperties(this);
 
+      int connectionTimeout = applyLoginTimeout();
+
+      if (log.isDetailed()) {
+        log.logDetailed(
+            "Opening JDBC connection..."
+                + (connectionTimeout > 0 ? " (login timeout " + 
connectionTimeout + "s)" : ""));
+      }
+
       if (databaseMeta.supportsOptionsInURL()) {
         if (!Utils.isEmpty(username) || !Utils.isEmpty(password)) {
           // Allow for empty username with given password, in this case 
username must be given with
@@ -525,12 +537,65 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
 
         connection = DriverManager.getConnection(url, properties);
       }
+
+      if (log.isDetailed()) {
+        log.logDetailed("JDBC connection opened successfully");
+      }
+
+      applyNetworkTimeout();
     } catch (Exception e) {
       throw new HopDatabaseException(
           "Error connecting to database: (using class " + classname + ")", e);
     }
   }
 
+  /**
+   * Apply the default login/connection timeout ({@link 
Const#HOP_DATABASE_CONNECTION_TIMEOUT}, in
+   * seconds) via {@link DriverManager#setLoginTimeout(int)}. This is a 
JVM-wide setting that bounds
+   * how long the driver waits while establishing a connection; the default is 
30 seconds and a
+   * value of 0 leaves the current default in place. The setting is advisory - 
some JDBC drivers
+   * ignore it in favor of their own connection options.
+   *
+   * @return the resolved connection timeout in seconds
+   */
+  int applyLoginTimeout() {
+    int connectionTimeout = 
Const.toInt(getVariable(Const.HOP_DATABASE_CONNECTION_TIMEOUT), 30);
+    if (connectionTimeout > 0) {
+      DriverManager.setLoginTimeout(connectionTimeout);
+    }
+    return connectionTimeout;
+  }
+
+  /**
+   * Apply the default network/socket timeout ({@link 
Const#HOP_DATABASE_SOCKET_TIMEOUT}, in
+   * seconds) to the freshly opened connection via {@link
+   * Connection#setNetworkTimeout(java.util.concurrent.Executor, int)}. This 
bounds reads on an
+   * already-established connection. A value of 0 (default) leaves it 
unbounded. Drivers that do not
+   * support the operation are ignored - an optional timeout must never fail 
the connection.
+   */
+  void applyNetworkTimeout() {
+    int socketTimeout = 
Const.toInt(getVariable(Const.HOP_DATABASE_SOCKET_TIMEOUT), 0);
+    if (socketTimeout <= 0 || connection == null) {
+      return;
+    }
+    try {
+      // Execute the driver's timeout callback inline; no extra thread pool to 
leak.
+      connection.setNetworkTimeout(command -> command.run(), socketTimeout * 
1000);
+      if (log.isDetailed()) {
+        log.logDetailed(
+            "Applied network/socket timeout of " + socketTimeout + "s to the 
connection");
+      }
+    } catch (Exception e) {
+      if (log.isDetailed()) {
+        log.logDetailed(
+            "Unable to apply network/socket timeout ("
+                + socketTimeout
+                + "s); driver may not support it: "
+                + e.getMessage());
+      }
+    }
+  }
+
   /** close() and disconnect() are the same. */
   @Override
   public synchronized void close() {
diff --git 
a/core/src/test/java/org/apache/hop/core/database/DatabaseTimeoutTest.java 
b/core/src/test/java/org/apache/hop/core/database/DatabaseTimeoutTest.java
new file mode 100644
index 0000000000..d7a6666a1c
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/database/DatabaseTimeoutTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.core.database;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.concurrent.Executor;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.junit.rules.RestoreHopEnvironmentExtension;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Verifies the login/connection ({@link 
Const#HOP_DATABASE_CONNECTION_TIMEOUT}) and network/socket
+ * ({@link Const#HOP_DATABASE_SOCKET_TIMEOUT}) timeout defaults are resolved 
and applied correctly.
+ */
+@ExtendWith(RestoreHopEnvironmentExtension.class)
+class DatabaseTimeoutTest {
+
+  private int originalLoginTimeout;
+
+  @BeforeAll
+  static void setUpClass() throws Exception {
+    HopClientEnvironment.init();
+  }
+
+  @BeforeEach
+  void rememberLoginTimeout() {
+    // DriverManager.setLoginTimeout is a JVM-wide setting; snapshot it so 
tests don't leak state.
+    originalLoginTimeout = DriverManager.getLoginTimeout();
+  }
+
+  @AfterEach
+  void restoreLoginTimeout() {
+    DriverManager.setLoginTimeout(originalLoginTimeout);
+  }
+
+  private Database databaseWith(String variableName, String value) {
+    Variables variables = new Variables();
+    if (value != null) {
+      variables.setVariable(variableName, value);
+    }
+    DatabaseMeta meta = new DatabaseMeta("test", "None", "", "", "", "", "", 
"");
+    return new Database(null, variables, meta);
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Login / connection timeout -> DriverManager.setLoginTimeout(seconds)
+  // 
---------------------------------------------------------------------------
+
+  @Test
+  void loginTimeout_defaultsTo30SecondsWhenUnset() {
+    DriverManager.setLoginTimeout(0);
+    Database db = databaseWith(Const.HOP_DATABASE_CONNECTION_TIMEOUT, null);
+
+    int resolved = db.applyLoginTimeout();
+
+    assertEquals(30, resolved);
+    assertEquals(30, DriverManager.getLoginTimeout());
+  }
+
+  @Test
+  void loginTimeout_appliesConfiguredValue() {
+    DriverManager.setLoginTimeout(0);
+    Database db = databaseWith(Const.HOP_DATABASE_CONNECTION_TIMEOUT, "45");
+
+    int resolved = db.applyLoginTimeout();
+
+    assertEquals(45, resolved);
+    assertEquals(45, DriverManager.getLoginTimeout());
+  }
+
+  @Test
+  void loginTimeout_zeroLeavesDriverManagerUntouched() {
+    DriverManager.setLoginTimeout(7);
+    Database db = databaseWith(Const.HOP_DATABASE_CONNECTION_TIMEOUT, "0");
+
+    int resolved = db.applyLoginTimeout();
+
+    // 0 means "impose no timeout" - the existing JVM default must be left in 
place.
+    assertEquals(0, resolved);
+    assertEquals(7, DriverManager.getLoginTimeout());
+  }
+
+  // 
---------------------------------------------------------------------------
+  // Network / socket timeout -> Connection.setNetworkTimeout(executor, millis)
+  // 
---------------------------------------------------------------------------
+
+  @Test
+  void socketTimeout_isDisabledByDefault() throws Exception {
+    Connection connection = mock(Connection.class);
+    Database db = databaseWith(Const.HOP_DATABASE_SOCKET_TIMEOUT, null);
+    db.setConnection(connection);
+
+    db.applyNetworkTimeout();
+
+    verify(connection, never()).setNetworkTimeout(any(), anyInt());
+  }
+
+  @Test
+  void socketTimeout_zeroIsDisabled() throws Exception {
+    Connection connection = mock(Connection.class);
+    Database db = databaseWith(Const.HOP_DATABASE_SOCKET_TIMEOUT, "0");
+    db.setConnection(connection);
+
+    db.applyNetworkTimeout();
+
+    verify(connection, never()).setNetworkTimeout(any(), anyInt());
+  }
+
+  @Test
+  void socketTimeout_appliesConfiguredValueConvertedToMillis() throws 
Exception {
+    Connection connection = mock(Connection.class);
+    Database db = databaseWith(Const.HOP_DATABASE_SOCKET_TIMEOUT, "30");
+    db.setConnection(connection);
+
+    db.applyNetworkTimeout();
+
+    // seconds -> milliseconds
+    verify(connection, times(1)).setNetworkTimeout(any(Executor.class), 
eq(30000));
+  }
+
+  @Test
+  void socketTimeout_nullConnectionIsNoop() {
+    Database db = databaseWith(Const.HOP_DATABASE_SOCKET_TIMEOUT, "30");
+    // No setConnection() call - the connection stays null.
+
+    assertDoesNotThrow(db::applyNetworkTimeout);
+  }
+
+  @Test
+  void socketTimeout_unsupportedDriverIsIgnored() throws Exception {
+    Connection connection = mock(Connection.class);
+    doThrow(new SQLFeatureNotSupportedException("not supported"))
+        .when(connection)
+        .setNetworkTimeout(any(), anyInt());
+    Database db = databaseWith(Const.HOP_DATABASE_SOCKET_TIMEOUT, "30");
+    db.setConnection(connection);
+
+    // An optional timeout must never fail the connection.
+    assertDoesNotThrow(db::applyNetworkTimeout);
+  }
+}
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/database/databases.adoc
index eae9f201ee..4d3f0dfc71 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases.adoc
@@ -112,6 +112,26 @@ jdbc:sqlserver://localhost:1433;" +
 |trustServerCertificate|true
 |===
 
+== Connection and socket timeouts
+
+If a database host becomes unreachable at the wrong moment (for example a 
half-open TCP connection that is never answered), a transform can block 
indefinitely while it waits for the driver. This makes a pipeline appear to 
"hang" during initialization, even though the databases themselves are healthy.
+
+Two variables set a default timeout for *every* JDBC connection Hop opens:
+
+[%header, width="90%", cols="2,1,4"]
+|===
+|Variable|Default|Description
+|`HOP_DATABASE_CONNECTION_TIMEOUT`|`30`|Login/connection timeout in 
**seconds** applied while establishing a connection (maps to 
`DriverManager.setLoginTimeout`). Set to `0` to leave the connect unbounded. 
The setting is advisory - some drivers ignore it in favor of their own 
connection options.
+|`HOP_DATABASE_SOCKET_TIMEOUT`|`0`|Network/socket timeout in **seconds** 
applied to a connection once it is open (maps to 
`Connection.setNetworkTimeout`). `0` (default) leaves reads unbounded. 
Unsupported drivers are ignored.
+|===
+
+[WARNING]
+====
+`HOP_DATABASE_SOCKET_TIMEOUT` is disabled by default (`0`) on purpose: when 
set, it applies to *every* request on the connection, not just the initial 
read. Any single query, read or bulk operation that runs longer than the 
timeout is aborted and its connection is closed. If your pipelines contain 
long-running SQL (heavy aggregations, large bulk loads, slow reports), only 
enable it with a value large enough to cover them.
+====
+
+These are global defaults applied to all connections. For finer control - or 
to bound the *initial* handshake read, which happens inside the driver before 
Hop can apply the socket timeout - set the driver's own timeout options in the 
connection's `Options` tab (see the Options table above). For example:
+
 == Hard-coded JDBC to Hop data type mappings
 
 Below are the mappings applied by Hop when converting from database column 
types to Hop data types when `HOP_DB_DDL_COMPATIBLE` is `false` (the default).
diff --git a/docs/hop-user-manual/modules/ROOT/pages/variables.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/variables.adoc
index fd767e1096..763424ee72 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/variables.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/variables.adoc
@@ -250,6 +250,14 @@ Otherwise by default NULL is ignored by the MIN aggregate 
and MIN is set to the
 See also the variable HOP_AGGREGATION_ALL_NULLS_ARE_ZERO.
 |HOP_ALLOW_EMPTY_FIELD_NAMES_AND_TYPES|N|Set this variable to Y to allow your 
pipeline to pass 'null' fields and/or empty types.
 |HOP_BATCHING_ROWSET|N|Set this variable to 'Y' if you want to test a more 
efficient batching row set.
+|HOP_DATABASE_CONNECTION_TIMEOUT|30|Default login/connection timeout in 
**seconds** applied when opening a JDBC database connection (maps to 
`DriverManager.setLoginTimeout`).
+Defaults to `30`; set to `0` to impose no timeout (a stalled connect can then 
block indefinitely).
+The setting is advisory: some JDBC drivers ignore it in favor of their own 
connection options.
+|HOP_DATABASE_SOCKET_TIMEOUT|0|Default network/socket timeout in **seconds** 
applied to a JDBC database connection after it is opened (maps to 
`Connection.setNetworkTimeout`).
+`0` (default) means no timeout is imposed.
+WARNING: when set, this applies to *every* request on the connection, so any 
single query, read or bulk operation that runs longer than the timeout is 
aborted and the connection is closed. Use a value large enough for your 
longest-running SQL.
+This bounds reads on an already-established connection; the initial handshake 
read still relies on the driver's own socket-timeout connection option.
+Drivers that do not support the operation are ignored.
 |HOP_DEFAULT_BIGNUMBER_FORMAT||The name of the variable containing an 
alternative default bignumber format
 |HOP_DEFAULT_BUFFER_POLLING_WAITTIME|20|This is the default polling frequency 
for the transforms input buffer (in ms)
 |HOP_DEFAULT_DATE_FORMAT||The name of the variable containing an alternative 
default date format

Reply via email to