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 * <jdbc>
46 * <pool-controller min="<i>5</i>" max="<i>10</i>" connection-class="<i>my.overrided.ConnectionClass</i>">
47 * <keep-alive>select 1</keep-alive>
48 * </pool-controller>
49 * <driver><i>com.database.jdbc.JdbcDriver</i></driver>
50 * <dburl><i>jdbc:driver://host/mydb</i></dburl>
51 * <user><i>username</i></user>
52 * <password><i>password</i></password>
53 * </jdbc>
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
66 public static final long ACTIVE_CONN_TIME_LIMIT = 60000;
67 public static final long ACTIVE_CONN_HARD_TIME_LIMIT = 2*ACTIVE_CONN_TIME_LIMIT;
68
69 public static final long CONN_IDLE_LIMIT = 600000;
70 private static final boolean DEEP_DEBUG = false;
71 private static int total_served = 0;
72
73
74
75 private int connCreationsInProgress = 0;
76
77 private String connErrorMessage = null;
78
79 private long connLastCreated = 0;
80
81 private int connectionCount;
82
83 private String jdbcDriver;
84
85 private String jdbcPassword;
86
87 private String jdbcURL;
88
89 private String jdbcUsername;
90
91 private int maxConn = 0;
92
93 private ArrayList pool;
94
95 private Thread reaper;
96
97 private boolean reaperActive = false;
98
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
112 if(connErrorMessage != null) {
113 throw new SQLException(connErrorMessage);
114 }
115
116
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
132
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
148
149 finalizeEntry(entry);
150 continue;
151 }
152
153 }
154
155 if(DEEP_DEBUG) {
156 System.out.println(pool.size());
157 }
158 try {
159 if(pool.size() == 0) {
160
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
175 } else {
176
177
178
179
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
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
209 try {
210 Thread.currentThread().sleep(50);
211 } catch(InterruptedException ie) {
212 }
213 }
214
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
230 verifyConnSQL = configuration.getChild("keep-alive").getValue(null);
231
232
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
241
242
243
244 Class.forName(jdbcDriver, true, Thread.currentThread().getContextClassLoader());
245
246
247
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
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
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
294 if(reaper != null) {
295 reaperActive = false;
296
297 reaper.interrupt();
298 reaper = null;
299 }
300
301
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
316
317 if(verifyConnSQL != null) {
318 try {
319
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
331 }
332 }
333
334 entry.unlock();
335 } catch(SQLException e1) {
336
337 finalizeEntry(entry);
338 }
339 } else {
340
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
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
396
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
412
413 continue;
414 }
415 if((entry.getStatus() == PoolConnEntry.AVAILABLE) &&
416 (age > CONN_IDLE_LIMIT)) {
417
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
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
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
471 return null;
472 }
473 long now = System.currentTimeMillis();
474 if((now - connLastCreated) < (1000 * pool.size())) {
475
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
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
513
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 }