001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     * 
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     * 
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    
018    package org.apache.commons.performance.dbcp;
019    
020    
021    import java.sql.Connection;
022    import java.sql.Driver;
023    import java.sql.DriverManager;
024    import java.sql.Statement;
025    import java.util.Properties;
026    import java.util.logging.Logger;
027    import javax.sql.DataSource;
028    import org.apache.commons.dbcp.AbandonedConfig;
029    import org.apache.commons.dbcp.AbandonedObjectPool;
030    import org.apache.commons.dbcp.BasicDataSource;
031    import org.apache.commons.dbcp.ConnectionFactory;
032    import org.apache.commons.dbcp.DriverConnectionFactory;
033    import org.apache.commons.dbcp.DriverManagerConnectionFactory;
034    import org.apache.commons.dbcp.PoolableConnectionFactory;
035    import org.apache.commons.dbcp.PoolingDataSource;
036    import org.apache.commons.pool.KeyedObjectPoolFactory;
037    import org.apache.commons.pool.PoolableObjectFactory;
038    import org.apache.commons.pool.impl.GenericKeyedObjectPool;
039    import org.apache.commons.pool.impl.GenericKeyedObjectPoolFactory;
040    import org.apache.commons.pool.impl.GenericObjectPool;
041    import org.apache.commons.math.random.RandomData;
042    import org.apache.commons.math.random.RandomDataImpl;
043    import org.apache.commons.performance.ConfigurationException;
044    import org.apache.commons.performance.ClientThread;
045    import org.apache.commons.performance.LoadGenerator;
046    import org.apache.commons.performance.Statistics;
047     
048    /**
049     * Configurable load / performance tester for commons dbcp.
050     * Uses Commons Digester to parse and load configuration and spawns
051     * DBCPClientThread instances to generate load and gather statistics.
052     *
053     */
054    public class DBCPSoak extends LoadGenerator {
055        // Connection properties
056        private String driverClass;
057        private String connectUrl;
058        private String connectUser;
059        private String connectPassword;
060        private String queryType;
061        
062        // Connection pool properties
063        private String poolType;
064        private String driverType;
065        private String factoryType;
066        private boolean autocommit;
067        private boolean readOnly;
068        private byte exhaustedAction;
069        private boolean testOnBorrow;
070        private boolean testOnReturn;
071        private long timeBetweenEvictions;
072        private int testsPerEviction;
073        private long idleTimeout;
074        private boolean testWhileIdle;
075        private String validationQuery;
076        private AbandonedConfig abandonedConfig = new AbandonedConfig();
077        private boolean poolPreparedStatements;
078        private int maxOpenStatements;
079        private int maxActive;
080        private int maxIdle;
081        private int minIdle;
082        private long maxWait;
083        
084        // DataSource type
085        private String dataSourceType;
086        
087        // Instance variables
088        private GenericObjectPool connectionPool;
089        private DataSource dataSource;
090        
091        /**
092         * Create connection pool and, if necessary, test table.
093         */
094        protected void init() throws Exception {
095            
096            if (dataSourceType.equals("BasicDataSource")) {
097                BasicDataSource bds = new BasicDataSource();
098                bds.setDefaultAutoCommit(autocommit);
099                bds.setPassword(connectPassword);
100                bds.setUrl(connectUrl);
101                bds.setUsername(connectUser);
102                bds.setDriverClassName(driverClass);
103                bds.setMinEvictableIdleTimeMillis(idleTimeout);
104                bds.setMaxActive(maxActive);
105                bds.setMaxIdle(maxIdle);
106                bds.setMaxWait(maxWait);
107                bds.setMinIdle(minIdle);
108                bds.setPoolPreparedStatements(poolPreparedStatements);
109                bds.setDefaultReadOnly(readOnly);
110                bds.setTestOnBorrow(testOnBorrow);
111                bds.setTestOnReturn(testOnReturn);
112                bds.setTestWhileIdle(testWhileIdle);
113                bds.setNumTestsPerEvictionRun(testsPerEviction);
114                bds.setTimeBetweenEvictionRunsMillis(timeBetweenEvictions);
115                bds.setValidationQuery(validationQuery);
116                if (poolType.equals("AbandonedObjectPool")) {
117                        bds.setRemoveAbandoned(true);
118                }
119                dataSource = bds; 
120                checkDatabase();
121                return;
122            }
123    
124            Class.forName(driverClass);
125            
126            // Create object pool
127            if (poolType.equals("GenericObjectPool")) {
128                connectionPool = new GenericObjectPool(
129                        null, maxActive, exhaustedAction,
130                        maxWait, maxIdle, minIdle, testOnBorrow, testOnReturn,
131                        timeBetweenEvictions, testsPerEviction, idleTimeout,
132                        testWhileIdle);
133            } else if (poolType.equals("AbandonedObjectPool")) {
134                connectionPool = new AbandonedObjectPool(null,abandonedConfig);
135            } else {
136                throw new ConfigurationException(
137                        "invalid pool type configuration: " + poolType);
138            }
139            
140            // Create raw connection factory
141            ConnectionFactory connectionFactory = null;
142            if (driverType.equals("DriverManager")) {
143                connectionFactory = new DriverManagerConnectionFactory(
144                        connectUrl,connectUser,
145                        connectPassword);
146            } else if (driverType.equals("Driver")) {
147                Properties props = new Properties();
148                props.put("user", connectUser);
149                props.put("password", connectPassword);
150                connectionFactory = new DriverConnectionFactory(
151                        (Driver) Class.forName(driverClass).newInstance(),
152                        connectUrl, props);
153            } else {
154                throw new ConfigurationException(
155                "Bad config setting for driver type"); 
156            } 
157       
158            // Create object factory
159            PoolableObjectFactory poolableConnectionFactory = null;
160            KeyedObjectPoolFactory statementPoolFactory = null;
161            if (poolPreparedStatements) { // Use same defaults as BasicDataSource
162                statementPoolFactory = new GenericKeyedObjectPoolFactory(null,
163                            -1, // unlimited maxActive (per key)
164                            GenericKeyedObjectPool.WHEN_EXHAUSTED_FAIL,
165                            0, // maxWait
166                            1, // maxIdle (per key)
167                            maxOpenStatements); //TODO: make all configurable
168            }
169            if (factoryType.equals("PoolableConnectionFactory")) {
170                poolableConnectionFactory = 
171                    new PoolableConnectionFactory(
172                            connectionFactory,connectionPool, statementPoolFactory,
173                            validationQuery, readOnly, autocommit);
174            } else if (factoryType.equals("CPDSConnectionFactory")) {
175                throw new ConfigurationException(
176                "CPDSConnectionFactory not implemented yet");
177            } else {
178                throw new ConfigurationException(
179                        "Invalid factory type: " + factoryType);
180            }
181            
182            // Create DataSource
183            dataSource = new PoolingDataSource(connectionPool); 
184            checkDatabase();   
185        }
186        
187        protected void checkDatabase() throws Exception {
188            // Try to connect and query test_table. If "test_table" appears in 
189            // exception message, assume table needs to be created. 
190            Connection conn = dataSource.getConnection();
191            try {
192                Statement stmnt = conn.createStatement();
193                stmnt.execute("select * from test_table where indexed=1;");
194                stmnt.close();
195            } catch (Exception ex) {
196                if (ex.getMessage().indexOf("test_table") > 0) {
197                    logger.info("Creating test_table");
198                    makeTable();
199                    logger.info("test_table created successfully");
200                } else {
201                    throw ex;
202                }
203            } finally {
204                conn.close();
205            }
206        }
207        
208        /**
209         * Close connection pool
210         */
211        protected void cleanUp() throws Exception {
212            if (dataSourceType.equals("BasicDataSource")) {
213                ((BasicDataSource) dataSource).close();
214            } else {
215                connectionPool.close();
216            }
217        }
218        
219        protected ClientThread makeClientThread(
220                long iterations, long minDelay, long maxDelay, double sigma,
221                String delayType, long rampPeriod, long peakPeriod,
222                long troughPeriod, String cycleType, String rampType,
223                Logger logger, Statistics stats) {
224            
225            return new DBCPClientThread(iterations, minDelay, maxDelay,
226                sigma, delayType, queryType, rampPeriod, peakPeriod, 
227                troughPeriod, cycleType, rampType, logger, dataSource,
228                stats);
229        }
230        
231        // ------------------------------------------------------------------------
232        // Configuration methods specific to this LoadGenerator invoked by Digester
233        // when superclass execute calls digester.parse.
234        // ------------------------------------------------------------------------
235        public void configureDataBase(String driver, String url,
236                String username, String password, String queryType) {
237            this.driverClass = driver;
238            this.connectUrl = url;
239            this.connectUser = username;
240            this.connectPassword = password;
241            this.queryType = queryType;
242        }
243        
244        public void configureDataSource(String type) {
245            this.dataSourceType = type;
246        }
247        
248        public void configureConnectionFactory(String type,
249                String autoCommit, String readOnly, String validationQuery) {
250            this.driverType = type;
251            this.autocommit = Boolean.parseBoolean(autoCommit);
252            this.readOnly = Boolean.parseBoolean(readOnly);
253            this.validationQuery = validationQuery;
254        }
255        
256        public void configurePoolableConnectionFactory(String type, 
257                String poolPreparedStatements, String maxOpenStatements) {
258            this.factoryType = type;
259            this.poolPreparedStatements = 
260                Boolean.parseBoolean(poolPreparedStatements);
261            this.maxOpenStatements = Integer.parseInt(maxOpenStatements);
262        }
263        
264        public void configurePool(String maxActive, String maxIdle, String minIdle,
265                String maxWait, String exhaustedAction, String testOnBorrow,
266                String testOnReturn, String timeBetweenEvictions,
267                String testsPerEviction, String idleTimeout, 
268                String testWhileIdle, String type) throws ConfigurationException { 
269            this.maxActive = Integer.parseInt(maxActive);
270            this.maxIdle = Integer.parseInt(maxIdle);
271            this.minIdle = Integer.parseInt(minIdle);
272            this.maxWait = Long.parseLong(maxWait);
273            this.testOnBorrow = Boolean.parseBoolean(testOnBorrow);
274            this.testOnReturn = Boolean.parseBoolean(testOnReturn);
275            this.timeBetweenEvictions = Long.parseLong(timeBetweenEvictions);
276            this.testsPerEviction = Integer.parseInt(testsPerEviction);
277            this.idleTimeout = Long.parseLong(idleTimeout);
278            this.testWhileIdle = Boolean.parseBoolean(testWhileIdle);
279            this.poolType = type;
280            if (exhaustedAction.equals("block")) {
281                this.exhaustedAction = GenericObjectPool.WHEN_EXHAUSTED_BLOCK;
282            } else if (exhaustedAction.equals("fail")) {
283                this.exhaustedAction = GenericObjectPool.WHEN_EXHAUSTED_FAIL;
284            } else if (exhaustedAction.equals("grow")) {
285                this.exhaustedAction = GenericObjectPool.WHEN_EXHAUSTED_GROW;
286            } else { 
287                throw new ConfigurationException(
288                "Bad configuration setting for exhausted action: "
289                        + exhaustedAction); 
290            }  
291        }
292        
293        public void configureAbandonedConfig(String logAbandoned,
294                String removeAbandoned, String abandonedTimeout) {
295            abandonedConfig.setLogAbandoned(Boolean.parseBoolean(logAbandoned));
296            abandonedConfig.setRemoveAbandoned(
297                    Boolean.parseBoolean(removeAbandoned));
298            abandonedConfig.setRemoveAbandonedTimeout(
299                    Integer.parseInt(abandonedTimeout));
300        }
301         
302        /**
303         * Creates and populates the test table (test_table) used in the 
304         * load tests. The created table will have 10,000 rows and 3 columns,
305         * populated with random data: <ul>
306         * <li> indexed (indexed integer column) </li>
307         * <li> not_indexed (non-indexed integer column) </li>
308         * <li> text (non-indexed varchar column) </li>
309         * </ul>
310         *
311         * @throws Exception
312         */
313        protected void makeTable() throws Exception {
314            Class.forName(driverClass); 
315            Connection db = DriverManager.getConnection(connectUrl,connectUser,
316                    connectPassword);
317            try {
318                Statement sql = db.createStatement();
319                String sqlText = 
320                    "create table test_table (indexed int, text varchar(20)," + 
321                    " not_indexed int)";
322                sql.executeUpdate(sqlText);
323                sqlText = "CREATE INDEX test1_id_index ON test_table (indexed);";
324                sql.executeUpdate(sqlText);
325                RandomData randomData = new RandomDataImpl();
326                for (int i = 0; i < 10000; i++) {
327                    int indexed = randomData.nextInt(0, 100);
328                    int not_indexed = randomData.nextInt(0, 1000);
329                    String text = randomData.nextHexString(20);
330                    sqlText = 
331                        "INSERT INTO test_table (indexed, text, not_indexed)" + 
332                        "VALUES (" + indexed + "," + "'"+ text + "'," + 
333                        not_indexed + ");";
334                    sql.executeUpdate(sqlText);
335                }
336                sql.close();
337            } finally {
338                db.close();
339            }
340        }
341        
342        /**
343         * Add dbcp configuration to parameters loaded by super.
344         * Also set config file name.
345         */
346        protected void configure() throws Exception {
347            
348            super.configure();
349            
350            digester.addCallMethod("configuration/database",
351                    "configureDataBase", 5);
352            digester.addCallParam("configuration/database/driver", 0);
353            digester.addCallParam("configuration/database/url", 1);
354            digester.addCallParam("configuration/database/username", 2);
355            digester.addCallParam("configuration/database/password", 3);
356            digester.addCallParam("configuration/database/query-type", 4);
357            
358            digester.addCallMethod("configuration", "configureDataSource", 1);
359            digester.addCallParam("configuration/datasource-type", 0);
360            
361            digester.addCallMethod("configuration/connection-factory", 
362                    "configureConnectionFactory", 4);
363            digester.addCallParam(
364                    "configuration/connection-factory/type", 0);
365            digester.addCallParam(
366                    "configuration/connection-factory/auto-commit", 1);
367            digester.addCallParam(
368                    "configuration/connection-factory/read-only", 2);
369            digester.addCallParam(
370                    "configuration/connection-factory/validation-query", 3);
371            
372            digester.addCallMethod("configuration/poolable-connection-factory", 
373                    "configurePoolableConnectionFactory", 3);
374            digester.addCallParam(
375                    "configuration/poolable-connection-factory/type", 0);
376            digester.addCallParam(
377                    "configuration/poolable-connection-factory/pool-prepared-statements", 1);
378            digester.addCallParam(
379                    "configuration/poolable-connection-factory/max-open-statements", 2);
380            
381            digester.addCallMethod("configuration/pool", 
382                    "configurePool", 12);
383            digester.addCallParam(
384                    "configuration/pool/max-active", 0);
385            digester.addCallParam(
386                    "configuration/pool/max-idle", 1);
387            digester.addCallParam(
388                    "configuration/pool/min-idle", 2);
389            digester.addCallParam(
390                    "configuration/pool/max-wait", 3);
391            digester.addCallParam(
392                    "configuration/pool/exhausted-action", 4);
393            digester.addCallParam(
394                    "configuration/pool/test-on-borrow", 5);
395            digester.addCallParam(
396                    "configuration/pool/test-on-return", 6);
397            digester.addCallParam(
398                    "configuration/pool/time-between-evictions", 7);
399            digester.addCallParam(
400                    "configuration/pool/tests-per-eviction", 8);
401            digester.addCallParam(
402                    "configuration/pool/idle-timeout", 9);
403            digester.addCallParam(
404                    "configuration/pool/test-while-idle", 10);
405            digester.addCallParam(
406                    "configuration/pool/type", 11);
407            
408            digester.addCallMethod("configuration/abandoned-config",
409                    "configureAbandonedConfig", 3);
410            digester.addCallParam(
411                    "configuration/abandoned-config/log-abandoned", 0);
412            digester.addCallParam(
413                    "configuration/abandoned-config/remove-abandoned", 1);
414            digester.addCallParam(
415                    "configuration/abandoned-config/abandoned-timeout", 2);
416            
417            this.configFile = "config-dbcp.xml";
418        }
419    
420        public GenericObjectPool getConnectionPool() {
421            return connectionPool;
422        }
423    }