From 4d970c2cffc406b2b42a65f893dd72fd78608020 Mon Sep 17 00:00:00 2001 From: Stathis Souris Date: Thu, 29 Oct 2026 11:30:57 +0000 Subject: [PATCH 1/4] test(jdbc-v2): reproduce socket timeout loss after validation Add a delayed-response test for the JDBC timeout save/set/restore sequence used by pools. The test intentionally fails on upstream because restoring the reported zero timeout disables the configured socket timeout. Keep the reproduction separate from any proposed fix. Refs #3260 Co-authored-by: codex (0.153.2) --- .../jdbc/ConnectionNetworkTimeoutTest.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionNetworkTimeoutTest.java diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionNetworkTimeoutTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionNetworkTimeoutTest.java new file mode 100644 index 000000000..e28875121 --- /dev/null +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionNetworkTimeoutTest.java @@ -0,0 +1,63 @@ +package com.clickhouse.jdbc; + +import com.clickhouse.jdbc.internal.ExceptionUtils; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import org.testng.annotations.Test; + +import java.net.SocketTimeoutException; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; +import java.util.concurrent.Executor; + +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + +public class ConnectionNetworkTimeoutTest { + @Test(timeOut = 15000) + public void testRestoringNetworkTimeoutPreservesConfiguredSocketTimeout() throws Exception { + WireMockServer server = new WireMockServer(WireMockConfiguration.options() + .bindAddress("127.0.0.1").dynamicPort()); + server.start(); + try { + // RowBinaryWithNamesAndTypes: one UInt8 column named "1", containing one row with value 1. + server.stubFor(WireMock.post(WireMock.anyUrl()) + .withRequestBody(WireMock.equalTo("SELECT 1")) + .willReturn(WireMock.aResponse().withFixedDelay(1800) + .withHeader("X-ClickHouse-Format", "RowBinaryWithNamesAndTypes") + .withHeader("X-ClickHouse-Summary", "{\"read_rows\":\"1\",\"read_bytes\":\"1\"}") + .withBody(new byte[] {1, 1, '1', 5, 'U', 'I', 'n', 't', '8', 1}))); + + Properties properties = new Properties(); + properties.setProperty("socket_timeout", "500"); + properties.setProperty("server_time_zone", "UTC"); + properties.setProperty("user", "default"); + properties.setProperty("password", "local-test"); + properties.setProperty("retry", "0"); + properties.setProperty("compress", "false"); + try (Connection connection = new ConnectionImpl( + "jdbc:clickhouse://127.0.0.1:" + server.port() + "/default", properties)) { + Executor executor = Runnable::run; + int originalTimeout = connection.getNetworkTimeout(); + connection.setNetworkTimeout(executor, 5000); + connection.setNetworkTimeout(executor, originalTimeout); + + try (Statement statement = connection.createStatement()) { + SQLException error = expectThrows(SQLException.class, () -> { + try (ResultSet result = statement.executeQuery("SELECT 1")) { + result.next(); + } + }); + assertTrue(ExceptionUtils.getRootCause(error) instanceof SocketTimeoutException, + "Expected a socket timeout, got: " + error); + } + } + } finally { + server.stop(); + } + } +} From 334852fb3952100e6f6d00bfa83a86224c840d79 Mon Sep 17 00:00:00 2001 From: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:41:18 +0000 Subject: [PATCH 2/4] [jdbc-v2] Report the configured socket_timeout from getNetworkTimeout Connection#getNetworkTimeout() returned 0 when socket_timeout was set in the client configuration and setNetworkTimeout was not called. A pool that saved this value and restored it after validation stored a socket_timeout=0 override in the connection settings, and that override disabled the configured timeout for every later request. getNetworkTimeout() now returns the connection override if one is set, else the socket_timeout value of the client configuration. setNetworkTimeout(executor, 0) still disables the socket timeout. Fixes: https://github.com/ClickHouse/clickhouse-java/issues/3260 Co-Authored-By: Claude Opus 5.5 --- docs/features.md | 2 +- history/latest/3260.md | 7 ++ .../com/clickhouse/jdbc/ConnectionImpl.java | 9 ++- .../jdbc/ConnectionNetworkTimeoutTest.java | 68 +++++++++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 history/latest/3260.md diff --git a/docs/features.md b/docs/features.md index ab14fd4cc..89f35ee6b 100644 --- a/docs/features.md +++ b/docs/features.md @@ -76,7 +76,7 @@ Compatibility-sensitive traits: - Driver and client properties: Separates JDBC-specific properties from passthrough client options used by the underlying `client-v2` transport. A passthrough client option that has no default value is reported with a `null` default by `Driver#getPropertyInfo` and is not sent to the transport unless the connection sets it — this is the case for the socket buffer properties `socket_rcvbuf` and `socket_sndbuf`, which are left to the operating system. - Metrics recording: The `jdbc_metrics_recorder` property names the `client-v2` `MetricsRecorder` implementation the connection reports the metrics of its operations to. A JDBC URL carries strings only, so the value is the fully-qualified class name of a recorder with a public no-argument constructor, and each connection creates its own instance and registers it with its own client. A class that is not on the classpath, does not implement `MetricsRecorder`, or cannot be instantiated is rejected when the connection is created. When the property is not set, nothing is recorded. - DataSource support: Provides a JDBC `DataSource` implementation backed by the same driver configuration model. -- Connection lifecycle: Supports connection close, validity checks, ping-based health checks, and network timeout management. +- Connection lifecycle: Supports connection close, validity checks, ping-based health checks, and network timeout management. `Connection#getNetworkTimeout()` returns the value set with `setNetworkTimeout`, or else the `socket_timeout` client property. Thus, when a connection pool saves the timeout, sets another one, and restores the saved one, the configured `socket_timeout` stays in effect. `setNetworkTimeout(executor, 0)` disables the socket timeout. - Underlying client access: `ConnectionImpl#getClient()` exposes the connection-owned `client-v2` instance for operations that are not representable through the JDBC API, including direct consumption of raw response streams. - Schema and database context: Supports database selection through URL, `setSchema`, `USE`, and statement-level settings. - Non-transactional operation: Exposes ClickHouse-appropriate transaction behavior with auto-commit semantics and unsupported transactional features. diff --git a/history/latest/3260.md b/history/latest/3260.md new file mode 100644 index 000000000..f4298de30 --- /dev/null +++ b/history/latest/3260.md @@ -0,0 +1,7 @@ +- **[jdbc-v2]** Fixed `Connection#getNetworkTimeout()`, which returned `0` (no limit) when the `socket_timeout` + property was set and `setNetworkTimeout` was not called. A connection pool that saved this value and restored it + after validation disabled the configured socket timeout. The method now returns the configured `socket_timeout` + until `setNetworkTimeout` sets a connection value. `setNetworkTimeout(executor, 0)` still disables the socket + timeout. With a pool, the configured `socket_timeout` now applies after validation: a request that takes longer + fails with a socket timeout, and the connection is closed because the pool set a network timeout. + (https://github.com/ClickHouse/clickhouse-java/issues/3260) 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 ff7e1a8fe..49cf0d955 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java @@ -735,7 +735,14 @@ public synchronized void onNetworkTimeout() { @Override public int getNetworkTimeout() throws SQLException { - return defaultQuerySettings.getNetworkTimeout().intValue(); + if (defaultQuerySettings.getOption(ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey()) != null) { + return defaultQuerySettings.getNetworkTimeout().intValue(); + } + // Without a connection override, requests use the socket timeout from the client configuration. + String socketTimeout = client.getConfiguration().getOrDefault( + ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey(), + ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getDefaultValue()); + return ((Number) ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.parseValue(socketTimeout)).intValue(); } /** diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionNetworkTimeoutTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionNetworkTimeoutTest.java index e28875121..a3d3ea18a 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionNetworkTimeoutTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionNetworkTimeoutTest.java @@ -4,6 +4,7 @@ import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.net.SocketTimeoutException; @@ -14,6 +15,7 @@ import java.util.Properties; import java.util.concurrent.Executor; +import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; import static org.testng.Assert.expectThrows; @@ -60,4 +62,70 @@ public void testRestoringNetworkTimeoutPreservesConfiguredSocketTimeout() throws server.stop(); } } + + @DataProvider + public static Object[][] configuredSocketTimeouts() { + return new Object[][] { + {"500", 500}, + {"0", 0}, + {null, 0}, + }; + } + + @Test(dataProvider = "configuredSocketTimeouts") + public void testGetNetworkTimeoutReportsEffectiveSocketTimeout(String socketTimeout, int expected) throws Exception { + try (Connection connection = new ConnectionImpl("jdbc:clickhouse://127.0.0.1:1/default", + connectionProperties(socketTimeout))) { + Executor executor = Runnable::run; + int originalTimeout = connection.getNetworkTimeout(); + assertEquals(originalTimeout, expected); + + connection.setNetworkTimeout(executor, 5000); + assertEquals(connection.getNetworkTimeout(), 5000); + + connection.setNetworkTimeout(executor, originalTimeout); + assertEquals(connection.getNetworkTimeout(), expected); + } + } + + @Test(timeOut = 15000) + public void testZeroNetworkTimeoutDisablesConfiguredSocketTimeout() throws Exception { + WireMockServer server = new WireMockServer(WireMockConfiguration.options() + .bindAddress("127.0.0.1").dynamicPort()); + server.start(); + try { + server.stubFor(WireMock.post(WireMock.anyUrl()) + .withRequestBody(WireMock.equalTo("SELECT 1")) + .willReturn(WireMock.aResponse().withFixedDelay(1800) + .withHeader("X-ClickHouse-Format", "RowBinaryWithNamesAndTypes") + .withHeader("X-ClickHouse-Summary", "{\"read_rows\":\"1\",\"read_bytes\":\"1\"}") + .withBody(new byte[] {1, 1, '1', 5, 'U', 'I', 'n', 't', '8', 1}))); + try (Connection connection = new ConnectionImpl( + "jdbc:clickhouse://127.0.0.1:" + server.port() + "/default", connectionProperties("500"))) { + connection.setNetworkTimeout(Runnable::run, 0); + assertEquals(connection.getNetworkTimeout(), 0); + + try (Statement statement = connection.createStatement(); + ResultSet result = statement.executeQuery("SELECT 1")) { + assertTrue(result.next()); + assertEquals(result.getInt(1), 1); + } + } + } finally { + server.stop(); + } + } + + private static Properties connectionProperties(String socketTimeout) { + Properties properties = new Properties(); + if (socketTimeout != null) { + properties.setProperty("socket_timeout", socketTimeout); + } + properties.setProperty("server_time_zone", "UTC"); + properties.setProperty("user", "default"); + properties.setProperty("password", "local-test"); + properties.setProperty("retry", "0"); + properties.setProperty("compress", "false"); + return properties; + } } From 97e08d6d8bea7fdd78a68016055532cf6d670270 Mon Sep 17 00:00:00 2001 From: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:25:12 +0000 Subject: [PATCH 3/4] [jdbc-v2] Parse socket_timeout without a nullable cast in getNetworkTimeout SonarCloud reported a possible NullPointerException (java:S2259) on the fallback path of ConnectionImpl#getNetworkTimeout(): parseValue() can return null, and the result was unboxed without a check. Read the value the same way as StatementImpl reads async: getOrDefault() with the property default, then Integer.parseInt(). The returned value does not change: socket_timeout is an Integer property, and parseValue() uses Integer.parseInt() for it. Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/com/clickhouse/jdbc/ConnectionImpl.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 49cf0d955..4e9e8da97 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java @@ -739,10 +739,9 @@ public int getNetworkTimeout() throws SQLException { return defaultQuerySettings.getNetworkTimeout().intValue(); } // Without a connection override, requests use the socket timeout from the client configuration. - String socketTimeout = client.getConfiguration().getOrDefault( + return Integer.parseInt(client.getConfiguration().getOrDefault( ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey(), - ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getDefaultValue()); - return ((Number) ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.parseValue(socketTimeout)).intValue(); + ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getDefaultValue())); } /** From c50992a016f8af96a1639e2e5bcf2e5e86fa0b2e Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 3 Nov 2026 18:25:29 -0800 Subject: [PATCH 4/4] Moved setting timeout to client side for better consistency and as useful feature --- .../com/clickhouse/client/api/Client.java | 45 ++++++ .../com/clickhouse/client/ClientTests.java | 81 +++++++++++ docs/features.md | 4 +- docs/integration-client.md | 4 +- history/latest/3260.md | 12 +- .../com/clickhouse/jdbc/ConnectionImpl.java | 13 +- .../jdbc/ConnectionNetworkTimeoutTest.java | 131 ------------------ .../com/clickhouse/jdbc/ConnectionTest.java | 116 ++++++++++++++++ 8 files changed, 256 insertions(+), 150 deletions(-) delete mode 100644 jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionNetworkTimeoutTest.java diff --git a/client-v2/src/main/java/com/clickhouse/client/api/Client.java b/client-v2/src/main/java/com/clickhouse/client/api/Client.java index 4ecde77c6..0d843d3c0 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/Client.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/Client.java @@ -2558,6 +2558,51 @@ public Map getConfiguration() { return readOnlyConfig; } + /** + * Sets the network timeout for subsequent requests. + *

+ * The timeout is measured in milliseconds. A value of {@code 0} disables the timeout. + * Requests that have already been created, including their retries, keep the timeout they + * started with. An operation-level timeout in {@link QuerySettings} or {@link InsertSettings} + * overrides this client-level value. + * + * @param timeout network timeout in milliseconds + */ + public void setNetworkTimeout(int timeout) { + ValidationUtils.checkRange(timeout, 0, Integer.MAX_VALUE, + ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey()); + configuration.put(ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey(), timeout); + } + + /** + * Sets the network timeout for subsequent requests. + * + * @param timeout network timeout value + * @param unit timeout unit + * @see #setNetworkTimeout(int) + */ + public void setNetworkTimeout(long timeout, ChronoUnit unit) { + ValidationUtils.checkNotNull(unit, "unit"); + long timeoutMillis; + try { + timeoutMillis = Duration.of(timeout, unit).toMillis(); + } catch (ArithmeticException e) { + throw new IllegalArgumentException("Network timeout is outside the supported millisecond range", e); + } + ValidationUtils.checkRange(timeoutMillis, 0, Integer.MAX_VALUE, + ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey()); + setNetworkTimeout((int) timeoutMillis); + } + + /** + * Returns the current client-level network timeout in milliseconds. + * + * @return network timeout in milliseconds; {@code 0} means disabled + */ + public int getNetworkTimeout() { + return ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getOrDefault(configuration); + } + /** * Returns operation ({@link ClientConfigProperties#MAX_EXECUTION_TIME}) timeout value in milliseconds. * @return timeout value in milliseconds. 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 2d8f8ad85..489e4fef7 100644 --- a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java @@ -9,6 +9,7 @@ import com.clickhouse.client.api.ConnectionInitiationException; import com.clickhouse.client.api.ConnectionReuseStrategy; import com.clickhouse.client.api.ServerException; +import com.clickhouse.client.api.command.CommandResponse; import com.clickhouse.client.api.command.CommandSettings; import com.clickhouse.client.api.enums.Protocol; import com.clickhouse.client.api.insert.InsertSettings; @@ -24,6 +25,9 @@ import com.clickhouse.client.config.ClickHouseClientOption; import com.clickhouse.data.ClickHouseFormat; import com.clickhouse.data.ClickHouseVersion; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; import org.apache.commons.lang3.RandomStringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +39,7 @@ import java.io.ByteArrayInputStream; import java.net.ConnectException; +import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; @@ -833,6 +838,71 @@ public void testQueryFormatBuilder() { } } + @Test(timeOut = 15000) + public void testRuntimeNetworkTimeoutAppliesToSubsequentRequests() throws Exception { + WireMockServer server = new WireMockServer(WireMockConfiguration.options() + .bindAddress("127.0.0.1").dynamicPort()); + server.start(); + try { + server.stubFor(WireMock.post(WireMock.anyUrl()) + .willReturn(WireMock.aResponse().withFixedDelay(500) + .withHeader("X-ClickHouse-Summary", "{\"read_rows\":\"0\",\"read_bytes\":\"0\"}"))); + + try (Client client = new Client.Builder() + .addEndpoint("http://127.0.0.1:" + server.port()) + .setUsername("default") + .setPassword("") + .setDefaultDatabase("default") + .setSocketTimeout(200) + .setMaxRetries(0) + .compressServerResponse(false) + .build()) { + Assert.assertEquals(client.getNetworkTimeout(), 200); + + client.setNetworkTimeout(50); + Assert.assertEquals(client.getNetworkTimeout(), 50); + Exception error = Assert.expectThrows(Exception.class, + () -> client.execute("SELECT 1").get(5, TimeUnit.SECONDS)); + Assert.assertTrue(hasCause(error, SocketTimeoutException.class), + "Expected a socket timeout, got: " + error); + + CommandSettings requestSettings = new CommandSettings(); + requestSettings.setNetworkTimeout(0, MILLIS); + try (CommandResponse response = client.execute("SELECT 1", requestSettings) + .get(5, TimeUnit.SECONDS)) { + Assert.assertEquals(response.getReadRows(), 0); + } + Assert.assertEquals(client.getNetworkTimeout(), 50); + + client.setNetworkTimeout(0, MILLIS); + Assert.assertEquals(client.getNetworkTimeout(), 0); + try (CommandResponse response = client.execute("SELECT 1").get(5, TimeUnit.SECONDS)) { + Assert.assertEquals(response.getReadRows(), 0); + } + } + } finally { + server.stop(); + } + } + + @Test + public void testNetworkTimeoutUnitConversionAndValidation() { + try (Client client = new Client.Builder() + .addEndpoint("http://127.0.0.1:1") + .setUsername("default") + .setPassword("") + .build()) { + client.setNetworkTimeout(2, SECONDS); + Assert.assertEquals(client.getNetworkTimeout(), 2000); + + Assert.assertThrows(IllegalArgumentException.class, () -> client.setNetworkTimeout(-1)); + Assert.assertThrows(IllegalArgumentException.class, () -> client.setNetworkTimeout(-1, MILLIS)); + Assert.assertThrows(IllegalArgumentException.class, + () -> client.setNetworkTimeout((long) Integer.MAX_VALUE + 1, MILLIS)); + Assert.assertThrows(IllegalArgumentException.class, () -> client.setNetworkTimeout(1, null)); + } + } + @Test(groups = {"integration"}) public void testOverrideSettings() throws Exception { final String clientTimezone = "America/Los_Angeles"; @@ -866,6 +936,17 @@ private void assertTableNotFoundInSession(Client client, String tableName, Query } } + private static boolean hasCause(Throwable error, Class type) { + Throwable current = error; + while (current != null) { + if (type.isInstance(current)) { + return true; + } + current = current.getCause(); + } + return false; + } + protected Client.Builder newClient() { ClickHouseNode node = getServer(ClickHouseProtocol.HTTP); boolean isSecure = isCloud(); diff --git a/docs/features.md b/docs/features.md index 89f35ee6b..b8ac382ed 100644 --- a/docs/features.md +++ b/docs/features.md @@ -12,7 +12,7 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t - Authentication modes: Supports username/password credentials, ClickHouse auth headers, bearer tokens, and optional HTTP Basic authentication. - Runtime credential updates: Existing `Client` instances can update username/password or bearer-token credentials for subsequent requests without rebuilding the client. - Proxy support: Can send requests through configured HTTP proxies, including proxy credentials. -- Connection and socket tuning: Exposes pool sizing, keep-alive, reuse strategy, connect/request/socket timeouts, and low-level socket options. The socket buffer options `socket_rcvbuf` and `socket_sndbuf` (`Client.Builder#setSocketRcvbuf`/`#setSocketSndbuf`) have no default value: unless the application sets one, the buffers are left to the operating system, which sizes and auto-tunes them. Setting a fixed size disables that auto-tuning and is capped by the operating system limits, so it is not recommended unless a measurement shows a benefit. +- Connection and socket tuning: Exposes pool sizing, keep-alive, reuse strategy, connect/request/socket timeouts, and low-level socket options. `Client#setNetworkTimeout(int)` (or the `long`/`ChronoUnit` overload) changes the socket-operation timeout for subsequent requests at runtime; requests already created keep their captured value, and an operation-level timeout overrides the client value. The socket buffer options `socket_rcvbuf` and `socket_sndbuf` (`Client.Builder#setSocketRcvbuf`/`#setSocketSndbuf`) have no default value: unless the application sets one, the buffers are left to the operating system, which sizes and auto-tunes them. Setting a fixed size disables that auto-tuning and is capped by the operating system limits, so it is not recommended unless a measurement shows a benefit. - Query execution: Executes SQL asynchronously and returns streaming query responses with response metadata and metrics. - Query settings: Supports per-query database selection, output format, execution limits, roles, log comments, headers, reusable `Session` objects, session settings, server settings, and network timeout overrides. Settings explicitly set to `null` or empty string will not be sent to the server. The default format (`RowBinaryWithNamesAndTypes`) is configured at the client level (`ClientConfigProperties.INPUT_OUTPUT_FORMAT`), sending the `X-ClickHouse-Format` request header. ClickHouse `26.8+` uses that request header in preference to a `FORMAT` clause in the SQL query. Configuring `format=` (empty) or `null` omits the format request header, allowing explicit SQL `FORMAT` clauses to take effect; however, on a JDBC connection this is an expert setting, as queries without a `FORMAT` clause fall back to server `default_format` (`TabSeparated`), which JDBC rejects. - Parameterized SQL: Accepts named query parameters and can send them through supported HTTP request encodings. @@ -76,7 +76,7 @@ Compatibility-sensitive traits: - Driver and client properties: Separates JDBC-specific properties from passthrough client options used by the underlying `client-v2` transport. A passthrough client option that has no default value is reported with a `null` default by `Driver#getPropertyInfo` and is not sent to the transport unless the connection sets it — this is the case for the socket buffer properties `socket_rcvbuf` and `socket_sndbuf`, which are left to the operating system. - Metrics recording: The `jdbc_metrics_recorder` property names the `client-v2` `MetricsRecorder` implementation the connection reports the metrics of its operations to. A JDBC URL carries strings only, so the value is the fully-qualified class name of a recorder with a public no-argument constructor, and each connection creates its own instance and registers it with its own client. A class that is not on the classpath, does not implement `MetricsRecorder`, or cannot be instantiated is rejected when the connection is created. When the property is not set, nothing is recorded. - DataSource support: Provides a JDBC `DataSource` implementation backed by the same driver configuration model. -- Connection lifecycle: Supports connection close, validity checks, ping-based health checks, and network timeout management. `Connection#getNetworkTimeout()` returns the value set with `setNetworkTimeout`, or else the `socket_timeout` client property. Thus, when a connection pool saves the timeout, sets another one, and restores the saved one, the configured `socket_timeout` stays in effect. `setNetworkTimeout(executor, 0)` disables the socket timeout. +- Connection lifecycle: Supports connection close, validity checks, ping-based health checks, and network timeout management. `Connection#getNetworkTimeout()` returns the underlying client's current network timeout, initially configured by `socket_timeout`, and `setNetworkTimeout` updates that client-level value so subsequent requests from existing as well as new statements use it. When a connection pool saves the timeout, sets another one, and restores the saved one, the configured `socket_timeout` stays in effect. `setNetworkTimeout(executor, 0)` disables the socket timeout. - Underlying client access: `ConnectionImpl#getClient()` exposes the connection-owned `client-v2` instance for operations that are not representable through the JDBC API, including direct consumption of raw response streams. - Schema and database context: Supports database selection through URL, `setSchema`, `USE`, and statement-level settings. - Non-transactional operation: Exposes ClickHouse-appropriate transaction behavior with auto-commit semantics and unsupported transactional features. diff --git a/docs/integration-client.md b/docs/integration-client.md index 89219b385..4e1d291bb 100644 --- a/docs/integration-client.md +++ b/docs/integration-client.md @@ -422,11 +422,11 @@ See [SSLExamples](../examples/client-v2/src/main/java/com/clickhouse/examples/cl Timeouts are critical parameters that directly impact application stability under load and over long distances: - **Connection timeout** (`.setConnectTimeout()`): The TCP connect timeout. Setting this value too low can cause failures when the application and server are in different geographical regions. Additionally, connection timeouts are closely tied to the connection pool: if the application issues concurrent requests that exceed the available pool size, it may manifest as a connection timeout because no free connections are present. -- **Socket timeout** (`.setSocketTimeout()`): The timeout for underlying socket read/write operations. While it applies strictly to socket activity, it is vital because it dictates how long the client will wait for long-running queries to return data. If your workload involves heavy analytical queries, you may need a very long socket timeout. However, the trade-off of a long socket timeout is the increased risk of encountering stale or silently dropped connections. +- **Socket timeout** (`Client.Builder.setSocketTimeout()`): The initial timeout for underlying socket read/write operations. While it applies strictly to socket activity, it is vital because it dictates how long the client will wait for long-running queries to return data. If your workload involves heavy analytical queries, you may need a very long socket timeout. However, the trade-off of a long socket timeout is the increased risk of encountering stale or silently dropped connections. - **TCP keepalive**: Can be enabled to mitigate stale connections, though the host operating system's settings may ultimately override it. System-level TCP keepalive defaults are often several hours; configuring a shorter keepalive period makes sense for long-running operations. Keep in mind that executing extremely long operations over the public internet remains inherently risky. - **Socket buffers** (`.setSocketRcvbuf()`, `.setSocketSndbuf()`): Not set by default, so the operating system sizes the socket buffers and auto-tunes them for the connection. Setting a fixed size turns that auto-tuning off and is additionally capped by the operating system limits, so a large value may have no effect. Configure these options only when a measurement shows a benefit for your workload. -> **Note on runtime configuration:** You can optionally override the default network timeout on a per-operation basis using `QuerySettings.setNetworkTimeout(long timeout, ChronoUnit unit)`. This allows you to set stricter boundaries on specific queries without altering the client-wide defaults. +> **Note on runtime configuration:** `Client.setNetworkTimeout(int milliseconds)` and `Client.setNetworkTimeout(long timeout, ChronoUnit unit)` change the client-wide timeout for subsequent requests; `0` disables it. Requests already created, including their retries, retain the timeout they started with. You can override the client value for one operation with `QuerySettings.setNetworkTimeout(long timeout, ChronoUnit unit)`. ### Ping - check connectivity diff --git a/history/latest/3260.md b/history/latest/3260.md index f4298de30..821a50b24 100644 --- a/history/latest/3260.md +++ b/history/latest/3260.md @@ -1,7 +1,9 @@ -- **[jdbc-v2]** Fixed `Connection#getNetworkTimeout()`, which returned `0` (no limit) when the `socket_timeout` +- **[client-v2, jdbc-v2]** Fixed `Connection#getNetworkTimeout()`, which returned `0` (no limit) when the `socket_timeout` property was set and `setNetworkTimeout` was not called. A connection pool that saved this value and restored it - after validation disabled the configured socket timeout. The method now returns the configured `socket_timeout` - until `setNetworkTimeout` sets a connection value. `setNetworkTimeout(executor, 0)` still disables the socket - timeout. With a pool, the configured `socket_timeout` now applies after validation: a request that takes longer - fails with a socket timeout, and the connection is closed because the pool set a network timeout. + after validation disabled the configured socket timeout. The client now exposes a runtime network timeout, and + JDBC delegates `getNetworkTimeout()` and `setNetworkTimeout()` to it. Restoring the saved value therefore keeps + the configured timeout and applies it to subsequent requests from existing as well as new statements. + `setNetworkTimeout(executor, 0)` still disables the socket timeout. With a pool, the configured `socket_timeout` + now applies after validation: a request that takes longer fails with a socket timeout, and the connection is + closed because the pool set a network timeout. (https://github.com/ClickHouse/clickhouse-java/issues/3260) 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 4e9e8da97..a96357360 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java @@ -39,7 +39,6 @@ import java.sql.Statement; import java.sql.Struct; import java.time.Duration; -import java.time.temporal.ChronoUnit; import java.util.Calendar; import java.util.HashSet; import java.util.Map; @@ -710,9 +709,9 @@ public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLExc // if timeout is set with this method then any timeout exception should be reported to the connection // when connection get signal about timeout it uses executor to abort itself // Some connection pools set timeout before calling Connection#close() to ensure that this operation will not hang - // Socket timeout is propagated with QuerySettings this connection has. + // The client keeps this as live connection-level state, so existing statements use the new value too. networkTimeoutExecutor = executor; - defaultQuerySettings.setNetworkTimeout(milliseconds, ChronoUnit.MILLIS); + client.setNetworkTimeout(milliseconds); } @@ -735,13 +734,7 @@ public synchronized void onNetworkTimeout() { @Override public int getNetworkTimeout() throws SQLException { - if (defaultQuerySettings.getOption(ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey()) != null) { - return defaultQuerySettings.getNetworkTimeout().intValue(); - } - // Without a connection override, requests use the socket timeout from the client configuration. - return Integer.parseInt(client.getConfiguration().getOrDefault( - ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey(), - ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getDefaultValue())); + return client.getNetworkTimeout(); } /** diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionNetworkTimeoutTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionNetworkTimeoutTest.java deleted file mode 100644 index a3d3ea18a..000000000 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionNetworkTimeoutTest.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.clickhouse.jdbc; - -import com.clickhouse.jdbc.internal.ExceptionUtils; -import com.github.tomakehurst.wiremock.WireMockServer; -import com.github.tomakehurst.wiremock.client.WireMock; -import com.github.tomakehurst.wiremock.core.WireMockConfiguration; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.net.SocketTimeoutException; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.Properties; -import java.util.concurrent.Executor; - -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.expectThrows; - -public class ConnectionNetworkTimeoutTest { - @Test(timeOut = 15000) - public void testRestoringNetworkTimeoutPreservesConfiguredSocketTimeout() throws Exception { - WireMockServer server = new WireMockServer(WireMockConfiguration.options() - .bindAddress("127.0.0.1").dynamicPort()); - server.start(); - try { - // RowBinaryWithNamesAndTypes: one UInt8 column named "1", containing one row with value 1. - server.stubFor(WireMock.post(WireMock.anyUrl()) - .withRequestBody(WireMock.equalTo("SELECT 1")) - .willReturn(WireMock.aResponse().withFixedDelay(1800) - .withHeader("X-ClickHouse-Format", "RowBinaryWithNamesAndTypes") - .withHeader("X-ClickHouse-Summary", "{\"read_rows\":\"1\",\"read_bytes\":\"1\"}") - .withBody(new byte[] {1, 1, '1', 5, 'U', 'I', 'n', 't', '8', 1}))); - - Properties properties = new Properties(); - properties.setProperty("socket_timeout", "500"); - properties.setProperty("server_time_zone", "UTC"); - properties.setProperty("user", "default"); - properties.setProperty("password", "local-test"); - properties.setProperty("retry", "0"); - properties.setProperty("compress", "false"); - try (Connection connection = new ConnectionImpl( - "jdbc:clickhouse://127.0.0.1:" + server.port() + "/default", properties)) { - Executor executor = Runnable::run; - int originalTimeout = connection.getNetworkTimeout(); - connection.setNetworkTimeout(executor, 5000); - connection.setNetworkTimeout(executor, originalTimeout); - - try (Statement statement = connection.createStatement()) { - SQLException error = expectThrows(SQLException.class, () -> { - try (ResultSet result = statement.executeQuery("SELECT 1")) { - result.next(); - } - }); - assertTrue(ExceptionUtils.getRootCause(error) instanceof SocketTimeoutException, - "Expected a socket timeout, got: " + error); - } - } - } finally { - server.stop(); - } - } - - @DataProvider - public static Object[][] configuredSocketTimeouts() { - return new Object[][] { - {"500", 500}, - {"0", 0}, - {null, 0}, - }; - } - - @Test(dataProvider = "configuredSocketTimeouts") - public void testGetNetworkTimeoutReportsEffectiveSocketTimeout(String socketTimeout, int expected) throws Exception { - try (Connection connection = new ConnectionImpl("jdbc:clickhouse://127.0.0.1:1/default", - connectionProperties(socketTimeout))) { - Executor executor = Runnable::run; - int originalTimeout = connection.getNetworkTimeout(); - assertEquals(originalTimeout, expected); - - connection.setNetworkTimeout(executor, 5000); - assertEquals(connection.getNetworkTimeout(), 5000); - - connection.setNetworkTimeout(executor, originalTimeout); - assertEquals(connection.getNetworkTimeout(), expected); - } - } - - @Test(timeOut = 15000) - public void testZeroNetworkTimeoutDisablesConfiguredSocketTimeout() throws Exception { - WireMockServer server = new WireMockServer(WireMockConfiguration.options() - .bindAddress("127.0.0.1").dynamicPort()); - server.start(); - try { - server.stubFor(WireMock.post(WireMock.anyUrl()) - .withRequestBody(WireMock.equalTo("SELECT 1")) - .willReturn(WireMock.aResponse().withFixedDelay(1800) - .withHeader("X-ClickHouse-Format", "RowBinaryWithNamesAndTypes") - .withHeader("X-ClickHouse-Summary", "{\"read_rows\":\"1\",\"read_bytes\":\"1\"}") - .withBody(new byte[] {1, 1, '1', 5, 'U', 'I', 'n', 't', '8', 1}))); - try (Connection connection = new ConnectionImpl( - "jdbc:clickhouse://127.0.0.1:" + server.port() + "/default", connectionProperties("500"))) { - connection.setNetworkTimeout(Runnable::run, 0); - assertEquals(connection.getNetworkTimeout(), 0); - - try (Statement statement = connection.createStatement(); - ResultSet result = statement.executeQuery("SELECT 1")) { - assertTrue(result.next()); - assertEquals(result.getInt(1), 1); - } - } - } finally { - server.stop(); - } - } - - private static Properties connectionProperties(String socketTimeout) { - Properties properties = new Properties(); - if (socketTimeout != null) { - properties.setProperty("socket_timeout", socketTimeout); - } - properties.setProperty("server_time_zone", "UTC"); - properties.setProperty("user", "default"); - properties.setProperty("password", "local-test"); - properties.setProperty("retry", "0"); - properties.setProperty("compress", "false"); - return properties; - } -} diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java index 645496141..86fda21bd 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java @@ -11,6 +11,7 @@ import com.clickhouse.client.api.internal.Versions; import com.clickhouse.client.api.query.QueryResponse; import com.clickhouse.data.ClickHouseFormat; +import com.clickhouse.jdbc.internal.ExceptionUtils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tomakehurst.wiremock.WireMockServer; @@ -25,6 +26,7 @@ import java.math.BigDecimal; import java.net.Inet4Address; import java.net.Inet6Address; +import java.net.SocketTimeoutException; import java.nio.charset.StandardCharsets; import java.sql.Array; import java.sql.Connection; @@ -47,6 +49,7 @@ import java.util.Collections; import java.util.Properties; import java.util.UUID; +import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -59,6 +62,7 @@ import static org.testng.Assert.assertNull; import static org.testng.Assert.assertThrows; import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; import static org.testng.Assert.fail; public class ConnectionTest extends JdbcIntegrationTest { @@ -789,6 +793,118 @@ public void testHandlingTimeoutWithoutTimeoutIsSet() throws Exception { } } + @Test(timeOut = 15000) + public void testRestoringNetworkTimeoutPreservesConfiguredSocketTimeout() throws Exception { + WireMockServer server = new WireMockServer(WireMockConfiguration.options() + .bindAddress("127.0.0.1").dynamicPort()); + server.start(); + try { + stubDelayedSelectOne(server, 800); + + try (ConnectionImpl connection = new ConnectionImpl( + "jdbc:clickhouse://127.0.0.1:" + server.port() + "/default", + connectionProperties("200")); + Statement statement = connection.createStatement()) { + Executor executor = Runnable::run; + int originalTimeout = connection.getNetworkTimeout(); + assertEquals(originalTimeout, 200); + assertNull(connection.getDefaultQuerySettings() + .getOption(ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey())); + assertSocketTimeout(statement); + + connection.setNetworkTimeout(executor, 0); + assertEquals(connection.getNetworkTimeout(), 0); + assertSelectOne(statement); + + connection.setNetworkTimeout(executor, 5000); + assertEquals(connection.getNetworkTimeout(), 5000); + connection.setNetworkTimeout(executor, originalTimeout); + assertEquals(connection.getNetworkTimeout(), originalTimeout); + assertNull(connection.getDefaultQuerySettings() + .getOption(ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey())); + assertSocketTimeout(statement); + } + } finally { + server.stop(); + } + } + + @DataProvider + public static Object[][] configuredSocketTimeouts() { + return new Object[][] { + {"100", 100, true}, + {"0", 0, false}, + {null, 0, false}, + }; + } + + @Test(dataProvider = "configuredSocketTimeouts", timeOut = 10000) + public void testGetNetworkTimeoutReportsEffectiveSocketTimeout(String socketTimeout, int expected, + boolean expectTimeout) throws Exception { + WireMockServer server = new WireMockServer(WireMockConfiguration.options() + .bindAddress("127.0.0.1").dynamicPort()); + server.start(); + try { + stubDelayedSelectOne(server, 500); + try (ConnectionImpl connection = new ConnectionImpl( + "jdbc:clickhouse://127.0.0.1:" + server.port() + "/default", + connectionProperties(socketTimeout)); + Statement statement = connection.createStatement()) { + assertEquals(connection.getNetworkTimeout(), expected); + assertNull(connection.getDefaultQuerySettings() + .getOption(ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey())); + + if (expectTimeout) { + assertSocketTimeout(statement); + } else { + assertSelectOne(statement); + } + } + } finally { + server.stop(); + } + } + + private static void stubDelayedSelectOne(WireMockServer server, int delayMillis) { + // RowBinaryWithNamesAndTypes: one UInt8 column named "1", containing one row with value 1. + server.stubFor(WireMock.post(WireMock.anyUrl()) + .withRequestBody(WireMock.equalTo("SELECT 1")) + .willReturn(WireMock.aResponse().withFixedDelay(delayMillis) + .withHeader("X-ClickHouse-Format", "RowBinaryWithNamesAndTypes") + .withHeader("X-ClickHouse-Summary", "{\"read_rows\":\"1\",\"read_bytes\":\"1\"}") + .withBody(new byte[] {1, 1, '1', 5, 'U', 'I', 'n', 't', '8', 1}))); + } + + private static void assertSocketTimeout(Statement statement) { + SQLException error = expectThrows(SQLException.class, () -> { + try (ResultSet result = statement.executeQuery("SELECT 1")) { + result.next(); + } + }); + assertTrue(ExceptionUtils.getRootCause(error) instanceof SocketTimeoutException, + "Expected a socket timeout, got: " + error); + } + + private static void assertSelectOne(Statement statement) throws SQLException { + try (ResultSet result = statement.executeQuery("SELECT 1")) { + assertTrue(result.next()); + assertEquals(result.getInt(1), 1); + } + } + + private static Properties connectionProperties(String socketTimeout) { + Properties properties = new Properties(); + if (socketTimeout != null) { + properties.setProperty("socket_timeout", socketTimeout); + } + properties.setProperty("server_time_zone", "UTC"); + properties.setProperty("user", "default"); + properties.setProperty("password", "local-test"); + properties.setProperty("retry", "0"); + properties.setProperty("compress", "false"); + return properties; + } + @Test(groups = { "integration" }) public void testMaxResultRowsProperty() throws Exception { Properties properties = new Properties();