Talk about idleTimeout and minimumIdle properties of hikari connection pool.

  jdbc

Order

This paper mainly studies the idleTimeout and minimumIdle properties of a hikari connection pool.

idleTimeout

The default is 600000 milliseconds, or 10 minutes. If idleTimeout+1 second > maxLifetime and maxLifetime>0, it will be reset to 0; If idleTimeout! =0 and less than 10 seconds, it will be reset to 10 seconds. If idleTimeout=0, idle connections will never be removed from the connection pool.

This parameter takes effect only when the minimumIdle is less than maximumPoolSize, and is removed when the number of idle connections exceeds the minimumIdle and the idle time exceeds idleTimeout.

minimumIdle

Controls the minimum number of connection pool idle connections. When the connection pool idle connections are less than minimumIdle and the total number of connections is not more than maximumPoolSize, HikariCP will try its best to supplement new connections. For performance consideration, it is not recommended to set this value, but let HikariCP treat connection pool as a fixed size. the default minimum is the same as maximumPoolSize.

When minIdle<0 or minIdle>maxPoolSize, it is reset to maxPoolSize, which defaults to 10.

HikariPool.HouseKeeper

HikariCP-2.7.6-sources.jar! /com/zaxxer/hikari/pool/HikariPool.java

    private final long HOUSEKEEPING_PERIOD_MS = Long.getLong("com.zaxxer.hikari.housekeeping.periodMs", SECONDS.toMillis(30));

    this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, HOUSEKEEPING_PERIOD_MS, MILLISECONDS);

   /**
    * The house keeping task to retire and maintain minimum idle connections.
    */
   private final class HouseKeeper implements Runnable
   {
      private volatile long previous = plusMillis(currentTime(), -HOUSEKEEPING_PERIOD_MS);

      @Override
      public void run()
      {
         try {
            // refresh timeouts in case they changed via MBean
            connectionTimeout = config.getConnectionTimeout();
            validationTimeout = config.getValidationTimeout();
            leakTaskFactory.updateLeakDetectionThreshold(config.getLeakDetectionThreshold());

            final long idleTimeout = config.getIdleTimeout();
            final long now = currentTime();

            // Detect retrograde time, allowing +128ms as per NTP spec.
            if (plusMillis(now, 128) < plusMillis(previous, HOUSEKEEPING_PERIOD_MS)) {
               LOGGER.warn("{} - Retrograde clock change detected (housekeeper delta={}), soft-evicting connections from pool.",
                           poolName, elapsedDisplayString(previous, now));
               previous = now;
               softEvictConnections();
               return;
            }
            else if (now > plusMillis(previous, (3 * HOUSEKEEPING_PERIOD_MS) / 2)) {
               // No point evicting for forward clock motion, this merely accelerates connection retirement anyway
               LOGGER.warn("{} - Thread starvation or clock leap detected (housekeeper delta={}).", poolName, elapsedDisplayString(previous, now));
            }

            previous = now;

            String afterPrefix = "Pool ";
            if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) {
               logPoolState("Before cleanup ");
               afterPrefix = "After cleanup  ";

               final List<PoolEntry> notInUse = connectionBag.values(STATE_NOT_IN_USE);
               int toRemove = notInUse.size() - config.getMinimumIdle();
               for (PoolEntry entry : notInUse) {
                  if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
                     closeConnection(entry, "(connection has passed idleTimeout)");
                     toRemove--;
                  }
               }
            }

            logPoolState(afterPrefix);

            fillPool(); // Try to maintain minimum connections
         }
         catch (Exception e) {
            LOGGER.error("Unexpected exception in housekeeping task", e);
         }
      }
   }

This HouseKeeper is a timing task, initialized in HikariPool constructor. The default is to execute it 100 milliseconds after initialization. After each execution, it will be executed at intervals of HOUSEKEEPING_PERIOD_MS (30 seconds) time execution.
The role of this timed task is to remove idleTimeout connections according to the idle timeout value.
Firstly, it detects whether the clock is retrogressive, and if so, it immediately marks the expired connection evict;; Only when idleTimeout>0 and configured minimumIdle<maximumPoolSize will the timeout idle connection be processed.
Check out the number of connections whose state is STATE_NOT_IN_USE. if it is greater than minimumIdle, traverse the connection of STATE_NOT_IN_USE, remove the connection whose idleTimeout reaches idle timeout from connectionBag, close the connection if the removal is successful, and then toRemove–.
After the free connection is removed, call fillPool to try to replenish the space connection to the minimumIdle value.

HikariPool.fillPool

HikariCP-2.7.6-sources.jar! /com/zaxxer/hikari/pool/HikariPool.java

         private final PoolEntryCreator POOL_ENTRY_CREATOR = new PoolEntryCreator(null /*logging prefix*/);
      private final PoolEntryCreator POST_FILL_POOL_ENTRY_CREATOR = new PoolEntryCreator("After adding ");
      LinkedBlockingQueue<Runnable> addConnectionQueue = new LinkedBlockingQueue<>(config.getMaximumPoolSize());
      this.addConnectionQueue = unmodifiableCollection(addConnectionQueue);
      this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + " connection adder", threadFactory, new ThreadPoolExecutor.DiscardPolicy());

   /**
    * Fill pool up from current idle connections (as they are perceived at the point of execution) to minimumIdle connections.
    */
   private synchronized void fillPool()
   {
      final int connectionsToAdd = Math.min(config.getMaximumPoolSize() - getTotalConnections(), config.getMinimumIdle() - getIdleConnections())
                                   - addConnectionQueue.size();
      for (int i = 0; i < connectionsToAdd; i++) {
         addConnectionExecutor.submit((i < connectionsToAdd - 1) ? POOL_ENTRY_CREATOR : POST_FILL_POOL_ENTRY_CREATOR);
      }
   }

PoolEntryCreator

   /**
    * Creating and adding poolEntries (connections) to the pool.
    */
   private final class PoolEntryCreator implements Callable<Boolean>
   {
      private final String loggingPrefix;

      PoolEntryCreator(String loggingPrefix)
      {
         this.loggingPrefix = loggingPrefix;
      }

      @Override
      public Boolean call() throws Exception
      {
         long sleepBackoff = 250L;
         while (poolState == POOL_NORMAL && shouldCreateAnotherConnection()) {
            final PoolEntry poolEntry = createPoolEntry();
            if (poolEntry != null) {
               connectionBag.add(poolEntry);
               LOGGER.debug("{} - Added connection {}", poolName, poolEntry.connection);
               if (loggingPrefix != null) {
                  logPoolState(loggingPrefix);
               }
               return Boolean.TRUE;
            }

            // failed to get connection from db, sleep and retry
            quietlySleep(sleepBackoff);
            sleepBackoff = Math.min(SECONDS.toMillis(10), Math.min(connectionTimeout, (long) (sleepBackoff * 1.5)));
         }
         // Pool is suspended or shutdown or at max size
         return Boolean.FALSE;
      }

      /**
       * We only create connections if we need another idle connection or have threads still waiting
       * for a new connection.  Otherwise we bail out of the request to create.
       *
       * @return true if we should create a connection, false if the need has disappeared
       */
      private boolean shouldCreateAnotherConnection() {
         return getTotalConnections() < config.getMaximumPoolSize() &&
            (connectionBag.getWaitingThreadCount() > 0 || getIdleConnections() < config.getMinimumIdle());
      }
   }

The shouldCreateAnotherConnection method determines whether a new connection needs to be added

createPoolEntry

   /**
    * Creating new poolEntry.  If maxLifetime is configured, create a future End-of-life task with 2.5% variance from
    * the maxLifetime time to ensure there is no massive die-off of Connections in the pool.
    */
   private PoolEntry createPoolEntry()
   {
      try {
         final PoolEntry poolEntry = newPoolEntry();

         final long maxLifetime = config.getMaxLifetime();
         if (maxLifetime > 0) {
            // variance up to 2.5% of the maxlifetime
            final long variance = maxLifetime > 10_000 ? ThreadLocalRandom.current().nextLong( maxLifetime / 40 ) : 0;
            final long lifetime = maxLifetime - variance;
            poolEntry.setFutureEol(houseKeepingExecutorService.schedule(
               () -> {
                  if (softEvictConnection(poolEntry, "(connection has passed maxLifetime)", false /* not owner */)) {
                     addBagItem(connectionBag.getWaitingThreadCount());
                  }
               },
               lifetime, MILLISECONDS));
         }

         return poolEntry;
      }
      catch (Exception e) {
         if (poolState == POOL_NORMAL) { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently
            LOGGER.debug("{} - Cannot acquire connection from data source", poolName, (e instanceof ConnectionSetupException ? e.getCause() : e));
         }
         return null;
      }
   }

The createpoolEntry method creates a PoolEntry and sets a delay task for its lifetime expiration.

Summary

  • HouseKeeper is a timed task that is initialized in the HikariPool constructor. The default is to execute it 100 milliseconds after initialization. After each execution, it is executed every other time at intervals of HOUSEKEEPING_PERIOD_MS (30 seconds) time execution.
  • If the clock is found to be backward, mark the evict connection immediately and then exit. Otherwise, fillPool will be executed to try to maintain the value of the idle connection to minimumIdle.
  • When idleTimeout>0 and configured minimumIdle<maximumPoolSize, idle connections exceeding idleTimeout will be removed; otherwise, no operation will be performed and fillPool will continue to be executed.
  • When minIdle<0 or minIdle>maxPoolSize, minIdle is reset to maxPoolSize, which defaults to 10. the official recommendation is to set it to be consistent, and treat it as a fixed-size connection pool to improve performance.

IdleTimeout is somewhat similar to the min-evictable-idle-time-millis parameter in tomcat jdbc pool. The difference is that tomcat jdbc pool’s connection leak detection and idle connection cleanup are all handled in a timerTask called PoolCleaner. The execution interval of this task is timeBetweenEvictionRunsMillis, which defaults to 5 seconds. However, hikari’s connection leak is handled by triggering a delay task separately every time getConnection is used, while the clearance of idle connection is handled by using HouseKeeper timing task, and its running interval is controlled by COM.zaxer.Hikari.House Keeping.Periodms environment variable, with a default of 30 seconds.

doc