Repository: housepower/ClickHouse-Native-JDBC
Pull request #411 (2022-03-03 04:28:22)

Extend JDBC URL pattern to support failover
This PR proposes to extend the JDBC URL pattern as follows to support failover.

`jdbc:clickhouse://host1:port1,host2:port2[/database][?prop1=value1&prop2=value2]`

To be clear, do not expect it to act as load balancing, the mechanism of failover is: 
1. try to connect the first host at the beginning
2. if the first host is unreachable, then try the second, and so on
3. when the connection is broken, go 1

codecov[bot]: # [Codecov](https://codecov.io/gh/housepower/ClickHouse-Native-JDBC/pull/411?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#411](https://codecov.io/gh/housepower/ClickHouse-Native-JDBC/pull/411?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (6f02da0) into [master](https://codecov.io/gh/housepower/ClickHouse-Native-JDBC/commit/c5d1c14784f7388a5947f42111bd49d999e7e4db?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (c5d1c14) will **increase** coverage by `0.37%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/housepower/ClickHouse-Native-JDBC/pull/411/graphs/tree.svg?width=650&height=150&src=pr&token=Izz9Qy1I72&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)](https://codecov.io/gh/housepower/ClickHouse-Native-JDBC/pull/411?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

```diff
@@             Coverage Diff              @@
##             master     #411      +/-   ##
============================================
+ Coverage     62.60%   62.98%   +0.37%     
- Complexity     1256     1262       +6     
============================================
  Files           135      135              
  Lines          6662     6659       -3     
  Branches        519      518       -1     
============================================
+ Hits           4171     4194      +23     
+ Misses         2216     2197      -19     
+ Partials        275      268       -7     
```


| [Impacted Files](https://codecov.io/gh/housepower/ClickHouse-Native-JDBC/pull/411?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [...ava/com/github/housepower/client/NativeClient.java](https://codecov.io/gh/housepower/ClickHouse-Native-JDBC/pull/411/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y2xpY2tob3VzZS1uYXRpdmUtamRiYy9zcmMvbWFpbi9qYXZhL2NvbS9naXRodWIvaG91c2Vwb3dlci9jbGllbnQvTmF0aXZlQ2xpZW50LmphdmE=) | `84.41% <100.00%> (+10.73%)` | :arrow_up: |
| [...m/github/housepower/jdbc/ClickHouseConnection.java](https://codecov.io/gh/housepower/ClickHouse-Native-JDBC/pull/411/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y2xpY2tob3VzZS1uYXRpdmUtamRiYy9zcmMvbWFpbi9qYXZhL2NvbS9naXRodWIvaG91c2Vwb3dlci9qZGJjL0NsaWNrSG91c2VDb25uZWN0aW9uLmphdmE=) | `65.35% <100.00%> (+11.14%)` | :arrow_up: |
| [...ithub/housepower/jdbc/ClickhouseJdbcUrlParser.java](https://codecov.io/gh/housepower/ClickHouse-Native-JDBC/pull/411/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y2xpY2tob3VzZS1uYXRpdmUtamRiYy9zcmMvbWFpbi9qYXZhL2NvbS9naXRodWIvaG91c2Vwb3dlci9qZGJjL0NsaWNraG91c2VKZGJjVXJsUGFyc2VyLmphdmE=) | `95.34% <100.00%> (+15.63%)` | :arrow_up: |
| [...m/github/housepower/settings/ClickHouseConfig.java](https://codecov.io/gh/housepower/ClickHouse-Native-JDBC/pull/411/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y2xpY2tob3VzZS1uYXRpdmUtamRiYy9zcmMvbWFpbi9qYXZhL2NvbS9naXRodWIvaG91c2Vwb3dlci9zZXR0aW5ncy9DbGlja0hvdXNlQ29uZmlnLmphdmE=) | `77.41% <100.00%> (+0.29%)` | :arrow_up: |
| [...m/github/housepower/buffer/SocketBuffedReader.java](https://codecov.io/gh/housepower/ClickHouse-Native-JDBC/pull/411/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y2xpY2tob3VzZS1uYXRpdmUtamRiYy9zcmMvbWFpbi9qYXZhL2NvbS9naXRodWIvaG91c2Vwb3dlci9idWZmZXIvU29ja2V0QnVmZmVkUmVhZGVyLmphdmE=) | `78.57% <0.00%> (+3.57%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/housepower/ClickHouse-Native-JDBC/pull/411?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/housepower/ClickHouse-Native-JDBC/pull/411?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Last update [c5d1c14...6f02da0](https://codecov.io/gh/housepower/ClickHouse-Native-JDBC/pull/411?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

baibaichen: LGTM
pan3793: Thanks a lot to @loneylee for bringing this amazing feature, I update the PR description to make it clear, and please double-check if I misunderstand something.
loneylee: I am honored to be able to help the community, and if there are issues related to this issue in the future, I am willing to assist in solving it
sundy-li: LGTM, Let's merge !

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 e6f4a5f..27da677 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 19c5e91..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;
@@ -28,6 +29,7 @@ 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;
 
@@ -38,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, () -> {
@@ -115,6 +181,36 @@ 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();
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);
+    }
+}