Skip to content

Commit

Permalink
feat: gracy destory datasource (#579)
Browse files Browse the repository at this point in the history
* feat: allow delay close datasource
---------

Co-authored-by: guop <[email protected]>
  • Loading branch information
alvinkwok1 and guop committed Oct 23, 2023
1 parent b4a0ee2 commit 03c7cff
Show file tree
Hide file tree
Showing 13 changed files with 335 additions and 12 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ spring:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
grace-destroy: false #是否优雅关闭数据源,默认为false,设置为true时,关闭数据源时如果数据源中还存在活跃连接,至多等待10s后强制关闭
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ public class DynamicDataSourceProperties {
* 是否懒加载数据源
*/
private Boolean lazy = false;
/**
* 是否优雅关闭数据源,等待一段时间后再将数据源销毁
*/
private Boolean graceDestroy = false;
/**
* seata使用模式,默认AT
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public DataSource dataSource(List<DynamicDataSourceProvider> providers) {
dataSource.setStrategy(properties.getStrategy());
dataSource.setP6spy(properties.getP6spy());
dataSource.setSeata(properties.getSeata());
dataSource.setGraceDestroy(properties.getGraceDestroy());
return dataSource;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ void testAddAndRemoveDataSource() {
dataSourceProperty.setUrl("jdbc:h2:mem:test1");
dataSourceProperty.setDriverClassName("org.h2.Driver");
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
// async destroy datasource
ds.setGraceDestroy(true);
ds.addDataSource(dataSourceProperty.getPoolName(), dataSourceCreator.createDataSource(dataSourceProperty));
assertThat(ds.getDataSources().keySet()).contains("slave_1");
ds.removeDataSource("slave_1");
// close directly
ds.setGraceDestroy(false);
ds.addDataSource(dataSourceProperty.getPoolName(), dataSourceCreator.createDataSource(dataSourceProperty));
assertThat(ds.getDataSources().keySet()).contains("slave_1");
ds.removeDataSource("slave_1");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public DataSource dataSource(List<DynamicDataSourceProvider> providers) {
dataSource.setStrategy(properties.getStrategy());
dataSource.setP6spy(properties.getP6spy());
dataSource.setSeata(properties.getSeata());
dataSource.setGraceDestroy(properties.getGraceDestroy());
return dataSource;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ void testAddAndRemoveDataSource() {
dataSourceProperty.setUrl("jdbc:h2:mem:test1");
dataSourceProperty.setDriverClassName("org.h2.Driver");
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
// async destroy datasource
ds.setGraceDestroy(true);
ds.addDataSource(dataSourceProperty.getPoolName(), dataSourceCreator.createDataSource(dataSourceProperty));
assertThat(ds.getDataSources().keySet()).contains("slave_1");
ds.removeDataSource("slave_1");
// close directly
ds.setGraceDestroy(false);
ds.addDataSource(dataSourceProperty.getPoolName(), dataSourceCreator.createDataSource(dataSourceProperty));
assertThat(ds.getDataSources().keySet()).contains("slave_1");
ds.removeDataSource("slave_1");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.baomidou.dynamic.datasource;

import com.baomidou.dynamic.datasource.destroyer.DataSourceDestroyer;
import com.baomidou.dynamic.datasource.destroyer.DefaultDataSourceDestroyer;
import com.baomidou.dynamic.datasource.ds.AbstractRoutingDataSource;
import com.baomidou.dynamic.datasource.ds.GroupDataSource;
import com.baomidou.dynamic.datasource.ds.ItemDataSource;
Expand All @@ -30,12 +32,10 @@
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -71,6 +71,8 @@ public class DynamicRoutingDataSource extends AbstractRoutingDataSource implemen
private Boolean p6spy = false;
@Setter
private Boolean seata = false;
@Setter
private Boolean graceDestroy = false;

public DynamicRoutingDataSource(List<DynamicDataSourceProvider> providers) {
this.providers = providers;
Expand Down Expand Up @@ -153,7 +155,7 @@ public synchronized void addDataSource(String ds, DataSource dataSource) {
this.addGroupDataSource(ds, dataSource);
// 关闭老的数据源
if (oldDataSource != null) {
closeDataSource(ds, oldDataSource);
closeDataSource(ds, oldDataSource, graceDestroy);
}
log.info("dynamic-datasource - add a datasource named [{}] success", ds);
}
Expand Down Expand Up @@ -194,7 +196,7 @@ public synchronized void removeDataSource(String ds) {
}
if (dataSourceMap.containsKey(ds)) {
DataSource dataSource = dataSourceMap.remove(ds);
closeDataSource(ds, dataSource);
closeDataSource(ds, dataSource, graceDestroy);
if (ds.contains(UNDERLINE)) {
String group = ds.split(UNDERLINE)[0];
if (groupDataSources.containsKey(group)) {
Expand All @@ -214,7 +216,7 @@ public synchronized void removeDataSource(String ds) {
public void destroy() {
log.info("dynamic-datasource start closing ....");
for (Map.Entry<String, DataSource> item : dataSourceMap.entrySet()) {
closeDataSource(item.getKey(), item.getValue());
closeDataSource(item.getKey(), item.getValue(), false);
}
log.info("dynamic-datasource all closed success,bye");
}
Expand Down Expand Up @@ -268,28 +270,34 @@ private void checkEnv() {
*
* @param ds dsName
* @param dataSource db
* @param graceDestroy If true, close the connection after a delay.
*/
private void closeDataSource(String ds, DataSource dataSource) {
private void closeDataSource(String ds, DataSource dataSource, boolean graceDestroy) {
try {
DataSource realDataSource = null;
if (dataSource instanceof ItemDataSource) {
((ItemDataSource) dataSource).close();
realDataSource = ((ItemDataSource) dataSource).getRealDataSource();
} else {
if (seata) {
if (dataSource instanceof DataSourceProxy) {
DataSourceProxy dataSourceProxy = (DataSourceProxy) dataSource;
dataSource = dataSourceProxy.getTargetDataSource();
realDataSource = dataSourceProxy.getTargetDataSource();
}
}
if (p6spy) {
if (dataSource instanceof P6DataSource) {
Field realDataSourceField = P6DataSource.class.getDeclaredField("realDataSource");
realDataSourceField.setAccessible(true);
dataSource = (DataSource) realDataSourceField.get(dataSource);
realDataSource = (DataSource) realDataSourceField.get(dataSource);
}
}
Method closeMethod = ReflectionUtils.findMethod(dataSource.getClass(), "close");
if (closeMethod != null) {
closeMethod.invoke(dataSource);
}
if (null != realDataSource) {
DataSourceDestroyer destroyer = new DefaultDataSourceDestroyer();
if (graceDestroy) {
destroyer.asyncDestroy(ds, realDataSource);
} else {
destroyer.destroy(ds, realDataSource);
}
}
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright © 2018 organization baomidou
*
* 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.baomidou.dynamic.datasource.destroyer;

import javax.sql.DataSource;

/**
* Description
* Detect if the datasource contains active connections
*
* @author alvinkwok
* @since 2023/10/18
*/
public interface DataSourceActiveDetector {
boolean containsActiveConnection(DataSource dataSource);

boolean support(DataSource dataSource);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright © 2018 organization baomidou
*
* 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.baomidou.dynamic.datasource.destroyer;

import javax.sql.DataSource;

/**
* Used to destroy sources
*/
public interface DataSourceDestroyer {

void asyncDestroy(String name, DataSource dataSource);

/**
* Immediately destroy the data source
*
* @param realDataSource wait destroy data source
*/
void destroy(String name, DataSource realDataSource);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright © 2018 organization baomidou
*
* 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.baomidou.dynamic.datasource.destroyer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ReflectionUtils;

import javax.sql.DataSource;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* Description
* DefaultDataSourceDestroyer, support check hikari、druid and dhcp2
*
* @author alvinkwok
* @since 2023/10/18
*/
@Slf4j
public class DefaultDataSourceDestroyer implements DataSourceDestroyer {

private static final String THREAD_NAME = "close-datasource";

private static final long TIMEOUT_CLOSE = 10 * 1000;

private final List<DataSourceActiveDetector> detectors = new LinkedList<>();

public DefaultDataSourceDestroyer() {
detectors.add(new HikariDataSourceActiveDetector());
detectors.add(new DruidDataSourceActiveDetector());
detectors.add(new Dhcp2DataSourceActiveDetector());
}


public void asyncDestroy(String name, DataSource dataSource) {
log.info("dynamic-datasource start asynchronous task to close the datasource named [{}],", name);
ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread thread = new Thread(r);
thread.setName(THREAD_NAME);
return thread;
});
executor.execute(() -> graceDestroy(name, dataSource));
executor.shutdown();
}

private void graceDestroy(String name, DataSource dataSource) {
try {
DataSourceActiveDetector detector = detectors.stream()
.filter(x -> x.support(dataSource))
.findFirst()
.orElse(null);
long start = System.currentTimeMillis();
while (detector == null || detector.containsActiveConnection(dataSource)) {
// make sure the datasource close
if (System.currentTimeMillis() - start > TIMEOUT_CLOSE) {
break;
}
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (Exception e) {
log.warn("dynamic-datasource check the datasource named [{}] contains active connection failed,", name, e);
}
destroy(name, dataSource);
}

/**
* Immediately destroy the data source
*
* @param realDataSource wait destroy data source
*/
public void destroy(String name, DataSource realDataSource) {
Class<? extends DataSource> clazz = realDataSource.getClass();
try {
Method closeMethod = ReflectionUtils.findMethod(clazz, "close");
if (closeMethod != null) {
closeMethod.invoke(realDataSource);
log.info("dynamic-datasource close the datasource named [{}] success,", name);
}
} catch (IllegalAccessException | InvocationTargetException e) {
log.warn("dynamic-datasource close the datasource named [{}] failed,", name, e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright © 2018 organization baomidou
*
* 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.baomidou.dynamic.datasource.destroyer;

import lombok.SneakyThrows;

import javax.sql.DataSource;

/**
* Description
* DHCP2 data source pool active detector.
*
* @author alvinkwok
* @since 2023/10/18
*/
public class Dhcp2DataSourceActiveDetector implements DataSourceActiveDetector {
@Override
@SneakyThrows(ReflectiveOperationException.class)
public boolean containsActiveConnection(DataSource dataSource) {
int activeCount = (int) dataSource.getClass().getMethod("getNumActive").invoke(dataSource);
return activeCount != 0;
}

@Override
public boolean support(DataSource dataSource) {
return "org.apache.commons.dbcp2.BasicDataSource".equals(dataSource.getClass().getName());
}
}
Loading

0 comments on commit 03c7cff

Please sign in to comment.