This is an automated email from the ASF dual-hosted git repository.

deniskuzZ pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git


The following commit(s) were added to refs/heads/master by this push:
     new aaa04ef6cdf HIVE-28265: Fix JDBC timeout message for 
hive.query.timeout.seconds
aaa04ef6cdf is described below

commit aaa04ef6cdf44e1cdd672e3b016e9fcbe9edf997
Author: Asish Kumar Patra <[email protected]>
AuthorDate: Fri Jul 31 21:32:27 2026 +0530

    HIVE-28265: Fix JDBC timeout message for hive.query.timeout.seconds
---
 .../java/org/apache/hive/jdbc/TestJdbcDriver2.java | 174 ++++++++++++++++++---
 .../java/org/apache/hive/jdbc/HiveConnection.java  |  35 +++--
 .../java/org/apache/hive/jdbc/HiveStatement.java   |  97 +++++++-----
 .../hive/service/cli/operation/Operation.java      |   5 +
 .../hive/service/cli/operation/SQLOperation.java   |  11 ++
 5 files changed, 254 insertions(+), 68 deletions(-)

diff --git 
a/itests/hive-unit/src/test/java/org/apache/hive/jdbc/TestJdbcDriver2.java 
b/itests/hive-unit/src/test/java/org/apache/hive/jdbc/TestJdbcDriver2.java
index 6947b0b4b88..ac5263cfd8d 100644
--- a/itests/hive-unit/src/test/java/org/apache/hive/jdbc/TestJdbcDriver2.java
+++ b/itests/hive-unit/src/test/java/org/apache/hive/jdbc/TestJdbcDriver2.java
@@ -42,6 +42,7 @@
 import 
org.apache.hive.service.cli.operation.ClassicTableTypeMapping.ClassicTableTypes;
 import org.apache.hive.service.cli.operation.HiveTableTypeMapping;
 import 
org.apache.hive.service.cli.operation.TableTypeMappingFactory.TableTypeMappings;
+import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Rule;
@@ -128,8 +129,45 @@ public class TestJdbcDriver2 {
   private static Connection con;
   private static final float floatCompareDelta = 0.0001f;
 
-  @Rule public ExpectedException thrown = ExpectedException.none();
-  @Rule public final TestName testName = new TestName();
+  /**
+   * Required prefix of {@link SQLTimeoutException#getMessage()} for a 1s 
limit. HS2 may append
+   * {@code ; Query ID: ...} after the base text from {@code HiveSQLException}.
+   */
+  private static final String QUERY_TIMED_OUT_AFTER_1_SECONDS = "Query timed 
out after 1 seconds";
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Rule
+  public final TestName testName = new TestName();
+
+  /**
+   * {@code SET hive.query.timeout.seconds} applies to the whole HS2 session. 
Tests such as
+   * {@link #testQueryTimeoutFromSetStatement()} must not leave a short limit 
on the shared
+   * {@link #con}, or unrelated tests will see {@link SQLTimeoutException}.
+   */
+  @After
+  public void resetHiveSessionQueryTimeout() throws SQLException {
+    if (con == null || con.isClosed()) {
+      return;
+    }
+    try (Statement st = con.createStatement()) {
+      try {
+        st.execute("set hive.query.timeout.seconds=0s");
+      } catch (SQLException e) {
+        LOG.warn("Could not reset hive.query.timeout.seconds after {}", 
testName.getMethodName(), e);
+      }
+    }
+  }
+
+  private static void assertTimeoutMessageShowsOneSecond(String context, 
SQLTimeoutException e) {
+    String msg = e.getMessage();
+    assertNotNull(context + ": message should not be null", msg);
+    assertTrue(
+        context + ": should start with " + QUERY_TIMED_OUT_AFTER_1_SECONDS
+            + " (HS2 may append ; Query ID: ...); actual=" + msg,
+        msg.startsWith(QUERY_TIMED_OUT_AFTER_1_SECONDS));
+  }
 
   private static Connection getConnection(String prefix, String postfix) 
throws SQLException {
     Connection con1;
@@ -336,11 +374,39 @@ private void checkBadUrl(String url) throws SQLException {
    * @throws SQLException
    */
   public void testURLWithFetchSize() throws SQLException {
-    Connection con = getConnection(testDbName + ";fetchSize=1234", "");
-    Statement stmt = con.createStatement();
-    assertEquals(stmt.getFetchSize(), 1234);
-    stmt.close();
-    con.close();
+    try (Connection connectionWithFetchSize = getConnection(testDbName + 
";fetchSize=1234", "");
+         Statement stmt = connectionWithFetchSize.createStatement()) {
+      assertEquals(stmt.getFetchSize(), 1234);
+    }
+  }
+
+  /**
+   * Same idea as {@link #testURLWithFetchSize}: drive session behavior from 
the JDBC URL instead of
+   * only {@link Statement#setQueryTimeout(int)} or an explicit {@code SET}. 
The timeout is supplied
+   * in the URL query ({@code ?hive_conf_list}) per the driver format
+   * {@code jdbc:hive2://.../db;sess?hive_conf#hive_var}.
+   * <p>
+   * {@link SQLTimeoutException#getMessage()} must reflect the configured 
limit (1s),
+   * not {@code after 0 seconds}.
+   */
+  @Test
+  public void testURLWithHiveQueryTimeoutSeconds() throws Exception {
+    String udfName = SleepMsUDF.class.getName();
+    // Postfix appends to the query string after test overlay / lock manager 
settings.
+    try (Connection connectionWithUrlQueryTimeout = getConnection(testDbName, 
"hive.query.timeout.seconds=1");
+         Statement stmt = connectionWithUrlQueryTimeout.createStatement()) {
+      stmt.execute("create temporary function sleepMsUDF as '" + udfName + 
"'");
+      try {
+        stmt.executeQuery("select sleepMsUDF(t1.under_col, 5) as u0, 
t1.under_col as u1, "
+            + "t2.under_col as u2 from " + tableName + " t1 join " + tableName
+            + " t2 on t1.under_col = t2.under_col");
+        fail("Expecting SQLTimeoutException");
+      } catch (SQLTimeoutException e) {
+        assertTimeoutMessageShowsOneSecond("JDBC URL 
hive.query.timeout.seconds=1 (query string)", e);
+      } catch (SQLException e) {
+        fail("Expecting SQLTimeoutException, but got SQLException: " + e);
+      }
+    }
   }
 
   @Test
@@ -349,13 +415,13 @@ public void testURLWithFetchSize() throws SQLException {
    * @throws SQLException
    */
   public void testCreateTableAsExternal() throws SQLException {
-    Connection con = getConnection(testDbName + 
";hiveCreateAsExternalLegacy=true", "");
-    Statement stmt = con.createStatement();
-    ResultSet res = stmt.executeQuery("set hive.create.as.external.legacy");
-    assertTrue("ResultSet is empty", res.next());
-    assertEquals("hive.create.as.external.legacy=true", res.getObject(1));
-    stmt.close();
-    con.close();
+    try (Connection connection =
+             getConnection(testDbName + ";hiveCreateAsExternalLegacy=true", 
"");
+         Statement stmt = connection.createStatement();
+         ResultSet res = stmt.executeQuery("set 
hive.create.as.external.legacy")) {
+      assertTrue("ResultSet is empty", res.next());
+      assertEquals("hive.create.as.external.legacy=true", res.getObject(1));
+    }
   }
 
   @Test
@@ -2653,7 +2719,6 @@ public void testQueryTimeout() throws Exception {
     // Test a query where timeout kicks in
     // Set query timeout to 1 second
     stmt.setQueryTimeout(1);
-    System.err.println("Executing query: ");
     try {
       // The test table has 500 rows, so total query time should be ~ 2500ms
       stmt.executeQuery("select sleepMsUDF(t1.under_col, 5) as u0, 
t1.under_col as u1, "
@@ -2661,11 +2726,10 @@ public void testQueryTimeout() throws Exception {
           + " t2 on t1.under_col = t2.under_col");
       fail("Expecting SQLTimeoutException");
     } catch (SQLTimeoutException e) {
-      assertNotNull(e);
-      System.err.println(e.toString());
+      assertTimeoutMessageShowsOneSecond(
+          "JDBC query timeout (1s)", e);
     } catch (SQLException e) {
       fail("Expecting SQLTimeoutException, but got SQLException: " + e);
-      e.printStackTrace();
     }
 
     // Test a query where timeout does not kick in. Set it to 5s;
@@ -2675,7 +2739,79 @@ public void testQueryTimeout() throws Exception {
       stmt.executeQuery("show tables");
     } catch (SQLException e) {
       fail("Unexpected SQLException: " + e);
-      e.printStackTrace();
+    }
+    stmt.close();
+  }
+
+  /**
+   * When the session timeout is configured via a {@code SET 
hive.query.timeout.seconds=...}
+   * statement and no {@link Statement#setQueryTimeout(int)} is called, the
+   * {@link SQLTimeoutException#getMessage()} must still reflect the real 
limit.
+   * Message must begin with {@link #QUERY_TIMED_OUT_AFTER_1_SECONDS};
+   * HS2 may append {@code ; Query ID: ...}.
+   */
+  @Test
+  public void testQueryTimeoutFromSetStatement() throws Exception {
+    String udfName = SleepMsUDF.class.getName();
+    Statement stmt1 = con.createStatement();
+    stmt1.execute("create temporary function sleepMsUDF as '" + udfName + "'");
+    stmt1.close();
+
+    Statement stmt = con.createStatement();
+    stmt.execute("set hive.query.timeout.seconds=1s");
+
+    try {
+      stmt.executeQuery("select sleepMsUDF(t1.under_col, 5) as u0, 
t1.under_col as u1, "
+          + "t2.under_col as u2 from " + tableName + " t1 join " + tableName
+          + " t2 on t1.under_col = t2.under_col");
+      fail("Expecting SQLTimeoutException");
+    } catch (SQLTimeoutException e) {
+      assertTimeoutMessageShowsOneSecond(
+          "Session query timeout (1s)", e);
+    } catch (SQLException e) {
+      fail("Expecting SQLTimeoutException, but got SQLException: " + e);
+    }
+    stmt.close();
+  }
+
+  /**
+   * Variant of {@link #testQueryTimeoutFromSetStatement}: the {@code SET} is 
issued on a separate,
+   * already-closed statement; the timed-out query runs on a brand-new 
statement with no
+   * {@link Statement#setQueryTimeout(int)} call. The timeout lives on the HS2 
session, so it
+   * persists across client statement instances and the server-supplied {@link 
SQLTimeoutException}
+   * message must still reflect it correctly.
+   */
+  @Test
+  public void testQueryTimeoutMessagePersistedAcrossStatements() throws 
Exception {
+    String udfName = SleepMsUDF.class.getName();
+    Statement stmt1 = con.createStatement();
+    stmt1.execute("create temporary function sleepMsUDF as '" + udfName + "'");
+    stmt1.close();
+
+    // SET is issued on stmt2, which is then closed – timeout must survive on 
the connection
+    Statement stmt2 = con.createStatement();
+    stmt2.execute("set hive.query.timeout.seconds=1s");
+    stmt2.close();
+
+    // Brand-new statement, no setQueryTimeout() call – relies solely on the 
HS2 session timeout
+    Statement stmt = con.createStatement();
+    try {
+      stmt.executeQuery("select sleepMsUDF(t1.under_col, 5) as u0, 
t1.under_col as u1, "
+          + "t2.under_col as u2 from " + tableName + " t1 join " + tableName
+          + " t2 on t1.under_col = t2.under_col");
+      fail("Expecting SQLTimeoutException");
+    } catch (SQLTimeoutException e) {
+      assertTimeoutMessageShowsOneSecond("SET on closed stmt2, executeQuery on 
new stmt", e);
+    } catch (SQLException e) {
+      fail("Expecting SQLTimeoutException, but got SQLException: " + e);
+    }
+
+    // A fast query must still complete when the per-statement timeout 
overrides
+    stmt.setQueryTimeout(5);
+    try {
+      stmt.executeQuery("show tables");
+    } catch (SQLException e) {
+      fail("Unexpected SQLException: " + e);
     }
     stmt.close();
   }
diff --git a/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java 
b/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java
index 8f7c3ea8acd..7e75cd1b390 100644
--- a/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java
+++ b/jdbc/src/java/org/apache/hive/jdbc/HiveConnection.java
@@ -163,6 +163,7 @@
  */
 public class HiveConnection implements java.sql.Connection {
   private static final Logger LOG = 
LoggerFactory.getLogger(HiveConnection.class);
+
   private String jdbcUriString;
   private String host;
   private int port;
@@ -396,18 +397,7 @@ protected HiveConnection(String uri, Properties info,
         executeInitSql();
       }
     } else {
-      long retryInterval = 1000L;
-      try {
-        String strRetries = sessConfMap.get(JdbcConnectionParams.RETRIES);
-        if (StringUtils.isNotBlank(strRetries)) {
-          maxRetries = Integer.parseInt(strRetries);
-        }
-        String strRetryInterval = 
sessConfMap.get(JdbcConnectionParams.RETRY_INTERVAL);
-        if(StringUtils.isNotBlank(strRetryInterval)){
-          retryInterval = Long.parseLong(strRetryInterval);
-        }
-      } catch(NumberFormatException e) { // Ignore the exception
-      }
+      long retryInterval = getRetryIntervalMillis();
 
       for (int numRetries = 0;;) {
         try {
@@ -469,6 +459,27 @@ protected HiveConnection(String uri, Properties info,
     client = newSynchronizedClient(client);
   }
 
+  /**
+   * Gets {@code retries} and {@code retryInterval} from the session 
configuration, updating
+   * {@link #maxRetries} as a side-effect, and returns the interval in 
milliseconds (default 1000).
+   * Extracted from the constructor to keep its length within Sonar's 150-line 
limit.
+   */
+  private long getRetryIntervalMillis() {
+    long retryInterval = 1000L;
+    try {
+      String strRetries = sessConfMap.get(JdbcConnectionParams.RETRIES);
+      if (StringUtils.isNotBlank(strRetries)) {
+        maxRetries = Integer.parseInt(strRetries);
+      }
+      String strRetryInterval = 
sessConfMap.get(JdbcConnectionParams.RETRY_INTERVAL);
+      if (StringUtils.isNotBlank(strRetryInterval)) {
+        retryInterval = Long.parseLong(strRetryInterval);
+      }
+    } catch (NumberFormatException e) { // Ignore — bad values are silently 
skipped
+    }
+    return retryInterval;
+  }
+
   private void executeInitSql() throws SQLException {
     if (initFile != null) {
       try (Statement st = createStatement()) {
diff --git a/jdbc/src/java/org/apache/hive/jdbc/HiveStatement.java 
b/jdbc/src/java/org/apache/hive/jdbc/HiveStatement.java
index aba982670ac..8531780e3f7 100644
--- a/jdbc/src/java/org/apache/hive/jdbc/HiveStatement.java
+++ b/jdbc/src/java/org/apache/hive/jdbc/HiveStatement.java
@@ -398,20 +398,74 @@ private TGetOperationStatusResp waitForResultSetStatus() 
throws SQLException {
     return statusResp;
   }
 
+  /**
+   * Returns the timeout message for a {@code TIMEDOUT_STATE} response. The 
server is authoritative
+   * when the SQL state is {@code HYT00} ("timeout expired"): it reflects the 
effective operation
+   * timeout (e.g. the minimum of session {@code hive.query.timeout.seconds} 
and
+   * {@link #setQueryTimeout(int)}). Otherwise (e.g. an older server) falls 
back to the
+   * per-statement {@link #setQueryTimeout(int)}.
+   */
+  private String sqlTimeoutMessageForTimedOutState(String serverMessage, 
String sqlState) {
+    if ("HYT00".equals(sqlState) && StringUtils.isNotBlank(serverMessage)) {
+      return serverMessage;
+    }
+    if (queryTimeout > 0) {
+      return "Query timed out after " + queryTimeout + " seconds";
+    }
+    return "Query timed out";
+  }
+
+  /**
+   * Handles one {@code GetOperationStatus} response: applies a progress 
update if in-place updates
+   * are enabled, verifies the Thrift status, and dispatches on the operation 
state.
+   */
+  private void processOperationStatusResponse(TGetOperationStatusResp 
statusResp) throws SQLException {
+    if (!isOperationComplete && inPlaceUpdateStream.isPresent()) {
+      inPlaceUpdateStream.get().update(statusResp.getProgressUpdateResponse());
+    }
+    Utils.verifySuccessWithInfo(statusResp.getStatus());
+    if (!statusResp.isSetOperationState()) {
+      return;
+    }
+    switch (statusResp.getOperationState()) {
+    case CLOSED_STATE:
+    case FINISHED_STATE:
+      isOperationComplete = true;
+      isLogBeingGenerated = false;
+      break;
+    case CANCELED_STATE:
+      // 01000 -> warning
+      final String errMsg = statusResp.getErrorMessage();
+      final String fullErrMsg =
+          (errMsg == null || errMsg.isEmpty()) ? QUERY_CANCELLED_MESSAGE : 
QUERY_CANCELLED_MESSAGE + " " + errMsg;
+      throw new SQLException(fullErrMsg, "01000");
+    case TIMEDOUT_STATE:
+      throw new SQLTimeoutException(
+          sqlTimeoutMessageForTimedOutState(statusResp.getErrorMessage(), 
statusResp.getSqlState()));
+    case ERROR_STATE:
+      throw new SQLException(statusResp.getErrorMessage(), 
statusResp.getSqlState(), statusResp.getErrorCode());
+    case UKNOWN_STATE:
+      throw new SQLException("Unknown query", "HY000");
+    case INITIALIZED_STATE:
+    case PENDING_STATE:
+    case RUNNING_STATE:
+      break;
+    }
+  }
+
   TGetOperationStatusResp waitForOperationToComplete() throws SQLException {
     TGetOperationStatusResp statusResp = null;
 
     final TGetOperationStatusReq statusReq = new 
TGetOperationStatusReq(stmtHandle.get());
-    statusReq.setGetProgressUpdate(inPlaceUpdateStream.isPresent());
+    boolean progressUpdates = inPlaceUpdateStream.isPresent();
+    statusReq.setGetProgressUpdate(progressUpdates);
 
-    // Progress bar is completed if there is nothing to request
-    if (inPlaceUpdateStream.isPresent()) {
+    if (progressUpdates) {
       inPlaceUpdateStream.get().getEventNotifier().progressBarCompleted();
     }
 
     LOG.debug("Waiting on operation to complete: Polling operation status");
 
-    // Poll on the operation status, till the operation is complete
     do {
       try {
         if (Thread.currentThread().isInterrupted()) {
@@ -424,37 +478,7 @@ TGetOperationStatusResp waitForOperationToComplete() 
throws SQLException {
          */
         statusResp = client.GetOperationStatus(statusReq);
         LOG.debug("Status response: {}", statusResp);
-        if (!isOperationComplete && inPlaceUpdateStream.isPresent()) {
-          
inPlaceUpdateStream.get().update(statusResp.getProgressUpdateResponse());
-        }
-        Utils.verifySuccessWithInfo(statusResp.getStatus());
-        if (statusResp.isSetOperationState()) {
-          switch (statusResp.getOperationState()) {
-          case CLOSED_STATE:
-          case FINISHED_STATE:
-            isOperationComplete = true;
-            isLogBeingGenerated = false;
-            break;
-          case CANCELED_STATE:
-            // 01000 -> warning
-            final String errMsg = statusResp.getErrorMessage();
-            final String fullErrMsg =
-                (errMsg == null || errMsg.isEmpty()) ? QUERY_CANCELLED_MESSAGE 
: QUERY_CANCELLED_MESSAGE + " " + errMsg;
-            throw new SQLException(fullErrMsg, "01000");
-          case TIMEDOUT_STATE:
-            throw new SQLTimeoutException("Query timed out after " + 
queryTimeout + " seconds");
-          case ERROR_STATE:
-            // Get the error details from the underlying exception
-            throw new SQLException(statusResp.getErrorMessage(), 
statusResp.getSqlState(),
-                statusResp.getErrorCode());
-          case UKNOWN_STATE:
-            throw new SQLException("Unknown query", "HY000");
-          case INITIALIZED_STATE:
-          case PENDING_STATE:
-          case RUNNING_STATE:
-            break;
-          }
-        }
+        processOperationStatusResponse(statusResp);
       } catch (SQLException e) {
         isLogBeingGenerated = false;
         throw e;
@@ -464,8 +488,7 @@ TGetOperationStatusResp waitForOperationToComplete() throws 
SQLException {
       }
     } while (!isOperationComplete);
 
-    // set progress bar to be completed when hive query execution has completed
-    if (inPlaceUpdateStream.isPresent()) {
+    if (progressUpdates) {
       inPlaceUpdateStream.get().getEventNotifier().progressBarCompleted();
     }
     return statusResp;
diff --git 
a/service/src/java/org/apache/hive/service/cli/operation/Operation.java 
b/service/src/java/org/apache/hive/service/cli/operation/Operation.java
index 6a2b12bd558..9c51b1f14f3 100644
--- a/service/src/java/org/apache/hive/service/cli/operation/Operation.java
+++ b/service/src/java/org/apache/hive/service/cli/operation/Operation.java
@@ -202,6 +202,11 @@ public long getNumModifiedRows() {
   }
 
   protected void setOperationException(HiveSQLException operationException) {
+    // Do not overwrite a completed outcome (success, cancel, timeout, etc.). 
ERROR is excluded so
+    // the background thread can still record the SQLException after 
runQuery() sets ERROR.
+    if (state.isTerminal() && state != OperationState.ERROR) {
+      return;
+    }
     this.operationException = operationException;
   }
 
diff --git 
a/service/src/java/org/apache/hive/service/cli/operation/SQLOperation.java 
b/service/src/java/org/apache/hive/service/cli/operation/SQLOperation.java
index 5f781a3bbb6..4caa963e2a6 100644
--- a/service/src/java/org/apache/hive/service/cli/operation/SQLOperation.java
+++ b/service/src/java/org/apache/hive/service/cli/operation/SQLOperation.java
@@ -177,8 +177,19 @@ private void prepare(QueryState queryState) throws 
HiveSQLException {
         timeoutExecutor = Executors.newSingleThreadScheduledExecutor();
         timeoutExecutor.schedule(() -> {
           try {
+            // The query may have already finished, failed, or been cancelled 
before this task
+            // fires (the executor is only shut down when the operation is 
closed). Skip in that
+            // case so we don't overwrite a real result/exception with a 
spurious timeout message.
+            if (getState().isTerminal()) {
+              return null;
+            }
             final String queryId = queryState.getQueryId();
             log.info("Query timed out after: {} seconds. Cancelling the 
execution now: {}", queryTimeout, queryId);
+            setOperationException(new HiveSQLException(
+                "Query timed out after " + queryTimeout + " seconds",
+                "HYT00",
+                0,
+                queryId));
             SQLOperation.this.cancel(OperationState.TIMEDOUT);
           } catch (HiveSQLException e) {
             log.error("Error cancelling the query after timeout: {} seconds", 
queryTimeout, e);

Reply via email to