From a4a2a8e7addc5cc7c18011ed6d189ee9d2c850e1 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 25 Sep 2026 13:12:54 -0700 Subject: [PATCH 1/7] Made 159 EXECUTION_TIMEOUT server error not retriable --- CHANGELOG.md | 7 ++++ .../client/api/ServerException.java | 5 ++- .../com/clickhouse/client/ClientTests.java | 19 ++++++++++ .../client/metrics/MetricsTest.java | 2 +- .../jdbc/metadata/DatabaseMetaDataImpl.java | 22 ++++++++--- .../com/clickhouse/jdbc/StatementTest.java | 37 ++++++++++++------- .../src/test/resources/StatementSQLTests.yaml | 2 +- 7 files changed, 71 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9c2829e6..00f307990 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -721,6 +721,13 @@ of `NULL` was not set and read. (https://github.com/ClickHouse/clickhouse-java/i and `getObject(column, Object.class)` and the no-type `getObject(column)` overloads now return a decoded `String` instead of the internal holder. +## 0.9.9 + +### Bug Fixes + +- **[client-v2]** `ServerException` with code `159 Execution Timeout` is retried unconditionally. After the fix this +error treated as non-retriable. + ## 0.9.8 ### Improvements diff --git a/client-v2/src/main/java/com/clickhouse/client/api/ServerException.java b/client-v2/src/main/java/com/clickhouse/client/api/ServerException.java index c324e30ea..0abfabe39 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/ServerException.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/ServerException.java @@ -8,6 +8,8 @@ public class ServerException extends ClickHouseException { public static final int UNKNOWN_SETTING = 115; + public static final int EXECUTION_TIMEOUT = 159; + private final int code; private final int transportProtocolCode; @@ -56,10 +58,9 @@ public String getQueryId() { private boolean discoverIsRetryable(int code, String message, int transportProtocolCode) { // Let's check if we have a ServerException to reference the error code // https://github.com/ClickHouse/ClickHouse/blob/master/src/Common/ErrorCodes.cpp - switch (code) { // UNEXPECTED_END_OF_FILE + switch (code) { case 3: // UNEXPECTED_END_OF_FILE case 107: // FILE_DOESNT_EXIST - case 159: // TIMEOUT_EXCEEDED case 164: // READONLY case 202: // TOO_MANY_SIMULTANEOUS_QUERIES case 203: // NO_FREE_CONNECTION diff --git a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java index 50a80f589..fa77286f1 100644 --- a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java @@ -709,6 +709,25 @@ public void testQueryIdGenerator() throws Exception { Assert.assertEquals(actualIds, new ArrayList<>(queryIds)); } + @Test(groups = {"integration"}) + public void testExecutionTimeout() throws Exception{ + final String query = "SELECT count(), sum(sipHash64(number)) " + + "FROM numbers(1000000000) " + + "SETTINGS max_threads = 1;"; + + long startTime = System.currentTimeMillis(); + int maxExecTime = 4000; + try (Client client = newClient().serverSetting("max_execution_time", String.valueOf(TimeUnit.MILLISECONDS.toSeconds(maxExecTime))).build(); + QueryResponse response = client.query(query).get()) { + + } catch (ServerException e) { + long queryTime = System.currentTimeMillis() - startTime; + System.out.println(queryTime + " - query time"); + Assert.assertTrue(Math.abs(queryTime - maxExecTime) < 1000); + Assert.assertEquals(e.getCode(), ServerException.EXECUTION_TIMEOUT); + } + } + @Test(groups = {"integration"}) public void testHostnameWithUnderscore() throws Exception { diff --git a/client-v2/src/test/java/com/clickhouse/client/metrics/MetricsTest.java b/client-v2/src/test/java/com/clickhouse/client/metrics/MetricsTest.java index fd36c18eb..bfe671d99 100644 --- a/client-v2/src/test/java/com/clickhouse/client/metrics/MetricsTest.java +++ b/client-v2/src/test/java/com/clickhouse/client/metrics/MetricsTest.java @@ -41,7 +41,7 @@ void tearDown() { meterRegistry.clear(); Metrics.globalRegistry.clear(); } - + @Test(groups = {"integration"}, enabled = true) public void testRegisterMetrics() throws Exception { ClickHouseNode node = getServer(ClickHouseProtocol.HTTP); diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java index 84bb1886c..482237e03 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java @@ -794,26 +794,26 @@ public ResultSet getProcedureColumns(String catalog, String schemaPattern, Strin static final Map ENGINE_TO_TABLE_TYPE; static { Map map = new java.util.HashMap<>(); - + // Log tables map.put("Log", TableType.LOG_TABLE.getTypeName()); map.put("StripeLog", TableType.LOG_TABLE.getTypeName()); map.put("TinyLog", TableType.LOG_TABLE.getTypeName()); - + // Memory tables map.put("Buffer", TableType.MEMORY_TABLE.getTypeName()); map.put("Memory", TableType.MEMORY_TABLE.getTypeName()); map.put("Set", TableType.MEMORY_TABLE.getTypeName()); - + // Views map.put("View", TableType.VIEW.getTypeName()); map.put("LiveView", TableType.VIEW.getTypeName()); map.put("MaterializedView", TableType.MATERIALIZED_VIEW.getTypeName()); map.put("WindowView", TableType.VIEW.getTypeName()); - + // Dictionary map.put("Dictionary", TableType.DICTIONARY.getTypeName()); - + // Remote/External tables map.put("AzureBlobStorage", TableType.REMOTE_TABLE.getTypeName()); map.put("AzureQueue", TableType.REMOTE_TABLE.getTypeName()); @@ -899,6 +899,18 @@ public ResultSet getProcedureColumns(String catalog, String schemaPattern, Strin // Remote engines (appended 08/31/2026) map.put("BigQuery", TableType.REMOTE_TABLE.getTypeName()); + // Paimon (appended 05/27/2026) + map.put("Paimon", TableType.REMOTE_TABLE.getTypeName()); + map.put("PaimonAzure", TableType.REMOTE_TABLE.getTypeName()); + map.put("PaimonHDFS", TableType.REMOTE_TABLE.getTypeName()); + map.put("PaimonLocal", TableType.REMOTE_TABLE.getTypeName()); + map.put("PaimonS3", TableType.REMOTE_TABLE.getTypeName()); + + // Remote engines (appended 07/21/2026) + map.put("QueryRunner", TableType.REMOTE_TABLE.getTypeName()); + map.put("Remote", TableType.REMOTE_TABLE.getTypeName()); + map.put("RemoteSecure", TableType.REMOTE_TABLE.getTypeName()); + // Special map.put("TimeSeries", TableType.TABLE.getTypeName()); map.put("Null", TableType.TABLE.getTypeName()); diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java index 7d9d7f461..125b9d741 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java @@ -37,14 +37,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.assertNull; -import static org.testng.Assert.assertSame; -import static org.testng.Assert.assertThrows; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.fail; +import static org.testng.Assert.*; @Test(groups = {"integration"}) @@ -289,7 +282,7 @@ public static Object[][] asyncInsertSettingsDP() { @Test(groups = {"integration"}, dataProvider = "asyncInsertSettingsDP") public void testInsertWithAsyncInsert(String asyncInsert, String waitAsyncInsert, int expectedUpdateCount, int expectedSelectCount, boolean fails) throws Exception { String tableName = "test_async_insert_param_" + asyncInsert + "_" + waitAsyncInsert + "_" + UUID.randomUUID().toString().replace("-", "_"); - + Properties props = new Properties(); props.setProperty(ClientConfigProperties.serverSetting(ServerSettings.ASYNC_INSERT), asyncInsert); props.setProperty(ClientConfigProperties.serverSetting(ServerSettings.WAIT_ASYNC_INSERT), waitAsyncInsert); @@ -466,14 +459,30 @@ public void testJdbcEscapeSyntax() throws Exception { @Test(groups = {"integration"}) public void testExecuteQueryTimeout() throws Exception { - try (Connection conn = getJdbcConnection()) { + Properties config = new Properties(); + config.setProperty(ClientConfigProperties.ASYNC_OPERATIONS.getKey(), "true"); + try (Connection conn = getJdbcConnection(config)) { try (Statement stmt = conn.createStatement()) { - stmt.setQueryTimeout(1); - assertThrows(SQLException.class, () -> { - try (ResultSet rs = stmt.executeQuery("SELECT sleep(5)")) { - assertFalse(rs.next()); + long woTimeoutStart = System.currentTimeMillis(); + final String query = "SELECT count(), sum(sipHash64(number)) " + + "FROM numbers(1000000000) " + + "SETTINGS max_threads = 1;"; + try (ResultSet rs = stmt.executeQuery(query)) { + assertTrue(rs.next()); + } + long woTimeoutTime = System.currentTimeMillis() - woTimeoutStart; + + int queryTimeoutMs = (int) (woTimeoutTime * 0.75); + stmt.setQueryTimeout((int) TimeUnit.MILLISECONDS.toSeconds(queryTimeoutMs)); + + long wTimeoutStart = System.currentTimeMillis(); + SQLException ex = expectThrows(SQLException.class, () -> { + try (ResultSet rs = stmt.executeQuery(query)) { + assertTrue(rs.next()); } }); + long wTimeoutTime = System.currentTimeMillis() - wTimeoutStart; + assertTrue(Math.abs(wTimeoutTime - queryTimeoutMs) < 1000); } } } diff --git a/jdbc-v2/src/test/resources/StatementSQLTests.yaml b/jdbc-v2/src/test/resources/StatementSQLTests.yaml index 304d495a9..c9552b06e 100644 --- a/jdbc-v2/src/test/resources/StatementSQLTests.yaml +++ b/jdbc-v2/src/test/resources/StatementSQLTests.yaml @@ -77,7 +77,7 @@ - name: column_types expected: ["UInt64", "UInt64"] - name: explain_stmt_01 - query: EXPLAIN SELECT 1 + query: EXPLAIN AST SELECT 1 tables: events: datasets/empty_table checks: From f8939c09083de7b9404fb5044a74f5a389d40c0a Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 25 Sep 2026 13:34:47 -0700 Subject: [PATCH 2/7] throw SQLTimeoutException on query timeout --- .../com/clickhouse/jdbc/StatementImpl.java | 21 ++++-- .../com/clickhouse/jdbc/StatementTest.java | 71 ++++++++++++++++--- 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java index 4583b6ef8..76cddd938 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java @@ -1,6 +1,7 @@ package com.clickhouse.jdbc; import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.ServerException; import com.clickhouse.client.api.data_formats.ClickHouseFormatReader; import com.clickhouse.client.api.data_formats.JSONEachRowFormatReader; import com.clickhouse.client.api.internal.ServerSettings; @@ -15,10 +16,7 @@ import org.slf4j.LoggerFactory; import java.net.SocketTimeoutException; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.SQLWarning; -import java.sql.Statement; +import java.sql.*; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; @@ -26,6 +24,7 @@ import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -360,6 +359,7 @@ protected ResultSetImpl executeQueryImpl(String sql, QuerySettings settings) thr } handleSocketTimeoutException(e); onResultSetClosed(null); + throwOnExecutionTimeout(e, mergedSettings.getQueryId()); throw ExceptionUtils.toSqlState(e); } } @@ -370,6 +370,18 @@ protected void handleSocketTimeoutException(Exception e) { } } + protected void throwOnExecutionTimeout(Exception e, String queryId) throws SQLTimeoutException { + boolean shouldThrow = e instanceof TimeoutException; + ServerException se = e instanceof ServerException ? (ServerException) e : e.getCause() instanceof ServerException ? (ServerException) e.getCause() : null; + if (se != null && se.getCode() == ServerException.EXECUTION_TIMEOUT) { + shouldThrow = true; + } + + if (shouldThrow) { + throw new SQLTimeoutException("Query execution time exceeded limit (queryId=" + queryId + ", timeout = " + queryTimeout + "s)", e); + } + } + @Override public int executeUpdate(String sql) throws SQLException { ensureOpen(); @@ -396,6 +408,7 @@ protected long executeUpdateImpl(String sql, QuerySettings settings) throws SQLE lastQueryId = response.getQueryId(); } catch (Exception e) { handleSocketTimeoutException(e); + throwOnExecutionTimeout(e, mergedSettings.getQueryId()); throw ExceptionUtils.toSqlState(e); } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java index 125b9d741..3a697d200 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java @@ -18,13 +18,7 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import java.sql.Array; -import java.sql.Connection; -import java.sql.Date; -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.SQLException; -import java.sql.Statement; +import java.sql.*; import java.time.LocalDate; import java.util.Arrays; import java.util.Collections; @@ -458,7 +452,7 @@ public void testJdbcEscapeSyntax() throws Exception { } @Test(groups = {"integration"}) - public void testExecuteQueryTimeout() throws Exception { + public void testExecuteQueryTimeoutAsyncOperation() throws Exception { Properties config = new Properties(); config.setProperty(ClientConfigProperties.ASYNC_OPERATIONS.getKey(), "true"); try (Connection conn = getJdbcConnection(config)) { @@ -476,7 +470,66 @@ public void testExecuteQueryTimeout() throws Exception { stmt.setQueryTimeout((int) TimeUnit.MILLISECONDS.toSeconds(queryTimeoutMs)); long wTimeoutStart = System.currentTimeMillis(); - SQLException ex = expectThrows(SQLException.class, () -> { + expectThrows(SQLTimeoutException.class, () -> { + try (ResultSet rs = stmt.executeQuery(query)) { + assertTrue(rs.next()); + } + }); + long wTimeoutTime = System.currentTimeMillis() - wTimeoutStart; + assertTrue(Math.abs(wTimeoutTime - queryTimeoutMs) < 1000); + } + } + } + + @Test(groups = {"integration"}) + public void testExecuteQueryTimeoutServerTimeout() throws Exception { + + long woTimeoutTime; + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + long woTimeoutStart = System.currentTimeMillis(); + final String query = "SELECT count(), sum(sipHash64(number)) " + + "FROM numbers(1000000000) " + + "SETTINGS max_threads = 1;"; + try (ResultSet rs = stmt.executeQuery(query)) { + assertTrue(rs.next()); + } + woTimeoutTime = System.currentTimeMillis() - woTimeoutStart; + } + } + + int queryTimeoutMs = (int) (woTimeoutTime * 0.75); + Properties config = new Properties(); + config.setProperty(ClientConfigProperties.serverSetting("max_execution_time"), String.valueOf(TimeUnit.MILLISECONDS.toSeconds(queryTimeoutMs))); + try (Connection conn = getJdbcConnection(config)) { + try (Statement stmt = conn.createStatement()) { + final String query = "SELECT count(), sum(sipHash64(number)) " + + "FROM numbers(1000000000) " + + "SETTINGS max_threads = 1;"; + + long wTimeoutStart = System.currentTimeMillis(); + expectThrows(SQLTimeoutException.class, () -> { + try (ResultSet rs = stmt.executeQuery(query)) { + assertTrue(rs.next()); + } + }); + long wTimeoutTime = System.currentTimeMillis() - wTimeoutStart; + assertTrue(Math.abs(wTimeoutTime - queryTimeoutMs) < 1000); + } + } + + // test async because it wraps exceptions + config = new Properties(); + config.setProperty(ClientConfigProperties.serverSetting("max_execution_time"), String.valueOf(TimeUnit.MILLISECONDS.toSeconds(queryTimeoutMs))); + config.setProperty(ClientConfigProperties.ASYNC_OPERATIONS.getKey(), "true"); + try (Connection conn = getJdbcConnection(config)) { + try (Statement stmt = conn.createStatement()) { + final String query = "SELECT count(), sum(sipHash64(number)) " + + "FROM numbers(1000000000) " + + "SETTINGS max_threads = 1;"; + + long wTimeoutStart = System.currentTimeMillis(); + expectThrows(SQLTimeoutException.class, () -> { try (ResultSet rs = stmt.executeQuery(query)) { assertTrue(rs.next()); } From 91af918f69d96520095e00a6ed407b86506d0162 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 25 Sep 2026 13:36:21 -0700 Subject: [PATCH 3/7] make setQueryTimeout set max execution time setting conditionally --- .../com/clickhouse/jdbc/ConnectionImpl.java | 14 +++ .../com/clickhouse/jdbc/StatementImpl.java | 30 ++++- .../com/clickhouse/jdbc/StatementTest.java | 117 ++++++++++++++++++ 3 files changed, 160 insertions(+), 1 deletion(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java index 090ceefd2..f947f7b70 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java @@ -122,6 +122,20 @@ public ConnectionImpl(String url, Properties info) throws SQLException { this.schema = client.getDefaultDatabase(); this.defaultQuerySettings = new QuerySettings(); + String defaultQuerySettingsProp = config.getDriverProperty(DriverProperties.DEFAULT_QUERY_SETTINGS.getKey(), null); + if (defaultQuerySettingsProp != null) { + ClientConfigProperties.toKeyValuePairs(defaultQuerySettingsProp) + .forEach((k, v) -> this.defaultQuerySettings.serverSetting(k, v)); + } + Map clientProps = config.getClientProperties(); + for (Map.Entry entry : clientProps.entrySet()) { + if (entry.getKey() != null && entry.getValue() != null) { + if (entry.getKey().startsWith(ClientConfigProperties.SERVER_SETTING_PREFIX)) { + this.defaultQuerySettings.setOption(entry.getKey(), entry.getValue()); + } + } + } + this.metadata = new DatabaseMetaDataImpl(this, false, url); this.defaultCalendar = Calendar.getInstance(); diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java index 76cddd938..7577468f3 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java @@ -65,6 +65,8 @@ public class StatementImpl implements Statement, JdbcV2Wrapper { // settings local to a statement protected QuerySettings localSettings; + protected Integer connectionLvlExecTimeout; // to properly reset + public StatementImpl(ConnectionImpl connection) throws SQLException { this.connection = connection; @@ -78,6 +80,8 @@ public StatementImpl(ConnectionImpl connection) throws SQLException { this.escapeProcessingEnabled = true; this.featureManager = new FeatureManager(connection.getJdbcConfig()); this.queryIdGenerator = connection.getJdbcConfig().getQueryIdGenerator(); + + this.connectionLvlExecTimeout = connection.getDefaultQuerySettings().getMaxExecutionTime(); } protected void ensureOpen() throws SQLException { @@ -371,7 +375,7 @@ protected void handleSocketTimeoutException(Exception e) { } protected void throwOnExecutionTimeout(Exception e, String queryId) throws SQLTimeoutException { - boolean shouldThrow = e instanceof TimeoutException; + boolean shouldThrow = e instanceof TimeoutException || e.getCause() instanceof TimeoutException; ServerException se = e instanceof ServerException ? (ServerException) e : e.getCause() instanceof ServerException ? (ServerException) e.getCause() : null; if (se != null && se.getCode() == ServerException.EXECUTION_TIMEOUT) { shouldThrow = true; @@ -482,6 +486,30 @@ public int getQueryTimeout() throws SQLException { @Override public void setQueryTimeout(int seconds) throws SQLException { ensureOpen(); + if (seconds < 0) { + throw new SQLException("Timeout should be >= 0 but " + seconds + " was passed"); + } + + if (seconds > 0) { + boolean isAsyncEnabled; + try { + isAsyncEnabled = Boolean.parseBoolean( + getConnection().getClient().getConfiguration().getOrDefault(ClientConfigProperties.ASYNC_OPERATIONS.getKey(), + ClientConfigProperties.ASYNC_OPERATIONS.getDefaultValue())); + } catch (Exception e) { + LOG.error("Failed to read client configuration " + ClientConfigProperties.ASYNC_OPERATIONS.getKey(), e); + isAsyncEnabled = false; + } + + if (!isAsyncEnabled) { + // `max_execution_time` is only option when not async operations enabled + getLocalSettings().setMaxExecutionTime(seconds); + } + } else if (connectionLvlExecTimeout != null) { + getLocalSettings().setMaxExecutionTime(connectionLvlExecTimeout); + } else { + getLocalSettings().resetOption(ClientConfigProperties.serverSetting(ServerSettings.MAX_EXECUTION_TIME)); + } queryTimeout = seconds; } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java index 3a697d200..2dba637e9 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java @@ -1825,6 +1825,123 @@ public void testEscapedSQLToNative(String sql, String expected) { assertEquals(StatementImpl.escapedSQLToNative(sql), expected); } + + private void assertQueryTimeout(Statement stmt, int expectedTimeoutSec) { + final String slowQuery = "SELECT count(), sum(sipHash64(number)) FROM numbers(1000000000) SETTINGS max_threads = 1;"; + long start = System.currentTimeMillis(); + expectThrows(SQLTimeoutException.class, () -> { + try (ResultSet rs = stmt.executeQuery(slowQuery)) { + assertTrue(rs.next()); + } + }); + long elapsed = System.currentTimeMillis() - start; + long expectedMs = expectedTimeoutSec * 1000L; + assertTrue(Math.abs(elapsed - expectedMs) < 1000, + "Expected timeout ~" + expectedMs + "ms, but execution took " + elapsed + "ms"); + } + + @Test(groups = {"integration"}) + public void testConnectionLevelExecutionTimeoutOverriddenByStatement() throws Exception { + Properties config = new Properties(); + final int connExecTimeout = 7; + config.setProperty(ClientConfigProperties.serverSetting(ServerSettings.MAX_EXECUTION_TIME), String.valueOf(connExecTimeout)); + try (Connection conn = getJdbcConnection(config); + StatementImpl stmt = (StatementImpl) conn.createStatement()) { + assertEquals(stmt.connectionLvlExecTimeout, Integer.valueOf(connExecTimeout)); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), connExecTimeout); + assertEquals(stmt.getQueryTimeout(), 0); + assertQueryTimeout(stmt, connExecTimeout); + + final int stmtExecTimeout = 5; + stmt.setQueryTimeout(stmtExecTimeout); + assertEquals(stmt.getQueryTimeout(), stmtExecTimeout); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), stmtExecTimeout); + assertQueryTimeout(stmt, stmtExecTimeout); + + // reset back to connection + stmt.setQueryTimeout(0); + assertEquals(stmt.getQueryTimeout(), 0); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), connExecTimeout); + } + + config = new Properties(); + config.setProperty(DriverProperties.DEFAULT_QUERY_SETTINGS.getKey(), "max_execution_time=" + connExecTimeout); + try (Connection conn = getJdbcConnection(config); + StatementImpl stmt = (StatementImpl) conn.createStatement()) { + assertEquals(stmt.connectionLvlExecTimeout, connExecTimeout); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), connExecTimeout); + } + } + + @Test(groups = {"integration"}) + public void testAsyncOperationsEnabledTimeout() throws Exception { + Properties config = new Properties(); + config.setProperty(ClientConfigProperties.ASYNC_OPERATIONS.getKey(), "true"); + try (Connection conn = getJdbcConnection(config); + StatementImpl stmt = (StatementImpl) conn.createStatement()) { + assertNull(stmt.connectionLvlExecTimeout); + assertNull(stmt.getLocalSettings().getMaxExecutionTime()); + assertEquals(stmt.getQueryTimeout(), 0); + + final int stmtExecTimeout = 5; + stmt.setQueryTimeout(stmtExecTimeout); + assertEquals(stmt.getQueryTimeout(), stmtExecTimeout); + assertNull(stmt.getLocalSettings().getMaxExecutionTime()); + assertQueryTimeout(stmt, stmtExecTimeout); + + stmt.setQueryTimeout(0); + assertEquals(stmt.getQueryTimeout(), 0); + assertNull(stmt.getLocalSettings().getMaxExecutionTime()); + } + } + + @Test(groups = {"integration"}) + public void testAsyncOperationsEnabledWithConnectionLevelTimeout() throws Exception { + Properties config = new Properties(); + final int connExecTimeout = 7; + config.setProperty(ClientConfigProperties.ASYNC_OPERATIONS.getKey(), "true"); + config.setProperty(ClientConfigProperties.serverSetting(ServerSettings.MAX_EXECUTION_TIME), String.valueOf(connExecTimeout)); + try (Connection conn = getJdbcConnection(config); + StatementImpl stmt = (StatementImpl) conn.createStatement()) { + assertEquals(stmt.connectionLvlExecTimeout, connExecTimeout); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), connExecTimeout); + assertEquals(stmt.getQueryTimeout(), 0); + + assertQueryTimeout(stmt, connExecTimeout); + + int stmtExecTimeout = 5; + stmt.setQueryTimeout(stmtExecTimeout); + assertEquals(stmt.getQueryTimeout(), stmtExecTimeout); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), connExecTimeout); + assertQueryTimeout(stmt, stmtExecTimeout); + + stmt.setQueryTimeout(0); + assertEquals(stmt.getQueryTimeout(), 0); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), connExecTimeout); + } + } + + @Test(groups = {"integration"}) + public void testNoConnectionLevelTimeoutOverriddenAndReset() throws Exception { + try (Connection conn = getJdbcConnection(); + StatementImpl stmt = (StatementImpl) conn.createStatement()) { + assertNull(stmt.connectionLvlExecTimeout); + assertNull(stmt.getLocalSettings().getMaxExecutionTime()); + assertEquals(stmt.getQueryTimeout(), 0); + + stmt.setQueryTimeout(1); + assertEquals(stmt.getQueryTimeout(), 1); + assertEquals(stmt.getLocalSettings().getMaxExecutionTime(), Integer.valueOf(1)); + assertQueryTimeout(stmt, 1); + + stmt.setQueryTimeout(0); + assertEquals(stmt.getQueryTimeout(), 0); + assertNull(stmt.getLocalSettings().getMaxExecutionTime()); + + assertThrows(SQLException.class, () -> stmt.setQueryTimeout(-1)); + } + } + private static String getDBName(Statement stmt) throws SQLException { try (ResultSet rs = stmt.executeQuery("SELECT database()")) { rs.next(); From 48b0ea69523a286cde4f41342b3e69ddc6720269 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 25 Sep 2026 13:37:51 -0700 Subject: [PATCH 4/7] Updated change log with 0.9.9 patch content --- CHANGELOG.md | 5 ++++- .../src/test/java/com/clickhouse/client/ClientTests.java | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00f307990..41120026d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -726,7 +726,10 @@ of `NULL` was not set and read. (https://github.com/ClickHouse/clickhouse-java/i ### Bug Fixes - **[client-v2]** `ServerException` with code `159 Execution Timeout` is retried unconditionally. After the fix this -error treated as non-retriable. +error treated as non-retriable. (part of https://github.com/ClickHouse/clickhouse-java/issues/2637) +- **[jdbc-v2]** Fixes `Statement#setQueryTimeout`. By default, client executes query in calling thread and future timeout +has no effect. Fix makes `setQueryTimeout` to set `max_execution_time` server setting in this case to overcome limitation. + (https://github.com/ClickHouse/clickhouse-java/issues/2637) ## 0.9.8 diff --git a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java index fa77286f1..a0df7ad1d 100644 --- a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java @@ -722,7 +722,6 @@ public void testExecutionTimeout() throws Exception{ } catch (ServerException e) { long queryTime = System.currentTimeMillis() - startTime; - System.out.println(queryTime + " - query time"); Assert.assertTrue(Math.abs(queryTime - maxExecTime) < 1000); Assert.assertEquals(e.getCode(), ServerException.EXECUTION_TIMEOUT); } From 0a45207f5f6aad858235fa1e16b2b8a472f9b54e Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 25 Sep 2026 13:38:45 -0700 Subject: [PATCH 5/7] removed confusing timeout value from SQLTimeoutException message --- jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java index 7577468f3..c615ec4d1 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java @@ -382,7 +382,7 @@ protected void throwOnExecutionTimeout(Exception e, String queryId) throws SQLTi } if (shouldThrow) { - throw new SQLTimeoutException("Query execution time exceeded limit (queryId=" + queryId + ", timeout = " + queryTimeout + "s)", e); + throw new SQLTimeoutException("Query execution time exceeded limit (queryId=" + queryId + ")", e); } } From d39db152d39cb3299c01e89e5b3c707089ea102d Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 25 Sep 2026 13:43:00 -0700 Subject: [PATCH 6/7] fixed source min version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 99fbfc46d..4026554a3 100644 --- a/pom.xml +++ b/pom.xml @@ -149,7 +149,7 @@ 2.22 17 - 17 + 1.8 1.8 false ${skipTests} From b3769a7f49d5703d8e4cb539d5aa65175e8c956e Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 25 Sep 2026 20:41:03 -0700 Subject: [PATCH 7/7] updated tests after making execution timeout not retriable --- .../client/api/internal/HttpAPIClientHelperTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client-v2/src/test/java/com/clickhouse/client/api/internal/HttpAPIClientHelperTest.java b/client-v2/src/test/java/com/clickhouse/client/api/internal/HttpAPIClientHelperTest.java index 0654d8df2..5c3860b59 100644 --- a/client-v2/src/test/java/com/clickhouse/client/api/internal/HttpAPIClientHelperTest.java +++ b/client-v2/src/test/java/com/clickhouse/client/api/internal/HttpAPIClientHelperTest.java @@ -341,8 +341,7 @@ public void testExecuteRequestThrowsConnectExceptionOn503() throws Exception { @DataProvider(name = "serverExceptionRetryCases") public static Object[][] serverExceptionRetryCases() { - // Server code 159 (TIMEOUT_EXCEEDED) is retryable; code 60 (TABLE_NOT_FOUND) is not. - ServerException retryable = new ServerException(159, "TIMEOUT_EXCEEDED", 500, "q1"); + ServerException retryable = new ServerException(202, "TOO_MANY_SIMULTANEOUS_QUERIES", 500, "q1"); ServerException nonRetryable = new ServerException(60, "TABLE_NOT_FOUND", 404, "q2"); return new Object[][]{ // ServerException thrown directly (behaviour that already worked; pinned as contrast).