/**
* Copyright 2016 LinkedIn Corp. All rights reserved.
*
* 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.
*/
package com.github.ambry.utils;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeoutException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* An implementation of <B>bounded</B> {@link ByteBufferPool}. A pool
* is bounded by non-varying {@code capacity}, and it ensures that the
* available memory in the pool never goes above this capacity, or goes
* below zero. It does not actually "pool" deallocated buffers.
*/
public class SimpleByteBufferPool implements ByteBufferPool {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final long capacity;
private final Object lock;
private long availableMemory;
/**
* Create a new buffer pool
* @param capacity the maximum amount of memory that this buffer pool can
* allocate. The caller has the responsibility to assign
* a reasonable value for {@code capacity}.
*/
public SimpleByteBufferPool(long capacity) {
this.lock = new Object();
this.capacity = capacity;
this.availableMemory = capacity;
}
/**
* Allocate a byte buffer at the requested size
* @param size the buffer size to allocate in bytes
* @param timeToBlockInMs a non-negative maximum time in milliseconds to block a request
* until the requested size of memory becomes available. Zero value
* will make the pool to try a single time, either return a {@link
* ByteBuffer} if memory is available, or a {@code TimeoutException}
* @return A {@link ByteBuffer} at the requested size
* @throws TimeoutException if request cannot be served within {@code timeToBlockInMs}
* @throws InterruptedException if the current thread is interrupted while waiting
* @throws IllegalArgumentException if {@code size} is larger than the pool capacity, or
* if {@code timeToBlockInMs} is negative.
*/
@Override
public ByteBuffer allocate(int size, final long timeToBlockInMs) throws TimeoutException, InterruptedException {
if (size > capacity) {
throw new IllegalArgumentException("Requested size cannot exceed pool capacity.");
} else if (timeToBlockInMs < 0) {
throw new IllegalArgumentException("timeToBlockInMs cannot be negative.");
}
final long startTimeInMs = System.currentTimeMillis();
synchronized (lock) {
while (size > availableMemory) {
long timeout = timeToBlockInMs - (System.currentTimeMillis() - startTimeInMs);
if (timeout <= 0) {
throw new TimeoutException("Timed out waiting for allocation.");
}
lock.wait(timeout);
}
availableMemory -= size;
if (availableMemory > 0) {
lock.notify();
}
}
return ByteBuffer.allocate(size);
}
/**
* This method claims back the memory of {@code buffer}. It does not
* check if the buffer was originally allocated from the pool. If a
* deallocation will exceed the pool's capacity, the method simply sets
* the pool's available memory to its {@code capacity}.
* @param buffer the {@link ByteBuffer} to be deallocated back to the pool
*/
@Override
public void deallocate(ByteBuffer buffer) {
synchronized (lock) {
availableMemory += buffer.capacity();
if (availableMemory > capacity) {
availableMemory = capacity;
logger.warn("The total deallocated memory is more than that had been allocated from the buffer pool.");
}
lock.notify();
}
}
/**
* @return the amount of memory currently available
*/
public long availableMemory() {
return availableMemory;
}
/**
* @return the capacity of the pool
*/
public long capacity() {
return capacity;
}
}