diff --git a/src/main/java/com/alibaba/druid/filter/FilterChain.java b/src/main/java/com/alibaba/druid/filter/FilterChain.java index 681b211ff8..9b63e082d7 100644 --- a/src/main/java/com/alibaba/druid/filter/FilterChain.java +++ b/src/main/java/com/alibaba/druid/filter/FilterChain.java @@ -62,6 +62,8 @@ public interface FilterChain { int getFilterSize(); + int getPos(); + FilterChain cloneChain(); T unwrap(Wrapper wrapper, java.lang.Class iface) throws java.sql.SQLException; diff --git a/src/main/java/com/alibaba/druid/filter/FilterChainImpl.java b/src/main/java/com/alibaba/druid/filter/FilterChainImpl.java index e1279e3d24..c9542f7c5f 100644 --- a/src/main/java/com/alibaba/druid/filter/FilterChainImpl.java +++ b/src/main/java/com/alibaba/druid/filter/FilterChainImpl.java @@ -92,6 +92,10 @@ public class FilterChainImpl implements FilterChain { return filterSize; } + public int getPos() { + return pos; + } + public void reset() { pos = 0; } diff --git a/src/main/java/com/alibaba/druid/filter/config/ConfigFilter.java b/src/main/java/com/alibaba/druid/filter/config/ConfigFilter.java index 2b7d75d6dd..fbd6280f95 100644 --- a/src/main/java/com/alibaba/druid/filter/config/ConfigFilter.java +++ b/src/main/java/com/alibaba/druid/filter/config/ConfigFilter.java @@ -111,12 +111,12 @@ public class ConfigFilter extends FilterAdapter { } DruidDataSource dataSource = (DruidDataSource) dataSourceProxy; - Properties connectinProperties = dataSource.getConnectProperties(); + Properties connectionProperties = dataSource.getConnectProperties(); - Properties configFileProperties = loadPropertyFromConfigFile(connectinProperties); + Properties configFileProperties = loadPropertyFromConfigFile(connectionProperties); // 判断是否需要解密,如果需要就进行解密行动 - boolean decrypt = isDecrypt(connectinProperties, configFileProperties); + boolean decrypt = isDecrypt(connectionProperties, configFileProperties); if (configFileProperties == null) { if (decrypt) { @@ -136,8 +136,8 @@ public class ConfigFilter extends FilterAdapter { } } - public boolean isDecrypt(Properties connectinProperties, Properties configFileProperties) { - String decrypterId = connectinProperties.getProperty(CONFIG_DECRYPT); + public boolean isDecrypt(Properties connectionProperties, Properties configFileProperties) { + String decrypterId = connectionProperties.getProperty(CONFIG_DECRYPT); if (decrypterId == null || decrypterId.length() == 0) { if (configFileProperties != null) { decrypterId = configFileProperties.getProperty(CONFIG_DECRYPT); @@ -151,8 +151,8 @@ public class ConfigFilter extends FilterAdapter { return Boolean.valueOf(decrypterId); } - Properties loadPropertyFromConfigFile(Properties connectinProperties) { - String configFile = connectinProperties.getProperty(CONFIG_FILE); + Properties loadPropertyFromConfigFile(Properties connectionProperties) { + String configFile = connectionProperties.getProperty(CONFIG_FILE); if (configFile == null) { configFile = System.getProperty(SYS_PROP_CONFIG_FILE); @@ -206,14 +206,14 @@ public class ConfigFilter extends FilterAdapter { } } - public PublicKey getPublicKey(Properties connectinProperties, Properties configFileProperties) { + public PublicKey getPublicKey(Properties connectionProperties, Properties configFileProperties) { String key = null; if (configFileProperties != null) { key = configFileProperties.getProperty(CONFIG_KEY); } - if (StringUtils.isEmpty(key) && connectinProperties != null) { - key = connectinProperties.getProperty(CONFIG_KEY); + if (StringUtils.isEmpty(key) && connectionProperties != null) { + key = connectionProperties.getProperty(CONFIG_KEY); } if (StringUtils.isEmpty(key)) { diff --git a/src/main/java/com/alibaba/druid/pool/ha/DataSourceCreator.java b/src/main/java/com/alibaba/druid/pool/ha/DataSourceCreator.java new file mode 100644 index 0000000000..b73be23af3 --- /dev/null +++ b/src/main/java/com/alibaba/druid/pool/ha/DataSourceCreator.java @@ -0,0 +1,155 @@ +package com.alibaba.druid.pool.ha; + +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.support.logging.Log; +import com.alibaba.druid.support.logging.LogFactory; + +import javax.sql.DataSource; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * An utility class to create DruidDataSource dynamically. + * + * @author DigitalSonic + */ +public class DataSourceCreator { + private final static Log LOG = LogFactory.getLog(DataSourceCreator.class); + + private Properties properties = new Properties(); + private List nameList = new ArrayList(); + + public DataSourceCreator(String file) { + loadProperties(file); + loadNameList(); + } + + public Map createMap(HighAvailableDataSource haDataSource) throws SQLException { + Map map = new ConcurrentHashMap(); + + if (nameList == null || nameList.isEmpty()) { + LOG.error("No DataSource will be created!"); + return map; + } + + for (String n : nameList) { + String url = properties.getProperty(n + ".url"); + String username = properties.getProperty(n + ".username"); + String password = properties.getProperty(n + ".password"); + LOG.info("Creating " + n + " with url[" + url + "] and username[" + username + "]."); + DruidDataSource dataSource = create(n, url, username, password, haDataSource); + map.put(n, dataSource); + } + LOG.info(map.size() + " DruidDataSource(s) created. "); + return map; + } + + protected DruidDataSource create(String name, String url, String username, String password, + HighAvailableDataSource haDataSource) throws SQLException { + DruidDataSource dataSource = new DruidDataSource(); + + dataSource.setName(name); + dataSource.setUrl(url); + dataSource.setUsername(username); + dataSource.setPassword(password); + + dataSource.setDriverClassName(haDataSource.getDriverClassName()); + dataSource.setConnectProperties(haDataSource.getConnectProperties()); + dataSource.setConnectionProperties(haDataSource.getConnectionProperties()); + + dataSource.setInitialSize(haDataSource.getInitialSize()); + dataSource.setMaxActive(haDataSource.getMaxActive()); + dataSource.setMinIdle(haDataSource.getMinIdle()); + dataSource.setMaxWait(haDataSource.getMaxWait()); + + dataSource.setValidationQuery(haDataSource.getValidationQuery()); + dataSource.setValidationQueryTimeout(haDataSource.getValidationQueryTimeout()); + dataSource.setTestOnBorrow(haDataSource.isTestOnBorrow()); + dataSource.setTestOnReturn(haDataSource.isTestOnReturn()); + dataSource.setTestWhileIdle(haDataSource.isTestWhileIdle()); + + dataSource.setPoolPreparedStatements(haDataSource.isPoolPreparedStatements()); + dataSource.setSharePreparedStatements(haDataSource.isSharePreparedStatements()); + dataSource.setMaxPoolPreparedStatementPerConnectionSize( + haDataSource.getMaxPoolPreparedStatementPerConnectionSize()); + + dataSource.setQueryTimeout(haDataSource.getQueryTimeout()); + dataSource.setTransactionQueryTimeout(haDataSource.getTransactionQueryTimeout()); + + dataSource.setTimeBetweenEvictionRunsMillis(haDataSource.getTimeBetweenEvictionRunsMillis()); + dataSource.setMinEvictableIdleTimeMillis(haDataSource.getMinEvictableIdleTimeMillis()); + dataSource.setMaxEvictableIdleTimeMillis(haDataSource.getMaxEvictableIdleTimeMillis()); + dataSource.setPhyTimeoutMillis(haDataSource.getPhyTimeoutMillis()); + dataSource.setTimeBetweenConnectErrorMillis(haDataSource.getTimeBetweenConnectErrorMillis()); + + dataSource.setRemoveAbandoned(haDataSource.isRemoveAbandoned()); + dataSource.setRemoveAbandonedTimeoutMillis(haDataSource.getRemoveAbandonedTimeoutMillis()); + dataSource.setLogAbandoned(haDataSource.isLogAbandoned()); + + dataSource.setProxyFilters(haDataSource.getProxyFilters()); + dataSource.setFilters(haDataSource.getFilters()); + dataSource.setLogWriter(haDataSource.getLogWriter()); + + dataSource.init(); + + return dataSource; + } + + private void loadNameList() { + Set names = new HashSet(); + for (String n : properties.stringPropertyNames()) { + if (n.contains(".")) { + names.add(n.split("\\.")[0]); + } + } + if (!names.isEmpty()) { + nameList.addAll(names); + } + } + + private void loadProperties(String file) { + Properties properties = new Properties(); + if (file == null) { + return; + } + InputStream is = null; + try { + LOG.debug("Trying to load " + file + " from FileSystem."); + is = new FileInputStream(file); + } catch(FileNotFoundException e) { + LOG.debug("Trying to load " + file + " from Classpath."); + try { + is = DataSourceCreator.class.getResourceAsStream(file); + } catch (Exception ex) { + LOG.warn("Can not load resource " + file, ex); + } + } + if (is != null) { + try { + properties.load(is); + this.properties = properties; + } catch(Exception e) { + LOG.error("Exception occurred while loading " + file, e); + } finally { + if (is != null) { + try { + is.close(); + } catch (Exception e) { + // ignore + } + } + } + } else { + LOG.warn("File " + file + " can't be loaded!"); + } + } +} diff --git a/src/main/java/com/alibaba/druid/pool/ha/HighAvailableDataSource.java b/src/main/java/com/alibaba/druid/pool/ha/HighAvailableDataSource.java new file mode 100644 index 0000000000..e8ef1ccd51 --- /dev/null +++ b/src/main/java/com/alibaba/druid/pool/ha/HighAvailableDataSource.java @@ -0,0 +1,385 @@ +package com.alibaba.druid.pool.ha; + +import com.alibaba.druid.filter.Filter; +import com.alibaba.druid.pool.DruidAbstractDataSource; +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.pool.WrapperAdapter; +import com.alibaba.druid.pool.ha.selector.DataSourceSelector; +import com.alibaba.druid.pool.ha.selector.DataSourceSelectorFactory; +import com.alibaba.druid.pool.ha.selector.RandomDataSourceSelector; +import com.alibaba.druid.support.logging.Log; +import com.alibaba.druid.support.logging.LogFactory; + +import javax.sql.DataSource; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.Logger; + +/** + * DataSource class which contains multiple DataSource objects. + * + * @author DigitalSonic + */ +public class HighAvailableDataSource extends WrapperAdapter implements DataSource { + private final static Log LOG = LogFactory.getLog(HighAvailableDataSource.class); + private final static String DEFAULT_DATA_SOURCE_FILE = "ha-datasource.properties"; + + // Properties copied from DruidAbstractDataSource BEGIN + private String driverClassName; + private Properties connectProperties = new Properties(); + private String connectionProperties = null; + + private int initialSize = DruidAbstractDataSource.DEFAULT_INITIAL_SIZE; + private int maxActive = DruidAbstractDataSource.DEFAULT_MAX_ACTIVE_SIZE; + private int minIdle = DruidAbstractDataSource.DEFAULT_MIN_IDLE; + private long maxWait = DruidAbstractDataSource.DEFAULT_MAX_WAIT; + + private String validationQuery = DruidAbstractDataSource.DEFAULT_VALIDATION_QUERY; + private int validationQueryTimeout = -1; + private boolean testOnBorrow = DruidAbstractDataSource.DEFAULT_TEST_ON_BORROW; + private boolean testOnReturn = DruidAbstractDataSource.DEFAULT_TEST_ON_RETURN; + private boolean testWhileIdle = DruidAbstractDataSource.DEFAULT_WHILE_IDLE; + + private boolean poolPreparedStatements = false; + private boolean sharePreparedStatements = false; + private int maxPoolPreparedStatementPerConnectionSize = 10; + + private int queryTimeout; + private int transactionQueryTimeout; + + private long timeBetweenEvictionRunsMillis = DruidAbstractDataSource.DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS; + private long minEvictableIdleTimeMillis = DruidAbstractDataSource.DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS; + private long maxEvictableIdleTimeMillis = DruidAbstractDataSource.DEFAULT_MAX_EVICTABLE_IDLE_TIME_MILLIS; + private long phyTimeoutMillis = DruidAbstractDataSource.DEFAULT_PHY_TIMEOUT_MILLIS; + private long timeBetweenConnectErrorMillis = DruidAbstractDataSource.DEFAULT_TIME_BETWEEN_CONNECT_ERROR_MILLIS; + + private boolean removeAbandoned; + private long removeAbandonedTimeoutMillis = 300 * 1000; + private boolean logAbandoned; + + private String filters; + private List proxyFilters = new CopyOnWriteArrayList(); + private PrintWriter logWriter = new PrintWriter(System.out); + // Properties copied from DruidAbstractDataSource END + + private Map dataSourceMap = new ConcurrentHashMap(); + private DataSourceSelector selector = new RandomDataSourceSelector(this); + private String dataSourceFile = DEFAULT_DATA_SOURCE_FILE; + + public void init() throws SQLException { + synchronized (this) { + if (dataSourceMap == null || dataSourceMap.isEmpty()) { + dataSourceMap = new DataSourceCreator(dataSourceFile).createMap(this); + } + if (selector == null) { + selector = new RandomDataSourceSelector(this); + } + } + } + + public void destroy() { + if (dataSourceMap == null || dataSourceMap.isEmpty()) { + return; + } + for (DataSource dataSource : dataSourceMap.values()) { + if (dataSource instanceof DruidDataSource) { + ((DruidDataSource) dataSource).close(); + } + } + } + + public void setTargetDataSource(String targetName) { + selector.setTarget(targetName); + } + + @Override + public Connection getConnection() throws SQLException { + DataSource dataSource = selector.get(); + return dataSource.getConnection(); + } + + public String getDataSourceFile() { + return dataSourceFile; + } + + public void setDataSourceFile(String dataSourceFile) { + this.dataSourceFile = dataSourceFile; + } + + public void setDataSourceMap(Map dataSourceMap) { + if (dataSourceMap != null) { + this.dataSourceMap = dataSourceMap; + } + } + + public Map getDataSourceMap() { + return dataSourceMap; + } + + public void setSelector(String name) { + if (name != null && this.selector != null && this.selector.isSame(name)) { + return; + } + DataSourceSelector selector = DataSourceSelectorFactory.getSelector(name, this); + if (selector != null) { + this.selector = selector; + } + } + + public DataSourceSelector getSelector() { + return selector; + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + throw new UnsupportedOperationException("Not supported by DruidDataSource"); + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + return logWriter; + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + this.logWriter = out; + } + + @Override + public void setLoginTimeout(int seconds) { + DriverManager.setLoginTimeout(seconds); + } + + @Override + public int getLoginTimeout() { + return DriverManager.getLoginTimeout(); + } + + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException(); + } + + public String getDriverClassName() { + return driverClassName; + } + + public void setDriverClassName(String driverClassName) { + this.driverClassName = driverClassName; + } + + public Properties getConnectProperties() { + return connectProperties; + } + + public void setConnectProperties(Properties connectProperties) { + this.connectProperties = connectProperties; + } + + public int getInitialSize() { + return initialSize; + } + + public void setInitialSize(int initialSize) { + this.initialSize = initialSize; + } + + public int getMaxActive() { + return maxActive; + } + + public void setMaxActive(int maxActive) { + this.maxActive = maxActive; + } + + public int getMinIdle() { + return minIdle; + } + + public void setMinIdle(int minIdle) { + this.minIdle = minIdle; + } + + public long getMaxWait() { + return maxWait; + } + + public void setMaxWait(long maxWait) { + this.maxWait = maxWait; + } + + public String getValidationQuery() { + return validationQuery; + } + + public void setValidationQuery(String validationQuery) { + this.validationQuery = validationQuery; + } + + public int getValidationQueryTimeout() { + return validationQueryTimeout; + } + + public void setValidationQueryTimeout(int validationQueryTimeout) { + this.validationQueryTimeout = validationQueryTimeout; + } + + public boolean isTestOnBorrow() { + return testOnBorrow; + } + + public void setTestOnBorrow(boolean testOnBorrow) { + this.testOnBorrow = testOnBorrow; + } + + public boolean isTestOnReturn() { + return testOnReturn; + } + + public void setTestOnReturn(boolean testOnReturn) { + this.testOnReturn = testOnReturn; + } + + public boolean isTestWhileIdle() { + return testWhileIdle; + } + + public void setTestWhileIdle(boolean testWhileIdle) { + this.testWhileIdle = testWhileIdle; + } + + public boolean isPoolPreparedStatements() { + return poolPreparedStatements; + } + + public void setPoolPreparedStatements(boolean poolPreparedStatements) { + this.poolPreparedStatements = poolPreparedStatements; + } + + public boolean isSharePreparedStatements() { + return sharePreparedStatements; + } + + public void setSharePreparedStatements(boolean sharePreparedStatements) { + this.sharePreparedStatements = sharePreparedStatements; + } + + public int getMaxPoolPreparedStatementPerConnectionSize() { + return maxPoolPreparedStatementPerConnectionSize; + } + + public void setMaxPoolPreparedStatementPerConnectionSize(int maxPoolPreparedStatementPerConnectionSize) { + this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize; + } + + public int getQueryTimeout() { + return queryTimeout; + } + + public void setQueryTimeout(int queryTimeout) { + this.queryTimeout = queryTimeout; + } + + public int getTransactionQueryTimeout() { + return transactionQueryTimeout; + } + + public void setTransactionQueryTimeout(int transactionQueryTimeout) { + this.transactionQueryTimeout = transactionQueryTimeout; + } + + public long getTimeBetweenEvictionRunsMillis() { + return timeBetweenEvictionRunsMillis; + } + + public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) { + this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis; + } + + public long getMinEvictableIdleTimeMillis() { + return minEvictableIdleTimeMillis; + } + + public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) { + this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis; + } + + public long getMaxEvictableIdleTimeMillis() { + return maxEvictableIdleTimeMillis; + } + + public void setMaxEvictableIdleTimeMillis(long maxEvictableIdleTimeMillis) { + this.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis; + } + + public long getPhyTimeoutMillis() { + return phyTimeoutMillis; + } + + public void setPhyTimeoutMillis(long phyTimeoutMillis) { + this.phyTimeoutMillis = phyTimeoutMillis; + } + + public long getTimeBetweenConnectErrorMillis() { + return timeBetweenConnectErrorMillis; + } + + public void setTimeBetweenConnectErrorMillis(long timeBetweenConnectErrorMillis) { + this.timeBetweenConnectErrorMillis = timeBetweenConnectErrorMillis; + } + + public boolean isRemoveAbandoned() { + return removeAbandoned; + } + + public void setRemoveAbandoned(boolean removeAbandoned) { + this.removeAbandoned = removeAbandoned; + } + + public long getRemoveAbandonedTimeoutMillis() { + return removeAbandonedTimeoutMillis; + } + + public void setRemoveAbandonedTimeoutMillis(long removeAbandonedTimeoutMillis) { + this.removeAbandonedTimeoutMillis = removeAbandonedTimeoutMillis; + } + + public boolean isLogAbandoned() { + return logAbandoned; + } + + public void setLogAbandoned(boolean logAbandoned) { + this.logAbandoned = logAbandoned; + } + + public String getConnectionProperties() { + return connectionProperties; + } + + public void setConnectionProperties(String connectionProperties) { + this.connectionProperties = connectionProperties; + } + + public String getFilters() { + return filters; + } + + public void setFilters(String filters) { + this.filters = filters; + } + + public List getProxyFilters() { + return proxyFilters; + } + + public void setProxyFilters(List proxyFilters) { + this.proxyFilters = proxyFilters; + } +} diff --git a/src/main/java/com/alibaba/druid/pool/ha/selector/DataSourceSelector.java b/src/main/java/com/alibaba/druid/pool/ha/selector/DataSourceSelector.java new file mode 100644 index 0000000000..af6099dc33 --- /dev/null +++ b/src/main/java/com/alibaba/druid/pool/ha/selector/DataSourceSelector.java @@ -0,0 +1,16 @@ +package com.alibaba.druid.pool.ha.selector; + +import javax.sql.DataSource; +import java.util.Map; + +/** + * Interface for those selector to implement. + * e.g. Random and Named + * + * @author DigitalSonic + */ +public interface DataSourceSelector { + DataSource get(); + void setTarget(String name); + boolean isSame(String name); +} diff --git a/src/main/java/com/alibaba/druid/pool/ha/selector/DataSourceSelectorFactory.java b/src/main/java/com/alibaba/druid/pool/ha/selector/DataSourceSelectorFactory.java new file mode 100644 index 0000000000..469e619731 --- /dev/null +++ b/src/main/java/com/alibaba/druid/pool/ha/selector/DataSourceSelectorFactory.java @@ -0,0 +1,19 @@ +package com.alibaba.druid.pool.ha.selector; + +import com.alibaba.druid.pool.ha.HighAvailableDataSource; + +/** + * A Factory pattern for DataSourceSelector. + * + * @author DigitalSonic + */ +public class DataSourceSelectorFactory { + public static DataSourceSelector getSelector(String name, HighAvailableDataSource highAvailableDataSource) { + if ("random".equalsIgnoreCase(name)) { + return new RandomDataSourceSelector(highAvailableDataSource); + } else if ("byName".equalsIgnoreCase(name)) { + return new NamedDataSourceSelector(highAvailableDataSource); + } + return null; + } +} diff --git a/src/main/java/com/alibaba/druid/pool/ha/selector/NamedDataSourceSelector.java b/src/main/java/com/alibaba/druid/pool/ha/selector/NamedDataSourceSelector.java new file mode 100644 index 0000000000..42d1f04c2a --- /dev/null +++ b/src/main/java/com/alibaba/druid/pool/ha/selector/NamedDataSourceSelector.java @@ -0,0 +1,74 @@ +package com.alibaba.druid.pool.ha.selector; + +import com.alibaba.druid.pool.ha.HighAvailableDataSource; + +import javax.sql.DataSource; +import java.util.Map; + +/** + * Use the given name in ThreadLocal variable to choose DataSource. + * + * @author DigitalSonic + */ +public class NamedDataSourceSelector implements DataSourceSelector { + public static final String DEFAULT_NAME = "default"; + private HighAvailableDataSource highAvailableDataSource; + private ThreadLocal targetDataSourceName = new ThreadLocal(); + private String defaultName = DEFAULT_NAME; + + public NamedDataSourceSelector(HighAvailableDataSource highAvailableDataSource) { + this.highAvailableDataSource = highAvailableDataSource; + } + + @Override + public boolean isSame(String name) { + return "byName".equalsIgnoreCase(name); + } + + @Override + public DataSource get() { + if (highAvailableDataSource == null) { + return null; + } + + Map dataSourceMap = highAvailableDataSource.getDataSourceMap(); + if (dataSourceMap == null || dataSourceMap.isEmpty()) { + return null; + } + if (dataSourceMap.size() == 1) { + for (DataSource v : dataSourceMap.values()) { + return v; + } + } + String name = getTarget(); + if (name == null) { + if (dataSourceMap.get(getDefaultName()) != null) { + return dataSourceMap.get(getDefaultName()); + } + } else { + return dataSourceMap.get(name); + } + return null; + } + + @Override + public void setTarget(String name) { + targetDataSourceName.set(name); + } + + public String getTarget() { + return targetDataSourceName.get(); + } + + public void resetDataSourceName() { + targetDataSourceName.remove(); + } + + public String getDefaultName() { + return defaultName; + } + + public void setDefaultName(String defaultName) { + this.defaultName = defaultName; + } +} diff --git a/src/main/java/com/alibaba/druid/pool/ha/selector/RandomDataSourceRecoverThread.java b/src/main/java/com/alibaba/druid/pool/ha/selector/RandomDataSourceRecoverThread.java new file mode 100644 index 0000000000..b0a6b52a36 --- /dev/null +++ b/src/main/java/com/alibaba/druid/pool/ha/selector/RandomDataSourceRecoverThread.java @@ -0,0 +1,74 @@ +package com.alibaba.druid.pool.ha.selector; + +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.support.logging.Log; +import com.alibaba.druid.support.logging.LogFactory; +import com.alibaba.druid.util.JdbcUtils; + +import javax.sql.DataSource; +import java.sql.Connection; + +/** + * A Thread trying to test if DataSource in blacklist has been recovered. + * + * @author DigitalSonic + */ +public class RandomDataSourceRecoverThread implements Runnable { + private final static Log LOG = LogFactory.getLog(RandomDataSourceRecoverThread.class); + private RandomDataSourceSelector selector; + private int sleepSeconds = 30; + + public RandomDataSourceRecoverThread(RandomDataSourceSelector selector) { + this.selector = selector; + } + + @Override + public void run() { + while (true) { + if (selector != null && selector.getBlacklist() != null + && !selector.getBlacklist().isEmpty()) { + LOG.info(selector.getBlacklist().size() + " DataSource in blacklist."); + for (DataSource dataSource : selector.getBlacklist()) { + if (!(dataSource instanceof DruidDataSource)) { + continue; + } + tryOneDataSource((DruidDataSource) dataSource); + } + } + sleep(); + } + } + + private void tryOneDataSource(DruidDataSource dataSource) { + if (dataSource == null) { + return; + } + Connection connection = null; + try { + connection = dataSource.getConnection(); + dataSource.validateConnection(connection); + LOG.info(dataSource.getName() + " is available now."); + selector.removeBlacklist(dataSource); + } catch(Exception e) { + LOG.warn("DataSource[" + dataSource.getName() + "] is still unavailable. Exception: " + + e.getMessage()); + if (connection != null) { + dataSource.discardConnection(connection); + } + } finally { + JdbcUtils.close(connection); + } + } + + private void sleep() { + try { + Thread.sleep(sleepSeconds * 1000); + } catch (InterruptedException e) { + // ignore + } + } + + public void setSleepSeconds(int sleepSeconds) { + this.sleepSeconds = sleepSeconds; + } +} diff --git a/src/main/java/com/alibaba/druid/pool/ha/selector/RandomDataSourceSelector.java b/src/main/java/com/alibaba/druid/pool/ha/selector/RandomDataSourceSelector.java new file mode 100644 index 0000000000..71def65eea --- /dev/null +++ b/src/main/java/com/alibaba/druid/pool/ha/selector/RandomDataSourceSelector.java @@ -0,0 +1,105 @@ +package com.alibaba.druid.pool.ha.selector; + +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.pool.ha.HighAvailableDataSource; + +import javax.sql.DataSource; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A selector which uses java.util.Random to choose DataSource. + * + * @author DigitalSonic + */ +public class RandomDataSourceSelector implements DataSourceSelector { + private Random random = new Random(); + private List blacklist = new CopyOnWriteArrayList(); + private HighAvailableDataSource highAvailableDataSource; + private RandomDataSourceValidateThread validateThread; + private RandomDataSourceRecoverThread recoverThread; + + public RandomDataSourceSelector(HighAvailableDataSource highAvailableDataSource) { + this.highAvailableDataSource = highAvailableDataSource; + validateThread = new RandomDataSourceValidateThread(this); + recoverThread = new RandomDataSourceRecoverThread(this); + new Thread(validateThread, "RandomDataSourceSelector-validate-thread").start(); + new Thread(recoverThread, "RandomDataSourceSelector-recover-thread").start(); + } + + @Override + public boolean isSame(String name) { + return "random".equalsIgnoreCase(name); + } + + @Override + public DataSource get() { + Map dataSourceMap = getDataSourceMap(); + if (dataSourceMap == null || dataSourceMap.isEmpty()) { + return null; + } + + Collection targetDataSourceSet; + if (blacklist == null || blacklist.isEmpty() || blacklist.size() >= dataSourceMap.size()) { + targetDataSourceSet = dataSourceMap.values(); + } else { + targetDataSourceSet = new HashSet(dataSourceMap.values()); + for (DataSource b : blacklist) { + targetDataSourceSet.remove(b); + } + } + + DataSource[] dataSources = targetDataSourceSet.toArray(new DataSource[] {}); + if (dataSources != null && dataSources.length > 0) { + return dataSources[random.nextInt(targetDataSourceSet.size())]; + } + return null; + } + + @Override + public void setTarget(String name) { + // do nothing + } + + public Map getDataSourceMap() { + if (highAvailableDataSource != null) { + return highAvailableDataSource.getDataSourceMap(); + } + return new HashMap(); + } + + public List getBlacklist() { + return blacklist; + } + + public void addBlacklist(DataSource dataSource) { + if (dataSource != null && !blacklist.contains(dataSource)) { + blacklist.add(dataSource); + if (dataSource instanceof DruidDataSource) { + ((DruidDataSource) dataSource).setTestOnReturn(true); + } + } + } + + public void removeBlacklist(DataSource dataSource) { + if (dataSource != null && blacklist.contains(dataSource)) { + blacklist.remove(dataSource); + if (dataSource instanceof DruidDataSource) { + ((DruidDataSource) dataSource).setTestOnReturn(highAvailableDataSource.isTestOnReturn()); + } + } + } + + public RandomDataSourceValidateThread getValidateThread() { + return validateThread; + } + + public RandomDataSourceRecoverThread getRecoverThread() { + return recoverThread; + } +} diff --git a/src/main/java/com/alibaba/druid/pool/ha/selector/RandomDataSourceValidateThread.java b/src/main/java/com/alibaba/druid/pool/ha/selector/RandomDataSourceValidateThread.java new file mode 100644 index 0000000000..6ccc85e44b --- /dev/null +++ b/src/main/java/com/alibaba/druid/pool/ha/selector/RandomDataSourceValidateThread.java @@ -0,0 +1,114 @@ +package com.alibaba.druid.pool.ha.selector; + +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.support.logging.Log; +import com.alibaba.druid.support.logging.LogFactory; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * A Thread trying to test all DataSource provided by HADataSource. + * If a DataSource failed this test for 3 times, it will be put into a blacklist. + * + * @author DigitalSonic + */ +public class RandomDataSourceValidateThread implements Runnable { + private final static Log LOG = LogFactory.getLog(RandomDataSourceValidateThread.class); + private int sleepSeconds = 30; + private int blacklistThreshold = 3; + + private RandomDataSourceSelector selector; + private Map errorCounts = new HashMap(); + + public RandomDataSourceValidateThread(RandomDataSourceSelector selector) { + this.selector = selector; + } + + @Override + public void run() { + while (true) { + if (selector != null) { + checkAllDataSources(); + maintainBlacklist(); + } + try { + Thread.sleep(sleepSeconds * 1000); + } catch (InterruptedException e) { + // ignore + } + } + } + + private void maintainBlacklist() { + Map dataSourceMap = selector.getDataSourceMap(); + for (Map.Entry e : errorCounts.entrySet()) { + if (e.getValue() <= 0) { + selector.removeBlacklist(dataSourceMap.get(e.getKey())); + } else if (e.getValue() >= blacklistThreshold) { + LOG.warn("Adding " + e.getKey() + " to blacklist."); + selector.addBlacklist(dataSourceMap.get(e.getKey())); + } + } + } + + private void checkAllDataSources() { + Map dataSourceMap = selector.getDataSourceMap(); + for (Map.Entry e : dataSourceMap.entrySet()) { + if (!(e.getValue() instanceof DruidDataSource)) { + continue; + } + boolean flag = check(e.getKey(), (DruidDataSource) e.getValue()); + + if (flag) { + errorCounts.put(e.getKey(), 0); + } else { + if (!errorCounts.containsKey(e.getKey())) { + errorCounts.put(e.getKey(), 0); + } + int count = errorCounts.get(e.getKey()); + errorCounts.put(e.getKey(), count + 1); + } + } + } + + private boolean check(String name, DruidDataSource dataSource) { + boolean result = true; + Driver driver = dataSource.getRawDriver(); + Properties info = dataSource.getConnectProperties(); + String url = dataSource.getRawJdbcUrl(); + Connection conn = null; + + try { + LOG.debug("Validating " + name + " every " + sleepSeconds + " seconds."); + conn = driver.connect(url, info); + dataSource.validateConnection(conn); + } catch (SQLException e) { + LOG.warn("Validation FAILED for " + name + ". Exception: " + e.getMessage()); + result = false; + } finally { + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + LOG.error("Can not close connection for HostAndPort Validation.", e); + } + } + } + + return result; + } + + public void setSleepSeconds(int sleepSeconds) { + this.sleepSeconds = sleepSeconds; + } + + public void setBlacklistThreshold(int blacklistThreshold) { + this.blacklistThreshold = blacklistThreshold; + } +} diff --git a/src/test/java/com/alibaba/druid/pool/ha/DataSourceCreatorTest.java b/src/test/java/com/alibaba/druid/pool/ha/DataSourceCreatorTest.java new file mode 100644 index 0000000000..f3052702b6 --- /dev/null +++ b/src/test/java/com/alibaba/druid/pool/ha/DataSourceCreatorTest.java @@ -0,0 +1,22 @@ +package com.alibaba.druid.pool.ha; + +import org.junit.Test; + +import javax.sql.DataSource; +import java.util.Map; + +import static org.junit.Assert.*; + +public class DataSourceCreatorTest { + @Test + public void createMap() throws Exception { + HighAvailableDataSource highAvailableDataSource = new HighAvailableDataSource(); + String file = "/com/alibaba/druid/pool/ha/ha-datasource.properties"; + DataSourceCreator creator = new DataSourceCreator(file); + Map map = creator.createMap(highAvailableDataSource); + assertEquals(2, map.size()); + assertNotNull(map.get("foo")); + assertNotNull(map.get("bar")); + } + +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/druid/pool/ha/HighAvailableDataSourceTest.java b/src/test/java/com/alibaba/druid/pool/ha/HighAvailableDataSourceTest.java new file mode 100644 index 0000000000..28291352c8 --- /dev/null +++ b/src/test/java/com/alibaba/druid/pool/ha/HighAvailableDataSourceTest.java @@ -0,0 +1,47 @@ +package com.alibaba.druid.pool.ha; + +import com.alibaba.druid.mock.MockConnection; +import org.junit.Test; + +import javax.sql.DataSource; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class HighAvailableDataSourceTest { + @Test + public void testByNameGet() throws Exception { + HighAvailableDataSource haDataSource = new HighAvailableDataSource(); + Map map = new HashMap(); + map.put("foo", new MockDataSource("foo")); + map.put("bar", new MockDataSource("bar")); + haDataSource.setDataSourceMap(map); + haDataSource.setSelector("byname"); + haDataSource.init(); + haDataSource.setTargetDataSource("foo"); + assertNotNull(haDataSource.getConnection()); + } + + @Test + public void testRandomGet() throws Exception { + HighAvailableDataSource haDataSource = new HighAvailableDataSource(); + Map map = new HashMap(); + map.put("foo", new MockDataSource("foo")); + map.put("bar", new MockDataSource("bar")); + int[] count = new int[2]; + + haDataSource.setDataSourceMap(map); + for (int i = 0; i < 100; i++) { + String name = ((MockConnection)haDataSource.getConnection()).getUrl(); + if (name.equalsIgnoreCase("foo")) { + count[0]++; + } else if (name.equalsIgnoreCase("bar")) { + count[1]++; + } + } + assertTrue(count[0] > 0); + assertTrue(count[1] > 0); + } +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/druid/pool/ha/MockDataSource.java b/src/test/java/com/alibaba/druid/pool/ha/MockDataSource.java new file mode 100644 index 0000000000..70341b91a0 --- /dev/null +++ b/src/test/java/com/alibaba/druid/pool/ha/MockDataSource.java @@ -0,0 +1,57 @@ +package com.alibaba.druid.pool.ha; + +import com.alibaba.druid.mock.MockConnection; +import com.alibaba.druid.pool.WrapperAdapter; + +import javax.sql.DataSource; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.logging.Logger; + +public class MockDataSource extends WrapperAdapter implements DataSource { + private String name; + + public MockDataSource(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public Connection getConnection() throws SQLException { + return new MockConnection(null, name, null); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + return null; + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + return null; + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException { + + } + + @Override + public int getLoginTimeout() throws SQLException { + return 0; + } + + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return null; + } +} diff --git a/src/test/java/com/alibaba/druid/pool/ha/selector/NamedDataSourceSelectorTest.java b/src/test/java/com/alibaba/druid/pool/ha/selector/NamedDataSourceSelectorTest.java new file mode 100644 index 0000000000..41e552de1a --- /dev/null +++ b/src/test/java/com/alibaba/druid/pool/ha/selector/NamedDataSourceSelectorTest.java @@ -0,0 +1,64 @@ +package com.alibaba.druid.pool.ha.selector; + +import com.alibaba.druid.pool.ha.HighAvailableDataSource; +import com.alibaba.druid.pool.ha.MockDataSource; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.sql.DataSource; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class NamedDataSourceSelectorTest { + private Map dataSourceMap; + private HighAvailableDataSource dataSource; + + @Before + public void setUp() { + dataSourceMap = new HashMap(); + dataSourceMap.put("foo", new MockDataSource("foo")); + dataSourceMap.put("bar", new MockDataSource("bar")); + dataSource = new HighAvailableDataSource(); + dataSource.setDataSourceMap(dataSourceMap); + } + + @After + public void tearDown() { + dataSourceMap = null; + dataSource = null; + } + + @Test + public void testEmptyMap() { + dataSourceMap.clear(); + NamedDataSourceSelector selector = new NamedDataSourceSelector(null); + assertNull(selector.get()); + selector = new NamedDataSourceSelector(dataSource); + assertNull(selector.get()); + } + + @Test + public void testOnlyOne() { + dataSourceMap.remove("foo"); + NamedDataSourceSelector selector = new NamedDataSourceSelector(dataSource); + for (int i = 0; i < 50; i++) { + assertEquals("bar", ((MockDataSource)selector.get()).getName()); + } + } + + @Test + public void testGetByName() throws Exception { + NamedDataSourceSelector selector = new NamedDataSourceSelector(dataSource); + + assertNull(selector.get()); + selector.setTarget("foo"); + assertEquals("foo", ((MockDataSource)selector.get()).getName()); + selector.resetDataSourceName(); + selector.setDefaultName("bar"); + assertEquals("bar", ((MockDataSource)selector.get()).getName()); + } +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/druid/pool/ha/selector/RandomDataSourceSelectorTest.java b/src/test/java/com/alibaba/druid/pool/ha/selector/RandomDataSourceSelectorTest.java new file mode 100644 index 0000000000..0421ef3c40 --- /dev/null +++ b/src/test/java/com/alibaba/druid/pool/ha/selector/RandomDataSourceSelectorTest.java @@ -0,0 +1,55 @@ +package com.alibaba.druid.pool.ha.selector; + +import com.alibaba.druid.pool.ha.HighAvailableDataSource; +import com.alibaba.druid.pool.ha.MockDataSource; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertTrue; + +public class RandomDataSourceSelectorTest { + private Map dataSourceMap; + private HighAvailableDataSource dataSource; + + @Before + public void setUp() { + dataSourceMap = new HashMap(); + for (int i = 0; i < 10; i++) { + dataSourceMap.put(Integer.toString(i), new MockDataSource(Integer.toString(i))); + } + dataSource = new HighAvailableDataSource(); + dataSource.setDataSourceMap(dataSourceMap); + } + + @After + public void tearDown() { + dataSourceMap = null; + dataSource = null; + } + + @Test + public void testRandomGet() throws Exception { + RandomDataSourceSelector selector = new RandomDataSourceSelector(dataSource); + int[] count = new int[10]; + + for (int i = 0; i < 100; i++) { + MockDataSource dataSource = (MockDataSource) selector.get(); + count[Integer.parseInt(dataSource.getName())]++; + } + + for (int i : count) { + assertTrue(i > 0); + } + } + + @Test + public void testOneDataSourceFail() { + + } + +} \ No newline at end of file diff --git a/src/test/java/com/alibaba/druid/pool/ha/selector/RandomDataSourceSelectorWithValidationTest.java b/src/test/java/com/alibaba/druid/pool/ha/selector/RandomDataSourceSelectorWithValidationTest.java new file mode 100644 index 0000000000..6a10653abb --- /dev/null +++ b/src/test/java/com/alibaba/druid/pool/ha/selector/RandomDataSourceSelectorWithValidationTest.java @@ -0,0 +1,86 @@ +package com.alibaba.druid.pool.ha.selector; + +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.pool.ha.HighAvailableDataSource; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.sql.DataSource; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public class RandomDataSourceSelectorWithValidationTest { + private HighAvailableDataSource highAvailableDataSource; + + @Before + public void setUp() throws Exception { + highAvailableDataSource = new HighAvailableDataSource(); + String file = "/com/alibaba/druid/pool/ha/ha-datasource.properties"; + highAvailableDataSource.setDataSourceFile(file); + highAvailableDataSource.init(); + } + + @After + public void tearDown() { + highAvailableDataSource = null; + } + + @Test + public void testOneDataSourceFailAndRecover() throws Exception { + ((RandomDataSourceSelector) highAvailableDataSource.getSelector()).getValidateThread().setSleepSeconds(3); + ((RandomDataSourceSelector) highAvailableDataSource.getSelector()).getRecoverThread().setSleepSeconds(3); + + Thread.sleep(30 * 1000); + + DruidDataSource dataSource = (DruidDataSource) highAvailableDataSource.getDataSourceMap().get("foo"); + dataSource.setValidationQuery("select xxx from yyy"); + Thread.sleep(10 * 1000); + assertTrue(dataSource.isTestOnReturn()); + for (int i = 0; i < 100; i++) { + assertNotEquals(dataSource, highAvailableDataSource.getSelector().get()); + } + + dataSource.setValidationQuery(null); + Thread.sleep(4 * 1000); + assertFalse(dataSource.isTestOnReturn()); + int count = 0; + for (int i = 0; i < 100; i++) { + if (dataSource == highAvailableDataSource.getSelector().get()) { + count++; + } + } + assertTrue(count > 0); + } + + @Test + public void testAllDataSourceFail() throws Exception { + ((RandomDataSourceSelector) highAvailableDataSource.getSelector()).getValidateThread().setSleepSeconds(3); + ((RandomDataSourceSelector) highAvailableDataSource.getSelector()).getRecoverThread().setSleepSeconds(3); + + Thread.sleep(30 * 1000); + + DruidDataSource foo = (DruidDataSource) highAvailableDataSource.getDataSourceMap().get("foo"); + DruidDataSource bar = (DruidDataSource) highAvailableDataSource.getDataSourceMap().get("bar"); + foo.setValidationQuery("select xxx from yyy"); + bar.setValidationQuery("select xxx from yyy"); + + Thread.sleep(10 * 1000); + assertTrue(foo.isTestOnReturn()); + assertTrue(bar.isTestOnReturn()); + + int[] count = new int[2]; + for (int i = 0; i < 100; i++) { + DataSource dataSource = highAvailableDataSource.getSelector().get(); + if (foo == dataSource) { + count[0]++; + } else if (bar == dataSource) { + count[1]++; + } + } + assertTrue(count[0] > 0); + assertTrue(count[1] > 0); + } +} diff --git a/src/test/resources/com/alibaba/druid/pool/ha/ha-datasource.properties b/src/test/resources/com/alibaba/druid/pool/ha/ha-datasource.properties new file mode 100644 index 0000000000..50f01147db --- /dev/null +++ b/src/test/resources/com/alibaba/druid/pool/ha/ha-datasource.properties @@ -0,0 +1,2 @@ +foo.url=jdbc:derby:memory:foo;create=true +bar.url=jdbc:derby:memory:bar;create=true \ No newline at end of file