GH-3826: Fix SimplePool for resizing from MAX

Fixes https://github.com/spring-projects/spring-integration/issues/3826

By default, the `SimplePool` is used with an `Integer.MAX_VALUE` pool size.
There is a performance degradation in the `setPoolSize()` when we try to
decrease the pool size: the `while` for `permits.tryAcquire()` is too long
close to the current `Integer.MAX_VALUE` pool size

* Revise the logic in the `setPoolSize()` to use `Semaphore.reducePermits()`
instead of the loop.
* Change the calculation for a new pool size for the current pool state:
or it is a size of a new request, or iti s equal to the `inUse.size()`.
It will be reduced on subsequent `releaseItem()` calls
* Reduce the number of `available` according a new pool size based on the `inUse`.
So, if `inUse > newPoolSize`, the `available` is cleared.
Otherewise, it is reduced to the number which would give `newPoolSize` together
with the `inUse` size

**Cherry-pick to `5.5.x`**
This commit is contained in:
Artem Bilan
2022-06-27 11:41:27 -04:00
committed by Gary Russell
parent b14e0014da
commit 6fe59a76aa
2 changed files with 116 additions and 34 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -48,7 +48,7 @@ public class SimplePool<T> implements Pool<T> {
protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR final
private final Semaphore permits = new Semaphore(0);
private final PoolSemaphore permits = new PoolSemaphore(0);
private final AtomicInteger poolSize = new AtomicInteger();
@@ -56,11 +56,11 @@ public class SimplePool<T> implements Pool<T> {
private long waitTimeout = Long.MAX_VALUE;
private final BlockingQueue<T> available = new LinkedBlockingQueue<T>();
private final BlockingQueue<T> available = new LinkedBlockingQueue<>();
private final Set<T> allocated = Collections.synchronizedSet(new HashSet<T>());
private final Set<T> allocated = Collections.synchronizedSet(new HashSet<>());
private final Set<T> inUse = Collections.synchronizedSet(new HashSet<T>());
private final Set<T> inUse = Collections.synchronizedSet(new HashSet<>());
private final PoolItemCallback<T> callback;
@@ -105,21 +105,27 @@ public class SimplePool<T> implements Pool<T> {
this.permits.release(delta);
}
else {
while (delta < 0) {
if (!this.permits.tryAcquire()) {
break;
}
this.permits.reducePermits(-delta);
int inUseSize = this.inUse.size();
int newPoolSize = Math.max(poolSize, inUseSize);
this.poolSize.set(newPoolSize);
for (int i = this.available.size(); i > newPoolSize - inUseSize; i--) {
T item = this.available.poll();
if (item != null) {
doRemoveItem(item);
}
this.poolSize.decrementAndGet();
delta++;
else {
break;
}
}
int inUseDelta = poolSize - inUseSize;
if (inUseDelta < 0 && this.logger.isDebugEnabled()) {
this.logger.debug(String.format("Pool is overcommitted by %d; items will be removed when returned",
-inUseDelta));
}
}
if (delta < 0 && this.logger.isDebugEnabled()) {
this.logger.debug(String.format("Pool is overcommitted by %d; items will be removed when returned",
-delta));
}
}
@@ -266,6 +272,19 @@ public class SimplePool<T> implements Pool<T> {
removeAllIdleItems();
}
private static class PoolSemaphore extends Semaphore {
PoolSemaphore(int permits) {
super(permits);
}
@Override
public void reducePermits(int reduction) { // NOSONAR increases visibility
super.reducePermits(reduction);
}
}
/**
* User of the pool provide an implementation of this interface; called during
* various pool operations.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 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.
@@ -20,7 +20,6 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.fail;
import java.util.ArrayList;
import java.util.HashSet;
@@ -36,6 +35,8 @@ import org.springframework.integration.test.util.TestUtils;
/**
* @author Gary Russell
* @author Sergey Bogatyrev
* @author Artem Bilan
*
* @since 2.2
*
*/
@@ -43,7 +44,7 @@ public class SimplePoolTests {
@Test
public void testReuseAndStale() {
final Set<String> strings = new HashSet<String>();
final Set<String> strings = new HashSet<>();
final AtomicBoolean stale = new AtomicBoolean();
SimplePool<String> pool = stringPool(2, strings, stale);
String s1 = pool.getItem();
@@ -62,7 +63,7 @@ public class SimplePoolTests {
@Test
public void testOverCommitAndResize() {
final Set<String> strings = new HashSet<String>();
final Set<String> strings = new HashSet<>();
final AtomicBoolean stale = new AtomicBoolean();
SimplePool<String> pool = stringPool(2, strings, stale);
String s1 = pool.getItem();
@@ -83,13 +84,9 @@ public class SimplePoolTests {
assertThat(pool.getIdleCount()).isEqualTo(0);
assertThat(pool.getActiveCount()).isEqualTo(2);
assertThat(pool.getAllocatedCount()).isEqualTo(2);
try {
pool.getItem();
fail("Expected exception");
}
catch (PoolItemNotAvailableException e) {
}
assertThatExceptionOfType(PoolItemNotAvailableException.class)
.isThrownBy(pool::getItem);
// resize up
pool.setPoolSize(4);
@@ -131,7 +128,7 @@ public class SimplePoolTests {
@Test
public void testForeignObject() {
final Set<String> strings = new HashSet<String>();
final Set<String> strings = new HashSet<>();
final AtomicBoolean stale = new AtomicBoolean();
SimplePool<String> pool = stringPool(2, strings, stale);
pool.getItem();
@@ -140,7 +137,7 @@ public class SimplePoolTests {
@Test
public void testDoubleReturn() {
final Set<String> strings = new HashSet<String>();
final Set<String> strings = new HashSet<>();
final AtomicBoolean stale = new AtomicBoolean();
SimplePool<String> pool = stringPool(2, strings, stale);
Semaphore permits = TestUtils.getPropertyValue(pool, "permits", Semaphore.class);
@@ -168,7 +165,27 @@ public class SimplePoolTests {
assertThat(allocatedItems).hasSize(5);
// no more items can be allocated (indirect check of permits)
assertThatExceptionOfType(PoolItemNotAvailableException.class).isThrownBy(() -> pool.getItem());
assertThatExceptionOfType(PoolItemNotAvailableException.class)
.isThrownBy(pool::getItem);
}
@Test
public void testMaxValueSizeUpdateIfNotAllocated() {
SimplePool<String> pool = stringPool(0, new HashSet<>(), new AtomicBoolean());
pool.setWaitTimeout(0);
pool.setPoolSize(5);
assertThat(pool.getPoolSize()).isEqualTo(5);
// allocating all available items to check permits
Set<String> allocatedItems = new HashSet<>();
for (int i = 0; i < 5; i++) {
allocatedItems.add(pool.getItem());
}
assertThat(allocatedItems).hasSize(5);
// no more items can be allocated (indirect check of permits)
assertThatExceptionOfType(PoolItemNotAvailableException.class)
.isThrownBy(pool::getItem);
}
@Test
@@ -207,7 +224,54 @@ public class SimplePoolTests {
assertThat(pool.getActiveCount()).isEqualTo(5);
// no more items can be allocated (indirect check of permits)
assertThatExceptionOfType(PoolItemNotAvailableException.class).isThrownBy(() -> pool.getItem());
assertThatExceptionOfType(PoolItemNotAvailableException.class)
.isThrownBy(pool::getItem);
}
@Test
public void testMaxValueSizeUpdateIfPartiallyAllocated() {
SimplePool<String> pool = stringPool(0, new HashSet<>(), new AtomicBoolean());
pool.setWaitTimeout(0);
List<String> allocated = new ArrayList<>();
for (int i = 0; i < 10; i++) {
allocated.add(pool.getItem());
}
// release only 2 items
for (int i = 0; i < 2; i++) {
pool.releaseItem(allocated.get(i));
}
// release only 2 items
for (int i = 0; i < 2; i++) {
pool.releaseItem(allocated.get(i));
}
// trying to reduce pool size
pool.setPoolSize(5);
// at this moment the actual pool size can be reduced only partially, because
// only 2 items have been released, so 8 items are in use
assertThat(pool.getPoolSize()).isEqualTo(8);
assertThat(pool.getAllocatedCount()).isEqualTo(8);
assertThat(pool.getIdleCount()).isEqualTo(0);
assertThat(pool.getActiveCount()).isEqualTo(8);
// releasing 3 items
for (int i = 2; i < 5; i++) {
pool.releaseItem(allocated.get(i));
}
// now pool size should be reduced
assertThat(pool.getPoolSize()).isEqualTo(5);
assertThat(pool.getAllocatedCount()).isEqualTo(5);
assertThat(pool.getIdleCount()).isEqualTo(0);
assertThat(pool.getActiveCount()).isEqualTo(5);
// no more items can be allocated (indirect check of permits)
assertThatExceptionOfType(PoolItemNotAvailableException.class)
.isThrownBy(pool::getItem);
}
@Test
@@ -240,7 +304,8 @@ public class SimplePoolTests {
assertThat(pool.getActiveCount()).isEqualTo(5);
// no more items can be allocated (indirect check of permits)
assertThatExceptionOfType(PoolItemNotAvailableException.class).isThrownBy(() -> pool.getItem());
assertThatExceptionOfType(PoolItemNotAvailableException.class)
.isThrownBy(pool::getItem);
// releasing remaining items
for (int i = 5; i < 10; i++) {
@@ -266,10 +331,9 @@ public class SimplePoolTests {
assertThatIllegalStateException().isThrownBy(pool::getItem);
}
private SimplePool<String> stringPool(int size, final Set<String> strings,
final AtomicBoolean stale) {
private SimplePool<String> stringPool(int size, Set<String> strings, AtomicBoolean stale) {
return new SimplePool<String>(size, new SimplePool.PoolItemCallback<String>() {
SimplePool<String> pool = new SimplePool<String>(size, new SimplePool.PoolItemCallback<String>() {
private int i;
@Override
@@ -293,7 +357,6 @@ public class SimplePoolTests {
}
});
return pool;
}
}