com.btoddb.fastpersitentqueue.JournalMgr.java Source code

Java tutorial

Introduction

Here is the source code for com.btoddb.fastpersitentqueue.JournalMgr.java

Source

package com.btoddb.fastpersitentqueue;

/*
 * #%L
 * fast-persistent-queue
 * %%
 * Copyright (C) 2014 btoddb.com
 * %%
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * #L%
 */

import com.btoddb.fastpersitentqueue.exceptions.FpqException;
import com.eaio.uuid.UUID;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 *
 */
public class JournalMgr {
    private static final Logger logger = LoggerFactory.getLogger(JournalMgr.class);

    private File directory;
    private long flushPeriodInMs = 10000;
    private int numberOfFlushWorkers = 4;
    private int numberOfGeneralWorkers = 2;
    private long maxJournalFileSize = 100000000L;
    private long maxJournalDurationInMs = (10 * 60 * 1000); // 10 minutes

    private volatile boolean shutdownInProgress;
    private long journalsLoadedAtStartup;
    private AtomicLong journalsCreated = new AtomicLong();
    private AtomicLong journalsRemoved = new AtomicLong();
    private AtomicLong numberOfEntries = new AtomicLong();

    private volatile JournalDescriptor currentJournalDescriptor;

    ReentrantReadWriteLock journalLock = new ReentrantReadWriteLock();
    private TreeMap<UUID, JournalDescriptor> journalIdMap = new TreeMap<UUID, JournalDescriptor>(
            new Comparator<UUID>() {
                @Override
                public int compare(UUID uuid, UUID uuid2) {
                    return uuid.compareTo(uuid2);
                }
            });

    private ScheduledThreadPoolExecutor flushExec;
    private ExecutorService generalExec;

    /**
     *
     * @throws IOException
     */
    public void init() throws IOException {
        flushExec = new ScheduledThreadPoolExecutor(numberOfFlushWorkers, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable runnable) {
                Thread t = new Thread(runnable);
                t.setName("FPQ-FSync");
                return t;
            }
        });
        generalExec = Executors.newFixedThreadPool(numberOfGeneralWorkers, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable runnable) {
                Thread t = new Thread(runnable);
                t.setName("FPQ-GeneralWork");
                return t;
            }
        });

        prepareJournaling();

        currentJournalDescriptor = createAndAddNewJournal();
    }

    private void prepareJournaling() throws IOException {
        FileUtils.forceMkdir(directory);
        Collection<File> files = FileUtils.listFiles(directory, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE);
        if (null == files || 0 == files.size()) {
            logger.info("no previous journal files found");
            return;
        }

        logger.info("loading journal descriptors");
        numberOfEntries.set(0);
        for (File f : files) {
            JournalFile jf = new JournalFile(f);
            jf.initForReading();
            jf.close();

            JournalDescriptor jd = new JournalDescriptor(jf);
            jd.setWritingFinished(true);
            jd.adjustEntryCount(jf.getNumberOfEntries());
            journalIdMap.put(jd.getId(), jd);
            numberOfEntries.addAndGet(jf.getNumberOfEntries());
            logger.info("loaded descriptor, {}, with {} entries", jd.getId(), jd.getNumberOfUnconsumedEntries());
            journalsLoadedAtStartup++;
        }

        logger.info("completed journal descriptor loading.  found a total of {} entries", numberOfEntries.get());
    }

    public JournalReplayIterable createReplayIterable() throws IOException {
        return new JournalReplayIterable();
    }

    private JournalDescriptor createAndAddNewJournal() throws IOException {
        UUID uid = new UUID();
        String fn = createNewJournalName(uid.toString());
        return addNewJournalToIdMap(uid, fn);
    }

    private JournalDescriptor addNewJournalToIdMap(UUID id, String fn) throws IOException {
        JournalFile jf = new JournalFile(new File(directory, fn));
        jf.initForWriting(id);

        // this could possibly create a race, if the flushPeriodInMs is very very low ;)
        ScheduledFuture future = flushExec.scheduleWithFixedDelay(new FlushRunner(jf), flushPeriodInMs,
                flushPeriodInMs, TimeUnit.MILLISECONDS);
        JournalDescriptor jd = new JournalDescriptor(id, jf, future);
        journalLock.writeLock().lock();
        try {
            journalIdMap.put(id, jd);
        } finally {
            journalLock.writeLock().unlock();
        }

        journalsCreated.incrementAndGet();
        logger.debug("new journal created : {}", jd.getId());
        return jd;
    }

    public FpqEntry append(FpqEntry entry) throws IOException {
        append(Collections.singleton(entry));
        return entry;
    }

    public Collection<FpqEntry> append(Collection<FpqEntry> events) throws IOException {
        if (shutdownInProgress) {
            throw new FpqException("FPQ has been shutdown or is in progress, cannot append");
        }

        // TODO:BTB - optimize this by calling file.append() with collection of data
        List<FpqEntry> entryList = new ArrayList<FpqEntry>(events.size());
        for (FpqEntry entry : events) {
            // TODO:BTB - could use multiple journal files to prevent lock contention - each thread in an "append" pool gets one?
            boolean appended = false;
            while (!appended) {
                JournalDescriptor desc = getCurrentJournalDescriptor();
                if (null == desc) {
                    Utils.logAndThrow(logger,
                            String.format("no current journal descriptor.  did you call %s.init()?",
                                    this.getClass().getSimpleName()));
                }
                synchronized (desc) {
                    if (!desc.isWritingFinished()) {
                        desc.getFile().append(entry);
                        entry.setJournalId(desc.getId());
                        entryList.add(entry);

                        if (0 >= desc.getStartTime()) {
                            desc.setStartTime(System.currentTimeMillis());
                        }
                        desc.adjustEntryCount(1);

                        rollJournalIfNeeded();
                        appended = true;
                    }
                }
            }
        }

        numberOfEntries.addAndGet(entryList.size());

        return entryList;
    }

    // this method must be synchronized from above
    private void rollJournalIfNeeded() throws IOException {
        if (!currentJournalDescriptor.isAnyWritesHappened()) {
            return;
        }

        long fileLength = currentJournalDescriptor.getFile().getFilePosition();
        if (fileLength >= maxJournalFileSize
                || (0 < fileLength && (currentJournalDescriptor.getStartTime() + maxJournalDurationInMs) < System
                        .currentTimeMillis())) {
            currentJournalDescriptor.setWritingFinished(true);
            currentJournalDescriptor.getFuture().cancel(false);
            currentJournalDescriptor.getFile().close();
            currentJournalDescriptor = createAndAddNewJournal();
        }

    }

    public void reportTake(FpqEntry entry) throws IOException {
        reportTake(Collections.singleton(entry));
    }

    public void reportTake(Collection<FpqEntry> entries) throws IOException {
        if (shutdownInProgress) {
            throw new FpqException("FPQ has been shutdown or is in progress, cannot report TAKE");
        }

        if (null == entries || entries.isEmpty()) {
            logger.debug("provided null or empty collection - ignoring");
            return;
        }

        Map<UUID, Integer> journalIds = new HashMap<UUID, Integer>();
        for (FpqEntry entry : entries) {
            Integer count = journalIds.get(entry.getJournalId());
            if (null == count) {
                count = 0;
            }
            journalIds.put(entry.getJournalId(), count + 1);
        }

        for (Map.Entry<UUID, Integer> entry : journalIds.entrySet()) {
            journalLock.readLock().lock();
            JournalDescriptor desc;
            try {
                desc = journalIdMap.get(entry.getKey());
            } finally {
                journalLock.readLock().unlock();
            }

            if (null == desc) {
                logger.error(
                        "illegal state - reported consumption of journal entry, but journal descriptor, {}, doesn't exist!",
                        entry.getKey());
                continue;
            }

            long remaining = desc.adjustEntryCount(-entry.getValue());

            // this is therad-safe, because only one thread can decrement down to zero when isWritingFinished
            if (0 == remaining && desc.isWritingFinished()) {
                submitJournalRemoval(desc);
            }
        }
    }

    private void submitJournalRemoval(final JournalDescriptor desc) {
        logger.debug("submitting journal, {}, for removal", desc.getId());
        generalExec.submit(new Runnable() {
            @Override
            public void run() {
                removeJournal(desc);
            }
        });
    }

    private void removeJournal(JournalDescriptor desc) {
        journalLock.writeLock().lock();
        try {
            journalIdMap.remove(desc.getId());
            try {
                FileUtils.forceDelete(desc.getFile().getFile());
            } catch (IOException e) {
                logger.error("could not delete journal file, {} - will not try again",
                        desc.getFile().getFile().getAbsolutePath());
            }
        } finally {
            journalLock.writeLock().unlock();
        }

        numberOfEntries.addAndGet(-desc.getFile().getNumberOfEntries());
        journalsRemoved.incrementAndGet();
        logger.debug("journal, {}, removed", desc.getId());
    }

    public void shutdown() {
        shutdownInProgress = true;
        if (null != flushExec) {
            flushExec.shutdown();
            try {
                flushExec.awaitTermination(60, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Thread.interrupted();
                // ignore
            }
        }
        if (null != generalExec) {
            generalExec.shutdown();
            try {
                generalExec.awaitTermination(60, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Thread.interrupted();
                // ignore
            }
        }

        // for journals that are completely popped, but not removed (because a client still could have pushed)
        Set<JournalDescriptor> removeThese = new HashSet<>();
        try {
            for (JournalDescriptor desc : journalIdMap.values()) {
                if (0 == desc.getNumberOfUnconsumedEntries()) {
                    removeThese.add(desc);
                } else if (desc.getFile().isOpen()) {
                    try {
                        desc.getFile().forceFlush();
                    } catch (IOException e) {
                        logger.error("on shutdown - could not fsync journal file, {} -- ignoring",
                                desc.getFile().getFile().getAbsolutePath());
                    }
                }
            }
        } finally {
            for (JournalDescriptor desc : removeThese) {
                removeJournal(desc);
            }
        }
    }

    private String createNewJournalName(String id) {
        return "journal-" + id;
    }

    public JournalDescriptor getCurrentJournalDescriptor() {
        return currentJournalDescriptor;
    }

    public long getFlushPeriodInMs() {
        return flushPeriodInMs;
    }

    public void setFlushPeriodInMs(long flushPeriodInMs) {
        this.flushPeriodInMs = flushPeriodInMs;
    }

    public File getDirectory() {
        return directory;
    }

    public void setDirectory(File directory) {
        this.directory = directory;
    }

    public Map<UUID, JournalDescriptor> getJournalFiles() {
        return journalIdMap;
    }

    public int getNumberOfFlushWorkers() {
        return numberOfFlushWorkers;
    }

    public void setNumberOfFlushWorkers(int numberOfFlushWorkers) {
        this.numberOfFlushWorkers = numberOfFlushWorkers;
    }

    public long getMaxJournalFileSize() {
        return maxJournalFileSize;
    }

    public void setMaxJournalFileSize(long maxJournalFileSize) {
        this.maxJournalFileSize = maxJournalFileSize;
    }

    public long getMaxJournalDurationInMs() {
        return maxJournalDurationInMs;
    }

    public void setMaxJournalDurationInMs(long journalMaxDurationInMs) {
        this.maxJournalDurationInMs = journalMaxDurationInMs;
    }

    public long getJournalsCreated() {
        return journalsCreated.get();
    }

    public long getJournalsRemoved() {
        return journalsRemoved.get();
    }

    public TreeMap<UUID, JournalDescriptor> getJournalIdMap() {
        return journalIdMap;
    }

    public long getNumberOfEntries() {
        return numberOfEntries.get();
    }

    public long getJournalsLoadedAtStartup() {
        return journalsLoadedAtStartup;
    }

    // ------------

    public class JournalReplayIterable implements Iterator<FpqEntry>, Iterable<FpqEntry> {
        private Iterator<JournalDescriptor> jdIter = journalIdMap.values().iterator();
        private Iterator<FpqEntry> entryIter = null;
        private JournalDescriptor jd = null;

        public JournalReplayIterable() throws IOException {
            advanceToNextJournalFile();
        }

        @Override
        public Iterator<FpqEntry> iterator() {
            return this;
        }

        @Override
        public boolean hasNext() {
            // if no descriptor, then we are done
            if (null == entryIter) {
                return false;
            }

            if (!entryIter.hasNext()) {
                try {
                    jd.getFile().close();
                    advanceToNextJournalFile();
                } catch (IOException e) {
                    logger.error("exception while closing journal file", e);
                }
            }

            return null != entryIter && entryIter.hasNext();
        }

        @Override
        public FpqEntry next() {
            if (hasNext()) {
                return entryIter.next();
            } else {
                throw new NoSuchElementException();
            }
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException(JournalMgr.class.getName() + " does not support remove");
        }

        private void advanceToNextJournalFile() throws IOException {
            while (jdIter.hasNext()) {
                jd = jdIter.next();
                if (jd.isWritingFinished()) {
                    entryIter = jd.getFile().iterator();
                    return;
                } else {
                    logger.debug("trying to replay a journal that is not 'write finished' : " + jd.getId());
                }
            }
            jd = null;
            entryIter = null;
        }

        public void close() throws IOException {
            if (null != jd) {
                jd.getFile().close();
            }
        }
    }
}