SLING-3642 - Support modifying of DataSource properties at runtime without restart
-- Added support to refresh connectionPool associated with DataSource with new config
-- Made the metatype more richer
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1600199 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/main/java/org/apache/sling/extensions/datasource/internal/DataSourceFactory.java b/src/main/java/org/apache/sling/extensions/datasource/internal/DataSourceFactory.java
index 43b5815..eb56710 100644
--- a/src/main/java/org/apache/sling/extensions/datasource/internal/DataSourceFactory.java
+++ b/src/main/java/org/apache/sling/extensions/datasource/internal/DataSourceFactory.java
@@ -20,25 +20,30 @@
import java.lang.management.ManagementFactory;
import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.Dictionary;
+import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Properties;
+import java.util.Set;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
-import javax.sql.DataSource;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Modified;
import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.PropertyOption;
import org.apache.felix.scr.annotations.Reference;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.apache.tomcat.jdbc.pool.ConnectionPool;
-import org.apache.tomcat.jdbc.pool.DataSourceProxy;
+import org.apache.tomcat.jdbc.pool.DataSource;
import org.apache.tomcat.jdbc.pool.PoolConfiguration;
import org.apache.tomcat.jdbc.pool.PoolProperties;
import org.osgi.framework.BundleContext;
@@ -61,6 +66,9 @@
@Property
static final String PROP_DATASOURCE_NAME = "datasource.name";
+ @Property(value = PROP_DATASOURCE_NAME)
+ static final String PROP_DS_SVC_PROP_NAME = "datasource.svc.prop.name";
+
@Property
static final String PROP_DRIVERCLASSNAME = "driverClassName";
@@ -73,12 +81,103 @@
@Property(passwordValue = "")
static final String PROP_PASSWORD = "password";
+ /**
+ * Value indicating default value should be used. if the value is set to
+ * this value then that value would be treated as null
+ */
+ static final String DEFAULT_VAL = "default";
+
+ @Property(value = DEFAULT_VAL, options = {
+ @PropertyOption(name = "Default", value = DEFAULT_VAL),
+ @PropertyOption(name = "true", value = "true"),
+ @PropertyOption(name = "false", value = "false")})
+ static final String PROP_DEFAULTAUTOCOMMIT = "defaultAutoCommit";
+
+ @Property(value = DEFAULT_VAL, options = {
+ @PropertyOption(name = "Default", value = DEFAULT_VAL),
+ @PropertyOption(name = "true", value = "true"),
+ @PropertyOption(name = "false", value = "false")})
+ static final String PROP_DEFAULTREADONLY = "defaultReadOnly";
+
+ @Property(value = DEFAULT_VAL, options = {
+ @PropertyOption(name = "Default", value = DEFAULT_VAL),
+ @PropertyOption(name = "NONE", value = "NONE"),
+ @PropertyOption(name = "READ_COMMITTED", value = "READ_COMMITTED"),
+ @PropertyOption(name = "READ_UNCOMMITTED", value = "READ_UNCOMMITTED"),
+ @PropertyOption(name = "REPEATABLE_READ", value = "REPEATABLE_READ"),
+ @PropertyOption(name = "SERIALIZABLE", value = "SERIALIZABLE")})
+ static final String PROP_DEFAULTTRANSACTIONISOLATION = "defaultTransactionIsolation";
+
+ @Property
+ static final String PROP_DEFAULTCATALOG = "defaultCatalog";
+
@Property(intValue = PoolProperties.DEFAULT_MAX_ACTIVE)
static final String PROP_MAXACTIVE = "maxActive";
+ @Property(intValue = PoolProperties.DEFAULT_MAX_ACTIVE)
+ static final String PROP_MAXIDLE = "maxIdle"; //defaults to maxActive
+
+ @Property(intValue = 10)
+ static final String PROP_MINIDLE = "minIdle"; //defaults to initialSize
+
+ @Property(intValue = 10)
+ static final String PROP_INITIALSIZE = "initialSize";
+
+ @Property(intValue = 30000)
+ static final String PROP_MAXWAIT = "maxWait";
+
+ @Property(intValue = 0)
+ static final String PROP_MAXAGE = "maxAge";
+
+ @Property(boolValue = false)
+ static final String PROP_TESTONBORROW = "testOnBorrow";
+
+ @Property(boolValue = false)
+ static final String PROP_TESTONRETURN = "testOnReturn";
+
+ @Property(boolValue = false)
+ static final String PROP_TESTWHILEIDLE = "testWhileIdle";
+
+ @Property
+ static final String PROP_VALIDATIONQUERY = "validationQuery";
+
+ @Property(intValue = -1)
+ static final String PROP_VALIDATIONQUERY_TIMEOUT = "validationQueryTimeout";
+
+ @Property(intValue = 5000)
+ protected static final String PROP_TIMEBETWEENEVICTIONRUNSMILLIS = "timeBetweenEvictionRunsMillis";
+
+ @Property(intValue = 60000)
+ protected static final String PROP_MINEVICTABLEIDLETIMEMILLIS = "minEvictableIdleTimeMillis";
+
+ @Property
+ protected static final String PROP_CONNECTIONPROPERTIES = "connectionProperties";
+
+ @Property
+ protected static final String PROP_INITSQL = "initSQL";
+
+ @Property(value = "StatementCache;SlowQueryReport(threshold=10000);ConnectionState")
+ protected static final String PROP_INTERCEPTORS = "jdbcInterceptors";
+
+ @Property(intValue = 30000)
+ protected static final String PROP_VALIDATIONINTERVAL = "validationInterval";
+
+ @Property(boolValue = true)
+ protected static final String PROP_LOGVALIDATIONERRORS = "logValidationErrors";
+
@Property(value = {}, cardinality = 1024)
static final String PROP_DATASOURCE_SVC_PROPS = "datasource.svc.properties";
+ /**
+ * Property names where we need to treat value 'default' as null
+ */
+ private static final Set<String> PROPS_WITH_DFEAULT =
+ Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
+ PROP_DEFAULTAUTOCOMMIT,
+ PROP_DEFAULTREADONLY,
+ PROP_DEFAULTTRANSACTIONISOLATION
+ )));
+
private final Logger log = LoggerFactory.getLogger(getClass());
@Reference
@@ -86,125 +185,167 @@
private String name;
+ private String svcPropName;
+
private ObjectName jmxName;
private ServiceRegistration dsRegistration;
private DataSource dataSource;
+ private BundleContext bundleContext;
+
@Activate
- protected void activate(BundleContext bundleContext, Map<String,?> config) throws Exception {
- Properties props = new Properties();
- name = PropertiesUtil.toString(config.get(PROP_DATASOURCE_NAME), null);
+ protected void activate(BundleContext bundleContext, Map<String, ?> config) throws Exception {
+ this.bundleContext = bundleContext;
+ name = getDataSourceName(config);
checkArgument(name != null, "DataSource name must be specified via [%s] property", PROP_DATASOURCE_NAME);
+ dataSource = new LazyJmxRegisteringDataSource(createPoolConfig(config));
+
+ svcPropName = getSvcPropName(config);
+ registerDataSource(svcPropName);
+
+ log.info("Created DataSource [{}] with properties {}", name, dataSource.getPoolProperties().toString());
+ }
+
+ @Modified
+ protected void modified(Map<String, ?> config) throws Exception {
+ String name = getDataSourceName(config);
+ String svcPropName = getSvcPropName(config);
+
+ if (!this.name.equals(name) || !this.svcPropName.equals(svcPropName)) {
+ log.info("Change in datasource name/service property name detected. DataSource would be recreated");
+ deactivate();
+ activate(bundleContext, config);
+ return;
+ }
+
+ //Other modifications can be applied at runtime itself
+ //Tomcat Connection Pool is decoupled from DataSource so can be closed and reset
+ dataSource.setPoolProperties(createPoolConfig(config));
+ closeConnectionPool();
+ dataSource.createPool();
+ log.info("Updated DataSource [{}] with properties {}", name, dataSource.getPoolProperties().toString());
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ if (dsRegistration != null) {
+ dsRegistration.unregister();
+ dsRegistration = null;
+ }
+
+ closeConnectionPool();
+ dataSource = null;
+ }
+
+ private void closeConnectionPool() {
+ unregisterJmx();
+ dataSource.close();
+ }
+
+ private PoolConfiguration createPoolConfig(Map<String, ?> config) {
+ Properties props = new Properties();
//Copy the other properties first
- Map<String,String> otherProps = PropertiesUtil.toMap(config.get(PROP_DATASOURCE_SVC_PROPS), new String[0]);
- for(Map.Entry<String, String> e : otherProps.entrySet()){
- props.setProperty(e.getKey(), e.getValue());
+ Map<String, String> otherProps = PropertiesUtil.toMap(config.get(PROP_DATASOURCE_SVC_PROPS), new String[0]);
+ for (Map.Entry<String, String> e : otherProps.entrySet()) {
+ set(e.getKey(), e.getValue(), props);
}
props.setProperty(org.apache.tomcat.jdbc.pool.DataSourceFactory.OBJECT_NAME, name);
- for(String propName : DummyDataSourceFactory.getPropertyNames()){
+ for (String propName : DummyDataSourceFactory.getPropertyNames()) {
String value = PropertiesUtil.toString(config.get(propName), null);
- if(value != null){
- props.setProperty(propName, value);
- }
+ set(propName, value, props);
}
- dataSource = createDataSource(props, bundleContext);
- registerDataSource(bundleContext);
- log.info("Created DataSource [{}] with properties {}", name, getDataSourceDetails());
- }
-
- @Deactivate
- protected void deactivate(){
- if(dsRegistration != null){
- dsRegistration.unregister();
- }
-
- unregisterJmx();
-
- if(dataSource instanceof DataSourceProxy){
- ((DataSourceProxy) dataSource).close();
- }
-
- }
-
- private DataSource createDataSource(Properties props, BundleContext bundleContext) throws Exception {
- PoolConfiguration poolProperties = org.apache.tomcat.jdbc.pool.DataSourceFactory.parsePoolProperties(props);
-
- DriverDataSource driverDataSource = new DriverDataSource(poolProperties, driverRegistry,
- bundleContext, this);
-
//Specify the DataSource such that connection creation logic is handled
//by us where we take care of OSGi env
- poolProperties.setDataSource(driverDataSource);
+ PoolConfiguration poolProperties = org.apache.tomcat.jdbc.pool.DataSourceFactory.parsePoolProperties(props);
+ poolProperties.setDataSource(createDriverDataSource(poolProperties));
- // Return the configured DataSource instance
- return new LazyJmxRegisteringDataSource(poolProperties);
+ return poolProperties;
}
- private void registerDataSource(BundleContext bundleContext) {
- Dictionary<String,Object> svcProps = new Hashtable<String, Object>();
- svcProps.put(PROP_DATASOURCE_NAME, name);
+ private DriverDataSource createDriverDataSource(PoolConfiguration poolProperties) {
+ return new DriverDataSource(poolProperties, driverRegistry, bundleContext, this);
+ }
+
+ private void registerDataSource(String svcPropName) {
+ Dictionary<String, Object> svcProps = new Hashtable<String, Object>();
+ svcProps.put(svcPropName, name);
svcProps.put(Constants.SERVICE_VENDOR, "Apache Software Foundation");
svcProps.put(Constants.SERVICE_DESCRIPTION, "DataSource service based on Tomcat JDBC");
- dsRegistration = bundleContext.registerService(DataSource.class, dataSource, svcProps);
+ dsRegistration = bundleContext.registerService(javax.sql.DataSource.class, dataSource, svcProps);
}
- void registerJmx(ConnectionPool pool) throws SQLException {
- org.apache.tomcat.jdbc.pool.jmx.ConnectionPool jmxPool = pool.getJmxPool();
- if (jmxPool == null) {
- //jmx not enabled
- return;
- }
- Hashtable<String, String> table = new Hashtable<String, String>();
- table.put("type", "ConnectionPool");
- table.put("class", DataSource.class.getName());
- table.put("name", ObjectName.quote(name));
+ private void registerJmx(ConnectionPool pool) throws SQLException {
+ org.apache.tomcat.jdbc.pool.jmx.ConnectionPool jmxPool = pool.getJmxPool();
+ if (jmxPool == null) {
+ //jmx not enabled
+ return;
+ }
+ Hashtable<String, String> table = new Hashtable<String, String>();
+ table.put("type", "ConnectionPool");
+ table.put("class", javax.sql.DataSource.class.getName());
+ table.put("name", ObjectName.quote(name));
- try {
- jmxName = new ObjectName("org.apache.sling", table);
- MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
- mbs.registerMBean(jmxPool, jmxName);
- } catch (Exception e) {
- log.warn("Error occurred while registering the JMX Bean for " +
- "connection pool with name {}", jmxName, e);
- }
+ try {
+ jmxName = new ObjectName("org.apache.sling", table);
+ MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
+ mbs.registerMBean(jmxPool, jmxName);
+ } catch (Exception e) {
+ log.warn("Error occurred while registering the JMX Bean for " +
+ "connection pool with name {}", jmxName, e);
+ }
}
ConnectionPool getPool() {
- return ((DataSourceProxy) dataSource).getPool();
+ return dataSource.getPool();
}
- private void unregisterJmx(){
+ private void unregisterJmx() {
try {
- if(jmxName != null) {
+ if (jmxName != null) {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
mbs.unregisterMBean(jmxName);
}
} catch (InstanceNotFoundException ignore) {
// NOOP
} catch (Exception e) {
- log.error("Unable to unregister JDBC pool with JMX",e);
+ log.error("Unable to unregister JDBC pool with JMX", e);
}
}
- private String getDataSourceDetails() {
- if(dataSource instanceof DataSourceProxy){
- return ((DataSourceProxy) dataSource).getPoolProperties().toString();
- }
- return "<UNKNOWN>";
+ //~----------------------------------------< Config Handling >
+
+ private static String getDataSourceName(Map<String, ?> config) {
+ return PropertiesUtil.toString(config.get(PROP_DATASOURCE_NAME), null);
}
+ private static String getSvcPropName(Map<String, ?> config) {
+ return PropertiesUtil.toString(config.get(PROP_DS_SVC_PROP_NAME), PROP_DATASOURCE_NAME);
+ }
+ private static void set(String name, String value, Properties props) {
+ if (PROPS_WITH_DFEAULT.contains(name) && DEFAULT_VAL.equals(value)) {
+ value = null;
+ }
- public static void checkArgument(boolean expression,
- String errorMessageTemplate,
- Object... errorMessageArgs) {
+ if (value != null) {
+ value = value.trim();
+ }
+
+ if (value != null && !value.isEmpty()) {
+ props.setProperty(name, value);
+ }
+ }
+
+ private static void checkArgument(boolean expression,
+ String errorMessageTemplate,
+ Object... errorMessageArgs) {
if (!expression) {
throw new IllegalArgumentException(
String.format(errorMessageTemplate, errorMessageArgs));
@@ -225,10 +366,16 @@
return pool;
}
+ @Override
+ public void close() {
+ initialized = false;
+ super.close();
+ }
+
private void registerJmxLazily(ConnectionPool pool) throws SQLException {
- if(!initialized){
- synchronized (this){
- if(initialized){
+ if (!initialized) {
+ synchronized (this) {
+ if (initialized) {
return;
}
DataSourceFactory.this.registerJmx(pool);
@@ -242,7 +389,7 @@
* Dummy impl to enable access to protected fields
*/
private static class DummyDataSourceFactory extends org.apache.tomcat.jdbc.pool.DataSourceFactory {
- static String[] getPropertyNames(){
+ static String[] getPropertyNames() {
return ALL_PROPERTIES;
}
}
diff --git a/src/main/resources/OSGI-INF/metatype/metatype.properties b/src/main/resources/OSGI-INF/metatype/metatype.properties
index e0e9e5c..0984696 100644
--- a/src/main/resources/OSGI-INF/metatype/metatype.properties
+++ b/src/main/resources/OSGI-INF/metatype/metatype.properties
@@ -19,8 +19,9 @@
# suppress inspection "UnusedProperty" for whole file
-datasource.component.name = Apache Sling JDBC DataSource
-datasource.component.description = Creates a DataSource services based on configuration provided
+datasource.component.name = Apache Sling Connection Pooled DataSource
+datasource.component.description = Creates a DataSource services based on configuration provided. For more details \
+ on the various properties refer to http://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html#Common_Attributes
datasource.name.name = Datasource name(*)
datasource.name.description = Name of this data source (required)
@@ -42,6 +43,94 @@
password.name=Password
password.description=The connection password to be passed to our JDBC driver to establish a connection.
+defaultAutoCommit.name=Auto Commit
+defaultAutoCommit.description=The default auto-commit state of connections created by this pool. (If 'default' \
+ then the setAutoCommit method will not be called.)
+
+defaultReadOnly.name=Readonly
+defaultReadOnly.description=The default read-only state of connections created by this pool.
+
+defaultTransactionIsolation.name=Transaction Isolation
+defaultTransactionIsolation.description=The default TransactionIsolation state of connections created by this \
+ pool. If 'default', the method will not be called and it defaults to the JDBC driver.
+
+defaultCatalog.name=Catalog Name
+defaultCatalog.description=The default catalog of connections created by this pool.
+
maxActive.name=Max Active Connections
maxActive.description=The maximum number of active connections that can be allocated from this pool at \
- the same time. The default value is 100
\ No newline at end of file
+ the same time. The default value is 100
+
+maxIdle.name=Max Idle Connections
+maxIdle.description=The maximum number of connections that should be kept in the pool at all times. Idle \
+ connections are checked periodically (if enabled) and connections that been idle for longer than \
+ minEvictableIdleTimeMillis will be released. (also see testWhileIdle)
+
+minIdle.name=Min Idle Connections
+minIdle.description=The minimum number of established connections that should be kept in the pool at all times. \
+ The connection pool can shrink below this number if validation queries fail. Default value is derived from\
+ initialSize:10 (also see testWhileIdle)
+
+initialSize.name=Initial Size
+initialSize.description=The initial number of connections that are created when the pool is started. \
+ Default value is 10
+
+maxWait.name=Max Wait
+maxWait.description=The maximum number of milliseconds that the pool will wait (when there are no available\
+ connections) for a connection to be returned before throwing an exception. Default value is 30000 (30 seconds)
+
+maxAge.name=Max Age
+maxAge.description=Time in milliseconds to keep this connection.
+
+validationQuery.name=Validation Query
+validationQuery.description=The SQL query that will be used to validate connections from this pool before \
+ returning them to the caller. If specified, this query does not have to return any data, it just can't \
+ throw a SQLException. The default value is null. Example values are SELECT 1(mysql), select 1 from \
+ dual(oracle), SELECT 1(MS Sql Server)
+
+validationQueryTimeout.name=Validation Query timeout
+validationQueryTimeout.description=The timeout in seconds before a connection validation queries fail. A value \
+ less than or equal to zero will disable this feature. The default value is -1.
+
+testOnBorrow.name=Test on Borrow
+testOnBorrow.description=The indication of whether objects will be validated before being borrowed from the pool.
+
+testOnReturn.name=Test on Return
+testOnReturn.description=The indication of whether objects will be validated before being returned to the pool.
+
+testWhileIdle.name=Test while Idle
+testWhileIdle.description=The indication of whether objects will be validated by the idle object evictor (if any).
+
+timeBetweenEvictionRunsMillis.name=Eviction Run Interval
+timeBetweenEvictionRunsMillis.description=The number of milliseconds to sleep between runs of the idle connection\
+ validation/cleaner thread.
+
+minEvictableIdleTimeMillis.name=Eviction Idle Time
+minEvictableIdleTimeMillis.description=The minimum amount of time an object may sit idle in the pool before it \
+ is eligible for eviction.
+
+connectionProperties.name=Connection Properties
+connectionProperties.description=The connection properties that will be sent to our JDBC driver when establishing \
+ new connections. Format of the string must be [propertyName=property;]* NOTE - The "user" and "password" properties\
+ will be passed explicitly, so they do not need to be included here.
+
+initSQL.name=Init Sql
+initSQL.description=A custom query to be run when a connection is first created
+
+jdbcInterceptors.name=JDBC Interceptors
+jdbcInterceptors.description= A semicolon separated list of classnames of JDBCInterceptor. See \
+ http://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html#Configuring_JDBC_interceptors for more details
+
+validationInterval.name=Validation Interval
+validationInterval.description=avoid excess validation, only run validation at most at this frequency - time in \
+ milliseconds. If a connection is due for validation, but has been validated previously within this interval, \
+ it will not be validated again.
+
+logValidationErrors.name=Log Validation Error
+logValidationErrors.description=Set this to true to log errors during the validation phase to the log file
+
+datasource.svc.prop.name.name=DataSource Service Property Name
+datasource.svc.prop.name.name.description=Name of the service property which would store the DataSource Name while\
+ registering the DataSource instance as OSGi service
+
+
diff --git a/src/test/java/org/apache/sling/extensions/datasource/DataSourceIT.java b/src/test/java/org/apache/sling/extensions/datasource/DataSourceIT.java
index e75e962..a830a5c 100644
--- a/src/test/java/org/apache/sling/extensions/datasource/DataSourceIT.java
+++ b/src/test/java/org/apache/sling/extensions/datasource/DataSourceIT.java
@@ -21,6 +21,7 @@
import java.sql.Connection;
import java.util.Dictionary;
import java.util.Hashtable;
+import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.sql.DataSource;
@@ -36,6 +37,7 @@
import static org.apache.commons.beanutils.BeanUtils.getProperty;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
@RunWith(PaxExam.class)
public class DataSourceIT extends DataSourceTestBase{
@@ -50,15 +52,18 @@
@Inject
ConfigurationAdmin ca;
+ @SuppressWarnings("unchecked")
@Test
public void testDataSourceAsService() throws Exception{
Configuration config = ca.createFactoryConfiguration(PID, null);
Dictionary<String, Object> p = new Hashtable<String, Object>();
p.put("url","jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
p.put("datasource.name","test");
+ p.put("initialSize","5");
+ p.put("defaultAutoCommit","default");
+ p.put("defaultReadOnly","false");
p.put("datasource.svc.properties",new String[]{
- "initialSize=5",
- "testOnBorrow=true",
+ "initSQL=SELECT 1",
});
p.put("maxActive",70);
config.update(p);
@@ -77,7 +82,17 @@
//Cannot access directly so access via reflection
assertEquals("70", getProperty(ds, "poolProperties.maxActive"));
assertEquals("5", getProperty(ds, "poolProperties.initialSize"));
- assertEquals("true", getProperty(ds, "poolProperties.testOnBorrow"));
+ assertEquals("SELECT 1", getProperty(ds, "poolProperties.initSQL"));
+ assertEquals("false", getProperty(ds, "poolProperties.defaultReadOnly"));
+ assertNull(getProperty(ds, "poolProperties.defaultAutoCommit"));
+
+ config = ca.listConfigurations("(datasource.name=test)")[0];
+ Dictionary dic = config.getProperties();
+ dic.put("defaultReadOnly", Boolean.TRUE);
+ config.update(dic);
+
+ TimeUnit.MILLISECONDS.sleep(100);
+ assertEquals("true", getProperty(ds, "poolProperties.defaultReadOnly"));
}
}