All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.logz.sender.com.bluejeans.common.bigqueue.BigQueue Maven / Gradle / Ivy

package com.bluejeans.common.bigqueue;

import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;

/**
 * A big, fast and persistent queue implementation.
 *
 * Main features: 1. FAST : close to the speed of direct memory access, both
 * enqueue and dequeue are close to O(1) memory access. 2. MEMORY-EFFICIENT :
 * automatic paging and swapping algorithm, only most-recently accessed data is
 * kept in memory. 3. THREAD-SAFE : multiple threads can concurrently enqueue
 * and dequeue without data corruption. 4. PERSISTENT - all data in queue is
 * persisted on disk, and is crash resistant. 5. BIG(HUGE) - the total size of
 * the queued data is only limited by the available disk space.
 *
 * @author bulldog
 */
public class BigQueue implements Closeable {

    final BigArray innerArray;

    // 2 ^ 3 = 8
    final static int QUEUE_FRONT_INDEX_ITEM_LENGTH_BITS = 3;
    // size in bytes of queue front index page
    final static int QUEUE_FRONT_INDEX_PAGE_SIZE = 1 << QUEUE_FRONT_INDEX_ITEM_LENGTH_BITS;
    // only use the first page
    static final long QUEUE_FRONT_PAGE_INDEX = 0;

    // folder name for queue front index page
    final static String QUEUE_FRONT_INDEX_PAGE_FOLDER = "front_index";

    // front index of the big queue,
    final AtomicLong queueFrontIndex = new AtomicLong();

    // factory for queue front index page management(acquire, release, cache)
    MappedPageFactory queueFrontIndexPageFactory;

    // locks for queue front write management
    final Lock queueFrontWriteLock = new ReentrantLock();

    // lock for dequeueFuture access
    private final Object futureLock = new Object();
    private SettableFuture dequeueFuture;
    private SettableFuture peekFuture;

    /**
     * A big, fast and persistent queue implementation, use default back data
     * page size, see {@link BigArray#DEFAULT_DATA_PAGE_SIZE}
     *
     * @param queueDir
     *            the directory to store queue data
     * @param queueName
     *            the name of the queue, will be appended as last part of the
     *            queue directory
     */
    public BigQueue(final String queueDir, final String queueName) {
        this(queueDir, queueName, BigArray.DEFAULT_DATA_PAGE_SIZE);
    }

    /**
     * A big, fast and persistent queue implementation.
     *
     * @param queueDir
     *            the directory to store queue data
     * @param queueName
     *            the name of the queue, will be appended as last part of the
     *            queue directory
     * @param pageSize
     *            the back data file size per page in bytes, see minimum allowed
     *            {@link BigArray#MINIMUM_DATA_PAGE_SIZE}
     */
    public BigQueue(final String queueDir, final String queueName, final int pageSize) {
        innerArray = new BigArray(queueDir, queueName, pageSize);

        // the ttl does not matter here since queue front index page is always
        // cached
        queueFrontIndexPageFactory = new MappedPageFactory(QUEUE_FRONT_INDEX_PAGE_SIZE, innerArray.getArrayDirectory()
                + QUEUE_FRONT_INDEX_PAGE_FOLDER, 10 * 1000/* does not matter */);
        final MappedPage queueFrontIndexPage = queueFrontIndexPageFactory.acquirePage(QUEUE_FRONT_PAGE_INDEX);

        final ByteBuffer queueFrontIndexBuffer = queueFrontIndexPage.getLocal(0);
        final long front = queueFrontIndexBuffer.getLong();
        queueFrontIndex.set(front);
    }

    /**
     * Determines whether a queue is empty
     *
     * @return ture if empty, false otherwise
     */

    public boolean isEmpty() {
        return queueFrontIndex.get() == innerArray.getHeadIndex();
    }

    /**
     * Adds an item at the back of a queue
     *
     * @param data
     *            to be enqueued data
     */

    public void enqueue(final byte[] data) {
        innerArray.append(data);

        this.completeFutures();
    }

    /**
     * Retrieves and removes the front of a queue
     *
     * @return data at the front of a queue
     */

    public byte[] dequeue() {
        long queueFrontIndex = -1L;
        try {
            queueFrontWriteLock.lock();
            if (this.isEmpty()) {
                return null;
            }
            queueFrontIndex = this.queueFrontIndex.get();
            final byte[] data = innerArray.get(queueFrontIndex);
            long nextQueueFrontIndex = queueFrontIndex;
            if (nextQueueFrontIndex == Long.MAX_VALUE) {
                nextQueueFrontIndex = 0L; // wrap
            } else {
                nextQueueFrontIndex++;
            }
            this.queueFrontIndex.set(nextQueueFrontIndex);
            // persist the queue front
            final MappedPage queueFrontIndexPage = queueFrontIndexPageFactory.acquirePage(QUEUE_FRONT_PAGE_INDEX);
            final ByteBuffer queueFrontIndexBuffer = queueFrontIndexPage.getLocal(0);
            queueFrontIndexBuffer.putLong(nextQueueFrontIndex);
            queueFrontIndexPage.setDirty(true);
            return data;
        } finally {
            queueFrontWriteLock.unlock();
        }

    }

    /**
     * Retrieves and removes the fronts of a queue upto given total number /
     * total size whichever is smaller
     *
     * @param max
     *            the maximum to dequque
     * @return data at the fronts of a queue
     */

    public List dequeueMulti(final int max) {
        long queueFrontIndex = -1L;
        final List dataList = new ArrayList();
        try {
            queueFrontWriteLock.lock();
            final long size = size();
            for (int i = 0; i < max && i < size; i++) {
                if (!this.isEmpty()) {
                    queueFrontIndex = this.queueFrontIndex.get();
                    final byte[] data = innerArray.get(queueFrontIndex);
                    dataList.add(data);
                    long nextQueueFrontIndex = queueFrontIndex;
                    if (nextQueueFrontIndex == Long.MAX_VALUE) {
                        nextQueueFrontIndex = 0L; // wrap
                    } else {
                        nextQueueFrontIndex++;
                    }
                    this.queueFrontIndex.set(nextQueueFrontIndex);
                    // persist the queue front
                    final MappedPage queueFrontIndexPage = queueFrontIndexPageFactory
                            .acquirePage(QUEUE_FRONT_PAGE_INDEX);
                    final ByteBuffer queueFrontIndexBuffer = queueFrontIndexPage.getLocal(0);
                    queueFrontIndexBuffer.putLong(nextQueueFrontIndex);
                    queueFrontIndexPage.setDirty(true);
                }
            }
            return dataList;
        } finally {
            queueFrontWriteLock.unlock();
        }

    }

    /**
     * Retrieves a Future which will complete if new Items where enqued.
     *
     * Use this method to retrieve a future where to register as Listener
     * instead of repeatedly polling the queues state. On complete this future
     * contains the result of the dequeue operation. Hence the item was
     * automatically removed from the queue.
     *
     * @return a ListenableFuture which completes with the first entry if items
     *         are ready to be dequeued.
     */

    public ListenableFuture dequeueAsync() {
        this.initializeDequeueFutureIfNecessary();
        return dequeueFuture;
    }

    /**
     * Removes all items of a queue, this will empty the queue and delete all
     * back data files.
     */

    public void removeAll() {
        try {
            queueFrontWriteLock.lock();
            innerArray.removeAll();
            queueFrontIndex.set(0L);
            final MappedPage queueFrontIndexPage = queueFrontIndexPageFactory.acquirePage(QUEUE_FRONT_PAGE_INDEX);
            final ByteBuffer queueFrontIndexBuffer = queueFrontIndexPage.getLocal(0);
            queueFrontIndexBuffer.putLong(0L);
            queueFrontIndexPage.setDirty(true);
        } finally {
            queueFrontWriteLock.unlock();
        }
    }

    /**
     * Retrieves the item at the front of a queue
     *
     * @return data at the front of a queue
     */

    public byte[] peek() {
        if (this.isEmpty()) {
            return null;
        }
        final byte[] data = innerArray.get(queueFrontIndex.get());
        return data;
    }

    /**
     * Retrieves the items at the front of a queue
     *
     * @param max
     *            the maximum elements to peek
     * @return data at the front of a queue
     */

    public List peekMulti(final int max) {
        final List dataList = new ArrayList();
        final long size = size();
        final long queueFront = queueFrontIndex.get();
        byte[] data = null;
        try {
            for (int i = 0; i < max && i < size; i++) {
                data = innerArray.get(queueFront - i);
                if (data == null) {
                    break;
                } else {
                    dataList.add(data);
                }
            }
        } catch (final RuntimeException rex) {
            // do nothing
        }
        return dataList;
    }

    /**
     * Retrieves the item at the front of a queue asynchronously. On complete
     * the value set in this future is the result of the peek operation. Hence
     * the item remains at the front of the list.
     *
     * @return a future containing the first item if available. You may register
     *         as listener at this future to be informed if a new item arrives.
     */

    public ListenableFuture peekAsync() {
        this.initializePeekFutureIfNecessary();
        return peekFuture;
    }

    public void applyForEach(final ItemIterator iterator) {
        try {
            queueFrontWriteLock.lock();
            if (this.isEmpty()) {
                return;
            }

            final long index = queueFrontIndex.get();
            for (long i = index; i < innerArray.size(); i++) {
                iterator.forEach(innerArray.get(i));
            }
        } finally {
            queueFrontWriteLock.unlock();
        }
    }

    @Override
    public void close() throws IOException {
        if (queueFrontIndexPageFactory != null) {
            queueFrontIndexPageFactory.releaseCachedPages();
        }

        synchronized (futureLock) {
            /*
             * Cancel the future but don't interrupt running tasks because they
             * might perform further work not refering to the queue
             */
            if (peekFuture != null) {
                peekFuture.cancel(false);
            }
            if (dequeueFuture != null) {
                dequeueFuture.cancel(false);
            }
        }

        innerArray.close();
    }

    /**
     * Delete all used data files to free disk space.
     *
     * BigQueue will persist enqueued data in disk files, these data files will
     * remain even after the data in them has been dequeued later, so your
     * application is responsible to periodically call this method to delete all
     * used data files and free disk space.
     */

    public void gc() {
        long beforeIndex = queueFrontIndex.get();
        if (beforeIndex == 0L) {
            beforeIndex = Long.MAX_VALUE;
        } else {
            beforeIndex--;
        }
        try {
            innerArray.removeBeforeIndex(beforeIndex);
        } catch (final IndexOutOfBoundsException ex) {
            // ignore
        }
    }

    /**
     * Force to persist current state of the queue,
     *
     * normally, you don't need to flush explicitly since: 1.) BigQueue will
     * automatically flush a cached page when it is replaced out, 2.) BigQueue
     * 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 {
            queueFrontWriteLock.lock();
            queueFrontIndexPageFactory.flush();
            innerArray.flush();
        } finally {
            queueFrontWriteLock.unlock();
        }

    }

    /**
     * Total number of items available in the queue.
     *
     * @return total number
     */

    public long size() {
        final long qFront = queueFrontIndex.get();
        final long qRear = innerArray.getHeadIndex();
        if (qFront <= qRear) {
            return qRear - qFront;
        } else {
            return Long.MAX_VALUE - qFront + 1 + qRear;
        }
    }

    /**
     * Completes the dequeue future
     */
    private void completeFutures() {
        synchronized (futureLock) {
            if (peekFuture != null && !peekFuture.isDone()) {
                peekFuture.set(this.peek());
            }
            if (dequeueFuture != null && !dequeueFuture.isDone()) {
                dequeueFuture.set(this.dequeue());
            }
        }
    }

    /**
     * Initializes the futures if it's null at the moment
     */
    private void initializeDequeueFutureIfNecessary() {
        synchronized (futureLock) {
            if (dequeueFuture == null || dequeueFuture.isDone()) {
                dequeueFuture = SettableFuture.create();
            }
            if (!this.isEmpty()) {
                dequeueFuture.set(this.dequeue());
            }
        }
    }

    /**
     * Initializes the futures if it's null at the moment
     */
    private void initializePeekFutureIfNecessary() {
        synchronized (futureLock) {
            if (peekFuture == null || peekFuture.isDone()) {
                peekFuture = SettableFuture.create();
            }
            if (!this.isEmpty()) {
                peekFuture.set(this.peek());
            }
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy