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