镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

[jdbc-v2] setNetworkTimeout is not applied to statements created before the call, RowBinary writer inserts, or the writer's schema lookup #3268

Open

Description

@polyglotAI-bot

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:

  1. Statements created before the call. executeQuery() / execute() on a Statement or PreparedStatement uses 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.
  2. RowBinary writer inserts (beta.row_binary_for_simple_insert=true, WriterStatementImpl). executeUpdate() and executeBatch() never use the connection network timeout, also for a statement created after the call.
  3. The table-schema lookup (DESCRIBE TABLE) that prepareStatement() 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.

  1. createStatement(), then setNetworkTimeout(exec, 500), then executeQuery("SELECT sleep(1.5)").
  2. With beta.row_binary_for_simple_insert=true: setNetworkTimeout(exec, 500), then prepareStatement("INSERT INTO nt_src VALUES (?)") on a table whose materialized view calls sleep(1.5), then executeUpdate().
  3. 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 into localSettings once. executeQueryImpl uses only that copy (StatementImpl.java:302; callers :251, :557, and PreparedStatementImpl.java:310). executeUpdateImpl merges the current connection defaults first and the copy second (StatementImpl.java:404), so an old socket_timeout in the copy overrides the current value.
  • WriterStatementImpl.java:123: executeLargeUpdate builds new InsertSettings() and does not read connection.getDefaultQuerySettings().
  • ConnectionImpl.java:486 calls client.getTableSchema(table, schema), which builds its own new 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 executeQueryImpl and executeUpdateImpl, merge the current connection defaults first and the statement's own overrides second, and do not keep socket_timeout in the per-statement copy. Statement-level overrides (database after USE, roles, maxRows) must continue to override the connection defaults.
  • Writer inserts: build the InsertSettings in WriterStatementImpl.executeLargeUpdate from 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=true

Environment

  • Cloud
  • Client version: jdbc-v2 main at 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 TABLE statements for tables involved: in the code example above (nt_src, nt_dst, nt_mv); the proxy cases use CREATE 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions