001: /****************************************************************
002: * Licensed to the Apache Software Foundation (ASF) under one *
003: * or more contributor license agreements. See the NOTICE file *
004: * distributed with this work for additional information *
005: * regarding copyright ownership. The ASF licenses this file *
006: * to you under the Apache License, Version 2.0 (the *
007: * "License"); you may not use this file except in compliance *
008: * with the License. You may obtain a copy of the License at *
009: * *
010: * http://www.apache.org/licenses/LICENSE-2.0 *
011: * *
012: * Unless required by applicable law or agreed to in writing, *
013: * software distributed under the License is distributed on an *
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
015: * KIND, either express or implied. See the License for the *
016: * specific language governing permissions and limitations *
017: * under the License. *
018: ****************************************************************/package org.apache.james.util.mordred;
019:
020: import org.apache.avalon.excalibur.datasource.DataSourceComponent;
021: import org.apache.avalon.framework.activity.Disposable;
022: import org.apache.avalon.framework.configuration.Configurable;
023: import org.apache.avalon.framework.configuration.Configuration;
024: import org.apache.avalon.framework.configuration.ConfigurationException;
025: import org.apache.avalon.framework.logger.AbstractLogEnabled;
026:
027: import java.io.PrintWriter;
028: import java.io.StringWriter;
029: import java.sql.Connection;
030: import java.sql.SQLException;
031: import java.util.ArrayList;
032:
033: /**
034: * <p>
035: * This is a <b>reliable</b> DataSource implementation, based on the pooling logic written for <a
036: * href="http://share.whichever.com/">Town</a> and the configuration found in Avalon's excalibur
037: * code.
038: * </p>
039: *
040: * <p>
041: * This uses the normal <code>java.sql.Connection</code> object and
042: * <code>java.sql.DriverManager</code>. The Configuration is like this:
043: * <pre>
044: * <jdbc>
045: * <pool-controller min="<i>5</i>" max="<i>10</i>" connection-class="<i>my.overrided.ConnectionClass</i>">
046: * <keep-alive>select 1</keep-alive>
047: * </pool-controller>
048: * <driver><i>com.database.jdbc.JdbcDriver</i></driver>
049: * <dburl><i>jdbc:driver://host/mydb</i></dburl>
050: * <user><i>username</i></user>
051: * <password><i>password</i></password>
052: * </jdbc>
053: * </pre>
054: * </p>
055: *
056: * @version CVS $Revision: 494012 $
057: * @since 4.0
058: */
059: public class JdbcDataSource extends AbstractLogEnabled implements
060: Configurable, Runnable, Disposable, DataSourceComponent {
061: // The limit that an active connection can be running
062: public static final long ACTIVE_CONN_TIME_LIMIT = 60000; // (one minute)
063: public static final long ACTIVE_CONN_HARD_TIME_LIMIT = 2 * ACTIVE_CONN_TIME_LIMIT;
064: // How long before you kill off a connection due to inactivity
065: public static final long CONN_IDLE_LIMIT = 600000; // (10 minutes)
066: private static final boolean DEEP_DEBUG = false;
067: private static int total_served = 0;
068: // This is a temporary variable used to track how many active threads
069: // are in createConnection(). This is to prevent to many connections
070: // from being opened at once.
071: private int connCreationsInProgress = 0;
072: // The error message is the conn pooler cannot serve connections for whatever reason
073: private String connErrorMessage = null;
074: // the last time a connection was created...
075: private long connLastCreated = 0;
076: // connection number for like of this broker
077: private int connectionCount;
078: // Driver class
079: private String jdbcDriver;
080: // Password to login to database
081: private String jdbcPassword;
082: // Server to connect to database (this really is the jdbc URL)
083: private String jdbcURL;
084: // Username to login to database
085: private String jdbcUsername;
086: // Maximum number of connections to have open at any point
087: private int maxConn = 0;
088: // collection of connection objects
089: private ArrayList pool;
090: // Thread that checks for dead/aged connections and removes them from pool
091: private Thread reaper;
092: // Flag to indicate whether reaper thread should run
093: private boolean reaperActive = false;
094: // a SQL command to execute to see if the connection is still ok
095: private String verifyConnSQL;
096:
097: /**
098: * Implements the ConnDefinition behavior when a connection is needed. Checks the pool of
099: * connections to see if there is one available. If there is not and we are below the max
100: * number of connections limit, it tries to create another connection. It retries this 10
101: * times until a connection is available or can be created
102: *
103: * @return java.sql.Connection
104: * @throws SQLException Document throws!
105: */
106: public Connection getConnection() throws SQLException {
107: //If the conn definition has a fatal connection problem, need to return that error
108: if (connErrorMessage != null) {
109: throw new SQLException(connErrorMessage);
110: }
111: //Look through our list of open connections right now, starting from beginning.
112: //If we find one, book it.
113: int count = total_served++;
114: if (DEEP_DEBUG) {
115: StringBuffer deepDebugBuffer = new StringBuffer(128)
116: .append((new java.util.Date()).toString()).append(
117: " trying to get a connection (").append(
118: count).append(")");
119: System.out.println(deepDebugBuffer.toString());
120: }
121: for (int attempts = 1; attempts <= 100; attempts++) {
122: synchronized (pool) {
123: for (int i = 0; i < pool.size(); i++) {
124: PoolConnEntry entry = (PoolConnEntry) pool.get(i);
125: //Set the appropriate flags to make this connection
126: //marked as in use
127: try {
128: if (entry.lock()) {
129: if (DEEP_DEBUG) {
130: StringBuffer deepDebugBuffer = new StringBuffer(
131: 128).append(
132: (new java.util.Date())
133: .toString()).append(
134: " return a connection (")
135: .append(count).append(")");
136: System.out.println(deepDebugBuffer
137: .toString());
138: }
139: return entry;
140: }
141: } catch (SQLException se) {
142: //Somehow a closed connection appeared in our pool.
143: //Remove it immediately.
144: finalizeEntry(entry);
145: continue;
146: }
147: //we were unable to get a lock on this entry.. so continue through list
148: } //loop through existing connections
149: //If we have 0, create another
150: if (DEEP_DEBUG) {
151: System.out.println(pool.size());
152: }
153: try {
154: if (pool.size() == 0) {
155: //create a connection
156: PoolConnEntry entry = createConn();
157: if (entry != null) {
158: if (DEEP_DEBUG) {
159: StringBuffer deepDebugBuffer = new StringBuffer(
160: 128).append(
161: (new java.util.Date())
162: .toString()).append(
163: " returning new connection (")
164: .append(count).append(")");
165: System.out.println(deepDebugBuffer
166: .toString());
167: }
168: return entry;
169: }
170: //looks like a connection was already created
171: } else {
172: //Since we didn't find one, and we have < max connections, then consider whether
173: // we create another
174: //if we've hit the 3rd attempt without getting a connection,
175: // let's create another to anticipate a slow down
176: if ((attempts == 2)
177: && (pool.size() < maxConn || maxConn == 0)) {
178: PoolConnEntry entry = createConn();
179: if (entry != null) {
180: if (DEEP_DEBUG) {
181: StringBuffer deepDebugBuffer = new StringBuffer(
182: 32)
183: .append(
184: " returning new connection (")
185: .append(count).append(")");
186: System.out.println(deepDebugBuffer
187: .toString());
188: }
189: return entry;
190: } else {
191: attempts = 1;
192: }
193: }
194: }
195: } catch (SQLException sqle) {
196: //Ignore... error creating the connection
197: StringWriter sout = new StringWriter();
198: PrintWriter pout = new PrintWriter(sout, true);
199: pout.println("Error creating connection: ");
200: sqle.printStackTrace(pout);
201: if (getLogger().isErrorEnabled()) {
202: getLogger().error(sout.toString());
203: }
204: }
205: }
206: //otherwise sleep 50ms 10 times, then create a connection
207: try {
208: Thread.currentThread().sleep(50);
209: } catch (InterruptedException ie) {
210: }
211: }
212: // Give up... no connections available
213: throw new SQLException("Giving up... no connections available.");
214: }
215:
216: /**
217: * @see org.apache.avalon.framework.configuration.Configurable#configure(Configuration)
218: */
219: public void configure(final Configuration configuration)
220: throws ConfigurationException {
221: try {
222: jdbcDriver = configuration.getChild("driver")
223: .getValue(null);
224: jdbcURL = configuration.getChild("dburl").getValue(null);
225: jdbcUsername = configuration.getChild("user")
226: .getValue(null);
227: jdbcPassword = configuration.getChild("password").getValue(
228: null);
229: maxConn = configuration.getChild("max")
230: .getValueAsInteger(2);
231: //logfilename?
232: verifyConnSQL = configuration.getChild("keep-alive")
233: .getValue(null);
234: //Not support from Town: logfilename
235: //Not supporting from Excalibur: pool-controller, min, auto-commit, oradb, connection-class
236: if (jdbcDriver == null) {
237: throw new ConfigurationException(
238: "You need to specify a valid driver, e.g., <driver>my.class</driver>");
239: }
240: try {
241: if (getLogger().isDebugEnabled()) {
242: getLogger().debug(
243: "Loading new driver: " + jdbcDriver);
244: }
245: // TODO: Figure out why this breaks when we change the Class.forName to
246: // a loadClass method call on the class loader.
247: // DO NOT MESS WITH THIS UNLESS YOU ARE WILLING TO TEST
248: // AND FIX THE PROBLEMS!
249: Class.forName(jdbcDriver, true, Thread.currentThread()
250: .getContextClassLoader());
251: // These variations do NOT work:
252: // getClass().getClassLoader().loadClass(jdbcDriver); -- DON'T USE -- BROKEN!!
253: // Thread.currentThread().getContextClassLoader().loadClass(jdbcDriver); -- DON'T USE -- BROKEN!!
254: } catch (ClassNotFoundException cnfe) {
255: StringBuffer exceptionBuffer = new StringBuffer(128)
256: .append("'")
257: .append(jdbcDriver)
258: .append(
259: "' could not be found in classloader. Please specify a valid JDBC driver");
260: String exceptionMessage = exceptionBuffer.toString();
261: getLogger().error(exceptionMessage);
262: throw new ConfigurationException(exceptionMessage);
263: }
264: if (jdbcURL == null) {
265: throw new ConfigurationException(
266: "You need to specify a valid JDBC connection string, e.g., <dburl>jdbc:driver:database</dburl>");
267: }
268: if (maxConn < 0) {
269: throw new ConfigurationException(
270: "Maximum number of connections specified must be at least 1 (0 means no limit).");
271: }
272: if (getLogger().isDebugEnabled()) {
273: getLogger().debug("Starting connection pooler");
274: getLogger().debug("driver = " + jdbcDriver);
275: getLogger().debug("dburl = " + jdbcURL);
276: getLogger().debug("username = " + jdbcUsername);
277: //We don't show the password
278: getLogger().debug("max connections = " + maxConn);
279: }
280: pool = new ArrayList();
281: reaperActive = true;
282: reaper = new Thread(this );
283: reaper.setDaemon(true);
284: reaper.start();
285: } catch (ConfigurationException ce) {
286: //Let this pass through...
287: throw ce;
288: } catch (Exception e) {
289: throw new ConfigurationException(
290: "Error configuring JdbcDataSource", e);
291: }
292: }
293:
294: /**
295: * The dispose operation is called at the end of a components lifecycle.
296: * Cleans up all JDBC connections.
297: *
298: * @throws Exception if an error is encountered during shutdown
299: */
300: public void dispose() {
301: // Stop the background monitoring thread
302: if (reaper != null) {
303: reaperActive = false;
304: //In case it's sleeping, help it quit faster
305: reaper.interrupt();
306: reaper = null;
307: }
308: // The various entries will finalize themselves once the reference
309: // is removed, so no need to do it here
310: }
311:
312: /**
313: * Implements the ConnDefinition behavior when something bad has happened to a connection. If a
314: * sql command was provided in the properties file, it will run this and attempt to determine
315: * whether the connection is still valid. If it is, it recycles this connection back into the
316: * pool. If it is not, it closes the connection.
317: *
318: * @param entry the connection that had problems
319: * @deprecated - No longer used in the new approach.
320: */
321: public void killConnection(PoolConnEntry entry) {
322: if (entry != null) {
323: // if we were provided SQL to test the connection with, we will use
324: // this and possibly just release the connection after clearing warnings
325: if (verifyConnSQL != null) {
326: try {
327: // Test this connection
328: java.sql.Statement stmt = null;
329: try {
330: stmt = entry.createStatement();
331: stmt.execute(verifyConnSQL);
332: } finally {
333: try {
334: if (stmt != null) {
335: stmt.close();
336: }
337: } catch (SQLException sqle) {
338: // Failure to close ignored on test connection
339: }
340: }
341: // Passed test... recycle the entry
342: entry.unlock();
343: } catch (SQLException e1) {
344: // Failed test... close the entry
345: finalizeEntry(entry);
346: }
347: } else {
348: // No SQL was provided... we have to kill this entry to be sure
349: finalizeEntry(entry);
350: }
351: return;
352: } else {
353: if (getLogger().isWarnEnabled()) {
354: getLogger().warn(
355: "----> Could not find connection to kill!!!");
356: }
357: return;
358: }
359: }
360:
361: /**
362: * Implements the ConnDefinition behavior when a connection is no longer needed. This resets
363: * flags on the wrapper of the connection to allow others to use this connection.
364: *
365: * @param entry
366: */
367: public void releaseConnection(PoolConnEntry entry) {
368: //PoolConnEntry entry = findEntry(conn);
369: if (entry != null) {
370: entry.unlock();
371: return;
372: } else {
373: if (getLogger().isWarnEnabled()) {
374: getLogger()
375: .warn(
376: "----> Could not find the connection to free!!!");
377: }
378: return;
379: }
380: }
381:
382: /**
383: * Background thread that checks if there are fewer connections open than minConn specifies,
384: * and checks whether connections have been checked out for too long, killing them.
385: */
386: public void run() {
387: try {
388: while (reaperActive) {
389: synchronized (pool) {
390: for (int i = 0; i < pool.size(); i++)
391: try {
392: PoolConnEntry entry = (PoolConnEntry) pool
393: .get(i);
394: long age = System.currentTimeMillis()
395: - entry.getLastActivity();
396: synchronized (entry) {
397: if ((entry.getStatus() == PoolConnEntry.ACTIVE)
398: && (age > ACTIVE_CONN_HARD_TIME_LIMIT)) {
399: StringBuffer logBuffer = new StringBuffer(
400: 128)
401: .append(
402: " ***** connection ")
403: .append(entry.getId())
404: .append(" is way too old: ")
405: .append(age)
406: .append(" > ")
407: .append(
408: ACTIVE_CONN_HARD_TIME_LIMIT)
409: .append(
410: " and will be closed.");
411: getLogger().info(
412: logBuffer.toString());
413: // This connection is way too old...
414: // kill it no matter what
415: finalizeEntry(entry);
416: continue;
417: }
418: if ((entry.getStatus() == PoolConnEntry.ACTIVE)
419: && (age > ACTIVE_CONN_TIME_LIMIT)) {
420: StringBuffer logBuffer = new StringBuffer(
421: 128)
422: .append(
423: " ***** connection ")
424: .append(entry.getId())
425: .append(" is way too old: ")
426: .append(age)
427: .append(" > ")
428: .append(
429: ACTIVE_CONN_TIME_LIMIT);
430: getLogger().info(
431: logBuffer.toString());
432: // This connection is way too old...
433: // just log it for now.
434: continue;
435: }
436: if ((entry.getStatus() == PoolConnEntry.AVAILABLE)
437: && (age > CONN_IDLE_LIMIT)) {
438: //We've got a connection that's too old... kill it
439: finalizeEntry(entry);
440: continue;
441: }
442: }
443: } catch (Throwable ex) {
444: StringWriter sout = new StringWriter();
445: PrintWriter pout = new PrintWriter(sout,
446: true);
447: pout.println("Reaper Error: ");
448: ex.printStackTrace(pout);
449: if (getLogger().isErrorEnabled()) {
450: getLogger().error(sout.toString());
451: }
452: }
453: }
454: try {
455: // Check for activity every 5 seconds
456: Thread.sleep(5000L);
457: } catch (InterruptedException ex) {
458: }
459: }
460: } finally {
461: Thread.currentThread().interrupted();
462: }
463: }
464:
465: protected void debug(String message) {
466: getLogger().debug(message);
467: }
468:
469: protected void info(String message) {
470: getLogger().info(message);
471: }
472:
473: /*
474: * This is a real hack, but oh well for now
475: */
476: protected void warn(String message) {
477: getLogger().warn(message);
478: }
479:
480: /**
481: * Creates a new connection as per these properties, adds it to the pool, and logs the creation.
482: *
483: * @return PoolConnEntry the new connection wrapped as an entry
484: * @throws SQLException
485: */
486: private PoolConnEntry createConn() throws SQLException {
487: PoolConnEntry entry = null;
488: synchronized (pool) {
489: if (connCreationsInProgress > 0) {
490: //We are already creating one in another place
491: return null;
492: }
493: long now = System.currentTimeMillis();
494: if ((now - connLastCreated) < (1000 * pool.size())) {
495: //We don't want to scale up too quickly...
496: if (DEEP_DEBUG) {
497: System.err
498: .println("We don't want to scale up too quickly");
499: }
500: return null;
501: }
502: if ((maxConn == 0) || (pool.size() < maxConn)) {
503: connCreationsInProgress++;
504: connLastCreated = now;
505: } else {
506: // We've already hit a limit... fail silently
507: if (getLogger().isDebugEnabled()) {
508: StringBuffer logBuffer = new StringBuffer(128)
509: .append("Connection limit hit... ").append(
510: pool.size())
511: .append(" in pool and ").append(
512: connCreationsInProgress).append(
513: " + on the way.");
514: getLogger().debug(logBuffer.toString());
515: }
516: return null;
517: }
518: try {
519: entry = new PoolConnEntry(this , java.sql.DriverManager
520: .getConnection(jdbcURL, jdbcUsername,
521: jdbcPassword), ++connectionCount);
522: if (getLogger().isDebugEnabled()) {
523: getLogger().debug("Opening connection " + entry);
524: }
525: entry.lock();
526: pool.add(entry);
527: return entry;
528: } catch (SQLException sqle) {
529: //Shouldn't ever happen, but it did, just return null.
530: // Exception from DriverManager.getConnection() - log it, and return null
531: StringWriter sout = new StringWriter();
532: PrintWriter pout = new PrintWriter(sout, true);
533: pout.println("Error creating connection: ");
534: sqle.printStackTrace(pout);
535: if (getLogger().isErrorEnabled()) {
536: getLogger().error(sout.toString());
537: }
538: return null;
539: } finally {
540: connCreationsInProgress--;
541: }
542: }
543: }
544:
545: /**
546: * Closes a connection and removes it from the pool.
547: *
548: * @param entry entry
549: */
550: private void finalizeEntry(PoolConnEntry entry) {
551: synchronized (pool) {
552: try {
553: entry.finalize();
554: } catch (Exception fe) {
555: }
556: pool.remove(entry);
557: }
558: }
559: }
|