INT-1871 TCP CachingClientConnectionFactory

Pool based on algorithm used for spring-integration-file
CachingSessionFactory introduced by INT-2146. Refactored
that code to use the common SimplePool.

One difference to the previous implementation is the
ability to change the pool size dynamically.

If the size is reduced and more than the new size are
in use, items are closed as they are returned until
the pool size is as requested.

Initial commit.

Allow Pool Size Changes

Factor out Pool

Polishing

Pool Tests

Default forever

Javadocs, File

Polishing

INT-1871 PR Polishing

* Consistent/cleaner method names
* Track checkouts; reject release of 'foreign' objects.
* Add 'getAllocatedCount()'
This commit is contained in:
Gary Russell
2012-02-29 17:38:59 -05:00
committed by Oleg Zhurakousky
parent ab0989d73a
commit 411aa296a4
8 changed files with 1190 additions and 57 deletions

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2002-2012 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;
/**
* Represents a pool of items.
* @author Gary Russell
* @since 2.2
*
*/
public interface Pool<T> {
/**
* Obtains an item from the pool.
* @return the item.
*/
T getItem();
/**
* Releases an item back into the pool. This must be an item that
* was previously retrieved using {@link #getItem()}.
* @param t the item.
* @throws IllegalArgumentException when a "foreign" object
* is released.
*/
void releaseItem(T t);
/**
* Removes all idle items from the pool.
*/
void removeAllIdleItems();
/**
* Returns the current size (limit) of the pool.
* @return the size.
*/
int getPoolSize();
/**
* Returns the number of items that have been allocated
* but are not currently in use.
* @return The number of items.
*/
int getIdleCount();
/**
* Returns the number of allocated items that are currently
* checked out of the pool.
* @return The number of items.
*/
int getActiveCount();
/**
* Returns the current count of allocated items (in use and
* idle). May be less than the pool size, and reflects the
* high water mark of pool usage.
* @return the number of items.
*/
int getAllocatedCount();
}

View File

@@ -0,0 +1,263 @@
/*
* Copyright 2002-2012 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.integration.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 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 (logger.isDebugEnabled()) {
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 && logger.isDebugEnabled()) {
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.
*/
public int getPoolSize() {
return this.poolSize.get();
}
public int getIdleCount() {
return this.available.size();
}
public int getActiveCount() {
return this.getAllocatedCount() - this.getIdleCount();
}
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.
*/
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 aquire a pool entry.");
}
T item = doGetItem();
return item;
}
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 && logger.isDebugEnabled()) {
logger.debug("Obtained " + item + " from pool.");
}
if (item == null) {
item = this.callback.createForPool();
if (logger.isDebugEnabled()) {
logger.debug("Obtained new " + item + ".");
}
allocated.add(item);
}
else if (this.callback.isStale(item)) {
if (logger.isDebugEnabled()) {
logger.debug("Received a stale item, will attempt to get a new one.");
}
item = doGetItem();
}
return item;
}
/**
* Returns an item to the pool. Item may be null, in which case a subsequent getItem()
* will return a new instance.
*/
public synchronized void releaseItem(T item) {
Assert.isTrue(item == null || this.allocated.contains(item),
"You can only release items that were obtained from the pool");
if (this.poolSize.get() > targetPoolSize.get()) {
poolSize.decrementAndGet();
if (item != null) {
doRemoveItem(item);
}
}
else {
if (logger.isDebugEnabled()){
logger.debug("Releasing " + item + " back to the pool");
}
if (item != null) {
available.add(item);
}
permits.release();
}
}
public synchronized void removeAllIdleItems() {
T item;
while ((item = this.available.poll()) != null) {
doRemoveItem(item);
}
}
private void doRemoveItem(T item) {
this.allocated.remove(item);
this.callback.removedFromPool(item);
}
/**
* User of the pool provide an implementation of this interface; called during
* various pool operations.
*
*/
public static 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);
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2002-2012 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import org.springframework.integration.MessagingException;
/**
* @author Gary Russell
* @since 2.2
*
*/
public class SimplePoolTests {
@Test
public void testReuseAndStale() {
final Set<String> strings = new HashSet<String>();
final AtomicBoolean stale = new AtomicBoolean();
SimplePool<String> pool = stringPool(2, strings, stale);
String s1 = pool.getItem();
String s2 = pool.getItem();
assertNotSame(s1, s2);
pool.releaseItem(s1);
String s3 = pool.getItem();
assertSame(s1, s3);
stale.set(true);
pool.releaseItem(s3);
s3 = pool.getItem();
assertNotSame(s1, s3);
assertFalse(strings.remove(s1));
}
@Test
public void testOverCommitandResize() {
final Set<String> strings = new HashSet<String>();
final AtomicBoolean stale = new AtomicBoolean();
SimplePool<String> pool = stringPool(2, strings, stale);
String s1 = pool.getItem();
assertEquals(0, pool.getIdleCount());
assertEquals(1, pool.getActiveCount());
assertEquals(1, pool.getAllocatedCount());
pool.releaseItem(s1);
assertEquals(1, pool.getIdleCount());
assertEquals(0, pool.getActiveCount());
assertEquals(1, pool.getAllocatedCount());
s1 = pool.getItem();
assertEquals(0, pool.getIdleCount());
assertEquals(1, pool.getActiveCount());
assertEquals(1, pool.getAllocatedCount());
String s2 = pool.getItem();
assertNotSame(s1, s2);
pool.setWaitTimeout(1);
assertEquals(0, pool.getIdleCount());
assertEquals(2, pool.getActiveCount());
assertEquals(2, pool.getAllocatedCount());
try {
pool.getItem();
fail("Expected exception");
} catch (MessagingException e) {}
// resize up
pool.setPoolSize(4);
assertEquals(0, pool.getIdleCount());
assertEquals(2, pool.getActiveCount());
assertEquals(2, pool.getAllocatedCount());
String s3 = pool.getItem();
String s4 = pool.getItem();
assertEquals(0, pool.getIdleCount());
assertEquals(4, pool.getActiveCount());
assertEquals(4, pool.getAllocatedCount());
pool.releaseItem(s4);
assertEquals(1, pool.getIdleCount());
assertEquals(3, pool.getActiveCount());
assertEquals(4, pool.getAllocatedCount());
// resize down
pool.setPoolSize(2);
assertEquals(0, pool.getIdleCount());
assertEquals(3, pool.getActiveCount());
assertEquals(3, pool.getPoolSize());
assertEquals(3, pool.getAllocatedCount());
pool.releaseItem(s3);
assertEquals(0, pool.getIdleCount());
assertEquals(2, pool.getActiveCount());
assertEquals(2, pool.getPoolSize());
assertEquals(2, pool.getAllocatedCount());
assertEquals(2, strings.size());
pool.releaseItem(s2);
pool.releaseItem(s1);
assertEquals(2, pool.getIdleCount());
assertEquals(0, pool.getActiveCount());
assertEquals(2, pool.getPoolSize());
assertEquals(2, strings.size());
assertEquals(2, pool.getAllocatedCount());
}
@Test(expected=IllegalArgumentException.class)
public void testForeignObject() {
final Set<String> strings = new HashSet<String>();
final AtomicBoolean stale = new AtomicBoolean();
SimplePool<String> pool = stringPool(2, strings, stale);
pool.getItem();
pool.releaseItem("Hello, world!");
}
private SimplePool<String> stringPool(int size, final Set<String> strings,
final AtomicBoolean stale) {
SimplePool<String> pool = new SimplePool<String>(size, new SimplePool.PoolItemCallback<String>() {
private int i;
public String createForPool() {
String string = new String("String" + i++);
strings.add(string);
return string;
}
public boolean isStale(String item) {
if (stale.get()) {
strings.remove(item);
}
return stale.get();
}
public void removedFromPool(String item) {
strings.remove(item);
}
});
return pool;
}
}