/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.jmeter.protocol.jdbc.config; import java.sql.Connection; import java.sql.SQLException; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.apache.commons.dbcp2.BasicDataSource; import org.apache.commons.lang3.StringUtils; import org.apache.jmeter.config.ConfigElement; import org.apache.jmeter.testbeans.TestBean; import org.apache.jmeter.testbeans.TestBeanHelper; import org.apache.jmeter.testelement.AbstractTestElement; import org.apache.jmeter.testelement.TestStateListener; import org.apache.jmeter.threads.JMeterContextService; import org.apache.jmeter.threads.JMeterVariables; import org.apache.jorphan.util.JOrphanUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DataSourceElement extends AbstractTestElement implements ConfigElement, TestStateListener, TestBean { private static final Logger log = LoggerFactory.getLogger(DataSourceElement.class); private static final long serialVersionUID = 234L; private transient String dataSource; private transient String driver; private transient String dbUrl; private transient String username; private transient String password; private transient String checkQuery; private transient String poolMax; private transient String connectionAge; private transient String timeout; private transient String trimInterval; private transient String transactionIsolation; private transient boolean keepAlive; private transient boolean autocommit; /* * The datasource is set up by testStarted and cleared by testEnded. * These are called from different threads, so access must be synchronized. * The same instance is called in each case. */ private transient BasicDataSource dbcpDataSource; // Keep a record of the pre-thread pools so that they can be disposed of at the end of a test private transient Set<BasicDataSource> perThreadPoolSet; public DataSourceElement() { } @Override public void testEnded() { synchronized (this) { if (dbcpDataSource != null) { try { dbcpDataSource.close(); } catch (SQLException ex) { log.error("Error closing pool: {}", getName(), ex); } } dbcpDataSource = null; } if (perThreadPoolSet != null) {// in case for(BasicDataSource dsc : perThreadPoolSet){ log.debug("Closing pool: {}@{}", getDataSourceName(), System.identityHashCode(dsc)); try { dsc.close(); } catch (SQLException ex) { log.error("Error closing pool:{}", getName(), ex); } } perThreadPoolSet=null; } } @Override public void testEnded(String host) { testEnded(); } @Override public void testStarted() { this.setRunningVersion(true); TestBeanHelper.prepare(this); JMeterVariables variables = getThreadContext().getVariables(); String poolName = getDataSource(); if(JOrphanUtils.isBlank(poolName)) { throw new IllegalArgumentException("Variable Name must not be empty for element:"+getName()); } else if (variables.getObject(poolName) != null) { log.error("JDBC data source already defined for: {}", poolName); } else { String maxPool = getPoolMax(); perThreadPoolSet = Collections.synchronizedSet(new HashSet<BasicDataSource>()); if (maxPool.equals("0")){ // i.e. if we want per thread pooling variables.putObject(poolName, new DataSourceComponentImpl()); // pool will be created later } else { BasicDataSource src = initPool(maxPool); synchronized(this){ dbcpDataSource = src; variables.putObject(poolName, new DataSourceComponentImpl(dbcpDataSource)); } } } } @Override public void testStarted(String host) { testStarted(); } @Override public Object clone() { DataSourceElement el = (DataSourceElement) super.clone(); synchronized (this) { el.dbcpDataSource = dbcpDataSource; el.perThreadPoolSet = perThreadPoolSet; } return el; } /* * Utility routine to get the connection from the pool. * Purpose: * - allows JDBCSampler to be entirely independent of the pooling classes * - allows the pool storage mechanism to be changed if necessary */ public static Connection getConnection(String poolName) throws SQLException{ Object poolObject = JMeterContextService.getContext().getVariables().getObject(poolName); if (poolObject == null) { throw new SQLException("No pool found named: '" + poolName + "', ensure Variable Name matches Variable Name of JDBC Connection Configuration"); } else { if(poolObject instanceof DataSourceComponentImpl) { DataSourceComponentImpl pool = (DataSourceComponentImpl) poolObject; return pool.getConnection(); } else { String errorMsg = "Found object stored under variable:'"+poolName +"' with class:"+poolObject.getClass().getName()+" and value: '"+poolObject+" but it's not a DataSourceComponent, check you're not already using this name as another variable"; log.error(errorMsg); throw new SQLException(errorMsg); } } } /* * Set up the DataSource - maxPool is a parameter, so the same code can * also be used for setting up the per-thread pools. */ private BasicDataSource initPool(String maxPool) { BasicDataSource dataSource = new BasicDataSource(); if (log.isDebugEnabled()) { StringBuilder sb = new StringBuilder(40); sb.append("MaxPool: "); sb.append(maxPool); sb.append(" Timeout: "); sb.append(getTimeout()); sb.append(" TrimInt: "); sb.append(getTrimInterval()); sb.append(" Auto-Commit: "); sb.append(isAutocommit()); log.debug(sb.toString()); } int poolSize = Integer.parseInt(maxPool); dataSource.setMinIdle(0); dataSource.setInitialSize(poolSize); dataSource.setMaxIdle(poolSize); dataSource.setMaxTotal(poolSize); dataSource.setMaxWaitMillis(Long.parseLong(getTimeout())); dataSource.setDefaultAutoCommit(Boolean.valueOf(isAutocommit())); if (log.isDebugEnabled()) { StringBuilder sb = new StringBuilder(40); sb.append("KeepAlive: "); sb.append(isKeepAlive()); sb.append(" Age: "); sb.append(getConnectionAge()); sb.append(" CheckQuery: "); sb.append(getCheckQuery()); log.debug(sb.toString()); } dataSource.setTestOnBorrow(false); dataSource.setTestOnReturn(false); dataSource.setTestOnCreate(false); dataSource.setTestWhileIdle(false); if(isKeepAlive()) { dataSource.setTestWhileIdle(true); String validationQuery = getCheckQuery(); if (StringUtils.isBlank(validationQuery)) { dataSource.setValidationQuery(null); } else { dataSource.setValidationQuery(validationQuery); } dataSource.setSoftMinEvictableIdleTimeMillis(Long.parseLong(getConnectionAge())); dataSource.setTimeBetweenEvictionRunsMillis(Integer.parseInt(getTrimInterval())); } int transactionIsolation = DataSourceElementBeanInfo.getTransactionIsolationMode(getTransactionIsolation()); if (transactionIsolation >= 0) { dataSource.setDefaultTransactionIsolation(transactionIsolation); } String _username = getUsername(); if (log.isDebugEnabled()) { StringBuilder sb = new StringBuilder(40); sb.append("Driver: "); sb.append(getDriver()); sb.append(" DbUrl: "); sb.append(getDbUrl()); sb.append(" User: "); sb.append(_username); log.debug(sb.toString()); } dataSource.setDriverClassName(getDriver()); dataSource.setUrl(getDbUrl()); if (_username.length() > 0){ dataSource.setUsername(_username); dataSource.setPassword(getPassword()); } log.debug("PoolConfiguration:{}", this.dataSource); return dataSource; } // used to hold per-thread singleton connection pools private static final ThreadLocal<Map<String, BasicDataSource>> perThreadPoolMap = ThreadLocal.withInitial(HashMap::new); /* * Wrapper class to allow getConnection() to be implemented for both shared * and per-thread pools. * */ private class DataSourceComponentImpl { private final BasicDataSource sharedDSC; DataSourceComponentImpl(){ sharedDSC=null; } DataSourceComponentImpl(BasicDataSource p_dsc){ sharedDSC=p_dsc; } /** * @return Connection * @throws SQLException if database access error occurred */ public Connection getConnection() throws SQLException { Connection conn; BasicDataSource dsc; if (sharedDSC != null){ // i.e. shared pool dsc = sharedDSC; } else { Map<String, BasicDataSource> poolMap = perThreadPoolMap.get(); dsc = poolMap.get(getDataSourceName()); if (dsc == null){ dsc = initPool("1"); poolMap.put(getDataSourceName(),dsc); log.debug("Storing pool: {}@{}", getName(), System.identityHashCode(dsc)); perThreadPoolSet.add(dsc); } } conn=dsc.getConnection(); int isolation = DataSourceElementBeanInfo.getTransactionIsolationMode(getTransactionIsolation()); if (isolation >= 0 && conn.getTransactionIsolation() != isolation) { try { // make sure setting the new isolation mode is done in an auto committed transaction conn.setTransactionIsolation(isolation); log.debug("Setting transaction isolation: {}@{}", isolation, System.identityHashCode(dsc)); } catch (SQLException ex) { log.error("Could not set transaction isolation: {}@{}", isolation, System.identityHashCode(dsc), ex); } } return conn; } } @Override public void addConfigElement(ConfigElement config) { } @Override public boolean expectsModification() { return false; } /** * @return Returns the checkQuery. */ public String getCheckQuery() { return checkQuery; } /** * @param checkQuery * The checkQuery to set. */ public void setCheckQuery(String checkQuery) { this.checkQuery = checkQuery; } /** * @return Returns the connectionAge. */ public String getConnectionAge() { return connectionAge; } /** * @param connectionAge * The connectionAge to set. */ public void setConnectionAge(String connectionAge) { this.connectionAge = connectionAge; } /** * @return Returns the poolname. */ public String getDataSource() { return dataSource; } /** * @param dataSource * The poolname to set. */ public void setDataSource(String dataSource) { this.dataSource = dataSource; } private String getDataSourceName() { return getDataSource(); } /** * @return Returns the dbUrl. */ public String getDbUrl() { return dbUrl; } /** * @param dbUrl * The dbUrl to set. */ public void setDbUrl(String dbUrl) { this.dbUrl = dbUrl; } /** * @return Returns the driver. */ public String getDriver() { return driver; } /** * @param driver * The driver to set. */ public void setDriver(String driver) { this.driver = driver; } /** * @return Returns the password. */ public String getPassword() { return password; } /** * @param password * The password to set. */ public void setPassword(String password) { this.password = password; } /** * @return Returns the poolMax. */ public String getPoolMax() { return poolMax; } /** * @param poolMax * The poolMax to set. */ public void setPoolMax(String poolMax) { this.poolMax = poolMax; } /** * @return Returns the timeout. */ public String getTimeout() { return timeout; } /** * @param timeout * The timeout to set. */ public void setTimeout(String timeout) { this.timeout = timeout; } /** * @return Returns the trimInterval. */ public String getTrimInterval() { return trimInterval; } /** * @param trimInterval * The trimInterval to set. */ public void setTrimInterval(String trimInterval) { this.trimInterval = trimInterval; } /** * @return Returns the username. */ public String getUsername() { return username; } /** * @param username * The username to set. */ public void setUsername(String username) { this.username = username; } /** * @return Returns the autocommit. */ public boolean isAutocommit() { return autocommit; } /** * @param autocommit * The autocommit to set. */ public void setAutocommit(boolean autocommit) { this.autocommit = autocommit; } /** * @return Returns the keepAlive. */ public boolean isKeepAlive() { return keepAlive; } /** * @param keepAlive * The keepAlive to set. */ public void setKeepAlive(boolean keepAlive) { this.keepAlive = keepAlive; } /** * @return the transaction isolation level */ public String getTransactionIsolation() { return transactionIsolation; } /** * @param transactionIsolation The transaction isolation level to set. <code>NULL</code> to * use the default of the driver. */ public void setTransactionIsolation(String transactionIsolation) { this.transactionIsolation = transactionIsolation; } }