001: /*
002: * Copyright 2003-2006 Rick Knowles <winstone-devel at lists sourceforge net>
003: * Distributed under the terms of either:
004: * - the common development and distribution license (CDDL), v1.0; or
005: * - the GNU Lesser General Public License, v2.1 or later
006: */
007: package winstone.jndi.resourceFactories;
008:
009: import java.io.PrintWriter;
010: import java.sql.Connection;
011: import java.sql.Driver;
012: import java.sql.PreparedStatement;
013: import java.sql.SQLException;
014: import java.util.ArrayList;
015: import java.util.Iterator;
016: import java.util.List;
017: import java.util.Map;
018: import java.util.Properties;
019:
020: import javax.sql.DataSource;
021:
022: import winstone.Logger;
023: import winstone.WebAppConfiguration;
024: import winstone.WinstoneResourceBundle;
025:
026: /**
027: * Implements a JDBC 2.0 pooling datasource. This is meant to act as a wrapper
028: * around a JDBC 1.0 driver, just providing the pool management functions.
029: *
030: * Supports keep alives, and check-connection-before-get options, as well
031: * as normal reclaimable pool management options like maxIdle, maxConnections and
032: * startConnections. Additionally it supports poll-retry on full, which means the
033: * getConnection call will block and retry after a certain period when the pool
034: * is maxed out (good for high load conditions).
035: *
036: * This class was originally drawn from the generator-runtime servlet framework and
037: * modified to make it more JDBC-API only compliant.
038: *
039: * @author <a href="mailto:rick_knowles@hotmail.com">Rick Knowles</a>
040: * @version $Id: WinstoneDataSource.java,v 1.8 2006/11/07 01:30:39 rickknowles Exp $
041: */
042: public class WinstoneDataSource implements DataSource, Runnable {
043:
044: public static final WinstoneResourceBundle DS_RESOURCES = new WinstoneResourceBundle(
045: "winstone.jndi.resourceFactories.LocalStrings");
046:
047: private String name;
048:
049: private String url;
050: private Driver driver;
051: private Properties connectProps;
052:
053: private int maxIdleCount;
054: private int maxHeldCount;
055: private int retryCount;
056: private int retryPeriod;
057:
058: private String keepAliveSQL;
059: private int keepAlivePeriod;
060: private boolean checkBeforeGet;
061: private int killInactivePeriod;
062:
063: private List usedWrappers;
064: private List unusedRealConnections; // sempahore
065: private List usedRealConnections;
066:
067: private Thread managementThread;
068:
069: private int loginTimeout;
070: private PrintWriter logWriter;
071:
072: /**
073: * Main constructor. Basically just calls the init method
074: */
075: public WinstoneDataSource(String name, Map args, ClassLoader loader) {
076: this .name = name;
077:
078: this .usedWrappers = new ArrayList();
079: this .usedRealConnections = new ArrayList();
080: this .unusedRealConnections = new ArrayList();
081: this .connectProps = new Properties();
082:
083: // Extract pool management properties
084: this .keepAliveSQL = WebAppConfiguration.stringArg(args,
085: "keepAliveSQL", "");
086: this .keepAlivePeriod = WebAppConfiguration.intArg(args,
087: "keepAlivePeriod", -1);
088: this .checkBeforeGet = WebAppConfiguration.booleanArg(args,
089: "checkBeforeGet", !this .keepAliveSQL.equals(""));
090: this .killInactivePeriod = WebAppConfiguration.intArg(args,
091: "killInactivePeriod", -1);
092:
093: this .url = WebAppConfiguration.stringArg(args, "url", null);
094: String driverClassName = WebAppConfiguration.stringArg(args,
095: "driverClassName", "");
096: if (args.get("username") != null)
097: this .connectProps.put("user", args.get("username"));
098: if (args.get("password") != null)
099: this .connectProps.put("password", args.get("password"));
100:
101: this .maxHeldCount = WebAppConfiguration.intArg(args,
102: "maxConnections", 20);
103: this .maxIdleCount = WebAppConfiguration.intArg(args, "maxIdle",
104: 10);
105: int startCount = WebAppConfiguration.intArg(args,
106: "startConnections", 1);
107:
108: this .retryCount = WebAppConfiguration.intArg(args,
109: "retryCount", 1);
110: this .retryPeriod = WebAppConfiguration.intArg(args,
111: "retryPeriod", 1000);
112:
113: log(Logger.FULL_DEBUG, "WinstoneDataSource.Init", this .url,
114: null);
115:
116: try {
117: synchronized (this .unusedRealConnections) {
118: if (!driverClassName.equals("")) {
119: Class driverClass = Class.forName(driverClassName
120: .trim(), true, loader);
121: this .driver = (Driver) driverClass.newInstance();
122:
123: for (int n = 0; n < startCount; n++) {
124: makeNewRealConnection(this .unusedRealConnections);
125: }
126: }
127: }
128: } catch (Throwable err) {
129: log(Logger.ERROR, "WinstoneDataSource.ErrorInCreate",
130: this .name, err);
131: }
132:
133: // Start management thread
134: this .managementThread = new Thread(this ,
135: "DBConnectionPool management thread");
136: this .managementThread.start();
137: }
138:
139: /**
140: * Close this pool - probably because we will want to re-init the pool
141: */
142: public void destroy() {
143: if (this .managementThread != null) {
144: this .managementThread.interrupt();
145: this .managementThread = null;
146: }
147:
148: synchronized (this .unusedRealConnections) {
149: killPooledConnections(this .unusedRealConnections, 0);
150: killPooledConnections(this .usedRealConnections, 0);
151: }
152:
153: this .usedRealConnections.clear();
154: this .unusedRealConnections.clear();
155: this .usedWrappers.clear();
156: }
157:
158: /**
159: * Gets a connection with a specific username/password. These are not pooled.
160: */
161: public Connection getConnection(String username, String password)
162: throws SQLException {
163: Properties newProps = new Properties();
164: newProps.put("user", username);
165: newProps.put("password", password);
166: Connection conn = this .driver.connect(this .url, newProps);
167: WinstoneConnection wrapper = null;
168: synchronized (this .unusedRealConnections) {
169: wrapper = new WinstoneConnection(conn, this );
170: this .usedWrappers.add(wrapper);
171: }
172: return wrapper;
173: }
174:
175: public Connection getConnection() throws SQLException {
176: return getConnection(this .retryCount);
177: }
178:
179: /**
180: * Get a read-write connection - preferably from the pool, but fresh if needed
181: */
182: protected Connection getConnection(int retriesAllowed)
183: throws SQLException {
184: Connection realConnection = null;
185:
186: synchronized (this .unusedRealConnections) {
187: // If we have any spare, get it from the unused pool
188: if (this .unusedRealConnections.size() > 0) {
189: realConnection = (Connection) this .unusedRealConnections
190: .get(0);
191: this .unusedRealConnections.remove(realConnection);
192: this .usedRealConnections.add(realConnection);
193: log(
194: Logger.FULL_DEBUG,
195: "WinstoneDataSource.UsingPooled",
196: new String[] {
197: "" + this .usedRealConnections.size(),
198: "" + this .unusedRealConnections.size() },
199: null);
200: try {
201: return prepareWrapper(realConnection);
202: } catch (SQLException err) {
203: // Leave the realConnection as non-null, so we know prepareWrapper failed
204: }
205: }
206:
207: // If we are out (and not over our limit), allocate a new one
208: else if (this .usedRealConnections.size() < maxHeldCount) {
209: realConnection = makeNewRealConnection(this .usedRealConnections);
210: log(
211: Logger.FULL_DEBUG,
212: "WinstoneDataSource.UsingNew",
213: new String[] {
214: "" + this .usedRealConnections.size(),
215: "" + this .unusedRealConnections.size() },
216: null);
217: try {
218: return prepareWrapper(realConnection);
219: } catch (SQLException err) {
220: // Leave the realConnection as non-null, so we know prepareWrapper failed
221: }
222: }
223: }
224:
225: if (realConnection != null) {
226: // prepareWrapper() must have failed, so call this method again
227: realConnection = null;
228: return getConnection(retriesAllowed);
229: } else if (retriesAllowed <= 0) {
230: // otherwise throw fail message - we've blown our limit
231: throw new SQLException(DS_RESOURCES.getString(
232: "WinstoneDataSource.Exceeded", "" + maxHeldCount));
233: } else {
234: log(Logger.FULL_DEBUG, "WinstoneDataSource.Retrying",
235: new String[] { "" + maxHeldCount,
236: "" + retriesAllowed, "" + retryPeriod },
237: null);
238:
239: // If we are here, it's because we need to retry for a connection
240: try {
241: Thread.sleep(retryPeriod);
242: } catch (InterruptedException err) {
243: }
244: return getConnection(retriesAllowed - 1);
245: }
246: }
247:
248: private Connection prepareWrapper(Connection realConnection)
249: throws SQLException {
250: // Check before get()
251: if (this .checkBeforeGet) {
252: try {
253: executeKeepAlive(realConnection);
254: } catch (SQLException err) {
255: // Dead connection, kill it and try again
256: killConnection(this .usedRealConnections, realConnection);
257: throw err;
258: }
259: }
260: realConnection.setAutoCommit(false);
261: WinstoneConnection wrapper = new WinstoneConnection(
262: realConnection, this );
263: this .usedWrappers.add(wrapper);
264: return wrapper;
265: }
266:
267: /**
268: * Releases connections back to the pool
269: */
270: void releaseConnection(WinstoneConnection wrapper,
271: Connection realConnection) throws SQLException {
272: synchronized (this .unusedRealConnections) {
273: if (wrapper != null) {
274: this .usedWrappers.remove(wrapper);
275: }
276: if (realConnection != null) {
277: if (this .usedRealConnections.contains(realConnection)) {
278: this .usedRealConnections.remove(realConnection);
279: this .unusedRealConnections.add(realConnection);
280: log(
281: Logger.FULL_DEBUG,
282: "WinstoneDataSource.Releasing",
283: new String[] {
284: ""
285: + this .usedRealConnections
286: .size(),
287: ""
288: + this .unusedRealConnections
289: .size() }, null);
290: } else {
291: log(Logger.WARNING,
292: "WinstoneDataSource.ReleasingUnknown", null);
293: realConnection.close();
294: }
295: }
296: }
297: }
298:
299: public int getLoginTimeout() {
300: return this .loginTimeout;
301: }
302:
303: public PrintWriter getLogWriter() {
304: return this .logWriter;
305: }
306:
307: public void setLoginTimeout(int timeout) {
308: this .loginTimeout = timeout;
309: }
310:
311: public void setLogWriter(PrintWriter writer) {
312: this .logWriter = writer;
313: }
314:
315: /**
316: * Clean up and keep-alive thread.
317: * Note - this needs a lot more attention to the semaphore use during keepAlive etc
318: */
319: public void run() {
320: log(Logger.DEBUG, "WinstoneDataSource.MaintenanceStart", null);
321:
322: int keepAliveCounter = -1;
323: int killInactiveCounter = -1;
324: boolean threadRunning = true;
325:
326: while (threadRunning) {
327: try {
328: long startTime = System.currentTimeMillis();
329:
330: // Keep alive if the time is right
331: if ((this .keepAlivePeriod != -1) && threadRunning) {
332: keepAliveCounter++;
333:
334: if (this .keepAlivePeriod <= keepAliveCounter) {
335: synchronized (this .unusedRealConnections) {
336: executeKeepAliveOnUnused();
337: }
338: keepAliveCounter = 0;
339: }
340: }
341:
342: if (Thread.interrupted()) {
343: threadRunning = false;
344: }
345:
346: // Kill inactive connections if the time is right
347: if ((this .killInactivePeriod != -1) && threadRunning) {
348: killInactiveCounter++;
349:
350: if (this .killInactivePeriod <= killInactiveCounter) {
351: synchronized (this .unusedRealConnections) {
352: killPooledConnections(
353: this .unusedRealConnections,
354: this .maxIdleCount);
355: }
356:
357: killInactiveCounter = 0;
358: }
359: }
360:
361: if ((killInactiveCounter == 0)
362: || (keepAliveCounter == 0)) {
363: log(
364: Logger.FULL_DEBUG,
365: "WinstoneDataSource.MaintenanceTime",
366: ""
367: + (System.currentTimeMillis() - startTime),
368: null);
369: }
370:
371: if (Thread.interrupted()) {
372: threadRunning = false;
373: } else {
374: Thread.sleep(60000); // sleep 1 minute
375: }
376:
377: if (Thread.interrupted()) {
378: threadRunning = false;
379: }
380: } catch (InterruptedException err) {
381: threadRunning = false;
382: continue;
383: }
384: }
385:
386: log(Logger.DEBUG, "WinstoneDataSource.MaintenanceFinish", null);
387: }
388:
389: /**
390: * Executes keep alive for each of the connections in the supplied pool
391: */
392: protected void executeKeepAliveOnUnused() {
393: // keep alive all unused roConns now
394: List dead = new ArrayList();
395:
396: for (Iterator i = this .unusedRealConnections.iterator(); i
397: .hasNext();) {
398: Connection conn = (Connection) i.next();
399:
400: try {
401: executeKeepAlive(conn);
402: } catch (SQLException errSQL) {
403: dead.add(conn);
404: }
405: }
406:
407: for (Iterator i = dead.iterator(); i.hasNext();) {
408: killConnection(this .unusedRealConnections, (Connection) i
409: .next());
410: }
411:
412: log(Logger.FULL_DEBUG, "WinstoneDataSource.KeepAliveFinished",
413: "" + this .unusedRealConnections.size(), null);
414: }
415:
416: protected void executeKeepAlive(Connection connection)
417: throws SQLException {
418: if (!this .keepAliveSQL.equals("")) {
419: PreparedStatement qryKeepAlive = null;
420: try {
421: qryKeepAlive = connection
422: .prepareStatement(keepAliveSQL);
423: qryKeepAlive.execute();
424: } catch (SQLException err) {
425: log(Logger.WARNING,
426: "WinstoneDataSource.KeepAliveFailed", err);
427: throw err;
428: } finally {
429: if (qryKeepAlive != null) {
430: qryKeepAlive.close();
431: }
432: }
433: }
434: }
435:
436: /**
437: * This makes a new rw connection. It assumes that the synchronization has taken
438: * place in the calling code, so is unsafe for use outside this class.
439: */
440: protected Connection makeNewRealConnection(List pool)
441: throws SQLException {
442: if (this .url == null) {
443: throw new SQLException("No JDBC URL supplied");
444: }
445:
446: Connection realConnection = this .driver.connect(this .url,
447: this .connectProps);
448: pool.add(realConnection);
449: log(Logger.FULL_DEBUG, "WinstoneDataSource.AddingNew",
450: new String[] { "" + this .usedRealConnections.size(),
451: "" + this .unusedRealConnections.size() }, null);
452:
453: return realConnection;
454: }
455:
456: /**
457: * Iterates through a list and kills off unused connections until we reach the
458: * minimum idle count for that pool.
459: */
460: protected void killPooledConnections(List pool, int maxIdleCount) {
461: // kill inactive unused roConns now
462: int killed = 0;
463:
464: while (pool.size() > maxIdleCount) {
465: killed++;
466: Connection conn = (Connection) pool.get(0);
467: killConnection(pool, conn);
468: }
469:
470: if (killed > 0) {
471: log(Logger.FULL_DEBUG, "WinstoneDataSource.Killed", ""
472: + killed, null);
473: }
474: }
475:
476: private static void killConnection(List pool, Connection conn) {
477: pool.remove(conn);
478: try {
479: conn.close();
480: } catch (SQLException err) {
481: }
482: }
483:
484: private void log(int level, String msgKey, Throwable err) {
485: if (getLogWriter() != null) {
486: getLogWriter().println(DS_RESOURCES.getString(msgKey));
487: if (err != null) {
488: err.printStackTrace(getLogWriter());
489: }
490: } else {
491: Logger.log(level, DS_RESOURCES, msgKey, err);
492: }
493: }
494:
495: private void log(int level, String msgKey, String arg, Throwable err) {
496: if (getLogWriter() != null) {
497: getLogWriter().println(DS_RESOURCES.getString(msgKey, arg));
498: if (err != null) {
499: err.printStackTrace(getLogWriter());
500: }
501: } else {
502: Logger.log(level, DS_RESOURCES, msgKey, arg, err);
503: }
504: }
505:
506: private void log(int level, String msgKey, String arg[],
507: Throwable err) {
508: if (getLogWriter() != null) {
509: getLogWriter().println(DS_RESOURCES.getString(msgKey, arg));
510: if (err != null) {
511: err.printStackTrace(getLogWriter());
512: }
513: } else {
514: Logger.log(level, DS_RESOURCES, msgKey, arg, err);
515: }
516: }
517:
518: public String toString() {
519: return DS_RESOURCES.getString("WinstoneDataSource.StatusMsg",
520: new String[] { this .name,
521: "" + this .usedRealConnections.size(),
522: "" + this.unusedRealConnections.size() });
523: }
524: }
|