Repository navigation
Description
Description
In jdbc-v2, the value set with Connection#setNetworkTimeout(executor, ms) is not applied to all requests that the connection and its statements send. Three request paths do not use the current value:
- Statements created before the call.
executeQuery()/execute()on aStatementorPreparedStatementuses the network timeout that was in effect when the statement was created. If the connection had a network timeout when the statement was created,executeUpdate()also uses that old value. - RowBinary writer inserts (
beta.row_binary_for_simple_insert=true,WriterStatementImpl).executeUpdate()andexecuteBatch()never use the connection network timeout, also for a statement created after the call. - The table-schema lookup (
DESCRIBE TABLE) thatprepareStatement()sends for a writer insert never uses the connection network timeout.
On these paths, a request can wait longer than the timeout that the application or the connection pool set. With no socket_timeout configured (default 0), it can wait without a limit. The opposite also occurs: after setNetworkTimeout(executor, 0), a statement created earlier still times out at the old value.
The java.sql.Connection#setNetworkTimeout javadoc says: "Sets the maximum period a Connection or objects created from the Connection will wait for the database to reply to any one request." Connection pools call setNetworkTimeout on live connections (see #3260 for the save / set / restore sequence), so statements that the application holds across those calls are affected.
This is a different defect from #3260 (getNetworkTimeout() returns 0). Open PR #3266 changes only getNetworkTimeout() and does not change the paths below.
Steps to reproduce
Timeout 500 ms, server response delayed by 1500 ms, socket_timeout not set. Every request that uses the network timeout must fail after about 500 ms.
createStatement(), thensetNetworkTimeout(exec, 500), thenexecuteQuery("SELECT sleep(1.5)").- With
beta.row_binary_for_simple_insert=true:setNetworkTimeout(exec, 500), thenprepareStatement("INSERT INTO nt_src VALUES (?)")on a table whose materialized view callssleep(1.5), thenexecuteUpdate(). - Compare with the same calls in the "contrast" order (statement created after the call, or writer disabled). These fail after about 500 ms, as expected.
Results on main (771b8bc), ClickHouse 26.9.1.1629. Rows marked "proxy" use a local TCP proxy between the driver and the server. The proxy holds the first response bytes of each request for 1500 ms. The other rows delay the response on the server with sleep(1.5).
| Case | Sequence | Delay | Result | Time |
|---|---|---|---|---|
| A (contrast) | setNetworkTimeout(500) → createStatement → executeQuery |
proxy | timeout ✅ | 527 ms |
| A2 (contrast) | setNetworkTimeout(500) → createStatement → executeQuery("SELECT sleep(1.5)") |
server | timeout ✅ | 504 ms |
| B | createStatement → setNetworkTimeout(500) → executeQuery |
proxy | no timeout ❌ | 1562 ms |
| B2 | createStatement → setNetworkTimeout(500) → executeQuery("SELECT sleep(1.5)") |
server | no timeout ❌ | 1508 ms |
| C | createStatement → setNetworkTimeout(500) → executeUpdate |
proxy | timeout ✅ | 503 ms |
| D | setNetworkTimeout(60000) → createStatement → setNetworkTimeout(500) → executeUpdate |
proxy | no timeout ❌ | 1569 ms |
| D2 | setNetworkTimeout(60000) → prepareStatement("SELECT ?") → setNetworkTimeout(500) → executeQuery |
proxy | no timeout ❌ | 1513 ms |
| H | setNetworkTimeout(500) → createStatement → setNetworkTimeout(0) → executeQuery |
proxy | timeout ❌ (expected no limit) | 503 ms |
| H2 (contrast) | setNetworkTimeout(500) → setNetworkTimeout(0) → createStatement → executeQuery |
proxy | no timeout ✅ | 1510 ms |
| E (contrast) | writer off: setNetworkTimeout(500) → prepareStatement(INSERT) → executeUpdate |
proxy | timeout ✅ | 505 ms |
| E3 (contrast) | writer off: same, insert into the sleep(1.5) materialized view |
server | timeout ✅ | 506 ms |
| F | writer on (WriterStatementImpl): setNetworkTimeout(500) → prepareStatement(INSERT) → executeUpdate |
proxy | no timeout ❌ | 1636 ms |
| F2 | writer on: same, addBatch ×2 → executeBatch |
proxy | no timeout ❌ | 1616 ms |
| F3 | writer on: same, insert into the sleep(1.5) materialized view |
server | no timeout ❌ | 1530 ms |
| G | writer on: setNetworkTimeout(500) → prepareStatement(INSERT) (the DESCRIBE TABLE lookup) |
proxy | no timeout ❌ | 1508 ms |
Cases B and C show that the same statement shape behaves differently for executeQuery and executeUpdate.
Error Log or Exception StackTrace
In the failing cases there is no error: the call returns normally after the full delay. In the contrast cases the driver correctly throws:
java.sql.SQLException ... caused by java.net.SocketTimeoutException: Read timed out
Expected Behaviour
After setNetworkTimeout(executor, ms), every request that the connection or a statement created from it sends uses ms as its socket timeout. This includes statements created before the call, RowBinary writer inserts, and the table-schema lookup in prepareStatement(). After setNetworkTimeout(executor, 0), no request has a network timeout (unless socket_timeout is configured).
Root cause
ConnectionImpl.setNetworkTimeout stores the value only as a socket_timeout override in the connection default settings (jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java:715). Each request gets its socket timeout only from the settings object that it passes to the client (HttpAPIClientHelper.createRequestHttpContext). Three paths do not pass the current connection settings:
StatementImpl.java:77: the constructor copies the connection defaults intolocalSettingsonce.executeQueryImpluses only that copy (StatementImpl.java:302; callers:251,:557, andPreparedStatementImpl.java:310).executeUpdateImplmerges the current connection defaults first and the copy second (StatementImpl.java:404), so an oldsocket_timeoutin the copy overrides the current value.WriterStatementImpl.java:123:executeLargeUpdatebuildsnew InsertSettings()and does not readconnection.getDefaultQuerySettings().ConnectionImpl.java:486callsclient.getTableSchema(table, schema), which builds its ownnew QuerySettings()(client-v2/.../Client.java:2397), so only the client configuration applies.
Suggested fix
- Statements: read the network timeout from the connection when the request is sent, not from the copy made in the constructor. For example, in both
executeQueryImplandexecuteUpdateImpl, merge the current connection defaults first and the statement's own overrides second, and do not keepsocket_timeoutin the per-statement copy. Statement-level overrides (database afterUSE, roles,maxRows) must continue to override the connection defaults. - Writer inserts: build the
InsertSettingsinWriterStatementImpl.executeLargeUpdatefrom the connection default settings (or at least copy their network timeout). - Schema lookup:
Client.getTableSchema(String, String)has no settings parameter. A fix needs a way to pass the connection network timeout (for example, an overload that accepts settings). This touches the client-v2 public API, so it needs a maintainer decision.
A regression test can use WireMock with a delayed response, as in ConnectionNetworkTimeoutTest from #3262 / #3266, with one case for each path above and the contrast cases (statement created after the call, writer disabled).
Code Example
Minimal version without a proxy (cases B2 and F3):
Properties p = new Properties();
p.setProperty("user", "default");
p.setProperty("password", "<password>");
Executor exec = Executors.newSingleThreadExecutor();
String url = "jdbc:clickhouse://localhost:8123/default";
// 1. Statement created before setNetworkTimeout
try (Connection c = DriverManager.getConnection(url, p)) {
Statement s = c.createStatement();
c.setNetworkTimeout(exec, 500);
s.executeQuery("SELECT sleep(1.5)").close(); // returns after ~1.5 s; expected: SQLException after ~0.5 s
}
// 2. RowBinary writer insert
// CREATE TABLE nt_src (a Int32) ENGINE = Null;
// CREATE TABLE nt_dst (a Int32, s UInt8) ENGINE = MergeTree ORDER BY a;
// CREATE MATERIALIZED VIEW nt_mv TO nt_dst AS SELECT a, sleep(1.5) AS s FROM nt_src;
p.setProperty("beta.row_binary_for_simple_insert", "true");
try (Connection c = DriverManager.getConnection(url, p)) {
c.setNetworkTimeout(exec, 500);
PreparedStatement ps = c.prepareStatement("INSERT INTO nt_src VALUES (?)"); // WriterStatementImpl
ps.setInt(1, 1);
ps.executeUpdate(); // returns after ~1.5 s; expected: SQLException after ~0.5 s
}Contrast: move createStatement() after setNetworkTimeout in (1), or remove beta.row_binary_for_simple_insert in (2). Both then throw SocketTimeoutException: Read timed out after about 500 ms.
Full repro program (all cases in the table, including the delaying proxy)
Host, port and password match our test setup (the repo's CI server config, HTTP on port 18123). The program calls new Driver().connect(...) directly only because of how it was launched (single-file source mode).
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Throwaway repro (not committed). A TCP proxy in front of the real server holds the first
* response chunk of every request for DELAY_MS while delay is enabled. Connection#setNetworkTimeout
* is set to TIMEOUT_MS < DELAY_MS, so every request that honors it must fail with a socket timeout.
*/
public class NetTimeoutRepro {
static final int DELAY_MS = 1500;
static final int TIMEOUT_MS = 500;
static final AtomicBoolean delay = new AtomicBoolean(false);
static final Executor EXEC = Executors.newCachedThreadPool(r -> { Thread t = new Thread(r); t.setDaemon(true); return t; });
public static void main(String[] args) throws Exception {
Class.forName("com.clickhouse.jdbc.Driver");
ServerSocket ss = new ServerSocket(18124);
Thread acceptor = new Thread(() -> {
while (true) {
try {
Socket c = ss.accept();
Socket s = new Socket("clickhouse", 18123);
AtomicBoolean requestSent = new AtomicBoolean(false);
pump(c.getInputStream(), s.getOutputStream(), () -> requestSent.set(true));
pump(s.getInputStream(), c.getOutputStream(), () -> {
if (delay.get() && requestSent.getAndSet(false)) sleep(DELAY_MS);
});
} catch (Exception e) { return; }
}
});
acceptor.setDaemon(true);
acceptor.start();
String proxyUrl = "jdbc:clickhouse://localhost:18124/default";
String directUrl = "jdbc:clickhouse://clickhouse:18123/default";
try (Connection c = conn(directUrl, false); Statement s = c.createStatement()) {
s.execute("DROP TABLE IF EXISTS nt_repro");
s.execute("CREATE TABLE nt_repro (a Int32) ENGINE = MergeTree ORDER BY a");
}
// A: contrast. Statement created AFTER setNetworkTimeout -> must time out.
run("A stmt created AFTER setNetworkTimeout, executeQuery", proxyUrl, false, c -> {
c.setNetworkTimeout(EXEC, TIMEOUT_MS);
Statement s = c.createStatement();
delay.set(true);
s.executeQuery("SELECT 1").close();
});
// B: statement created BEFORE setNetworkTimeout, executeQuery.
run("B stmt created BEFORE setNetworkTimeout, executeQuery", proxyUrl, false, c -> {
Statement s = c.createStatement();
c.setNetworkTimeout(EXEC, TIMEOUT_MS);
delay.set(true);
s.executeQuery("SELECT 1").close();
});
// B2: same as B, no proxy: the delay is a real server-side sleep.
run("B2 stmt created BEFORE setNetworkTimeout, executeQuery SELECT sleep(1.5) (no proxy)", directUrl, false, c -> {
Statement s = c.createStatement();
c.setNetworkTimeout(EXEC, TIMEOUT_MS);
s.executeQuery("SELECT sleep(1.5)").close();
});
// A2: contrast for B2.
run("A2 stmt created AFTER setNetworkTimeout, executeQuery SELECT sleep(1.5) (no proxy)", directUrl, false, c -> {
c.setNetworkTimeout(EXEC, TIMEOUT_MS);
Statement s = c.createStatement();
s.executeQuery("SELECT sleep(1.5)").close();
});
// C: statement created BEFORE setNetworkTimeout (never set before), executeUpdate.
run("C stmt created BEFORE setNetworkTimeout (none set before), executeUpdate", proxyUrl, false, c -> {
Statement s = c.createStatement();
c.setNetworkTimeout(EXEC, TIMEOUT_MS);
delay.set(true);
s.executeUpdate("INSERT INTO nt_repro VALUES (1)");
});
// D: setNetworkTimeout(60s) -> createStatement -> setNetworkTimeout(500ms) -> executeUpdate.
run("D setNetworkTimeout(60000) -> stmt -> setNetworkTimeout(500), executeUpdate", proxyUrl, false, c -> {
c.setNetworkTimeout(EXEC, 60_000);
Statement s = c.createStatement();
c.setNetworkTimeout(EXEC, TIMEOUT_MS);
delay.set(true);
s.executeUpdate("INSERT INTO nt_repro VALUES (2)");
});
// D2: same as D, PreparedStatement.
run("D2 setNetworkTimeout(60000) -> prepareStatement -> setNetworkTimeout(500), executeQuery", proxyUrl, false, c -> {
c.setNetworkTimeout(EXEC, 60_000);
PreparedStatement ps = c.prepareStatement("SELECT ?");
c.setNetworkTimeout(EXEC, TIMEOUT_MS);
ps.setInt(1, 1);
delay.set(true);
ps.executeQuery().close();
});
// H: reverse direction. setNetworkTimeout(500) -> stmt -> setNetworkTimeout(0) (no limit), executeQuery.
run("H setNetworkTimeout(500) -> stmt -> setNetworkTimeout(0), executeQuery", proxyUrl, false, c -> {
c.setNetworkTimeout(EXEC, TIMEOUT_MS);
Statement s = c.createStatement();
c.setNetworkTimeout(EXEC, 0);
delay.set(true);
s.executeQuery("SELECT 1").close();
});
// H2: contrast for H. Fresh statement after setNetworkTimeout(0) -> must succeed.
run("H2 setNetworkTimeout(500) -> setNetworkTimeout(0) -> stmt, executeQuery", proxyUrl, false, c -> {
c.setNetworkTimeout(EXEC, TIMEOUT_MS);
c.setNetworkTimeout(EXEC, 0);
Statement s = c.createStatement();
delay.set(true);
s.executeQuery("SELECT 1").close();
});
// E: contrast. Plain PreparedStatementImpl insert (writer off), stmt created after -> must time out.
run("E writer OFF: setNetworkTimeout -> prepareStatement INSERT, executeUpdate", proxyUrl, false, c -> {
c.setNetworkTimeout(EXEC, TIMEOUT_MS);
PreparedStatement ps = c.prepareStatement("INSERT INTO nt_repro VALUES (?)");
ps.setInt(1, 3);
delay.set(true);
ps.executeUpdate();
});
// F: writer ON (beta.row_binary_for_simple_insert=true), stmt created after setNetworkTimeout.
run("F writer ON: setNetworkTimeout -> prepareStatement INSERT, executeUpdate", proxyUrl, true, c -> {
c.setNetworkTimeout(EXEC, TIMEOUT_MS);
PreparedStatement ps = c.prepareStatement("INSERT INTO nt_repro VALUES (?)");
System.out.println(" statement class: " + ps.getClass().getSimpleName());
ps.setInt(1, 4);
delay.set(true);
ps.executeUpdate();
});
// F2: writer ON, executeBatch.
run("F2 writer ON: setNetworkTimeout -> prepareStatement INSERT, executeBatch", proxyUrl, true, c -> {
c.setNetworkTimeout(EXEC, TIMEOUT_MS);
PreparedStatement ps = c.prepareStatement("INSERT INTO nt_repro VALUES (?)");
ps.setInt(1, 5); ps.addBatch();
ps.setInt(1, 6); ps.addBatch();
delay.set(true);
ps.executeBatch();
});
// F3/E3: no proxy. Insert into a Null table whose materialized view sleeps 1.5 s.
try (Connection c = conn(directUrl, false); Statement s = c.createStatement()) {
s.execute("DROP TABLE IF EXISTS nt_mv");
s.execute("DROP TABLE IF EXISTS nt_src");
s.execute("DROP TABLE IF EXISTS nt_dst");
s.execute("CREATE TABLE nt_src (a Int32) ENGINE = Null");
s.execute("CREATE TABLE nt_dst (a Int32, s UInt8) ENGINE = MergeTree ORDER BY a");
s.execute("CREATE MATERIALIZED VIEW nt_mv TO nt_dst AS SELECT a, sleep(1.5) AS s FROM nt_src");
}
run("E3 writer OFF: setNetworkTimeout -> prepareStatement INSERT, MV sleep(1.5) (no proxy)", directUrl, false, c -> {
c.setNetworkTimeout(EXEC, TIMEOUT_MS);
PreparedStatement ps = c.prepareStatement("INSERT INTO nt_src VALUES (?)");
ps.setInt(1, 7);
ps.executeUpdate();
});
run("F3 writer ON: setNetworkTimeout -> prepareStatement INSERT, MV sleep(1.5) (no proxy)", directUrl, true, c -> {
c.setNetworkTimeout(EXEC, TIMEOUT_MS);
PreparedStatement ps = c.prepareStatement("INSERT INTO nt_src VALUES (?)");
System.out.println(" statement class: " + ps.getClass().getSimpleName());
ps.setInt(1, 8);
ps.executeUpdate();
});
// G: writer ON, the DESCRIBE TABLE schema lookup inside prepareStatement.
run("G writer ON: setNetworkTimeout -> prepareStatement INSERT (DESCRIBE lookup)", proxyUrl, true, c -> {
c.setNetworkTimeout(EXEC, TIMEOUT_MS);
delay.set(true);
c.prepareStatement("INSERT INTO nt_repro VALUES (?)");
});
try (Connection c = conn(directUrl, false); Statement s = c.createStatement()) {
java.sql.ResultSet rs = s.executeQuery("SELECT groupArray(a) FROM (SELECT a FROM nt_repro ORDER BY a)");
rs.next();
System.out.println("rows in nt_repro: " + rs.getString(1));
s.execute("DROP TABLE IF EXISTS nt_repro");
}
}
interface Body { void run(Connection c) throws Exception; }
static Connection conn(String url, boolean writer) throws Exception {
Properties p = new Properties();
p.setProperty("user", "default");
p.setProperty("password", "test_default_password");
if (writer) p.setProperty("beta.row_binary_for_simple_insert", "true");
return new com.clickhouse.jdbc.Driver().connect(url, p);
}
static void run(String name, String url, boolean writer, Body body) {
delay.set(false);
long t0 = System.nanoTime();
String outcome;
try (Connection c = conn(url, writer)) {
t0 = System.nanoTime();
body.run(c);
outcome = "SUCCEEDED (no timeout)";
} catch (Throwable e) {
Throwable r = e;
while (r.getCause() != null && r.getCause() != r) r = r.getCause();
outcome = "FAILED: " + e.getClass().getSimpleName() + " / root " + r.getClass().getName() + ": " + r.getMessage();
}
long ms = (System.nanoTime() - t0) / 1_000_000;
delay.set(false);
System.out.printf("%-92s %5d ms %s%n", name, ms, outcome);
}
static void pump(InputStream in, OutputStream out, Runnable beforeWrite) {
Thread t = new Thread(() -> {
byte[] b = new byte[8192];
try {
int n;
while ((n = in.read(b)) > 0) {
beforeWrite.run();
out.write(b, 0, n);
out.flush();
}
} catch (Exception ignored) {
} finally {
try { in.close(); } catch (Exception ignored) { }
try { out.close(); } catch (Exception ignored) { }
}
});
t.setDaemon(true);
t.start();
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) { }
}
}Configuration
Client Configuration
// Defaults. socket_timeout is not set.
// Writer cases only: beta.row_binary_for_simple_insert=trueEnvironment
- Cloud
- Client version: jdbc-v2
mainat 771b8bc (0.12.0-rc1-SNAPSHOT) - Language version: OpenJDK 17.0.20.1
- OS: Linux (Debian-based container)
ClickHouse Server
- ClickHouse Server version: 26.9.1.1629
- ClickHouse Server non-default settings, if any: none that affect this (the repo's CI test server config)
CREATE TABLEstatements for tables involved: in the code example above (nt_src,nt_dst,nt_mv); the proxy cases useCREATE TABLE nt_repro (a Int32) ENGINE = MergeTree ORDER BY a- Sample data for all these tables: not needed
Found by automated analysis (Polyglot AI) while working on #3260 / #3266. Verified at runtime against a live server (cases above), not only by reading the code.
Activity