/* * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.integration.util; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.messaging.MessagingException; import org.springframework.util.Assert; /** * Implementation of {@link Pool} supporting dynamic resizing and a variable * timeout when attempting to obtain an item from the pool. Pool grows on * demand up to the limit. * * @author Gary Russell * @since 2.2 * */ public class SimplePool<T> implements Pool<T> { protected final Log logger = LogFactory.getLog(this.getClass()); private final Semaphore permits = new Semaphore(0); private final AtomicInteger poolSize = new AtomicInteger(); private final AtomicInteger targetPoolSize = new AtomicInteger(); private long waitTimeout = Long.MAX_VALUE; private final BlockingQueue<T> available = new LinkedBlockingQueue<T>(); private final Set<T> allocated = Collections.synchronizedSet(new HashSet<T>()); private final Set<T> inUse = Collections.synchronizedSet(new HashSet<T>()); private final PoolItemCallback<T> callback; /** * Creates a SimplePool with a specific limit. * @param poolSize The maximum number of items the pool supports. * @param callback A {@link PoolItemCallback} implementation called during various * pool operations. */ public SimplePool(int poolSize, PoolItemCallback<T> callback) { if (poolSize <= 0) { this.poolSize.set(Integer.MAX_VALUE); this.targetPoolSize.set(Integer.MAX_VALUE); this.permits.release(Integer.MAX_VALUE); } else { this.poolSize.set(poolSize); this.targetPoolSize.set(poolSize); this.permits.release(poolSize); } this.callback = callback; } /** * Adjusts the current pool size. When reducing the pool size, attempts to * remove the delta from the pool. If there are not enough unused items in * the pool, the actual pool size will decrease to the specified size as in-use * items are returned. * @param poolSize The desired target pool size. */ public synchronized void setPoolSize(int poolSize) { int delta = poolSize - this.poolSize.get(); this.targetPoolSize.addAndGet(delta); if (this.logger.isDebugEnabled()) { this.logger.debug(String.format("Target pool size changed by %d, now %d", delta, this.targetPoolSize.get())); } if (delta > 0) { this.poolSize.addAndGet(delta); this.permits.release(delta); } else { while (delta < 0) { if (!this.permits.tryAcquire()) { break; } T item = this.available.poll(); if (item == null) { this.permits.release(); break; } doRemoveItem(item); this.poolSize.decrementAndGet(); delta++; } } if (delta < 0 && this.logger.isDebugEnabled()) { this.logger.debug(String.format("Pool is overcommitted by %d; items will be removed when returned", -delta)); } } /** * Returns the current size of the pool; may be greater than the target pool size * if it was recently reduced and too many items were in use to allow the new size * to be set. */ @Override public synchronized int getPoolSize() { return this.poolSize.get(); } @Override public int getIdleCount() { return this.available.size(); } @Override public int getActiveCount() { return this.inUse.size(); } @Override public int getAllocatedCount() { return this.allocated.size(); } /** * Adjusts the wait timeout - the time for which getItem() will wait if no idle * entries are available. * <br> * Default: infinity. * @param waitTimeout The wait timeout in milliseconds. */ public void setWaitTimeout(long waitTimeout) { this.waitTimeout = waitTimeout; } /** * Obtains an item from the pool; waits up to waitTime milliseconds (default infinity). * @throws MessagingException if no items become available in time. */ @Override public T getItem() { boolean permitted = false; try { try { permitted = this.permits.tryAcquire(this.waitTimeout, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new MessagingException("Interrupted awaiting a pooled resource", e); } if (!permitted) { throw new IllegalStateException("Timed out while waiting to acquire a pool entry."); } return doGetItem(); } catch (Exception e) { if (permitted) { this.permits.release(); } if (e instanceof MessagingException) { throw (MessagingException) e; } throw new MessagingException("Failed to obtain pooled item", e); } } private T doGetItem() { T item = this.available.poll(); if (item != null && this.logger.isDebugEnabled()) { this.logger.debug("Obtained " + item + " from pool."); } if (item == null) { item = this.callback.createForPool(); if (this.logger.isDebugEnabled()) { this.logger.debug("Obtained new " + item + "."); } this.allocated.add(item); } else if (this.callback.isStale(item)) { if (this.logger.isDebugEnabled()) { this.logger.debug("Received a stale item " + item + ", will attempt to get a new one."); } doRemoveItem(item); item = doGetItem(); } this.inUse.add(item); return item; } /** * Returns an item to the pool. */ @Override public synchronized void releaseItem(T item) { Assert.notNull(item, "Item cannot be null"); Assert.isTrue(this.allocated.contains(item), "You can only release items that were obtained from the pool"); if (this.inUse.contains(item)) { if (this.poolSize.get() > this.targetPoolSize.get()) { this.poolSize.decrementAndGet(); if (item != null) { doRemoveItem(item); } } else { if (this.logger.isDebugEnabled()) { this.logger.debug("Releasing " + item + " back to the pool"); } if (item != null) { this.available.add(item); this.inUse.remove(item); } this.permits.release(); } } else { if (this.logger.isDebugEnabled()) { this.logger.debug("Ignoring release of " + item + " back to the pool - not in use"); } } } @Override public synchronized void removeAllIdleItems() { T item; while ((item = this.available.poll()) != null) { doRemoveItem(item); } } private void doRemoveItem(T item) { if (this.logger.isDebugEnabled()) { this.logger.debug("Removing " + item + " from the pool"); } this.allocated.remove(item); this.inUse.remove(item); this.callback.removedFromPool(item); } /** * User of the pool provide an implementation of this interface; called during * various pool operations. * */ public interface PoolItemCallback<T> { /** * Called by the pool when a new instance is required to populate the pool. Only * called if no idle non-stale instances are available. * @return The item. */ T createForPool(); /** * Called by the pool when an idle item is retrieved from the pool. Indicates * whether that item is usable, or should be discarded. The pool takes no * further action on a stale item, discards it, and attempts to find or create * another item. * @param item The item. * @return true if the item should not be used. */ boolean isStale(T item); /** * Called by the pool when an item is forcibly removed from the pool - for example * when the pool size is reduced. The implementation should perform any cleanup * necessary on the item, such as closing connections etc. * @param item The item. */ void removedFromPool(T item); } }