001package org.avaje.datasource.pool;
002
003import org.avaje.datasource.PoolStatistics;
004import org.avaje.datasource.PoolStatus;
005import org.avaje.datasource.pool.ConnectionPool.Status;
006import org.avaje.datasource.pool.PooledConnectionStatistics.LoadValues;
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009
010import java.sql.SQLException;
011import java.util.concurrent.TimeUnit;
012import java.util.concurrent.locks.Condition;
013import java.util.concurrent.locks.ReentrantLock;
014
015public class PooledConnectionQueue {
016
017  private static final Logger logger = LoggerFactory.getLogger(PooledConnectionQueue.class);
018
019  private static final TimeUnit MILLIS_TIME_UNIT = TimeUnit.MILLISECONDS;
020
021  private final String name;
022
023  private final ConnectionPool pool;
024
025  /**
026   * A 'circular' buffer designed specifically for free connections.
027   */
028  private final FreeConnectionBuffer freeList;
029
030  /**
031   * A 'slots' buffer designed specifically for busy connections.
032   * Fast add remove based on slot id.
033   */
034  private final BusyConnectionBuffer busyList;
035
036  /**
037   * Load statistics collected off connections that have closed fully (left the pool).
038   */
039  private final PooledConnectionStatistics collectedStats = new PooledConnectionStatistics();
040
041  /**
042   * Currently accumulated load statistics.
043   */
044  private LoadValues accumulatedValues = new LoadValues();
045
046  /**
047   * Main lock guarding all access
048   */
049  private final ReentrantLock lock;
050
051  /**
052   * Condition for threads waiting to take a connection
053   */
054  private final Condition notEmpty;
055
056  private int connectionId;
057
058  private final long waitTimeoutMillis;
059
060  private final long leakTimeMinutes;
061
062  private final long maxAgeMillis;
063
064  private int warningSize;
065
066  private int maxSize;
067
068  private int minSize;
069
070  /**
071   * Number of threads in the wait queue.
072   */
073  private int waitingThreads;
074
075  /**
076   * Number of times a thread had to wait.
077   */
078  private int waitCount;
079
080  /**
081   * Number of times a connection was got from this queue.
082   */
083  private int hitCount;
084
085  /**
086   * The high water mark for the queue size.
087   */
088  private int highWaterMark;
089
090  /**
091   * Last time the pool was reset. Used to close busy connections as they are
092   * returned to the pool that where created prior to the lastResetTime.
093   */
094  private long lastResetTime;
095
096  private boolean doingShutdown;
097
098  PooledConnectionQueue(ConnectionPool pool) {
099
100    this.pool = pool;
101    this.name = pool.getName();
102    this.minSize = pool.getMinSize();
103    this.maxSize = pool.getMaxSize();
104
105    this.warningSize = pool.getWarningSize();
106    this.waitTimeoutMillis = pool.getWaitTimeoutMillis();
107    this.leakTimeMinutes = pool.getLeakTimeMinutes();
108    this.maxAgeMillis = pool.getMaxAgeMillis();
109
110    this.busyList = new BusyConnectionBuffer(maxSize, 20);
111    this.freeList = new FreeConnectionBuffer();
112
113    this.lock = new ReentrantLock(false);
114    this.notEmpty = lock.newCondition();
115  }
116
117  private PoolStatus createStatus() {
118    return new Status(minSize, maxSize, freeList.size(), busyList.size(), waitingThreads, highWaterMark, waitCount, hitCount);
119  }
120
121  public String toString() {
122    final ReentrantLock lock = this.lock;
123    lock.lock();
124    try {
125      return createStatus().toString();
126    } finally {
127      lock.unlock();
128    }
129  }
130
131  /**
132   * Collect statistics of a connection that is fully closing
133   */
134  void reportClosingConnection(PooledConnection pooledConnection) {
135
136    collectedStats.add(pooledConnection.getStatistics());
137  }
138
139  PoolStatistics getStatistics(boolean reset) {
140
141    final ReentrantLock lock = this.lock;
142    lock.lock();
143    try {
144
145      LoadValues aggregate = collectedStats.getValues(reset);
146
147      freeList.collectStatistics(aggregate, reset);
148      busyList.collectStatistics(aggregate, reset);
149
150      aggregate.plus(accumulatedValues);
151
152      this.accumulatedValues = (reset) ? new LoadValues() : aggregate;
153
154      return new DataSourcePoolStatistics(aggregate.getCollectionStart(), aggregate.getCount(), aggregate.getErrorCount(), aggregate.getHwmMicros(), aggregate.getTotalMicros());
155
156    } finally {
157      lock.unlock();
158    }
159  }
160
161  public PoolStatus getStatus(boolean reset) {
162    final ReentrantLock lock = this.lock;
163    lock.lock();
164    try {
165      PoolStatus s = createStatus();
166      if (reset) {
167        highWaterMark = busyList.size();
168        hitCount = 0;
169        waitCount = 0;
170      }
171      return s;
172    } finally {
173      lock.unlock();
174    }
175  }
176
177  void setMinSize(int minSize) {
178    final ReentrantLock lock = this.lock;
179    lock.lock();
180    try {
181      if (minSize > this.maxSize) {
182        throw new IllegalArgumentException("minSize " + minSize + " > maxSize " + this.maxSize);
183      }
184      this.minSize = minSize;
185    } finally {
186      lock.unlock();
187    }
188  }
189
190  void setMaxSize(int maxSize) {
191    final ReentrantLock lock = this.lock;
192    lock.lock();
193    try {
194      if (maxSize < this.minSize) {
195        throw new IllegalArgumentException("maxSize " + maxSize + " < minSize " + this.minSize);
196      }
197      this.busyList.setCapacity(maxSize);
198      this.maxSize = maxSize;
199    } finally {
200      lock.unlock();
201    }
202  }
203
204  void setWarningSize(int warningSize) {
205    final ReentrantLock lock = this.lock;
206    lock.lock();
207    try {
208      if (warningSize > this.maxSize) {
209        throw new IllegalArgumentException("warningSize " + warningSize + " > maxSize " + this.maxSize);
210      }
211      this.warningSize = warningSize;
212    } finally {
213      lock.unlock();
214    }
215  }
216
217  private int totalConnections() {
218    return freeList.size() + busyList.size();
219  }
220
221  void ensureMinimumConnections() throws SQLException {
222    final ReentrantLock lock = this.lock;
223    lock.lock();
224    try {
225      int add = minSize - totalConnections();
226      if (add > 0) {
227        for (int i = 0; i < add; i++) {
228          PooledConnection c = pool.createConnectionForQueue(connectionId++);
229          freeList.add(c);
230        }
231        notEmpty.signal();
232      }
233
234    } finally {
235      lock.unlock();
236    }
237  }
238
239  /**
240   * Return a PooledConnection.
241   */
242  void returnPooledConnection(PooledConnection c, boolean forceClose) {
243
244    final ReentrantLock lock = this.lock;
245    lock.lock();
246    try {
247      if (!busyList.remove(c)) {
248        logger.error("Connection [{}] not found in BusyList? ", c);
249      }
250      if (forceClose || c.shouldTrimOnReturn(lastResetTime, maxAgeMillis)) {
251        c.closeConnectionFully(false);
252
253      } else {
254        freeList.add(c);
255        notEmpty.signal();
256      }
257    } finally {
258      lock.unlock();
259    }
260  }
261
262  private PooledConnection extractFromFreeList() {
263    PooledConnection c = freeList.remove();
264    registerBusyConnection(c);
265    return c;
266  }
267
268  PooledConnection getPooledConnection() throws SQLException {
269
270    try {
271      PooledConnection pc = _getPooledConnection();
272      pc.resetForUse();
273      return pc;
274
275    } catch (InterruptedException e) {
276      // restore the interrupted status as we throw SQLException
277      Thread.currentThread().interrupt();
278      throw new SQLException("Interrupted getting connection from pool", e);
279    }
280  }
281
282  /**
283   * Register the PooledConnection with the busyList.
284   */
285  private int registerBusyConnection(PooledConnection c) {
286    int busySize = busyList.add(c);
287    if (busySize > highWaterMark) {
288      highWaterMark = busySize;
289    }
290    return busySize;
291  }
292
293  private PooledConnection _getPooledConnection() throws InterruptedException, SQLException {
294    final ReentrantLock lock = this.lock;
295    lock.lockInterruptibly();
296    try {
297      if (doingShutdown) {
298        throw new SQLException("Trying to access the Connection Pool when it is shutting down");
299      }
300
301      // this includes attempts that fail with InterruptedException
302      // or SQLException but that is ok as its only an indicator
303      hitCount++;
304
305      // are other threads already waiting? (they get priority)
306      if (waitingThreads == 0) {
307
308        if (!freeList.isEmpty()) {
309          // we have a free connection to return
310          return extractFromFreeList();
311        }
312
313        if (busyList.size() < maxSize) {
314          // grow the connection pool
315          PooledConnection c = pool.createConnectionForQueue(connectionId++);
316          int busySize = registerBusyConnection(c);
317
318          if (logger.isDebugEnabled()) {
319            logger.debug("DataSourcePool [{}] grow; id[{}] busy[{}] max[{}]", name, c.getName(), busySize, maxSize);
320          }
321          checkForWarningSize();
322          return c;
323        }
324      }
325
326      try {
327        // The pool is at maximum size. We are going to go into
328        // a wait loop until connections are returned into the pool.
329        waitCount++;
330        waitingThreads++;
331        return _getPooledConnectionWaitLoop();
332      } finally {
333        waitingThreads--;
334      }
335
336    } finally {
337      lock.unlock();
338    }
339  }
340
341  /**
342   * Got into a loop waiting for connections to be returned to the pool.
343   */
344  private PooledConnection _getPooledConnectionWaitLoop() throws SQLException, InterruptedException {
345
346    long nanos = MILLIS_TIME_UNIT.toNanos(waitTimeoutMillis);
347    for (; ; ) {
348
349      if (nanos <= 0) {
350        String msg = "Unsuccessfully waited [" + waitTimeoutMillis + "] millis for a connection to be returned."
351            + " No connections are free. You need to Increase the max connections of [" + maxSize + "]"
352            + " or look for a connection pool leak using datasource.xxx.capturestacktrace=true";
353        if (pool.isCaptureStackTrace()) {
354          dumpBusyConnectionInformation();
355        }
356
357        throw new SQLException(msg);
358      }
359
360      try {
361        nanos = notEmpty.awaitNanos(nanos);
362        if (!freeList.isEmpty()) {
363          // successfully waited
364          return extractFromFreeList();
365        }
366      } catch (InterruptedException ie) {
367        notEmpty.signal(); // propagate to non-interrupted thread
368        throw ie;
369      }
370    }
371  }
372
373  public void shutdown() {
374    final ReentrantLock lock = this.lock;
375    lock.lock();
376    try {
377      doingShutdown = true;
378      PoolStatus status = createStatus();
379      PoolStatistics statistics = pool.getStatistics(false);
380      logger.debug("DataSourcePool [{}] shutdown {} - Statistics {}", name, status, statistics);
381
382      closeFreeConnections(true);
383
384      if (!busyList.isEmpty()) {
385        logger.warn("Closing busy connections on shutdown size: " + busyList.size());
386        dumpBusyConnectionInformation();
387        closeBusyConnections(0);
388      }
389    } finally {
390      lock.unlock();
391    }
392  }
393
394  /**
395   * Close all the connections in the pool and any current busy connections
396   * when they are returned. New connections will be then created on demand.
397   * <p>
398   * This is typically done when a database down event occurs.
399   * </p>
400   */
401  public void reset(long leakTimeMinutes) {
402    final ReentrantLock lock = this.lock;
403    lock.lock();
404    try {
405      PoolStatus status = createStatus();
406      logger.info("Reseting DataSourcePool [{}] {}", name, status);
407      lastResetTime = System.currentTimeMillis();
408
409      closeFreeConnections(false);
410      closeBusyConnections(leakTimeMinutes);
411
412      logger.info("Busy Connections:\n" + getBusyConnectionInformation());
413
414    } finally {
415      lock.unlock();
416    }
417  }
418
419  public void trim(long maxInactiveMillis, long maxAgeMillis) {
420    final ReentrantLock lock = this.lock;
421    lock.lock();
422    try {
423      if (trimInactiveConnections(maxInactiveMillis, maxAgeMillis) > 0) {
424        try {
425          ensureMinimumConnections();
426        } catch (SQLException e) {
427          logger.error("Error trying to ensure minimum connections", e);
428        }
429      }
430    } finally {
431      lock.unlock();
432    }
433  }
434
435  /**
436   * Trim connections that have been not used for some time.
437   */
438  private int trimInactiveConnections(long maxInactiveMillis, long maxAgeMillis) {
439
440    long usedSince = System.currentTimeMillis() - maxInactiveMillis;
441    long createdSince = (maxAgeMillis == 0) ? 0 : System.currentTimeMillis() - maxAgeMillis;
442
443    int trimedCount = freeList.trim(usedSince, createdSince);
444    if (trimedCount > 0) {
445      logger.debug("DataSourcePool [{}] trimmed [{}] inactive connections. New size[{}]", name, trimedCount, totalConnections());
446    }
447    return trimedCount;
448  }
449
450  /**
451   * Close all the connections that are in the free list.
452   */
453  private void closeFreeConnections(boolean logErrors) {
454    final ReentrantLock lock = this.lock;
455    lock.lock();
456    try {
457      freeList.closeAll(logErrors);
458    } finally {
459      lock.unlock();
460    }
461  }
462
463  /**
464   * Close any busy connections that have not been used for some time.
465   * <p>
466   * These connections are considered to have leaked from the connection pool.
467   * </p>
468   * <p>
469   * Connection leaks occur when code doesn't ensure that connections are
470   * closed() after they have been finished with. There should be an
471   * appropriate try catch finally block to ensure connections are always
472   * closed and put back into the pool.
473   * </p>
474   */
475  void closeBusyConnections(long leakTimeMinutes) {
476
477    final ReentrantLock lock = this.lock;
478    lock.lock();
479    try {
480      busyList.closeBusyConnections(leakTimeMinutes);
481    } finally {
482      lock.unlock();
483    }
484  }
485
486  /**
487   * As the pool grows it gets closer to the maxConnections limit. We can send
488   * an Alert (or warning) as we get close to this limit and hence an
489   * Administrator could increase the pool size if desired.
490   * <p>
491   * This is called whenever the pool grows in size (towards the max limit).
492   * </p>
493   */
494  private void checkForWarningSize() {
495
496    // the the total number of connections that we can add
497    // to the pool before it hits the maximum
498    int availableGrowth = (maxSize - totalConnections());
499
500    if (availableGrowth < warningSize) {
501
502      closeBusyConnections(leakTimeMinutes);
503
504      String msg = "DataSourcePool [" + name + "] is [" + availableGrowth + "] connections from its maximum size.";
505      pool.notifyWarning(msg);
506    }
507  }
508
509  String getBusyConnectionInformation() {
510    return getBusyConnectionInformation(false);
511  }
512
513  void dumpBusyConnectionInformation() {
514    getBusyConnectionInformation(true);
515  }
516
517  /**
518   * Returns information describing connections that are currently being used.
519   */
520  private String getBusyConnectionInformation(boolean toLogger) {
521
522    final ReentrantLock lock = this.lock;
523    lock.lock();
524    try {
525
526      return busyList.getBusyConnectionInformation(toLogger);
527
528    } finally {
529      lock.unlock();
530    }
531  }
532
533}
534