![JAR search and dependency download from the Maven repository](/logo.png)
com.github.housepower.jdbc.BalancedClickhouseDataSource Maven / Gradle / Ivy
/*
* 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.exception.InvalidValueException;
import com.github.housepower.log.Logger;
import com.github.housepower.log.LoggerFactory;
import com.github.housepower.misc.StrUtil;
import com.github.housepower.misc.Validate;
import com.github.housepower.settings.ClickHouseConfig;
import com.github.housepower.settings.SettingKey;
import com.github.housepower.jdbc.wrapper.SQLWrapper;
import java.io.PrintWriter;
import java.io.Serializable;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.sql.DataSource;
/**
* Database for clickhouse jdbc connections.
*
It has list of database urls.
* For every {@link #getConnection() getConnection} invocation, it returns connection to random host from the list.
* Furthermore, this class has method { #scheduleActualization(int, TimeUnit) scheduleActualization}
* which test hosts for availability. By default, this option is turned off.
*/
public final class BalancedClickhouseDataSource implements DataSource, SQLWrapper {
private static final Logger LOG = LoggerFactory.getLogger(BalancedClickhouseDataSource.class);
private static final Pattern URL_TEMPLATE = Pattern.compile(ClickhouseJdbcUrlParser.JDBC_CLICKHOUSE_PREFIX +
"//([a-zA-Z0-9_:,.-]+)" +
"((/[a-zA-Z0-9_]+)?" +
"([?][a-zA-Z0-9_]+[=][a-zA-Z0-9_]+([&][a-zA-Z0-9_]+[=][a-zA-Z0-9_]*)*)?" +
")?");
private PrintWriter printWriter;
private int loginTimeoutSeconds = 0;
private final ThreadLocal randomThreadLocal = new ThreadLocal<>();
private final List allUrls;
private volatile List enabledUrls;
private final ClickHouseConfig cfg;
private final ClickHouseDriver driver = new ClickHouseDriver();
/**
* create Datasource for clickhouse JDBC connections
*
* @param url address for connection to the database, must have the next format
* {@code jdbc:clickhouse://:,:/?param1=value1¶m2=value2 }
* for example, {@code jdbc:clickhouse://localhost:9000,localhost:9000/database?compress=1&decompress=2 }
* @throws IllegalArgumentException if param have not correct format,
* or error happens when checking host availability
*/
public BalancedClickhouseDataSource(String url) {
this(splitUrl(url), new Properties());
}
/**
* create Datasource for clickhouse JDBC connections
*
* @param url address for connection to the database
* @param properties database properties
* @see #BalancedClickhouseDataSource(String)
*/
public BalancedClickhouseDataSource(String url, Properties properties) {
this(splitUrl(url), properties);
}
/**
* create Datasource for clickhouse JDBC connections
*
* @param url address for connection to the database
* @param settings clickhouse settings
* @see #BalancedClickhouseDataSource(String)
*/
public BalancedClickhouseDataSource(final String url, Map settings) {
this(splitUrl(url), settings);
}
private BalancedClickhouseDataSource(final List urls, Properties properties) {
this(urls, ClickhouseJdbcUrlParser.parseProperties(properties));
}
private BalancedClickhouseDataSource(final List urls, Map settings) {
Validate.ensure(!urls.isEmpty(), "Incorrect ClickHouse jdbc url list. It must be not empty");
this.cfg = ClickHouseConfig.Builder.builder()
.withJdbcUrl(urls.get(0))
.withSettings(settings)
.host("undefined")
.port(0)
.build();
List allUrls = new ArrayList<>(urls.size());
for (final String url : urls) {
try {
if (driver.acceptsURL(url)) {
allUrls.add(url);
} else {
LOG.warn("that url is has not correct format: {}", url);
}
} catch (Exception e) {
throw new InvalidValueException("error while checking url: " + url, e);
}
}
Validate.ensure(!allUrls.isEmpty(), "there are no correct urls");
this.allUrls = Collections.unmodifiableList(allUrls);
this.enabledUrls = this.allUrls;
}
static List splitUrl(final String url) {
Matcher m = URL_TEMPLATE.matcher(url);
Validate.ensure(m.matches(), "Incorrect url: " + url);
final String database = StrUtil.getOrDefault(m.group(2), "");
String[] hosts = m.group(1).split(",");
return Arrays.stream(hosts)
.map(host -> ClickhouseJdbcUrlParser.JDBC_CLICKHOUSE_PREFIX + "//" + host + database)
.collect(Collectors.toList());
}
private boolean ping(final String url) {
try (ClickHouseConnection connection = driver.connect(url, cfg)) {
return connection.ping(Duration.ofSeconds(1));
} catch (Exception e) {
return false;
}
}
/**
* Checks if clickhouse on url is alive, if it isn't, disable url, else enable.
*
* @return number of available clickhouse urls
*/
synchronized int actualize() {
List enabledUrls = new ArrayList<>(allUrls.size());
for (String url : allUrls) {
LOG.debug("Pinging disabled url: {}", url);
if (ping(url)) {
LOG.debug("Url is alive now: {}", url);
enabledUrls.add(url);
} else {
LOG.warn("Url is dead now: {}", url);
}
}
this.enabledUrls = Collections.unmodifiableList(enabledUrls);
return enabledUrls.size();
}
private String getAnyUrl() throws SQLException {
List localEnabledUrls = enabledUrls;
if (localEnabledUrls.isEmpty()) {
throw new SQLException("Unable to get connection: there are no enabled urls");
}
Random random = this.randomThreadLocal.get();
if (random == null) {
this.randomThreadLocal.set(new Random());
random = this.randomThreadLocal.get();
}
int index = random.nextInt(localEnabledUrls.size());
return localEnabledUrls.get(index);
}
/**
* {@inheritDoc}
*/
@Override
public ClickHouseConnection getConnection() throws SQLException {
return driver.connect(getAnyUrl(), cfg);
}
/**
* {@inheritDoc}
*/
@Override
public ClickHouseConnection getConnection(String user, String password) throws SQLException {
return driver.connect(getAnyUrl(), cfg.withCredentials(user, password));
}
/**
* {@inheritDoc}
*/
@Override
public PrintWriter getLogWriter() throws SQLException {
return printWriter;
}
/**
* {@inheritDoc}
*/
@Override
public void setLogWriter(PrintWriter printWriter) throws SQLException {
this.printWriter = printWriter;
}
/**
* {@inheritDoc}
*/
@Override
public void setLoginTimeout(int seconds) throws SQLException {
loginTimeoutSeconds = seconds;
}
/**
* {@inheritDoc}
*/
@Override
public int getLoginTimeout() throws SQLException {
return loginTimeoutSeconds;
}
/**
* {@inheritDoc}
*/
@Override
public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
throw new SQLFeatureNotSupportedException();
}
public List getAllClickhouseUrls() {
return allUrls;
}
public List getEnabledClickHouseUrls() {
return enabledUrls;
}
public List getDisabledUrls() {
List enabledUrls = this.enabledUrls;
if (!hasDisabledUrls()) {
return Collections.emptyList();
}
List disabledUrls = new ArrayList<>(allUrls);
disabledUrls.removeAll(enabledUrls);
return disabledUrls;
}
public boolean hasDisabledUrls() {
return allUrls.size() != enabledUrls.size();
}
public ClickHouseConfig getCfg() {
return cfg;
}
}