Repository: housepower/ClickHouse-Native-JDBC
Pull request #412 (2022-03-06 09:49:34)

Extend JDBC URL pattern to support failover (#411)
* support failover

* fix pattern and add ut

* fix ut wrong pair host and port


Patch:
diff --git a/clickhouse-native-jdbc/src/main/java/com/github/housepower/client/NativeClient.java b/clickhouse-native-jdbc/src/main/java/com/github/housepower/client/NativeClient.java
index 6eb4fd9..1409563 100644
--- a/clickhouse-native-jdbc/src/main/java/com/github/housepower/client/NativeClient.java
+++ b/clickhouse-native-jdbc/src/main/java/com/github/housepower/client/NativeClient.java
@@ -45,8 +45,12 @@ public class NativeClient {
     private static final Logger LOG = LoggerFactory.getLogger(NativeClient.class);
 
     public static NativeClient connect(ClickHouseConfig configure) throws SQLException {
+        return connect(configure.host(), configure.port(), configure);
+    }
+
+    public static NativeClient connect(String host, int port, ClickHouseConfig configure) throws SQLException {
         try {
-            SocketAddress endpoint = new InetSocketAddress(configure.host(), configure.port());
+            SocketAddress endpoint = new InetSocketAddress(host, port);
             // TODO support proxy
             Socket socket = new Socket();
             socket.setTcpNoDelay(true);
diff --git a/clickhouse-native-jdbc/src/main/java/com/github/housepower/jdbc/ClickHouseConnection.java b/clickhouse-native-jdbc/src/main/java/com/github/housepower/jdbc/ClickHouseConnection.java
index 1d5665a..df671ff 100644
--- a/clickhouse-native-jdbc/src/main/java/com/github/housepower/jdbc/ClickHouseConnection.java
+++ b/clickhouse-native-jdbc/src/main/java/com/github/housepower/jdbc/ClickHouseConnection.java
@@ -33,7 +33,17 @@ import com.github.housepower.stream.QueryResult;
 
 import javax.annotation.Nullable;
 import java.net.InetSocketAddress;
-import java.sql.*;
+import java.sql.Array;
+import java.sql.ClientInfoStatus;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLClientInfoException;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.sql.Statement;
+import java.sql.Struct;
 import java.time.Duration;
 import java.time.ZoneId;
 import java.util.HashMap;
@@ -45,6 +55,8 @@ import java.util.concurrent.atomic.AtomicReference;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import static com.github.housepower.jdbc.ClickhouseJdbcUrlParser.PORT_DELIMITER;
+
 public class ClickHouseConnection implements SQLConnection {
 
     private static final Logger LOG = LoggerFactory.getLogger(ClickHouseConnection.class);
@@ -305,7 +317,43 @@ public class ClickHouseConnection implements SQLConnection {
     }
 
     private static NativeContext createNativeContext(ClickHouseConfig configure) throws SQLException {
-        NativeClient nativeClient = NativeClient.connect(configure);
+        if (configure.hosts().size() == 1) {
+            NativeClient nativeClient = NativeClient.connect(configure);
+            return new NativeContext(clientContext(nativeClient, configure), serverContext(nativeClient, configure), nativeClient);
+        }
+
+        return createFailoverNativeContext(configure);
+    }
+
+    private static NativeContext createFailoverNativeContext(ClickHouseConfig configure) throws SQLException {
+        NativeClient nativeClient = null;
+        SQLException lastException = null;
+
+        int tryIndex = 0;
+        do {
+            String hostAndPort = configure.hosts().get(tryIndex);
+            String[] hostAndPortSplit = hostAndPort.split(PORT_DELIMITER, 2);
+            String host = hostAndPortSplit[0];
+            int port;
+
+            if (hostAndPortSplit.length == 2) {
+                port = Integer.parseInt(hostAndPortSplit[1]);
+            } else {
+                port = configure.port();
+            }
+
+            try {
+                nativeClient = NativeClient.connect(host, port, configure);
+            } catch (SQLException e) {
+                lastException = e;
+            }
+            tryIndex++;
+        } while (nativeClient == null && tryIndex < configure.hosts().size());
+
+        if (nativeClient == null) {
+            throw lastException;
+        }
+
         return new NativeContext(clientContext(nativeClient, configure), serverContext(nativeClient, configure), nativeClient);
     }
 
diff --git a/clickhouse-native-jdbc/src/main/java/com/github/housepower/jdbc/ClickhouseJdbcUrlParser.java b/clickhouse-native-jdbc/src/main/java/com/github/housepower/jdbc/ClickhouseJdbcUrlParser.java
index a6f04fd..51b5ee4 100644
--- a/clickhouse-native-jdbc/src/main/java/com/github/housepower/jdbc/ClickhouseJdbcUrlParser.java
+++ b/clickhouse-native-jdbc/src/main/java/com/github/housepower/jdbc/ClickhouseJdbcUrlParser.java
@@ -15,15 +15,17 @@
 package com.github.housepower.jdbc;
 
 import com.github.housepower.exception.InvalidValueException;
-import com.github.housepower.misc.Validate;
-import com.github.housepower.settings.SettingKey;
 import com.github.housepower.log.Logger;
 import com.github.housepower.log.LoggerFactory;
+import com.github.housepower.misc.Validate;
+import com.github.housepower.settings.SettingKey;
 
 import java.io.Serializable;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.*;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.StringTokenizer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -32,92 +34,65 @@ public class ClickhouseJdbcUrlParser {
     public static final String CLICKHOUSE_PREFIX = "clickhouse:";
     public static final String JDBC_CLICKHOUSE_PREFIX = JDBC_PREFIX + CLICKHOUSE_PREFIX;
 
-    public static final Pattern DB_PATH_PATTERN = Pattern.compile("/([a-zA-Z0-9_]+)");
-    public static final Pattern HOST_PORT_PATH_PATTERN = Pattern.compile("//(?<host>[^/:\\s]+)(:(?<port>\\d+))?");
+    public static final String HOST_DELIMITER = ",";
+    public static final String PORT_DELIMITER = ":";
+
+    /**
+     * Jdbc Url sames like:
+     * '//[host1][:port1],[host2][:port2],[host3][:port3]]...[/[database]][?propertyName1=propertyValue1[&propertyName2=propertyValue2]...]'
+     *
+     * Default_port is used when port does not exist.
+     */
+    public static final Pattern CONNECTION_PATTERN = Pattern.compile("//(?<hosts>([^/?:,\\s]+(:\\d+)?)(,[^/?:,\\s]+(:\\d+)?)*)" // hosts: required; starts with "//" followed by any char except "/", "?"
+            + "(?:/(?<database>([a-zA-Z0-9_]+)))?" // database: optional; starts with "/", and then followed by any char except "?"
+            + "(?:\\?(?<properties>.*))?"); // properties: optional; starts with "?", and then followed by any char
 
     private static final Logger LOG = LoggerFactory.getLogger(ClickhouseJdbcUrlParser.class);
 
     public static Map<SettingKey, Serializable> parseJdbcUrl(String jdbcUrl) {
-        try {
-            URI uri = new URI(jdbcUrl.substring(JDBC_PREFIX.length()));
-            String host = parseHost(jdbcUrl);
-            Integer port = parsePort(jdbcUrl);
-            String database = parseDatabase(jdbcUrl);
-            Map<SettingKey, Serializable> settings = new HashMap<>();
-            settings.put(SettingKey.host, host);
-            settings.put(SettingKey.port, port);
-            settings.put(SettingKey.database, database);
-            settings.putAll(extractQueryParameters(uri.getQuery()));
-
-            return settings;
-        } catch (URISyntaxException ex) {
-            throw new InvalidValueException(ex);
+        String uri = jdbcUrl.substring(JDBC_CLICKHOUSE_PREFIX.length());
+        Matcher matcher = CONNECTION_PATTERN.matcher(uri);
+        if (!matcher.matches()) {
+            throw new InvalidValueException("Connection is not support");
         }
-    }
 
-    public static Map<SettingKey, Serializable> parseProperties(Properties properties) {
         Map<SettingKey, Serializable> settings = new HashMap<>();
 
-        for (String name : properties.stringPropertyNames()) {
-            String value = properties.getProperty(name);
+        String hosts = matcher.group("hosts");
+        String database = matcher.group("database");
+        String properties = matcher.group("properties");
 
-            parseSetting(settings, name, value);
-        }
+        if (hosts.contains(HOST_DELIMITER)) { // multi-host
+            settings.put(SettingKey.host, hosts);
+        } else { // standard-host
+            String[] hostAndPort = hosts.split(PORT_DELIMITER, 2);
 
-        return settings;
-    }
+            settings.put(SettingKey.host, hostAndPort[0]);
 
-    private static String parseDatabase(String jdbcUrl) throws URISyntaxException {
-        URI uri = new URI(jdbcUrl.substring(JDBC_PREFIX.length()));
-        String database = uri.getPath();
-        if (database != null && !database.isEmpty()) {
-            Matcher m = DB_PATH_PATTERN.matcher(database);
-            if (m.matches()) {
-                database = m.group(1);
-            } else {
-                throw new URISyntaxException("wrong database name path: '" + database + "'", jdbcUrl);
+            if (hostAndPort.length == 2) {
+                if (Integer.parseInt(hostAndPort[1]) == 8123) {
+                    LOG.warn("8123 is default HTTP port, you may connect with error protocol!");
+                }
+                settings.put(SettingKey.port, Integer.parseInt(hostAndPort[1]));
             }
         }
-        if (database != null && database.isEmpty()) {
-            database = "default";
-        }
-        return database;
-    }
 
-    private static String parseHost(String jdbcUrl) throws URISyntaxException {
-        String uriStr = jdbcUrl.substring(JDBC_PREFIX.length());
-        URI uri = new URI(uriStr);
-        String host = uri.getHost();
-        if (host == null || host.isEmpty()) {
-            Matcher m = HOST_PORT_PATH_PATTERN.matcher(uriStr);
-            if (m.find()) {
-                host = m.group("host");
-            } else {
-                throw new URISyntaxException("No valid host was found", jdbcUrl);
-            }
-        }
-        return host;
+        settings.put(SettingKey.database, database);
+        settings.putAll(extractQueryParameters(properties));
+
+        return settings;
     }
 
-    private static int parsePort(String jdbcUrl) {
-        String uriStr = jdbcUrl.substring(JDBC_PREFIX.length());
-        URI uri;
-        try {
-            uri = new URI(uriStr);
-        } catch (Exception ex) {
-            throw new InvalidValueException(ex);
-        }
-        int port = uri.getPort();
-        if (port <= -1) {
-            Matcher m = HOST_PORT_PATH_PATTERN.matcher(uriStr);
-            if (m.find() && m.group("port") != null) {
-                port = Integer.parseInt(m.group("port"));
-            }
-        }
-        if (port == 8123) {
-            LOG.warn("8123 is default HTTP port, you may connect with error protocol!");
+    public static Map<SettingKey, Serializable> parseProperties(Properties properties) {
+        Map<SettingKey, Serializable> settings = new HashMap<>();
+
+        for (String name : properties.stringPropertyNames()) {
+            String value = properties.getProperty(name);
+
+            parseSetting(settings, name, value);
         }
-        return port;
+
+        return settings;
     }
 
     public static Map<SettingKey, Serializable> extractQueryParameters(String queryParameters) {
diff --git a/clickhouse-native-jdbc/src/main/java/com/github/housepower/settings/ClickHouseConfig.java b/clickhouse-native-jdbc/src/main/java/com/github/housepower/settings/ClickHouseConfig.java
index e04156d..cece71e 100644
--- a/clickhouse-native-jdbc/src/main/java/com/github/housepower/settings/ClickHouseConfig.java
+++ b/clickhouse-native-jdbc/src/main/java/com/github/housepower/settings/ClickHouseConfig.java
@@ -22,15 +22,21 @@ import javax.annotation.concurrent.Immutable;
 import java.io.Serializable;
 import java.nio.charset.Charset;
 import java.time.Duration;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
 
+import static com.github.housepower.jdbc.ClickhouseJdbcUrlParser.HOST_DELIMITER;
+
 @Immutable
 public class ClickHouseConfig implements Serializable {
 
+
     private final String host;
+    private final List<String> hosts;
     private final int port;
     private final String database;
     private final String user;
@@ -46,6 +52,7 @@ public class ClickHouseConfig implements Serializable {
                              Duration queryTimeout, Duration connectTimeout, boolean tcpKeepAlive,
                              String charset, String clientName, Map<SettingKey, Serializable> settings) {
         this.host = host;
+        this.hosts = Arrays.asList(host.split(HOST_DELIMITER));
         this.port = port;
         this.database = database;
         this.user = user;
@@ -62,6 +69,10 @@ public class ClickHouseConfig implements Serializable {
         return this.host;
     }
 
+    public List<String> hosts() {
+        return this.hosts;
+    }
+
     public int port() {
         return this.port;
     }
@@ -96,7 +107,13 @@ public class ClickHouseConfig implements Serializable {
 
     public String jdbcUrl() {
         StringBuilder builder = new StringBuilder(ClickhouseJdbcUrlParser.JDBC_CLICKHOUSE_PREFIX)
-                .append("//").append(host).append(":").append(port).append("/").append(database)
+                .append("//").append(host);
+
+        if (hosts.size() == 1) {
+            builder.append(":").append(port);
+        }
+
+        builder.append("/").append(database)
                 .append("?").append(SettingKey.query_timeout.name()).append("=").append(queryTimeout.getSeconds())
                 .append("&").append(SettingKey.connect_timeout.name()).append("=").append(connectTimeout.getSeconds())
                 .append("&").append(SettingKey.charset.name()).append("=").append(charset)


Test patch:
diff --git a/clickhouse-native-jdbc/src/test/java/com/github/housepower/jdbc/ConnectionParamITest.java b/clickhouse-native-jdbc/src/test/java/com/github/housepower/jdbc/ConnectionParamITest.java
index 55ffbfa..6b8bb56 100644
--- a/clickhouse-native-jdbc/src/test/java/com/github/housepower/jdbc/ConnectionParamITest.java
+++ b/clickhouse-native-jdbc/src/test/java/com/github/housepower/jdbc/ConnectionParamITest.java
@@ -14,6 +14,7 @@
 
 package com.github.housepower.jdbc;
 
+import com.github.housepower.exception.InvalidValueException;
 import com.github.housepower.settings.ClickHouseConfig;
 import com.github.housepower.settings.SettingKey;
 import org.junit.jupiter.api.BeforeEach;
@@ -25,9 +26,12 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.time.Duration;
 import java.util.Locale;
+import java.util.UUID;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class ConnectionParamITest extends AbstractITest {
 
@@ -36,6 +40,70 @@ public class ConnectionParamITest extends AbstractITest {
         resetDriverManager();
     }
 
+    @Test
+    public void connectionPatternTest() {
+        String[] jdbcFalse = new String[]{
+                "//ck1:",
+                "//,ck1",
+                "//ck1,ck2,",
+                "//ck1:,ck2",
+                "//ck1,ck2/"
+        };
+
+        for (String jdbc : jdbcFalse) {
+            assertFalse(ClickhouseJdbcUrlParser.CONNECTION_PATTERN.matcher(jdbc).matches());
+        }
+
+        String[] jdbcTrue = new String[]{
+                "//ch1?max_rows_to_read=1&connect_timeout=10",
+                "//ch1/default?max_rows_to_read=1&connect_timeout=10",
+                "//ch1:1234?max_rows_to_read=1&connect_timeout=10",
+                "//ch1,ch2?max_rows_to_read=1&connect_timeout=10",
+                "//ch1,ch2:1234?max_rows_to_read=1&connect_timeout=10",
+                "//ch1:1234,ch2:1234?max_rows_to_read=1&connect_timeout=10",
+                "//ch1:1234,ch2:1234/default?max_rows_to_read=1&connect_timeout=10",
+                "//ch1:1234,ch2:1234,ch3:2222/default?max_rows_to_read=1&connect_timeout=10",
+                "//ch1:1234,ch2,ch3/default?max_rows_to_read=1&connect_timeout=10"
+        };
+
+        for (String jdbc : jdbcTrue) {
+            ClickHouseConfig cfg = ClickHouseConfig.Builder.builder()
+                    .withJdbcUrl(ClickhouseJdbcUrlParser.JDBC_CLICKHOUSE_PREFIX + jdbc)
+                    .build();
+
+            if (jdbc.contains("?")) {
+                assertEquals(cfg.connectTimeout().getSeconds(), 10);
+                assertEquals((Long) cfg.settings().get(SettingKey.max_rows_to_read), 1L);
+            }
+
+            if (jdbc.contains("/")) {
+                assertEquals("default", cfg.database());
+            }
+
+            if (jdbc.contains(",")) {
+                assertEquals(jdbc.split(",").length, cfg.hosts().size());
+            }
+        }
+
+        String[] fullJdbc = new String[]{
+                "//ch1:9000/default?query_timeout=1&connect_timeout=10&charset=UTF-8&client_name=test&tcp_keep_alive=true",
+                "//ch1:9001/default?query_timeout=1&connect_timeout=10&charset=UTF-8&client_name=test&tcp_keep_alive=true",
+                "//ch1,ch2:9001/default?query_timeout=1&connect_timeout=10&charset=UTF-8&client_name=test&tcp_keep_alive=false",
+                "//ch1:9001,ch2:9002/default?query_timeout=1&connect_timeout=10&charset=UTF-8&client_name=test&tcp_keep_alive=false"
+        };
+
+        for (String jdbc : fullJdbc) {
+            jdbc = ClickhouseJdbcUrlParser.JDBC_CLICKHOUSE_PREFIX + jdbc;
+            ClickHouseConfig cfg = ClickHouseConfig.Builder.builder()
+                    .withJdbcUrl(jdbc)
+                    .build();
+
+            assertEquals(jdbc, cfg.jdbcUrl());
+        }
+
+    }
+
+
     @Test
     public void successfullyMaxRowsToRead() {
         assertThrows(SQLException.class, () -> {
@@ -113,4 +181,75 @@ public class ConnectionParamITest extends AbstractITest {
         assertEquals(Duration.ofSeconds(50), config.connectTimeout());
     }
 
+    @Test
+    public void successfullyFailoverHostNameWithCustomPort() {
+        String url = "jdbc:clickhouse://my_clickhouse_sever_host_name1:1940,my_clickhouse_sever_host_name2:1941/system?min_insert_block_size_rows=1000&connect_timeout=50";
+        ClickHouseConfig config = ClickHouseConfig.Builder.builder().withJdbcUrl(url).build();
+        assertEquals("my_clickhouse_sever_host_name1:1940,my_clickhouse_sever_host_name2:1941", config.host());
+        assertEquals(2, config.hosts().size());
+        assertEquals(9000, config.port());
+        assertEquals("system", config.database());
+        assertEquals(1000L, config.settings().get(SettingKey.min_insert_block_size_rows));
+        assertEquals(Duration.ofSeconds(50), config.connectTimeout());
+    }
+
+    @Test
+    public void successfullyFailoverHostNameWithDefaultPort() {
+        String url = "jdbc:clickhouse://my_clickhouse_sever_host_name1,my_clickhouse_sever_host_name2/system?min_insert_block_size_rows=1000&connect_timeout=50";
+        ClickHouseConfig config = ClickHouseConfig.Builder.builder().withJdbcUrl(url).build();
+        assertEquals("my_clickhouse_sever_host_name1,my_clickhouse_sever_host_name2", config.host());
+        assertEquals(2, config.hosts().size());
+        assertEquals(9000, config.port());
+        assertEquals("system", config.database());
+        assertEquals(1000L, config.settings().get(SettingKey.min_insert_block_size_rows));
+        assertEquals(Duration.ofSeconds(50), config.connectTimeout());
+    }
+
+    @Test
+    public void successWrongUrlParser() {
+        String url = "jdbc:clickhouse://127.0.0. 1/system?min_insert_block_size_rows=1000&connect_timeout=50";
+        assertThrows(InvalidValueException.class, () -> ClickHouseConfig.Builder.builder().withJdbcUrl(url).build());
+    }
+
+    @Test
+    public void successWithQueryId() throws Exception {
+        String queryId = UUID.randomUUID().toString();
+
+        try (Connection connection = DriverManager
+                .getConnection(String.format(Locale.ROOT, "jdbc:clickhouse://%s:%s", CK_HOST, CK_PORT))
+        ) {
+            withStatement(connection, stmt -> {
+                stmt.execute("SELECT 1");
+                Thread.sleep(12 * 1000); // wait insert into ch log table
+                ResultSet rs = stmt.executeQuery(String.format(Locale.ROOT, "SELECT count() FROM system.query_log where query_id='%s'", queryId));
+
+                long rows = 0;
+
+                if (rs.next()) {
+                    rows = rs.getLong(1);
+                }
+
+                assertTrue(rows == 0);
+            });
+        }
+
+        try (Connection connection = DriverManager
+                .getConnection(String.format(Locale.ROOT, "jdbc:clickhouse://%s:%s?query_id=%s", CK_HOST, CK_PORT,
+                        queryId))
+        ) {
+            withStatement(connection, stmt -> {
+                stmt.execute("SELECT 1");
+                Thread.sleep(12 * 1000); // wait insert into ch log table
+                ResultSet rs = stmt.executeQuery(String.format(Locale.ROOT, "SELECT count() FROM system.query_log where query_id='%s'", queryId));
+
+                long rows = 0;
+
+                if (rs.next()) {
+                    rows = rs.getLong(1);
+                }
+
+                assertTrue(rows > 0);
+            });
+        }
+    }
 }
diff --git a/clickhouse-native-jdbc/src/test/java/com/github/housepower/jdbc/FailoverClickhouseConnectionITest.java b/clickhouse-native-jdbc/src/test/java/com/github/housepower/jdbc/FailoverClickhouseConnectionITest.java
new file mode 100644
index 0000000..887dd45
--- /dev/null
+++ b/clickhouse-native-jdbc/src/test/java/com/github/housepower/jdbc/FailoverClickhouseConnectionITest.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.github.housepower.jdbc;
+
+import com.github.housepower.log.Logger;
+import com.github.housepower.log.LoggerFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.ClickHouseContainer;
+import org.testcontainers.junit.jupiter.Container;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Locale;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class FailoverClickhouseConnectionITest extends AbstractITest {
+    private static final Logger LOG = LoggerFactory.getLogger(FailoverClickhouseConnectionITest.class);
+
+    protected static String HA_HOST;
+    protected static int HA_PORT;
+
+    @Container
+    public static ClickHouseContainer containerHA = (ClickHouseContainer) new ClickHouseContainer(CLICKHOUSE_IMAGE)
+            .withEnv("CLICKHOUSE_USER", CLICKHOUSE_USER)
+            .withEnv("CLICKHOUSE_PASSWORD", CLICKHOUSE_PASSWORD)
+            .withEnv("CLICKHOUSE_DB", CLICKHOUSE_DB);
+
+
+    @BeforeEach
+    public void reset() throws SQLException {
+        resetDriverManager();
+        container.start();
+        containerHA.start();
+
+        CK_PORT = container.getMappedPort(ClickHouseContainer.NATIVE_PORT);
+        HA_HOST = containerHA.getHost();
+        HA_PORT = containerHA.getMappedPort(ClickHouseContainer.NATIVE_PORT);
+        LOG.info("Port1 {}, Port2 {}", CK_PORT, HA_PORT);
+    }
+
+    @Test
+    public void testClickhouseDownBeforeConnect() throws Exception {
+        String haHost = String.format(Locale.ROOT, "%s:%s,%s:%s", CK_HOST, CK_PORT, HA_HOST, HA_PORT);
+
+        container.stop();
+        try (Connection connection = DriverManager
+                .getConnection(String.format(Locale.ROOT, "jdbc:clickhouse://%s/default", haHost))
+        ) {
+            withStatement(connection, stmt -> {
+                ResultSet rs = stmt.executeQuery("select count() from system.tables");
+
+                if (rs.next()) {
+                    assertTrue(rs.getLong(1) > 0);
+                }
+            });
+        }
+    }
+
+    @Test
+    public void testClickhouseDownBeforeStatement() throws Exception {
+        String haHost = String.format(Locale.ROOT, "%s:%s,%s:%s", CK_HOST, CK_PORT, HA_HOST, HA_PORT);
+
+        try (Connection connection = DriverManager
+                .getConnection(String.format(Locale.ROOT, "jdbc:clickhouse://%s/default", haHost))
+        ) {
+            container.stop();
+            withStatement(connection, stmt -> {
+                ResultSet rs = stmt.executeQuery("select count() from system.tables");
+
+                if (rs.next()) {
+                    assertTrue(rs.getLong(1) > 0);
+                }
+            });
+        }
+    }
+
+    @Test
+    public void testClickhouseDownBeforePrepareStatement() throws Exception {
+        String haHost = String.format(Locale.ROOT, "%s:%s,%s:%s", CK_HOST, CK_PORT, HA_HOST, HA_PORT);
+
+        try (Connection connection = DriverManager
+                .getConnection(String.format(Locale.ROOT, "jdbc:clickhouse://%s/default", haHost))
+        ) {
+            container.stop();
+            withPreparedStatement(connection, "select count() from system.tables", stmt -> {
+                ResultSet rs = stmt.executeQuery();
+
+                if (rs.next()) {
+                    assertTrue(rs.getLong(1) > 0);
+                }
+            });
+        }
+    }
+
+    @Test
+    public void testClickhouseDownBeforeExecute() throws Exception {
+        String haHost = String.format(Locale.ROOT, "%s:%s,%s:%s", CK_HOST, CK_PORT, HA_HOST, HA_PORT);
+
+        try (Connection connection = DriverManager
+                .getConnection(String.format(Locale.ROOT, "jdbc:clickhouse://%s/default", haHost))
+        ) {
+            withStatement(connection, stmt -> {
+                container.stop();
+                ResultSet rs = stmt.executeQuery("select count() from system.tables");
+
+                if (rs.next()) {
+                    assertTrue(rs.getLong(1) > 0);
+                }
+            });
+        }
+    }
+
+    @Test
+    public void testClickhouseDownBeforeAndAfterConnect() {
+        String haHost = String.format(Locale.ROOT, "%s:%s,%s:%s", CK_HOST, CK_PORT, HA_HOST, HA_PORT);
+
+        Exception ex = null;
+        container.stop();
+        try (Connection connection = DriverManager
+                .getConnection(String.format(Locale.ROOT, "jdbc:clickhouse://%s/default?query_id=xxx", haHost))
+        ) {
+            containerHA.stop();
+            withStatement(connection, stmt -> {
+                ResultSet rs = stmt.executeQuery("select count() from system.tables");
+
+                if (rs.next()) {
+                    assertTrue(rs.getLong(1) > 0);
+                }
+            });
+        } catch (Exception e) {
+            ex = e;
+        }
+
+        assertNotNull(ex);
+    }
+
+    @Test
+    public void testClickhouseAllDownBeforeConnect() throws Exception {
+        String haHost = String.format(Locale.ROOT, "%s:%s,%s", CK_HOST, CK_PORT, HA_HOST);
+
+        Exception ex = null;
+        container.stop();
+        containerHA.stop();
+        try (Connection connection = DriverManager
+                .getConnection(String.format(Locale.ROOT, "jdbc:clickhouse://%s/default", haHost))
+        ) {
+            withStatement(connection, stmt -> {
+
+                ResultSet rs = stmt.executeQuery("select count() from system.tables");
+
+                if (rs.next()) {
+                    assertTrue(rs.getLong(1) > 0);
+                }
+            });
+        } catch (Exception e) {
+            ex = e;
+        }
+
+        assertNotNull(ex);
+    }
+}