View Javadoc

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