net.timewalker.ffmq4.storage.data.impl.journal.BlockBasedDataStoreJournal.java Source code

Java tutorial

Introduction

Here is the source code for net.timewalker.ffmq4.storage.data.impl.journal.BlockBasedDataStoreJournal.java

Source

/*
 * This file is part of FFMQ.
 *
 * FFMQ is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * FFMQ is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with FFMQ; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package net.timewalker.ffmq4.storage.data.impl.journal;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import javax.jms.JMSException;

import net.timewalker.ffmq4.storage.StorageSyncMethod;
import net.timewalker.ffmq4.storage.data.DataStoreException;
import net.timewalker.ffmq4.storage.data.impl.AbstractBlockBasedDataStore;
import net.timewalker.ffmq4.utils.async.AsyncTask;
import net.timewalker.ffmq4.utils.async.AsyncTaskManager;
import net.timewalker.ffmq4.utils.concurrent.SynchronizationBarrier;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * BlockBasedDataStoreJournal
 */
public final class BlockBasedDataStoreJournal {
    private static final Log log = LogFactory.getLog(BlockBasedDataStoreJournal.class);

    private static final int METADATA_FORWARD_SCAN_HORIZON = 10;

    // Global
    private String baseName;
    private File dataFolder;
    private AsyncTaskManager asyncTaskManager;

    // Store files
    private RandomAccessFile allocationTableRandomAccessFile;
    private RandomAccessFile dataRandomAccessFile;

    // Dirty blocks table
    private DirtyBlockTable dirtyBlockTable;

    // Settings
    private long maxJournalSize;
    private int maxWriteBatchSize;
    private int maxUnflushedJournalSize;
    private int maxUncommittedStoreSize;
    private int journalOutputBuffer;
    private int storageSyncMethod;
    private boolean preAllocateFiles;

    // Journal files management
    private LinkedList<JournalFile> journalFiles = new LinkedList<>();
    private JournalFile currentJournalFile;
    private int nextJournalFileIndex = 1;
    private long currentTransactionId = 1;
    private LinkedList<File> recycledJournalFiles = new LinkedList<>();

    // Journal write queues
    private JournalQueue journalWriteQueue = new JournalQueue();
    private JournalQueue journalProcessingQueue = new JournalQueue();
    private JournalQueue uncommittedJournalQueue = new JournalQueue();
    private List<SynchronizationBarrier> pendingBarriers = new ArrayList<>();
    private int unflushedJournalSize;
    private int writtenJournalOperations;
    private boolean flushingJournal;

    // Store write queues
    private JournalQueue storeWriteQueue = new JournalQueue();
    private JournalQueue storeProcessingQueue = new JournalQueue();
    private long lastStoreTransactionId;
    private int uncommittedStoreSize;
    private boolean flushingStore;

    // Async targets
    private FlushJournalAsyncTask flushJournalAsyncTask = new FlushJournalAsyncTask();
    private FlushStoreAsyncTask flushStoreAsyncTask = new FlushStoreAsyncTask();

    // Runtime
    private boolean traceEnabled;
    private boolean failing;
    private boolean closing;
    private volatile int totalPendingOperations; // same synchronization scope as journalWriteQueue
    private boolean keepJournalFiles = System.getProperty("ffmq.dataStore.keepJournalFiles", "false")
            .equals("true"); // For testing purposes

    /**
     * Constructor
     */
    public BlockBasedDataStoreJournal(String baseName, File dataFolder, long maxJournalSize, int maxWriteBatchSize,
            int maxUnflushedJournalSize, int maxUncommittedStoreSize, int journalOutputBuffer,
            int storageSyncMethod, boolean preAllocateFiles, RandomAccessFile allocationTableRandomAccessFile,
            RandomAccessFile dataRandomAccessFile, DirtyBlockTable dirtyBlockTable,
            AsyncTaskManager asyncTaskManager) {
        this.baseName = baseName;
        this.dataFolder = dataFolder;
        this.maxJournalSize = maxJournalSize;
        this.maxWriteBatchSize = maxWriteBatchSize;
        this.maxUnflushedJournalSize = maxUnflushedJournalSize;
        this.maxUncommittedStoreSize = maxUncommittedStoreSize;
        this.journalOutputBuffer = journalOutputBuffer;
        this.storageSyncMethod = storageSyncMethod;
        this.preAllocateFiles = preAllocateFiles;
        this.allocationTableRandomAccessFile = allocationTableRandomAccessFile;
        this.dataRandomAccessFile = dataRandomAccessFile;
        this.asyncTaskManager = asyncTaskManager;
        this.dirtyBlockTable = dirtyBlockTable;
        this.traceEnabled = log.isTraceEnabled();
    }

    private JournalFile createNewJournalFile() throws JournalException {
        // Look for a recycled file first
        File recycledFile = null;
        synchronized (recycledJournalFiles) {
            if (recycledJournalFiles.size() > 0)
                recycledFile = recycledJournalFiles.removeFirst();
        }

        JournalFile journalFile;
        if (recycledFile == null) {
            // Create a new one
            journalFile = new JournalFile(nextJournalFileIndex++, baseName, dataFolder, maxJournalSize,
                    journalOutputBuffer, storageSyncMethod, preAllocateFiles);
            log.debug("[" + baseName + "] Created a new journal file : " + journalFile);
        } else {
            // Recycle the old file
            journalFile = new JournalFile(nextJournalFileIndex++, baseName, dataFolder, recycledFile,
                    journalOutputBuffer, storageSyncMethod);
            log.debug("[" + baseName + "] Created a recycled journal file : " + journalFile);
        }

        synchronized (journalFiles) {
            journalFiles.addLast(journalFile);
        }

        return journalFile;
    }

    /**
     * Write a data block (asynchronous)
     */
    public void writeDataBlock(int blockIndex, long blockOffset, byte[] blockData) throws JournalException {
        // Refuse any further operation in case of previous failure
        if (failing)
            throw new JournalException("Store journal is failing");

        synchronized (journalWriteQueue) {
            if (traceEnabled)
                log.trace("[" + baseName + "] #" + currentTransactionId + " Queueing data block write : ["
                        + blockIndex + "] offset=" + blockOffset + " size=" + blockData.length + " / queueSize="
                        + (journalWriteQueue.size() + 1));

            DataBlockWriteOperation op = new DataBlockWriteOperation(currentTransactionId, blockIndex, blockOffset,
                    blockData);
            journalWriteQueue.addLast(op);
            unflushedJournalSize += op.size();
            totalPendingOperations++;
        }
    }

    /**
     * Write some metadata block (asynchronous)
     */
    public void writeMetaDataBlock(long metaDataOffset, byte[] metaData) throws JournalException {
        // Refuse any further operation in case of previous failure
        if (failing)
            throw new JournalException("Store journal is failing");

        synchronized (journalWriteQueue) {
            if (traceEnabled)
                log.trace("[" + baseName + "] #" + currentTransactionId + " Queueing metadata block write : offset="
                        + metaDataOffset + " size=" + metaData.length + " / queueSize="
                        + (journalWriteQueue.size() + 1));

            MetaDataBlockWriteOperation op = new MetaDataBlockWriteOperation(currentTransactionId, metaDataOffset,
                    metaData);
            journalWriteQueue.addLast(op);
            unflushedJournalSize += op.size();
            totalPendingOperations++;
        }
    }

    /**
     * Write some metadata value (asynchronous)
     */
    public void writeMetaData(long metaDataOffset, int metaData) throws JournalException {
        // Refuse any further operation in case of previous failure
        if (failing)
            throw new JournalException("Store journal is failing");

        synchronized (journalWriteQueue) {
            if (traceEnabled)
                log.trace("[" + baseName + "] #" + currentTransactionId + " Queueing metadata write : offset="
                        + metaDataOffset + " metaData=" + metaData + " / queueSize="
                        + (journalWriteQueue.size() + 1));

            MetaDataWriteOperation op = new MetaDataWriteOperation(currentTransactionId, metaDataOffset, metaData);
            unflushedJournalSize += op.size();
            journalWriteQueue.addLast(op);
            totalPendingOperations++;
        }
    }

    /**
     * Write some store extend operation (asynchronous)
     */
    public void extendStore(int blockSize, int oldBlockCount, int newBlockCount) throws JournalException {
        // Refuse any further operation in case of previous failure
        if (failing)
            throw new JournalException("Store journal is failing");

        synchronized (journalWriteQueue) {
            if (traceEnabled)
                log.trace("[" + baseName + "] #" + currentTransactionId + " Queueing store extend : blockSize="
                        + blockSize + " blockCount=" + oldBlockCount + " -> " + newBlockCount + " / queueSize="
                        + (journalWriteQueue.size() + 1));

            StoreExtendOperation op = new StoreExtendOperation(currentTransactionId, blockSize, oldBlockCount,
                    newBlockCount);

            unflushedJournalSize += op.size();
            journalWriteQueue.addLast(op);
            totalPendingOperations++;
        }
    }

    /**
     * Flush the journal write queue (asynchronous)
     */
    public void flush() throws JournalException {
        if (failing)
            throw new JournalException("Store journal is failing");

        boolean newFlushRequired = false;
        synchronized (journalWriteQueue) {
            if (unflushedJournalSize > maxUnflushedJournalSize) {
                newFlushRequired = !flushingJournal;
                if (newFlushRequired)
                    flushingJournal = true;
                unflushedJournalSize = 0;
            }
        }

        // Flush journal to store (asynchronous)
        if (newFlushRequired) {
            try {
                asyncTaskManager.execute(flushJournalAsyncTask);
            } catch (JMSException e) {
                throw new JournalException("Cannot flush journal asynchronously : " + e);
            }
        }
    }

    /**
     * Commit the journal (asynchronous)
     */
    public void commit(SynchronizationBarrier barrier) throws JournalException {
        // Refuse any further operation in case of previous failure
        if (failing)
            throw new JournalException("Store journal is failing");

        // Register as a barrier participant
        if (barrier != null)
            barrier.addParty();

        boolean newFlushRequired;
        synchronized (journalWriteQueue) {
            writeCommit(barrier);

            newFlushRequired = !flushingJournal;
            if (newFlushRequired)
                flushingJournal = true;
            unflushedJournalSize = 0;
        }

        if (newFlushRequired) {
            // Flush journal to store (asynchronous)
            try {
                asyncTaskManager.execute(flushJournalAsyncTask);
            } catch (JMSException e) {
                throw new JournalException("Cannot flush journal asynchronously : " + e);
            }
        }
    }

    private void writeCommit(SynchronizationBarrier barrier) {
        if (traceEnabled)
            log.trace("[" + baseName + "] #" + currentTransactionId
                    + " Queueing transaction commit -------------------");

        journalWriteQueue.addLast(new CommitOperation(currentTransactionId, barrier));
        totalPendingOperations++;

        // Move to next transaction
        currentTransactionId++;
    }

    protected void flushJournal() {
        try {
            while (!failing) {
                synchronized (journalWriteQueue) {
                    if (journalWriteQueue.size() == 0) {
                        flushingJournal = false;
                        break; // Nothing more for now, exit
                    }

                    // Move up to maxWriteBatchSize items to an intermediate queue 
                    int count = 0;
                    while (journalWriteQueue.size() > 0 && count < maxWriteBatchSize) {
                        journalProcessingQueue.addLast(journalWriteQueue.removeFirst());
                        count++;
                    }
                }

                if (traceEnabled)
                    log.trace("[" + baseName + "] [Journal] Flushing " + journalProcessingQueue.size()
                            + " operations");

                // Lazy creation of the journal file
                if (currentJournalFile == null)
                    currentJournalFile = createNewJournalFile();

                // Process intermediate queue
                boolean commitRequired = false;
                int commitCount = 0;
                int prunedOperations = 0;
                while (journalProcessingQueue.size() > 0) {
                    AbstractJournalOperation op = journalProcessingQueue.getFirst();

                    // Tuning : if write is superseeded by another write in the same transaction group, skip it
                    if (op instanceof AbstractMetaDataWriteOperation) {
                        //System.out.println(op.getClass().getSimpleName()+" "+((AbstractJournalWriteOperation)op).getOffset());
                        if (isMetaDataSuperseeded((AbstractMetaDataWriteOperation) op)) {
                            // Drop operation
                            journalProcessingQueue.removeFirst();
                            prunedOperations++;
                            continue;
                        }
                    }

                    // Remove from queue
                    journalProcessingQueue.removeFirst();

                    // If it's a commit operation we got more things to do
                    if (op instanceof CommitOperation) {
                        CommitOperation commitOp = (CommitOperation) op;

                        // Update operations count for this transaction
                        commitOp.setOperationsCount(writtenJournalOperations);
                        writtenJournalOperations = 0;

                        // Append to current journal file
                        op.writeTo(currentJournalFile);

                        commitCount++;

                        // Register the commit barrier for later use
                        if (commitOp.getBarrier() != null)
                            pendingBarriers.add(commitOp.getBarrier());

                        // Transaction boundary : check if we should rotate the journal file
                        if (rotateJournal()) {
                            onJournalCommit();
                            commitRequired = false; // The old journal file was synced on close
                        } else {
                            // Tuning : do not commit if the transaction was empty
                            if (commitOp.getOperationsCount() > 0)
                                commitRequired = true;
                        }
                    } else {
                        // Append to current journal file
                        op.writeTo(currentJournalFile);
                        writtenJournalOperations++;

                        // Retain operations until next commit
                        uncommittedJournalQueue.addLast(op);
                    }
                }

                // Should we commit ?
                if (commitRequired) {
                    if (traceEnabled)
                        log.trace(
                                "[" + baseName + "] [Journal] Syncing (" + pendingBarriers.size() + " barrier(s))");

                    // Sync journal
                    syncJournal();

                    // Post-process
                    onJournalCommit();
                }

                // Reach barriers
                if (pendingBarriers.size() > 0) {
                    for (int i = 0; i < pendingBarriers.size(); i++) {
                        SynchronizationBarrier barrier = pendingBarriers.get(i);
                        barrier.reach();
                    }
                    pendingBarriers.clear();
                }

                // Decrement global operations counter of completed commits
                if (commitCount + prunedOperations > 0) {
                    synchronized (journalWriteQueue) {
                        totalPendingOperations -= commitCount + prunedOperations;
                        if (closing && totalPendingOperations == 0)
                            journalWriteQueue.notifyAll();
                    }
                }
            }
        } catch (DataStoreException e) {
            notifyFailure(e);
        }
    }

    private boolean isMetaDataSuperseeded(AbstractMetaDataWriteOperation baseOp) {
        AbstractJournalOperation current = baseOp.next();
        int count = 0;
        while (current != null && count < METADATA_FORWARD_SCAN_HORIZON) {
            if (current instanceof CommitOperation)
                return false; // Stop at transaction boundary

            if (current instanceof AbstractMetaDataWriteOperation) {
                AbstractMetaDataWriteOperation writeOp = (AbstractMetaDataWriteOperation) current;
                if (writeOp.getOffset() == baseOp.getOffset())
                    return true;
                count++;
            }

            current = current.next();
        }
        return false; // Not found
    }

    private void onJournalCommit() throws JournalException {
        // Move completed journal operations to store write queue
        boolean newFlushRequired;
        synchronized (storeWriteQueue) {
            uncommittedJournalQueue.migrateTo(storeWriteQueue);

            newFlushRequired = storeWriteQueue.size() > 0 && !flushingStore;
            if (newFlushRequired)
                flushingStore = true;
        }

        if (newFlushRequired) {
            // Schedule asynchronous store flush
            try {
                asyncTaskManager.execute(flushStoreAsyncTask);
            } catch (JMSException e) {
                throw new JournalException("Cannot flush store asynchronously : " + e);
            }
        }
    }

    private void notifyFailure(Exception e) {
        failing = true;
        log.fatal("[" + baseName + "] Data store failure", e);
    }

    protected void flushStore() {
        try {
            while (!failing) {
                synchronized (storeWriteQueue) {
                    if (storeWriteQueue.size() == 0) {
                        flushingStore = false;
                        break; // Nothing more for now, exit
                    }

                    // Move up to maxWriteBatchSize items to an intermediate queue 
                    int count = 0;
                    while (storeWriteQueue.size() > 0 && count < maxWriteBatchSize) {
                        storeProcessingQueue.addLast(storeWriteQueue.removeFirst());
                        count++;
                    }
                }

                int flushCount = storeProcessingQueue.size();
                if (traceEnabled)
                    log.trace("[" + baseName + "] [Store] Flushing " + flushCount + " operations");

                // Process intermediate queue
                while (storeProcessingQueue.size() > 0) {
                    AbstractJournalOperation op = storeProcessingQueue.removeFirst();

                    if (op instanceof DataBlockWriteOperation) {
                        DataBlockWriteOperation blockOp = (DataBlockWriteOperation) op;
                        uncommittedStoreSize += blockOp.writeTo(dataRandomAccessFile);

                        // Update dirty block table
                        dirtyBlockTable.blockFlushed(blockOp.getBlockIndex());
                    } else if (op instanceof MetaDataWriteOperation) {
                        uncommittedStoreSize += ((MetaDataWriteOperation) op)
                                .writeTo(allocationTableRandomAccessFile);
                    } else if (op instanceof MetaDataBlockWriteOperation) {
                        uncommittedStoreSize += ((MetaDataBlockWriteOperation) op)
                                .writeTo(allocationTableRandomAccessFile);
                    } else if (op instanceof StoreExtendOperation) {
                        StoreExtendOperation extendOp = (StoreExtendOperation) op;
                        extendOp.extend(allocationTableRandomAccessFile, dataRandomAccessFile);
                        uncommittedStoreSize += (long) (extendOp.getNewBlockCount() - extendOp.getOldBlockCount())
                                * AbstractBlockBasedDataStore.AT_BLOCK_SIZE;
                    } else
                        throw new IllegalArgumentException("Unexpected journal operation : " + op);

                    lastStoreTransactionId = op.getTransactionId();
                }

                // Should we force commit ?
                if (uncommittedStoreSize > maxUncommittedStoreSize) {
                    uncommittedStoreSize = 0;

                    if (traceEnabled)
                        log.trace("[" + baseName + "] [Store] Committing store (lastTrsId=" + lastStoreTransactionId
                                + ")");

                    // Sync the store
                    syncStore();

                    // Recycle unused journal files
                    recycleUnusedJournalFiles();
                }

                // Notify waiting threads that the I/O queue is empty
                synchronized (journalWriteQueue) {
                    totalPendingOperations -= flushCount;
                    if (closing && totalPendingOperations == 0)
                        journalWriteQueue.notifyAll();
                }
            }
        } catch (DataStoreException e) {
            notifyFailure(e);
        }
    }

    private void syncJournal() throws JournalException {
        if (currentJournalFile != null)
            currentJournalFile.sync();
    }

    private boolean rotateJournal() throws JournalException {
        synchronized (journalFiles) {
            // If the journal file is full, mark it as 'complete' and create a new one
            if (currentJournalFile.size() > maxJournalSize) {
                log.debug("[" + baseName + "] Rotating journal : " + currentJournalFile);

                currentJournalFile.complete();
                currentJournalFile = createNewJournalFile();
                return true;
            }
        }

        return false;
    }

    private void syncStore() throws JournalException {
        syncStoreFile(allocationTableRandomAccessFile);
        synchronized (dataRandomAccessFile) {
            syncStoreFile(dataRandomAccessFile);
        }
    }

    private void syncStoreFile(RandomAccessFile storeFile) throws JournalException {
        try {
            switch (storageSyncMethod) {
            case StorageSyncMethod.FD_SYNC:
                storeFile.getFD().sync();
                break;
            case StorageSyncMethod.CHANNEL_FORCE_NO_META:
                storeFile.getChannel().force(false);
                break;
            default:
                throw new JournalException("Unsupported sync method : " + storageSyncMethod);
            }
        } catch (IOException e) {
            log.error("[" + baseName + "] Could not sync store file", e);
            throw new JournalException("Could not sync store file");
        }
    }

    private void recycleUnusedJournalFiles() throws JournalException {
        LinkedList<JournalFile> unusedJournalFiles = null;

        // Look for unused journal files
        synchronized (journalFiles) {
            while (journalFiles.size() > 0) {
                JournalFile journalFile = journalFiles.getFirst();
                if (journalFile.isComplete() && journalFile.getLastTransactionId() < lastStoreTransactionId) {
                    if (unusedJournalFiles == null)
                        unusedJournalFiles = new LinkedList<>();
                    unusedJournalFiles.addLast(journalFile);

                    journalFiles.removeFirst(); // Remove from list
                } else
                    break;
            }
        }

        // Recycle unused journal files
        if (unusedJournalFiles != null) {
            while (!unusedJournalFiles.isEmpty()) {
                JournalFile journalFile = unusedJournalFiles.removeFirst();

                if (keepJournalFiles)
                    journalFile.close();
                else {
                    log.debug("[" + baseName + "] Recycling unused journal file : " + journalFile);
                    File recycledFile = journalFile.closeAndRecycle();
                    synchronized (recycledJournalFiles) {
                        recycledJournalFiles.addLast(recycledFile);
                    }
                }
            }
        }
    }

    public void close() throws JournalException {
        if (failing)
            return;

        log.debug("[" + baseName + "] Waiting for async operations to complete ...");
        closing = true;
        boolean complete = false;
        while (true) {
            synchronized (journalWriteQueue) {
                if (totalPendingOperations == 0) {
                    complete = true;
                    break;
                } else {
                    // Passive wait, someone will wake us up when totalPendingOperations reaches 0
                    try {
                        journalWriteQueue.wait();
                    } catch (InterruptedException e) {
                        log.error("[" + baseName + "] Wait for async operations completion was interrupted", e);
                        break;
                    }
                }
            }
        }
        if (!complete)
            throw new JournalException("Timeout or interrupt waiting for async tasks completion.");

        // Sync everything to disk
        syncJournal();
        syncStore();

        // Destroy journal files
        destroyJournalFiles();
    }

    private void destroyJournalFiles() throws JournalException {
        log.debug("[" + baseName + "] Destroying recycled journal files ...");
        while (recycledJournalFiles.size() > 0) {
            File recycledFile = recycledJournalFiles.removeFirst();
            recycledFile.delete();
        }

        log.debug("[" + baseName + "] Destroying remaining journal files ...");
        while (journalFiles.size() > 0) {
            JournalFile journalFile = journalFiles.removeFirst();

            if (keepJournalFiles)
                journalFile.close();
            else
                journalFile.closeAndDelete();
        }
    }

    //-------------------------------------------------------------------------------------
    //     Stub classes to interface with the disk I/O asynchronous task manager
    //-------------------------------------------------------------------------------------

    private class FlushJournalAsyncTask implements AsyncTask {
        /**
         * Constructor
         */
        public FlushJournalAsyncTask() {
            super();
        }

        /* (non-Javadoc)
         * @see net.timewalker.ffmq4.utils.async.AsyncTask#execute()
         */
        @Override
        public void execute() {
            flushJournal();
        }

        /* (non-Javadoc)
         * @see net.timewalker.ffmq4.utils.async.AsyncTask#isMergeable()
         */
        @Override
        public boolean isMergeable() {
            return false;
        }
    }

    private class FlushStoreAsyncTask implements AsyncTask {
        /**
         * Constructor
         */
        public FlushStoreAsyncTask() {
            super();
        }

        /* (non-Javadoc)
         * @see net.timewalker.ffmq4.utils.async.AsyncTask#execute()
         */
        @Override
        public void execute() {
            flushStore();
        }

        /* (non-Javadoc)
         * @see net.timewalker.ffmq4.utils.async.AsyncTask#isMergeable()
         */
        @Override
        public boolean isMergeable() {
            return false;
        }
    }
}