
io.logz.sender.com.bluejeans.common.bigqueue.BigArray Maven / Gradle / Ivy
package com.bluejeans.common.bigqueue;
import java.io.Closeable;
import java.io.File;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* A big array implementation supporting sequential append and random read.
*
* Main features:
* 1. FAST : close to the speed of direct memory access, extremely fast in append only and sequential read modes,
* sequential append and read are close to O(1) memory access, random read is close to O(1) memory access if
* data is in cache and is close to O(1) disk access if data is not in cache.
* 2. MEMORY-EFFICIENT : automatic paging and swapping algorithm, only most-recently accessed data is kept in memory.
* 3. THREAD-SAFE : multiple threads can concurrently read/append the array without data corruption.
* 4. PERSISTENT - all array data is persisted on disk, and is crash resistant.
* 5. BIG(HUGE) - the total size of the array data is only limited by the available disk space.
*
*
* @author bulldog
*
*/
public class BigArray implements Closeable {
public static final long NOT_FOUND = -1;
// folder name for index page
final static String INDEX_PAGE_FOLDER = "index";
// folder name for data page
final static String DATA_PAGE_FOLDER = "data";
// folder name for meta data page
final static String META_DATA_PAGE_FOLDER = "meta_data";
// 2 ^ 20 = 1024 * 1024
final static int INDEX_ITEMS_PER_PAGE_BITS = 20; // 1024 * 1024
// number of items per page
final static int INDEX_ITEMS_PER_PAGE = 1 << INDEX_ITEMS_PER_PAGE_BITS;
// 2 ^ 5 = 32
final static int INDEX_ITEM_LENGTH_BITS = 5;
// length in bytes of an index item
final static int INDEX_ITEM_LENGTH = 1 << INDEX_ITEM_LENGTH_BITS;
// size in bytes of an index page
final static int INDEX_PAGE_SIZE = INDEX_ITEM_LENGTH * INDEX_ITEMS_PER_PAGE;
// size in bytes of a data page
final int DATA_PAGE_SIZE;
// default size in bytes of a data page
public final static int DEFAULT_DATA_PAGE_SIZE = 128 * 1024 * 1024;
// minimum size in bytes of a data page
public final static int MINIMUM_DATA_PAGE_SIZE = 32 * 1024 * 1024;
// seconds, time to live for index page cached in memory
final static int INDEX_PAGE_CACHE_TTL = 1000;
// seconds, time to live for data page cached in memory
final static int DATA_PAGE_CACHE_TTL = 1000;
// 2 ^ 4 = 16
final static int META_DATA_ITEM_LENGTH_BITS = 4;
// size in bytes of a meta data page
final static int META_DATA_PAGE_SIZE = 1 << META_DATA_ITEM_LENGTH_BITS;
// private final static int INDEX_ITEM_DATA_PAGE_INDEX_OFFSET = 0;
// private final static int INDEX_ITEM_DATA_ITEM_OFFSET_OFFSET = 8;
private final static int INDEX_ITEM_DATA_ITEM_LENGTH_OFFSET = 12;
// timestamp offset of an data item within an index item
final static int INDEX_ITEM_DATA_ITEM_TIMESTAMP_OFFSET = 16;
// directory to persist array data
String arrayDirectory;
// factory for index page management(acquire, release, cache)
MappedPageFactory indexPageFactory;
// factory for data page management(acquire, release, cache)
MappedPageFactory dataPageFactory;
// factory for meta data page management(acquire, release, cache)
MappedPageFactory metaPageFactory;
// only use the first page
static final long META_DATA_PAGE_INDEX = 0;
// head index of the big array, this is the read write barrier.
// readers can only read items before this index, and writes can write this index or after
final AtomicLong arrayHeadIndex = new AtomicLong();
// tail index of the big array,
// readers can't read items before this tail
final AtomicLong arrayTailIndex = new AtomicLong();
// head index of the data page, this is the to be appended data page index
long headDataPageIndex;
// head offset of the data page, this is the to be appended data offset
int headDataItemOffset;
// lock for appending state management
final Lock appendLock = new ReentrantLock();
// global lock for array read and write management
final ReadWriteLock arrayReadWritelock = new ReentrantReadWriteLock();
final Lock arrayReadLock = arrayReadWritelock.readLock();
final Lock arrayWriteLock = arrayReadWritelock.writeLock();
/**
*
* A big array implementation supporting sequential write and random read,
* use default back data file size per page, see {@link #DEFAULT_DATA_PAGE_SIZE}.
*
* @param arrayDir directory for array data store
* @param arrayName the name of the array, will be appended as last part of the array directory
*/
public BigArray(final String arrayDir, final String arrayName) {
this(arrayDir, arrayName, DEFAULT_DATA_PAGE_SIZE);
}
/**
* A big array implementation supporting sequential write and random read.
*
* @param arrayDir directory for array data store
* @param arrayName the name of the array, will be appended as last part of the array directory
* @param pageSize the back data file size per page in bytes, see minimum allowed {@link #MINIMUM_DATA_PAGE_SIZE}.
*/
public BigArray(final String arrayDir, final String arrayName, final int pageSize) {
arrayDirectory = arrayDir;
if (!arrayDirectory.endsWith(File.separator))
arrayDirectory += File.separator;
// append array name as part of the directory
arrayDirectory = arrayDirectory + arrayName + File.separator;
// validate directory
if (!FileUtil.isFilenameValid(arrayDirectory))
throw new IllegalArgumentException("invalid array directory : " + arrayDirectory);
if (pageSize < MINIMUM_DATA_PAGE_SIZE)
throw new IllegalArgumentException("invalid page size, allowed minimum is : " + MINIMUM_DATA_PAGE_SIZE + " bytes.");
DATA_PAGE_SIZE = pageSize;
this.commonInit();
}
String getArrayDirectory() {
return this.arrayDirectory;
}
void commonInit() {
// initialize page factories
this.indexPageFactory = new MappedPageFactory(INDEX_PAGE_SIZE, this.arrayDirectory + INDEX_PAGE_FOLDER, INDEX_PAGE_CACHE_TTL);
this.dataPageFactory = new MappedPageFactory(DATA_PAGE_SIZE, this.arrayDirectory + DATA_PAGE_FOLDER, DATA_PAGE_CACHE_TTL);
// the ttl does not matter here since meta data page is always cached
this.metaPageFactory = new MappedPageFactory(META_DATA_PAGE_SIZE, this.arrayDirectory + META_DATA_PAGE_FOLDER,
10 * 1000/*does not matter*/);
// initialize array indexes
initArrayIndex();
// initialize data page indexes
initDataPageIndex();
}
public void removeAll() {
try {
arrayWriteLock.lock();
this.indexPageFactory.deleteAllPages();
this.dataPageFactory.deleteAllPages();
this.metaPageFactory.deleteAllPages();
//FileUtil.deleteDirectory(new File(this.arrayDirectory));
this.commonInit();
}
finally {
arrayWriteLock.unlock();
}
}
/**
* Remove all data before specific index, this will advance the array tail to index and
* delete back page files before index.
*
* @param index an index
*/
public void removeBeforeIndex(final long index) {
try {
arrayWriteLock.lock();
validateIndex(index);
final long indexPageIndex = Calculator.div(index, INDEX_ITEMS_PER_PAGE_BITS);
final ByteBuffer indexItemBuffer = this.getIndexItemBuffer(index);
final long dataPageIndex = indexItemBuffer.getLong();
if (indexPageIndex > 0L)
this.indexPageFactory.deletePagesBeforePageIndex(indexPageIndex);
if (dataPageIndex > 0L)
this.dataPageFactory.deletePagesBeforePageIndex(dataPageIndex);
// advance the tail to index
this.arrayTailIndex.set(index);
}
finally {
arrayWriteLock.unlock();
}
}
/**
* Remove all data before specific timestamp, this will advance the array tail and delete back page files
* accordingly.
*
* @param timestamp a timestamp
*/
public void removeBefore(final long timestamp) {
try {
arrayWriteLock.lock();
final long firstIndexPageIndex = this.indexPageFactory.getFirstPageIndexBefore(timestamp);
if (firstIndexPageIndex >= 0) {
// long nextIndexPageIndex = firstIndexPageIndex;
// if (nextIndexPageIndex == Long.MAX_VALUE) { //wrap
// nextIndexPageIndex = 0L;
// } else {
// nextIndexPageIndex++;
// }
final long toRemoveBeforeIndex = Calculator.mul(firstIndexPageIndex, INDEX_ITEMS_PER_PAGE_BITS);
removeBeforeIndex(toRemoveBeforeIndex);
}
}
catch (final IndexOutOfBoundsException ex) {
// ignore
}
finally {
arrayWriteLock.unlock();
}
}
// find out array head/tail from the meta data
void initArrayIndex() {
final MappedPage metaDataPage = this.metaPageFactory.acquirePage(META_DATA_PAGE_INDEX);
final ByteBuffer metaBuf = metaDataPage.getLocal(0);
final long head = metaBuf.getLong();
final long tail = metaBuf.getLong();
arrayHeadIndex.set(head);
arrayTailIndex.set(tail);
}
// find out data page head index and offset
void initDataPageIndex() {
if (this.isEmpty()) {
headDataPageIndex = 0L;
headDataItemOffset = 0;
}
else {
MappedPage previousIndexPage = null;
long previousIndexPageIndex = -1;
try {
long previousIndex = this.arrayHeadIndex.get() - 1;
if (previousIndex < 0)
previousIndex = Long.MAX_VALUE; // wrap
previousIndexPageIndex = Calculator.div(previousIndex, INDEX_ITEMS_PER_PAGE_BITS); // shift optimization
previousIndexPage = this.indexPageFactory.acquirePage(previousIndexPageIndex);
final int previousIndexPageOffset = (int) Calculator.mul(Calculator.mod(previousIndex, INDEX_ITEMS_PER_PAGE_BITS),
INDEX_ITEM_LENGTH_BITS);
final ByteBuffer previousIndexItemBuffer = previousIndexPage.getLocal(previousIndexPageOffset);
final long previousDataPageIndex = previousIndexItemBuffer.getLong();
final int previousDataItemOffset = previousIndexItemBuffer.getInt();
final int perviousDataItemLength = previousIndexItemBuffer.getInt();
headDataPageIndex = previousDataPageIndex;
headDataItemOffset = previousDataItemOffset + perviousDataItemLength;
}
finally {
if (previousIndexPage != null)
this.indexPageFactory.releasePage(previousIndexPageIndex);
}
}
}
/**
* Append the data into the head of the array
*
* @param data binary data to append
* @return appended index
*/
public long append(final byte[] data) {
try {
arrayReadLock.lock();
MappedPage toAppendDataPage = null;
MappedPage toAppendIndexPage = null;
long toAppendIndexPageIndex = -1L;
long toAppendDataPageIndex = -1L;
long toAppendArrayIndex = -1L;
try {
appendLock.lock(); // only one thread can append
if (this.isFull())
throw new BigQueueException("ring space of java long type used up, the end of the world!!!");
// prepare the data pointer
if (this.headDataItemOffset + data.length > DATA_PAGE_SIZE) { // not enough space
if (this.headDataPageIndex == Long.MAX_VALUE)
this.headDataPageIndex = 0L; // wrap
else
this.headDataPageIndex++;
this.headDataItemOffset = 0;
}
toAppendDataPageIndex = this.headDataPageIndex;
final int toAppendDataItemOffset = this.headDataItemOffset;
toAppendArrayIndex = this.arrayHeadIndex.get();
// append data
toAppendDataPage = this.dataPageFactory.acquirePage(toAppendDataPageIndex);
final ByteBuffer toAppendDataPageBuffer = toAppendDataPage.getLocal(toAppendDataItemOffset);
toAppendDataPageBuffer.put(data);
toAppendDataPage.setDirty(true);
// update to next
this.headDataItemOffset += data.length;
toAppendIndexPageIndex = Calculator.div(toAppendArrayIndex, INDEX_ITEMS_PER_PAGE_BITS); // shift optimization
toAppendIndexPage = this.indexPageFactory.acquirePage(toAppendIndexPageIndex);
final int toAppendIndexItemOffset = (int) Calculator.mul(Calculator.mod(toAppendArrayIndex, INDEX_ITEMS_PER_PAGE_BITS),
INDEX_ITEM_LENGTH_BITS);
// update index
final ByteBuffer toAppendIndexPageBuffer = toAppendIndexPage.getLocal(toAppendIndexItemOffset);
toAppendIndexPageBuffer.putLong(toAppendDataPageIndex);
toAppendIndexPageBuffer.putInt(toAppendDataItemOffset);
toAppendIndexPageBuffer.putInt(data.length);
final long currentTime = System.currentTimeMillis();
toAppendIndexPageBuffer.putLong(currentTime);
toAppendIndexPage.setDirty(true);
// advance the head
this.arrayHeadIndex.incrementAndGet();
// update meta data
final MappedPage metaDataPage = this.metaPageFactory.acquirePage(META_DATA_PAGE_INDEX);
final ByteBuffer metaDataBuf = metaDataPage.getLocal(0);
metaDataBuf.putLong(this.arrayHeadIndex.get());
metaDataBuf.putLong(this.arrayTailIndex.get());
metaDataPage.setDirty(true);
}
finally {
appendLock.unlock();
if (toAppendDataPage != null)
this.dataPageFactory.releasePage(toAppendDataPageIndex);
if (toAppendIndexPage != null)
this.indexPageFactory.releasePage(toAppendIndexPageIndex);
}
return toAppendArrayIndex;
}
finally {
arrayReadLock.unlock();
}
}
/**
* Force to persist newly appended data,
*
* normally, you don't need to flush explicitly since:
* 1.) BigArray will automatically flush a cached page when it is replaced out,
* 2.) BigArray uses memory mapped file technology internally, and the OS will flush the changes even your process crashes,
*
* call this periodically only if you need transactional reliability and you are aware of the cost to performance.
*/
public void flush() {
try {
arrayReadLock.lock();
// try {
// appendLock.lock(); // make flush and append mutually exclusive
this.metaPageFactory.flush();
this.indexPageFactory.flush();
this.dataPageFactory.flush();
// } finally {
// appendLock.unlock();
// }
}
finally {
arrayReadLock.unlock();
}
}
/**
* Get the data at specific index
*
* @param index valid data index
* @return binary data if the index is valid
*/
public byte[] get(final long index) {
try {
arrayReadLock.lock();
validateIndex(index);
MappedPage dataPage = null;
long dataPageIndex = -1L;
try {
final ByteBuffer indexItemBuffer = this.getIndexItemBuffer(index);
dataPageIndex = indexItemBuffer.getLong();
final int dataItemOffset = indexItemBuffer.getInt();
final int dataItemLength = indexItemBuffer.getInt();
dataPage = this.dataPageFactory.acquirePage(dataPageIndex);
final byte[] data = dataPage.getLocal(dataItemOffset, dataItemLength);
return data;
}
finally {
if (dataPage != null)
this.dataPageFactory.releasePage(dataPageIndex);
}
}
finally {
arrayReadLock.unlock();
}
}
/**
* Get the timestamp of data at specific index,
*
* this is the timestamp when the data was appended.
*
* @param index valid data index
* @return timestamp when the data was appended
*/
public long getTimestamp(final long index) {
try {
arrayReadLock.lock();
validateIndex(index);
final ByteBuffer indexItemBuffer = this.getIndexItemBuffer(index);
// position to the timestamp
final int position = indexItemBuffer.position();
indexItemBuffer.position(position + INDEX_ITEM_DATA_ITEM_TIMESTAMP_OFFSET);
final long ts = indexItemBuffer.getLong();
return ts;
}
finally {
arrayReadLock.unlock();
}
}
ByteBuffer getIndexItemBuffer(final long index) {
MappedPage indexPage = null;
long indexPageIndex = -1L;
try {
indexPageIndex = Calculator.div(index, INDEX_ITEMS_PER_PAGE_BITS); // shift optimization
indexPage = this.indexPageFactory.acquirePage(indexPageIndex);
final int indexItemOffset = (int) Calculator.mul(Calculator.mod(index, INDEX_ITEMS_PER_PAGE_BITS), INDEX_ITEM_LENGTH_BITS);
final ByteBuffer indexItemBuffer = indexPage.getLocal(indexItemOffset);
return indexItemBuffer;
}
finally {
if (indexPage != null)
this.indexPageFactory.releasePage(indexPageIndex);
}
}
void validateIndex(final long index) {
if (this.arrayTailIndex.get() <= this.arrayHeadIndex.get()) {
if (index < this.arrayTailIndex.get() || index >= this.arrayHeadIndex.get())
throw new IndexOutOfBoundsException();
}
else if (index < this.arrayTailIndex.get() && index >= this.arrayHeadIndex.get())
throw new IndexOutOfBoundsException();
}
/**
* The total number of items has been appended into the array
*
* @return total number
*/
public long size() {
try {
arrayReadLock.lock();
if (this.arrayTailIndex.get() <= this.arrayHeadIndex.get())
return this.arrayHeadIndex.get() - this.arrayTailIndex.get();
else
return Long.MAX_VALUE - this.arrayTailIndex.get() + 1 + this.arrayHeadIndex.get();
}
finally {
arrayReadLock.unlock();
}
}
/**
* The head of the array.
*
* This is the next to append index, the index of the last appended data
* is [headIndex - 1] if the array is not empty.
*
* @return an index
*/
public long getHeadIndex() {
try {
arrayReadLock.lock();
return arrayHeadIndex.get();
}
finally {
arrayReadLock.unlock();
}
}
/**
* The tail of the array.
*
* The is the index of the first appended data
*
* @return an index
*/
public long getTailIndex() {
try {
arrayReadLock.lock();
return arrayTailIndex.get();
}
finally {
arrayReadLock.unlock();
}
}
/**
* Check if the array is empty or not
*
* @return true if empty false otherwise
*/
public boolean isEmpty() {
try {
arrayReadLock.lock();
return this.arrayHeadIndex.get() == this.arrayTailIndex.get();
}
finally {
arrayReadLock.unlock();
}
}
/**
* Check if the ring space of java long type has all been used up.
*
* can always assume false, if true, the world is end:)
*
* @return array full or not
*/
public boolean isFull() {
try {
arrayReadLock.lock();
final long currentIndex = this.arrayHeadIndex.get();
final long nextIndex = currentIndex == Long.MAX_VALUE ? 0 : currentIndex + 1;
return nextIndex == this.arrayTailIndex.get();
}
finally {
arrayReadLock.unlock();
}
}
@Override
public void close() {
try {
arrayWriteLock.lock();
if (this.metaPageFactory != null)
this.metaPageFactory.releaseCachedPages();
if (this.indexPageFactory != null)
this.indexPageFactory.releaseCachedPages();
if (this.dataPageFactory != null)
this.dataPageFactory.releaseCachedPages();
}
finally {
arrayWriteLock.unlock();
}
}
/**
* Get the back data file size per page.
*
* @return size per page
*/
public int getDataPageSize() {
return DATA_PAGE_SIZE;
}
/**
* Find an index closest to the specific timestamp when the corresponding item was appended
*
* @param timestamp when the corresponding item was appended
* @return an index
*/
public long findClosestIndex(final long timestamp) {
try {
arrayReadLock.lock();
long closestIndex = NOT_FOUND;
final long tailIndex = this.arrayTailIndex.get();
final long headIndex = this.arrayHeadIndex.get();
if (tailIndex == headIndex)
return closestIndex; // empty
long lastIndex = headIndex - 1;
if (lastIndex < 0)
lastIndex = Long.MAX_VALUE;
if (tailIndex < lastIndex)
closestIndex = closestBinarySearch(tailIndex, lastIndex, timestamp);
else {
final long lowPartClosestIndex = closestBinarySearch(0L, lastIndex, timestamp);
final long highPartClosetIndex = closestBinarySearch(tailIndex, Long.MAX_VALUE, timestamp);
final long lowPartTimestamp = this.getTimestamp(lowPartClosestIndex);
final long highPartTimestamp = this.getTimestamp(highPartClosetIndex);
closestIndex = Math.abs(timestamp - lowPartTimestamp) < Math.abs(timestamp - highPartTimestamp) ? lowPartClosestIndex
: highPartClosetIndex;
}
return closestIndex;
}
finally {
arrayReadLock.unlock();
}
}
private long closestBinarySearch(final long low, final long high, final long timestamp) {
long mid;
final long sum = low + high;
if (sum < 0) { // overflow
BigInteger bigSum = BigInteger.valueOf(low);
bigSum = bigSum.add(BigInteger.valueOf(high));
mid = bigSum.shiftRight(1).longValue();
}
else
mid = sum / 2;
final long midTimestamp = this.getTimestamp(mid);
if (midTimestamp < timestamp) {
final long nextLow = mid + 1;
if (nextLow >= high)
return high;
return closestBinarySearch(nextLow, high, timestamp);
}
else if (midTimestamp > timestamp) {
final long nextHigh = mid - 1;
if (nextHigh <= low)
return low;
return closestBinarySearch(low, nextHigh, timestamp);
}
else
return mid;
}
/**
* Get total size of back files(index and data files) of the big array
*
* @return total size of back files
*/
public long getBackFileSize() {
try {
arrayReadLock.lock();
return this._getBackFileSize();
}
finally {
arrayReadLock.unlock();
}
}
/**
* limit the back file size, truncate back file and advance array tail index accordingly,
* Note, this is a best effort call, exact size limit can't be guaranteed
*
* @param sizeLimit the size to limit
*/
public void limitBackFileSize(final long sizeLimit) {
if (sizeLimit < INDEX_PAGE_SIZE + DATA_PAGE_SIZE)
return; // ignore, one index page + one data page are minimum for big array to work correctly
long backFileSize = this.getBackFileSize();
if (backFileSize <= sizeLimit)
return; // nothing to do
long toTruncateSize = backFileSize - sizeLimit;
if (toTruncateSize < DATA_PAGE_SIZE)
return; // can't do anything
try {
arrayWriteLock.lock();
// double check
backFileSize = this._getBackFileSize();
if (backFileSize <= sizeLimit)
return; // nothing to do
toTruncateSize = backFileSize - sizeLimit;
if (toTruncateSize < DATA_PAGE_SIZE)
return; // can't do anything
long tailIndex = this.arrayTailIndex.get();
final long headIndex = this.arrayHeadIndex.get();
long totalLength = 0L;
while (true) {
if (tailIndex == headIndex)
break;
totalLength += this.getDataItemLength(tailIndex);
if (totalLength > toTruncateSize)
break;
if (tailIndex == Long.MAX_VALUE)
tailIndex = 0;
else
tailIndex++;
if (Calculator.mod(tailIndex, INDEX_ITEMS_PER_PAGE_BITS) == 0)
totalLength += INDEX_PAGE_SIZE;
}
this.removeBeforeIndex(tailIndex);
}
finally {
arrayWriteLock.unlock();
}
}
/**
* Get the data item length at specific index
*
* @param index valid data index
* @return the length of binary data if the index is valid
*/
public int getItemLength(final long index) {
try {
arrayReadLock.lock();
validateIndex(index);
return getDataItemLength(index);
}
finally {
arrayReadLock.unlock();
}
}
private int getDataItemLength(final long index) {
final ByteBuffer indexItemBuffer = this.getIndexItemBuffer(index);
// position to the data item length
final int position = indexItemBuffer.position();
indexItemBuffer.position(position + INDEX_ITEM_DATA_ITEM_LENGTH_OFFSET);
final int length = indexItemBuffer.getInt();
return length;
}
// inner getBackFileSize
private long _getBackFileSize() {
return this.indexPageFactory.getBackPageFileSize() + this.dataPageFactory.getBackPageFileSize();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy