Skip to content

Commit f9d3c98

Browse files
authored
Merge pull request #2885 from ClickHouse/06/18/26/session_id_in_server_settings
[jdbc-v2] Fixes cleaning session id from server setting
2 parents 51c7955 + 22f15ad commit f9d3c98

8 files changed

Lines changed: 93 additions & 39 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929

3030
### New Features
3131

32+
- **[client-v2]** Added `Session` API to encapsulate and manage ClickHouse session settings (`session_id`, `session_check`, `session_timeout`, `session_timezone`) as a reusable object. The `Session` instance can be applied to any request settings using `applyTo()`, and session state can be cleared via `clearSession()`. Additionally, added `resetOption(String)` to `InsertSettings`, `QuerySettings`, and `CommonSettings` to allow removing specific settings. Settings explicitly set to `null` will not be sent to the server, which is useful for overriding global settings.
33+
3234
- **[client-v2]** Added runtime credential update APIs on `Client`: `updateUserAndPassword(String, String)`, `updateAccessToken(String)`, and `updateBearerToken(String)`. Subsequent requests on the same `Client` instance use the new credentials without rebuilding the client. The authentication method is fixed at construction time; calling a runtime updater that does not match the configured method throws `ClientMisconfigurationException`. See `docs/authentication.md` for details and migration guidance.
3335

3436
- **[jdbc-v2]** Added `cluster_name` configuration property to specify a target cluster for statements like `KILL QUERY` that require an `ON CLUSTER` clause to execute across all nodes. (https://github.com/ClickHouse/clickhouse-java/issues/2837)
@@ -39,7 +41,7 @@
3941

4042
### Bug Fixes
4143

42-
- **[jdbc-v2]** Fixed `Statement.cancel()` throwing `SESSION_IS_LOCKED` when the statement was running inside a ClickHouse session (e.g. via `clickhouse_setting_session_id`). The `KILL QUERY` request issued by `cancel()` now runs outside the session, so it no longer contends with the running query for the session lock. (https://github.com/ClickHouse/clickhouse-java/issues/2690)
44+
- **[jdbc-v2]** Fixed `Statement.cancel()` throwing `SESSION_IS_LOCKED` when the statement was running inside a ClickHouse session. The driver now accepts `session_id`, `session_check`, and `session_timeout` as first-class connection properties and correctly suppresses them when issuing a `KILL QUERY` during cancellation. This ensures the cancellation request runs outside the session and no longer contends with the running query for the session lock. (https://github.com/ClickHouse/clickhouse-java/issues/2690, https://github.com/ClickHouse/clickhouse-java/issues/2881)
4345

4446
- **[client-v2]** Fixed inconsistent use of `executionTimeout` parameter in `Client` component. The timeout was previously set in milliseconds but mistakenly retrieved and used in seconds in some places. Now it correctly uses milliseconds consistently. (https://github.com/ClickHouse/clickhouse-java/issues/2358)
4547

client-v2/src/main/java/com/clickhouse/client/api/Session.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public synchronized void updateSessionId(String sessionId) {
8383
setSessionId(sessionId);
8484
}
8585

86-
public synchronized void applyTo(Map<String, Object> requestSettings) {
86+
public synchronized void applyTo(Map<? super String, Object> requestSettings) {
8787
putIfSet(requestSettings, ClickHouseHttpProto.QPARAM_SESSION_ID, sessionId);
8888
putIfSet(requestSettings, ClickHouseHttpProto.QPARAM_SESSION_CHECK,
8989
sessionCheck == null ? null : (sessionCheck ? "1" : "0"));
@@ -92,7 +92,15 @@ public synchronized void applyTo(Map<String, Object> requestSettings) {
9292
putIfSet(requestSettings, ClickHouseHttpProto.QPARAM_SESSION_TIMEZONE, sessionTimezone);
9393
}
9494

95-
private static void putIfSet(Map<String, Object> settings, String key, String value) {
95+
public static void clearSession(Map<String, Object> settings) {
96+
settings.put(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_ID), null);
97+
settings.put(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEOUT), null);
98+
settings.put(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_CHECK), null);
99+
// Do not clean `session_timezone` setting because it is not related to session management and used to
100+
// set timezone for consequent queries in some multi-user applications.
101+
}
102+
103+
private static void putIfSet(Map<? super String, Object> settings, String key, String value) {
96104
if (value != null) {
97105
settings.put(ClientConfigProperties.serverSetting(key), value);
98106
}

client-v2/src/main/java/com/clickhouse/client/api/insert/InsertSettings.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ public InsertSettings setOption(String option, Object value) {
6060
return this;
6161
}
6262

63+
/**
64+
* Removes options from the settings.
65+
* @param option - configuration option name
66+
*/
67+
public InsertSettings resetOption(String option) {
68+
settings.resetOption(option);
69+
return this;
70+
}
71+
6372
/**
6473
* Get all settings as an unmodifiable map.
6574
*

client-v2/src/main/java/com/clickhouse/client/api/internal/CommonSettings.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,7 @@ public CommonSettings use(Session session) {
140140
}
141141

142142
public void clearSession() {
143-
resetOption(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_ID));
144-
resetOption(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_CHECK));
145-
resetOption(ClientConfigProperties.serverSetting(ClickHouseHttpProto.QPARAM_SESSION_TIMEOUT));
146-
// Do not clean `session_timezone` setting because it is not related to session management and used to
147-
// set timezone for consequent queries in some multi-user applications.
143+
Session.clearSession(settings);
148144
}
149145

150146
/**

client-v2/src/test/java/com/clickhouse/client/ClientTests.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import java.util.concurrent.Executors;
5151
import java.util.concurrent.TimeUnit;
5252
import java.util.concurrent.atomic.AtomicBoolean;
53+
import java.util.function.BiConsumer;
5354
import java.util.function.BiFunction;
5455
import java.util.function.Consumer;
5556
import java.util.function.Supplier;
@@ -729,6 +730,24 @@ public void testInvalidAuthConfiguration() throws Exception {
729730
Assert.assertTrue(e.getMessage().contains("Trust store and certificates cannot be used together"), e.getMessage()));
730731
}
731732

733+
@Test(groups = {"integration"})
734+
public void testOverrideSettings() throws Exception {
735+
final String clientTimezone = "America/Los_Angeles";
736+
try (Client client = newClient().setSessionTimezone(clientTimezone).build()) {
737+
738+
final BiConsumer<QuerySettings, String> checkTzStmt = (settings, timezone) -> {
739+
GenericRecord firstRecord = client.queryAll("SELECT timezone()", settings).stream().findFirst().get();
740+
Assert.assertEquals(firstRecord.getString(1), timezone);
741+
};
742+
743+
checkTzStmt.accept(new QuerySettings(), clientTimezone);
744+
745+
final String altTimezone = "America/New_York";
746+
checkTzStmt.accept(new QuerySettings().setSessionTimezone(altTimezone), altTimezone);
747+
checkTzStmt.accept(new QuerySettings().setOption(ClientConfigProperties.serverSetting("session_timezone"), null), "UTC");
748+
}
749+
}
750+
732751
public boolean isVersionMatch(String versionExpression, Client client) {
733752
List<GenericRecord> serverVersion = client.queryAll("SELECT version()");
734753
return ClickHouseVersion.of(serverVersion.get(0).getString(1)).check(versionExpression);

client-v2/src/test/java/com/clickhouse/client/SettingsTests.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ public void testInsertSettingsSpecific() throws Exception {
177177
final InsertSettings settings = new InsertSettings();
178178
settings.setDatabase("test_db1");
179179
Assert.assertEquals(settings.getDatabase(), "test_db1");
180+
settings.resetOption(ClientConfigProperties.DATABASE.getKey());
181+
Assert.assertNull(settings.getDatabase());
180182
}
181183

182184
{

docs/features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t
1111
- Proxy support: Can send requests through configured HTTP proxies, including proxy credentials.
1212
- Connection and socket tuning: Exposes pool sizing, keep-alive, reuse strategy, connect/request/socket timeouts, and low-level socket options.
1313
- Query execution: Executes SQL asynchronously and returns streaming query responses with response metadata and metrics.
14-
- 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.
14+
- 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` will not be sent to the server.
1515
- Parameterized SQL: Accepts named query parameters and can send them through supported HTTP request encodings.
1616
- Result materialization helpers: Provides streaming `Records`, generic row access, and convenience APIs that materialize all rows into generic records or typed POJOs.
1717
- Binary format readers: Reads ClickHouse binary result formats including `Native`, `RowBinary`, `RowBinaryWithNames`, and `RowBinaryWithNamesAndTypes`.

jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.clickhouse.jdbc;
22

33
import com.clickhouse.client.api.ClientConfigProperties;
4+
import com.clickhouse.client.api.Session;
45
import com.clickhouse.client.api.internal.ServerSettings;
56
import com.clickhouse.client.api.query.GenericRecord;
67
import com.clickhouse.data.ClickHouseVersion;
@@ -754,40 +755,52 @@ public void testCancelQueryWithSession() throws Exception {
754755
throw new SkipException("Cloud + HTTP doesn't work well. Enough to test locally");
755756
}
756757

757-
// Regression test for #2690: cancelling a query that runs inside a session must not fail with
758-
// "Session is locked by a concurrent client" (SESSION_IS_LOCKED). The KILL QUERY request issued by
759-
// cancel() must not carry the session id of the query being cancelled.
760758
String sessionId = "test-session-" + UUID.randomUUID();
761-
try (Connection conn = getJdbcConnection()) {
762-
try (StatementImpl stmt = (StatementImpl) conn.createStatement()) {
763-
stmt.getLocalSettings().setSessionId(sessionId);
764-
stmt.setQueryTimeout(30); // safety net so a failed cancel cannot hang the test
759+
Properties properties = new Properties();
760+
Session session = new Session();
761+
session.setSessionId(sessionId);
762+
session.applyTo(properties);
763+
try (Connection conn = getJdbcConnection(properties)) {
764+
testCancelQueryWithSessionValidation(conn, sessionId);
765+
}
765766

766-
final AtomicReference<Throwable> threadError = new AtomicReference<>();
767-
final CountDownLatch started = new CountDownLatch(1);
768-
Thread worker = new Thread(() -> {
769-
started.countDown();
770-
// Long-running query that only completes when killed.
771-
try (ResultSet rs = stmt.executeQuery("SELECT count() FROM system.numbers_mt")) {
772-
rs.next();
773-
} catch (Throwable t) {
774-
System.out.println("Error: " + t.getMessage());
775-
threadError.set(t);
776-
}
777-
});
778-
worker.start();
779-
started.await();
767+
// Test case when session id is in custom_http_header
768+
properties = new Properties();
769+
properties.put(DriverProperties.CUSTOM_HTTP_PARAMS.getKey(), "session_id=" + sessionId);
770+
try (Connection conn = getJdbcConnection(properties)) {
771+
testCancelQueryWithSessionValidation(conn, sessionId);
772+
}
773+
}
780774

781-
String queryId = waitForQueryId(stmt, 15);
782-
assertNotNull(queryId, "Query id was not assigned in time");
783-
assertTrue(waitForQueryToStart(queryId, 15), "Query did not start on the server in time");
775+
private void testCancelQueryWithSessionValidation(Connection conn, String sessionId) throws Exception {
776+
try (StatementImpl stmt = (StatementImpl) conn.createStatement()) {
777+
stmt.getLocalSettings().setSessionId(sessionId);
778+
stmt.setQueryTimeout(30); // safety net so a failed cancel cannot hang the test
779+
780+
final AtomicReference<Throwable> threadError = new AtomicReference<>();
781+
final CountDownLatch started = new CountDownLatch(1);
782+
Thread worker = new Thread(() -> {
783+
started.countDown();
784+
// Long-running query that only completes when killed.
785+
try (ResultSet rs = stmt.executeQuery("SELECT count() FROM system.numbers_mt")) {
786+
rs.next();
787+
} catch (Throwable t) {
788+
System.out.println("Error: " + t.getMessage());
789+
threadError.set(t);
790+
}
791+
});
792+
worker.start();
793+
started.await();
784794

785-
// Cancel from the main thread - must not throw SESSION_IS_LOCKED.
786-
stmt.cancel();
795+
String queryId = waitForQueryId(stmt, 15);
796+
assertNotNull(queryId, "Query id was not assigned in time");
797+
assertTrue(waitForQueryToStart(queryId, 15), "Query did not start on the server in time");
787798

788-
worker.join(TimeUnit.SECONDS.toMillis(20));
789-
assertFalse(worker.isAlive(), "Query was not cancelled and is still running");
790-
}
799+
// Cancel from the main thread - must not throw SESSION_IS_LOCKED.
800+
stmt.cancel();
801+
802+
worker.join(TimeUnit.SECONDS.toMillis(20));
803+
assertFalse(worker.isAlive(), "Query was not cancelled and is still running");
791804
}
792805
}
793806

@@ -799,7 +812,12 @@ public void testCancelInsertWithSession() throws Exception {
799812
// Regression test for #2690 covering a long-running INSERT executed inside a session.
800813
String tableName = getDatabase() + ".cancel_insert_with_session";
801814
String sessionId = "test-session-" + UUID.randomUUID();
802-
try (Connection conn = getJdbcConnection(Map.of(ASYNC_INSERT_SETTING_KEY, ServerSettings.OFF))) {
815+
Properties properties = new Properties();
816+
properties.put(ASYNC_INSERT_SETTING_KEY, ServerSettings.OFF);
817+
Session session = new Session();
818+
session.setSessionId(sessionId);
819+
session.applyTo(properties);
820+
try (Connection conn = getJdbcConnection(properties)) {
803821
try (Statement setup = conn.createStatement()) {
804822
setup.execute("DROP TABLE IF EXISTS " + tableName);
805823
setup.execute("CREATE TABLE " + tableName + " (num UInt64) ENGINE = MergeTree ORDER BY ()");

0 commit comments

Comments
 (0)