Issue #1861 - Limit total bytes pooled by ByteBufferPools.

Code cleanups + javadocs.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2019-02-25 16:14:41 +01:00
parent 259e31856a
commit f2d15f12d5
5 changed files with 200 additions and 153 deletions

View File

@ -20,93 +20,115 @@ package org.eclipse.jetty.io;
import java.nio.ByteBuffer;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.annotation.ManagedOperation;
/**
* <p>A ByteBuffer pool where ByteBuffers are held in queues that are held in array elements.</p>
* <p>Given a capacity {@code factor} of 1024, the first array element holds a queue of ByteBuffers
* each of capacity 1024, the second array element holds a queue of ByteBuffers each of capacity
* 2048, and so on.</p>
*/
@ManagedObject
public class ArrayByteBufferPool implements ByteBufferPool
{
private final int _min;
private final int _maxQueue;
private final int _minCapacity;
private final ByteBufferPool.Bucket[] _direct;
private final ByteBufferPool.Bucket[] _indirect;
private final int _inc;
private final int _factor;
/**
* Creates a new ArrayByteBufferPool with a default configuration.
*/
public ArrayByteBufferPool()
{
this(-1,-1,-1,-1);
this(-1, -1, -1, -1);
}
public ArrayByteBufferPool(int minSize, int increment, int maxSize)
/**
* Creates a new ArrayByteBufferPool with the given configuration.
*
* @param minCapacity the minimum ByteBuffer capacity
* @param factor the capacity factor
* @param maxCapacity the maximum ByteBuffer capacity
*/
public ArrayByteBufferPool(int minCapacity, int factor, int maxCapacity)
{
this(minSize,increment,maxSize,-1);
this(minCapacity, factor, maxCapacity, -1);
}
public ArrayByteBufferPool(int minSize, int increment, int maxSize, int maxQueue)
/**
* Creates a new ArrayByteBufferPool with the given configuration.
*
* @param minCapacity the minimum ByteBuffer capacity
* @param factor the capacity factor
* @param maxCapacity the maximum ByteBuffer capacity
* @param maxQueueLength the maximum ByteBuffer queue length
*/
public ArrayByteBufferPool(int minCapacity, int factor, int maxCapacity, int maxQueueLength)
{
if (minSize<=0)
minSize=0;
if (increment<=0)
increment=1024;
if (maxSize<=0)
maxSize=64*1024;
if (minSize>=increment)
throw new IllegalArgumentException("minSize >= increment");
if ((maxSize%increment)!=0 || increment>=maxSize)
throw new IllegalArgumentException("increment must be a divisor of maxSize");
_min=minSize;
_inc=increment;
if (minCapacity <= 0)
minCapacity = 0;
if (factor <= 0)
factor = 1024;
if (maxCapacity <= 0)
maxCapacity = 64 * 1024;
if ((maxCapacity % factor) != 0 || factor >= maxCapacity)
throw new IllegalArgumentException("The capacity factor must be a divisor of maxCapacity");
_minCapacity = minCapacity;
_factor = factor;
_direct=new ByteBufferPool.Bucket[maxSize/increment];
_indirect=new ByteBufferPool.Bucket[maxSize/increment];
_maxQueue=maxQueue;
int length = maxCapacity / factor;
_direct = new ByteBufferPool.Bucket[length];
_indirect = new ByteBufferPool.Bucket[length];
int size=0;
for (int i=0;i<_direct.length;i++)
int capacity = 0;
for (int i = 0; i < _direct.length; ++i)
{
size+=_inc;
_direct[i]=new ByteBufferPool.Bucket(this,size,_maxQueue);
_indirect[i]=new ByteBufferPool.Bucket(this,size,_maxQueue);
capacity += _factor;
_direct[i] = new ByteBufferPool.Bucket(this, capacity, maxQueueLength);
_indirect[i] = new ByteBufferPool.Bucket(this, capacity, maxQueueLength);
}
}
@Override
public ByteBuffer acquire(int size, boolean direct)
{
ByteBufferPool.Bucket bucket = bucketFor(size,direct);
if (bucket==null)
return newByteBuffer(size,direct);
ByteBufferPool.Bucket bucket = bucketFor(size, direct);
if (bucket == null)
return newByteBuffer(size, direct);
return bucket.acquire(direct);
}
@Override
public void release(ByteBuffer buffer)
{
if (buffer!=null)
{
ByteBufferPool.Bucket bucket = bucketFor(buffer.capacity(),buffer.isDirect());
if (bucket!=null)
if (buffer != null)
{
ByteBufferPool.Bucket bucket = bucketFor(buffer.capacity(), buffer.isDirect());
if (bucket != null)
bucket.release(buffer);
}
}
@ManagedOperation(value = "Clears this ByteBufferPool", impact = "ACTION")
public void clear()
{
for (int i=0;i<_direct.length;i++)
for (int i = 0; i < _direct.length; ++i)
{
_direct[i].clear();
_indirect[i].clear();
}
}
private ByteBufferPool.Bucket bucketFor(int size,boolean direct)
private ByteBufferPool.Bucket bucketFor(int capacity, boolean direct)
{
if (size<=_min)
if (capacity <= _minCapacity)
return null;
int b=(size-1)/_inc;
if (b>=_direct.length)
int b = (capacity - 1) / _factor;
if (b >= _direct.length)
return null;
ByteBufferPool.Bucket bucket = direct?_direct[b]:_indirect[b];
return bucket;
return bucketsFor(direct)[b];
}
// Package local for testing

View File

@ -56,6 +56,13 @@ public interface ByteBufferPool
*/
public void release(ByteBuffer buffer);
/**
* <p>Creates a new ByteBuffer of the given capacity and the given directness.</p>
*
* @param capacity the ByteBuffer capacity
* @param direct the ByteBuffer directness
* @return a newly allocated ByteBuffer
*/
default ByteBuffer newByteBuffer(int capacity, boolean direct)
{
return direct ? BufferUtil.allocateDirect(capacity) : BufferUtil.allocate(capacity);
@ -131,10 +138,10 @@ public interface ByteBufferPool
private final int _capacity;
private final AtomicInteger _space;
public Bucket(ByteBufferPool pool, int bufferSize, int maxSize)
public Bucket(ByteBufferPool pool, int capacity, int maxSize)
{
_pool = pool;
_capacity = bufferSize;
_capacity = capacity;
_space = maxSize > 0 ? new AtomicInteger(maxSize) : null;
}
@ -204,7 +211,7 @@ public interface ByteBufferPool
@Override
public String toString()
{
return String.format("Bucket@%x{%d/%d}", hashCode(), size(), _capacity);
return String.format("%s@%x{%d/%d}", getClass().getSimpleName(), hashCode(), size(), _capacity);
}
}
}

View File

@ -22,36 +22,58 @@ import java.nio.ByteBuffer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.annotation.ManagedOperation;
/**
* <p>A ByteBuffer pool where ByteBuffers are held in queues that are held in a Map.</p>
* <p>Given a capacity {@code factor} of 1024, the Map entry with key {@code 1} holds a
* queue of ByteBuffers each of capacity 1024, the Map entry with key {@code 2} holds a
* queue of ByteBuffers each of capacity 2048, and so on.</p>
*/
@ManagedObject
public class MappedByteBufferPool implements ByteBufferPool
{
private final ConcurrentMap<Integer, Bucket> directBuffers = new ConcurrentHashMap<>();
private final ConcurrentMap<Integer, Bucket> heapBuffers = new ConcurrentHashMap<>();
private final int _factor;
private final Function<Integer, Bucket> _newBucket;
private final int _maxQueueLength;
/**
* Creates a new MappedByteBufferPool with a default configuration.
*/
public MappedByteBufferPool()
{
this(-1);
}
/**
* Creates a new MappedByteBufferPool with the given capacity factor.
*
* @param factor the capacity factor
*/
public MappedByteBufferPool(int factor)
{
this(factor,-1,null);
this(factor, -1);
}
public MappedByteBufferPool(int factor,int maxQueue)
/**
* Creates a new MappedByteBufferPool with the given capacity factor and max queue length.
*
* @param factor the capacity factor
* @param maxQueueLength the maximum ByteBuffer queue length
*/
public MappedByteBufferPool(int factor, int maxQueueLength)
{
this(factor,maxQueue,null);
_factor = factor <= 0 ? 1024 : factor;
_maxQueueLength = maxQueueLength;
}
public MappedByteBufferPool(int factor,int maxQueue,Function<Integer, Bucket> newBucket)
private Bucket newBucket(int key)
{
_factor = factor<=0?1024:factor;
_newBucket = newBucket!=null?newBucket:i->new Bucket(this,i*_factor,maxQueue);
return new Bucket(this, key * _factor, _maxQueueLength);
}
@Override
@ -59,10 +81,9 @@ public class MappedByteBufferPool implements ByteBufferPool
{
int b = bucketFor(size);
ConcurrentMap<Integer, Bucket> buffers = bucketsFor(direct);
Bucket bucket = buffers.get(b);
if (bucket==null)
return newByteBuffer(b*_factor, direct);
if (bucket == null)
return newByteBuffer(b * _factor, direct);
return bucket.acquire(direct);
}
@ -71,17 +92,18 @@ public class MappedByteBufferPool implements ByteBufferPool
{
if (buffer == null)
return; // nothing to do
// validate that this buffer is from this pool
assert((buffer.capacity() % _factor) == 0);
assert ((buffer.capacity() % _factor) == 0);
int b = bucketFor(buffer.capacity());
ConcurrentMap<Integer, Bucket> buckets = bucketsFor(buffer.isDirect());
Bucket bucket = buckets.computeIfAbsent(b,_newBucket);
Bucket bucket = buckets.computeIfAbsent(b, this::newBucket);
bucket.release(buffer);
}
@ManagedOperation(value = "Clears this ByteBufferPool", impact = "ACTION")
public void clear()
{
directBuffers.values().forEach(Bucket::clear);

View File

@ -18,32 +18,33 @@
package org.eclipse.jetty.io;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.eclipse.jetty.io.ByteBufferPool.Bucket;
import org.junit.jupiter.api.Test;
@SuppressWarnings("ReferenceEquality")
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ArrayByteBufferPoolTest
{
@Test
public void testMinimumRelease() throws Exception
public void testMinimumRelease()
{
ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(10,100,1000);
ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(10, 100, 1000);
ByteBufferPool.Bucket[] buckets = bufferPool.bucketsFor(true);
for (int size=1;size<=9;size++)
for (int size = 1; size <= 9; size++)
{
ByteBuffer buffer = bufferPool.acquire(size, true);
assertTrue(buffer.isDirect());
assertEquals(size,buffer.capacity());
assertEquals(size, buffer.capacity());
for (ByteBufferPool.Bucket bucket : buckets)
assertTrue(bucket.isEmpty());
@ -55,39 +56,12 @@ public class ArrayByteBufferPoolTest
}
@Test
public void testMaxRelease() throws Exception
public void testMaxRelease()
{
ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(10,100,1000);
ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(10, 100, 1000);
ByteBufferPool.Bucket[] buckets = bufferPool.bucketsFor(true);
for (int size=999;size<=1001;size++)
{
bufferPool.clear();
ByteBuffer buffer = bufferPool.acquire(size, true);
assertTrue(buffer.isDirect());
assertThat(buffer.capacity(),greaterThanOrEqualTo(size));
for (ByteBufferPool.Bucket bucket : buckets)
assertTrue(bucket.isEmpty());
bufferPool.release(buffer);
int pooled=0;
for (ByteBufferPool.Bucket bucket : buckets)
{
pooled+=bucket.size();
}
assertEquals(size<=1000,1==pooled);
}
}
@Test
public void testAcquireRelease() throws Exception
{
ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(10,100,1000);
ByteBufferPool.Bucket[] buckets = bufferPool.bucketsFor(true);
for (int size=390;size<=510;size++)
for (int size = 999; size <= 1001; size++)
{
bufferPool.clear();
ByteBuffer buffer = bufferPool.acquire(size, true);
@ -99,28 +73,54 @@ public class ArrayByteBufferPoolTest
bufferPool.release(buffer);
int pooled=0;
int pooled = 0;
for (ByteBufferPool.Bucket bucket : buckets)
{
if (!bucket.isEmpty())
{
pooled+=bucket.size();
// TODO assertThat(bucket._bufferSize,greaterThanOrEqualTo(size));
// TODO assertThat(bucket._bufferSize,Matchers.lessThan(size+100));
}
pooled += bucket.size();
}
assertEquals(1,pooled);
assertEquals(size <= 1000, 1 == pooled);
}
}
@Test
@SuppressWarnings("ReferenceEquality")
public void testAcquireReleaseAcquire() throws Exception
public void testAcquireRelease()
{
ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(10,100,1000);
ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(10, 100, 1000);
ByteBufferPool.Bucket[] buckets = bufferPool.bucketsFor(true);
for (int size=390;size<=510;size++)
for (int size = 390; size <= 510; size++)
{
bufferPool.clear();
ByteBuffer buffer = bufferPool.acquire(size, true);
assertTrue(buffer.isDirect());
assertThat(buffer.capacity(), greaterThanOrEqualTo(size));
for (ByteBufferPool.Bucket bucket : buckets)
assertTrue(bucket.isEmpty());
bufferPool.release(buffer);
int pooled = 0;
for (ByteBufferPool.Bucket bucket : buckets)
{
if (!bucket.isEmpty())
{
pooled += bucket.size();
// TODO assertThat(bucket._bufferSize,greaterThanOrEqualTo(size));
// TODO assertThat(bucket._bufferSize,Matchers.lessThan(size+100));
}
}
assertEquals(1, pooled);
}
}
@Test
public void testAcquireReleaseAcquire()
{
ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(10, 100, 1000);
ByteBufferPool.Bucket[] buckets = bufferPool.bucketsFor(true);
for (int size = 390; size <= 510; size++)
{
bufferPool.clear();
ByteBuffer buffer1 = bufferPool.acquire(size, true);
@ -130,45 +130,43 @@ public class ArrayByteBufferPoolTest
ByteBuffer buffer3 = bufferPool.acquire(size, false);
bufferPool.release(buffer3);
int pooled=0;
int pooled = 0;
for (ByteBufferPool.Bucket bucket : buckets)
{
if (!bucket.isEmpty())
{
pooled+=bucket.size();
pooled += bucket.size();
// TODO assertThat(bucket._bufferSize,greaterThanOrEqualTo(size));
// TODO assertThat(bucket._bufferSize,Matchers.lessThan(size+100));
}
}
assertEquals(1,pooled);
assertEquals(1, pooled);
assertTrue(buffer1==buffer2);
assertTrue(buffer1!=buffer3);
assertSame(buffer1, buffer2);
assertNotSame(buffer1, buffer3);
}
}
@Test
public void testMaxQueue() throws Exception
public void testMaxQueue()
{
ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(-1,-1,-1,2);
ArrayByteBufferPool bufferPool = new ArrayByteBufferPool(-1, -1, -1, 2);
ByteBuffer buffer1 = bufferPool.acquire(512, false);
ByteBuffer buffer2 = bufferPool.acquire(512, false);
ByteBuffer buffer3 = bufferPool.acquire(512, false);
Bucket[] buckets = bufferPool.bucketsFor(false);
Arrays.asList(buckets).forEach(b->assertEquals(0,b.size()));
Arrays.asList(buckets).forEach(b -> assertEquals(0, b.size()));
bufferPool.release(buffer1);
Bucket bucket=Arrays.asList(buckets).stream().filter(b->b.size()>0).findFirst().get();
Bucket bucket = Arrays.stream(buckets).filter(b -> b.size() > 0).findFirst().get();
assertEquals(1, bucket.size());
bufferPool.release(buffer2);
assertEquals(2, bucket.size());
bufferPool.release(buffer3);
assertEquals(2, bucket.size());
}
}

View File

@ -26,21 +26,21 @@ import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.StringUtil;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class MappedByteBufferPoolTest
{
@Test
public void testAcquireRelease() throws Exception
public void testAcquireRelease()
{
MappedByteBufferPool bufferPool = new MappedByteBufferPool();
ConcurrentMap<Integer,Bucket> buckets = bufferPool.bucketsFor(true);
ConcurrentMap<Integer, Bucket> buckets = bufferPool.bucketsFor(true);
int size = 512;
ByteBuffer buffer = bufferPool.acquire(size, true);
@ -56,10 +56,10 @@ public class MappedByteBufferPoolTest
}
@Test
public void testAcquireReleaseAcquire() throws Exception
public void testAcquireReleaseAcquire()
{
MappedByteBufferPool bufferPool = new MappedByteBufferPool();
ConcurrentMap<Integer,Bucket> buckets = bufferPool.bucketsFor(false);
ConcurrentMap<Integer, Bucket> buckets = bufferPool.bucketsFor(false);
ByteBuffer buffer1 = bufferPool.acquire(512, false);
bufferPool.release(buffer1);
@ -76,10 +76,10 @@ public class MappedByteBufferPoolTest
}
@Test
public void testAcquireReleaseClear() throws Exception
public void testAcquireReleaseClear()
{
MappedByteBufferPool bufferPool = new MappedByteBufferPool();
ConcurrentMap<Integer,Bucket> buckets = bufferPool.bucketsFor(true);
ConcurrentMap<Integer, Bucket> buckets = bufferPool.bucketsFor(true);
ByteBuffer buffer = bufferPool.acquire(512, true);
bufferPool.release(buffer);
@ -91,16 +91,14 @@ public class MappedByteBufferPoolTest
assertTrue(buckets.isEmpty());
}
/**
* In a scenario where MappedByteBufferPool is being used improperly,
* such as releasing a buffer that wasn't created/acquired by the
* MappedByteBufferPool, an assertion is tested for.
*
* @throws Exception test failure
*/
@Test
public void testReleaseAssertion() throws Exception
public void testReleaseAssertion()
{
int factor = 1024;
MappedByteBufferPool bufferPool = new MappedByteBufferPool(factor);
@ -110,8 +108,8 @@ public class MappedByteBufferPoolTest
// Release a few small non-pool buffers
bufferPool.release(ByteBuffer.wrap(StringUtil.getUtf8Bytes("Hello")));
/* NOTES:
*
/* NOTES:
*
* 1) This test will pass on command line maven build, as its surefire setup uses "-ea" already.
* 2) In Eclipse, goto the "Run Configuration" for this test case.
* Select the "Arguments" tab, and make sure "-ea" is present in the text box titled "VM arguments"
@ -123,24 +121,24 @@ public class MappedByteBufferPoolTest
// Expected path.
}
}
@Test
public void testTagged()
{
MappedByteBufferPool pool = new MappedByteBufferPool.Tagged();
ByteBuffer buffer = pool.acquire(1024,false);
ByteBuffer buffer = pool.acquire(1024, false);
assertThat(BufferUtil.toDetailString(buffer),containsString("@T00000001"));
buffer = pool.acquire(1024,false);
assertThat(BufferUtil.toDetailString(buffer),containsString("@T00000002"));
assertThat(BufferUtil.toDetailString(buffer), containsString("@T00000001"));
buffer = pool.acquire(1024, false);
assertThat(BufferUtil.toDetailString(buffer), containsString("@T00000002"));
}
@Test
public void testMaxQueue() throws Exception
public void testMaxQueue()
{
MappedByteBufferPool bufferPool = new MappedByteBufferPool(-1,2);
ConcurrentMap<Integer,Bucket> buckets = bufferPool.bucketsFor(false);
MappedByteBufferPool bufferPool = new MappedByteBufferPool(-1, 2);
ConcurrentMap<Integer, Bucket> buckets = bufferPool.bucketsFor(false);
ByteBuffer buffer1 = bufferPool.acquire(512, false);
ByteBuffer buffer2 = bufferPool.acquire(512, false);
@ -149,12 +147,12 @@ public class MappedByteBufferPoolTest
bufferPool.release(buffer1);
assertEquals(1, buckets.size());
Bucket bucket=buckets.values().iterator().next();
Bucket bucket = buckets.values().iterator().next();
assertEquals(1, bucket.size());
bufferPool.release(buffer2);
assertEquals(2, bucket.size());
bufferPool.release(buffer3);
assertEquals(2, bucket.size());
}