This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to annotated tag org.apache.sling.datasource-1.0.0 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-datasource.git
commit d5aa6ddbbf7394a6f471ec54bdf34dd3a9b4f524 Author: Chetan Mehrotra <[email protected]> AuthorDate: Wed Jun 4 13:24:57 2014 +0000 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/contrib/extensions/datasource@1600199 13f79535-47bb-0310-9956-ffa450edef68 --- .../datasource/internal/DataSourceFactory.java | 287 ++++++++++++++++----- .../OSGI-INF/metatype/metatype.properties | 95 ++++++- .../sling/extensions/datasource/DataSourceIT.java | 21 +- 3 files changed, 327 insertions(+), 76 deletions(-) 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 @@ package org.apache.sling.extensions.datasource.internal; 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 @@ public class DataSourceFactory { @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 @@ public class DataSourceFactory { @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 @@ public class DataSourceFactory { 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)); - //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()); - } + svcPropName = getSvcPropName(config); + registerDataSource(svcPropName); - props.setProperty(org.apache.tomcat.jdbc.pool.DataSourceFactory.OBJECT_NAME, name); + log.info("Created DataSource [{}] with properties {}", name, dataSource.getPoolProperties().toString()); + } - for(String propName : DummyDataSourceFactory.getPropertyNames()){ - String value = PropertiesUtil.toString(config.get(propName), null); - if(value != null){ - props.setProperty(propName, value); - } + @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; } - dataSource = createDataSource(props, bundleContext); - registerDataSource(bundleContext); - log.info("Created DataSource [{}] with properties {}", name, getDataSourceDetails()); + //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){ + protected void deactivate() { + if (dsRegistration != null) { dsRegistration.unregister(); + dsRegistration = null; } + closeConnectionPool(); + dataSource = null; + } + + private void closeConnectionPool() { unregisterJmx(); + dataSource.close(); + } - if(dataSource instanceof DataSourceProxy){ - ((DataSourceProxy) 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()) { + set(e.getKey(), e.getValue(), props); + } - private DataSource createDataSource(Properties props, BundleContext bundleContext) throws Exception { - PoolConfiguration poolProperties = org.apache.tomcat.jdbc.pool.DataSourceFactory.parsePoolProperties(props); + props.setProperty(org.apache.tomcat.jdbc.pool.DataSourceFactory.OBJECT_NAME, name); - DriverDataSource driverDataSource = new DriverDataSource(poolProperties, driverRegistry, - bundleContext, this); + for (String propName : DummyDataSourceFactory.getPropertyNames()) { + String value = PropertiesUtil.toString(config.get(propName), null); + set(propName, value, props); + } //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 poolProperties; + } - // Return the configured DataSource instance - return new LazyJmxRegisteringDataSource(poolProperties); + private DriverDataSource createDriverDataSource(PoolConfiguration poolProperties) { + return new DriverDataSource(poolProperties, driverRegistry, bundleContext, this); } - private void registerDataSource(BundleContext bundleContext) { - Dictionary<String,Object> svcProps = new Hashtable<String, Object>(); - svcProps.put(PROP_DATASOURCE_NAME, name); + 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; + } + if (value != null) { + value = value.trim(); + } - public static void checkArgument(boolean expression, - String errorMessageTemplate, - Object... errorMessageArgs) { + 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 @@ public class DataSourceFactory { 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 @@ public class DataSourceFactory { * 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 @@ username.description=The connection username to be passed to our JDBC driver to 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 @@ package org.apache.sling.extensions.datasource; 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 org.osgi.util.tracker.ServiceTracker; 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 @@ public class DataSourceIT extends DataSourceTestBase{ @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 @@ public class DataSourceIT extends DataSourceTestBase{ //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")); } } -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
