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

com.crankuptheamps.client.LoggedBookmarkStore Maven / Gradle / Ivy

///////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2010-2022 60East Technologies Inc., All Rights Reserved.
//
// This computer software is owned by 60East Technologies Inc. and is
// protected by U.S. copyright laws and other laws and by international
// treaties.  This computer software is furnished by 60East Technologies
// Inc. pursuant to a written license agreement and may be used, copied,
// transmitted, and stored only in accordance with the terms of such
// license agreement and with the inclusion of the above copyright notice.
// This computer software or any other copies thereof may not be provided
// or otherwise made available to any other person.
//
// U.S. Government Restricted Rights.  This computer software: (a) was
// developed at private expense and is in all respects the proprietary
// information of 60East Technologies Inc.; (b) was not developed with
// government funds; (c) is a trade secret of 60East Technologies Inc.
// for all purposes of the Freedom of Information Act; and (d) is a
// commercial item and thus, pursuant to Section 12.212 of the Federal
// Acquisition Regulations (FAR) and DFAR Supplement Section 227.7202,
// Government's use, duplication or disclosure of the computer software
// is subject to the restrictions set forth by 60East Technologies Inc..
//
////////////////////////////////////////////////////////////////////////////

package com.crankuptheamps.client;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.math.BigInteger;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.lang.Long;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.crankuptheamps.client.fields.BookmarkField;
import com.crankuptheamps.client.fields.BookmarkRangeField;
import com.crankuptheamps.client.fields.Field;
import com.crankuptheamps.client.exception.*;
/** LoggedBookmarkStore implements a sequentially written log
 * of incoming and discarded messages. This store tracks every
 * bookmark processed in a file. An application should periodically call
 * {@link #prune} to manage the size of the file by removing
 * outdated entries.
 */
public class LoggedBookmarkStore implements BookmarkStore
{
    // Each entry begins with a single byte indicating the type of entry:
    // a new bookmark, or a discard of a previous one.
    static final byte ENTRY_BOOKMARK  = (byte)'b';
    static final byte ENTRY_DISCARD   = (byte)'d';
    static final byte ENTRY_PERSISTED = (byte)'p';

    /**
     * The Subscription object is used to represent internal bookmark state
     * for the messages received and discarded on a specific subscription
     * within the bookmark store.
     */
    protected static class Subscription implements com.crankuptheamps.client.Subscription
    {
        // This subscription's ID.
        Field _sub;

        // The last persisted bookmark
        BookmarkField _lastPersisted;

        // If the subscription used a range, save it here
        BookmarkRangeField _range = new BookmarkRangeField();

        /**
         * The last-modified timestamp of the backing bookmark log file, just
         * before recovery is initiated. When this is not null we intend to
         * include it in the list of bookmarks returned by getMostRecentList()
         * until a message is discarded on the sub or the sub state is purged.
         *
         * Originally we thought the recovery timestamp could be stored
         * as the last-persisted for each recovered sub, but it must be
         * stored separately here because setting it as a sub's
         * last-persisted could cause us to lose a persisted ack that
         * is earlier than the backing file's last-modified time.
         */
        protected volatile String _recoveryTimestamp = null;

        static final Field EPOCH_FIELD = new Field(Client.Bookmarks.EPOCH);

        // A set of all of the entries recovered from the bookmark
        // log file after the file's most-recent -- i.e., whatever entries
        // are left in the bookmark ring buffer after recover().
        HashMap _recovered = new HashMap();

        BookmarkRingBuffer _ring = new BookmarkRingBuffer();

        // The per-subscription memory of what we've seen from publishers
        HashMap _publishers = new HashMap();

        // The store that we log to when the time comes.
        LoggedBookmarkStore _parent;

        // The Subscription lock
        final Lock       _lock           = new ReentrantLock();

        // The encoder/decoder for the Subscription
        final CharsetEncoder _encoder = StandardCharsets.UTF_8.newEncoder();
        final CharsetDecoder _decoder = StandardCharsets.UTF_8.newDecoder();

        public Subscription()
        {
            // Default starting point for last persisted is EPOCH
            _lastPersisted = new BookmarkField();
            _lastPersisted.copyFrom(EPOCH_FIELD);
        }

        /**
         * Reset the state of this subscription object such that it can be
         * returned to the pool for reuse.
         */
        public void reset() {
            _sub.reset();
            _lastPersisted.copyFrom(EPOCH_FIELD);
            _recoveryTimestamp = null;
            _recovered.clear();
            _ring.reset();
            _publishers.clear();
            _range.reset();
        }

        public void init(Field subscriptionId, LoggedBookmarkStore parent)
        {
            _sub = subscriptionId.copy();
            _ring.setSubId(_sub);
            _parent = parent;
        }

        public String getRecoveryTimestamp()
        {
            return _recoveryTimestamp;
        }

        protected final void setRecoveryTimestamp(String rts)
        {
            _recoveryTimestamp = rts;
        }

        public long log(BookmarkField bookmark) throws IOException, CommandException
        {
            _lock.lock();
            try {
                if (!bookmark.isRange()) {
                    // Check to see if this is a recovered bookmark.
                    final Long recoveredValue = _recovered.remove(bookmark);
                    if (recoveredValue != null)
                    {
                        return recoveredValue;
                    }

                    // Add this entry onto our list to remember in order.
                    if (!bookmark.isBookmarkList()) {
                        return _log(bookmark);
                    }
                    else {
                        // If we're logging a list, we need to mark all items
                        // in the list as discarded.
                        long seq = 0;
                        for (BookmarkField bm : bookmark.parseBookmarkList()) {
                            isDiscarded(bm);
                            seq = _log(bm);
                            if (seq != 0) {
                                _ring.discard(seq);
                                _parent._recentChanged = true;
                            }
                        }
                        if (_parent._adapter != null) {
                            _parent.adapterUpdate(_sub, bookmark);
                        }
                        return 0;
                    }
                }
                else {
                    _range.copyFrom(bookmark);
                    if (!_range.isValid()) {
                        throw new CommandException("Invalid bookmark range specified");
                    }
                    long seq = 0;
                    if (_range.isStartExclusive()) {
                        // Parse the start of the rannge and log/discard each
                        BookmarkField start = _range.getStart();
                        if (!start.isBookmarkList()) {
                            isDiscarded(start);
                            seq = _log(start);
                            if (seq != 0) {
                                _ring.discard(seq);
                                _parent._recentChanged = true;
                            }
                        }
                        else {
                            for (BookmarkField bm : start.parseBookmarkList()) {
                                isDiscarded(bm);
                                seq = _log(bm);
                                if (seq != 0) {
                                    _ring.discard(seq);
                                    _parent._recentChanged = true;
                                }
                            }
                            seq = 0;
                        }
                    }
                    if (_parent._adapter != null) {
                        _parent.adapterUpdate(_sub, _range);
                    }
                    return seq;
                }
            }
            finally {
                _lock.unlock();
            }
        }

        private long _log(BookmarkField bm) throws IOException
        {
            long seq = 0;
            if (!bm.isTimestamp()) {
                seq = _ring.log(bm);
                while (seq == 0) {
                    _lock.unlock();
                    try {
                        _ring.checkResize();
                    }
                    finally {
                        _lock.lock();
                    }
                    seq = _ring.log(bm);
                }
            }
            else {
                setLastPersisted(bm);
            }
            return seq;
        }

        public void discard(long index) throws IOException
        {
            _lock.lock();
            try {
                BookmarkRingBuffer.Entry entry = _ring.getByIndex(index);

                if(entry == null || !entry.isActive())
                {
                    return;
                }

                if (!_parent._recovering) {
                    _parent.write(_sub, LoggedBookmarkStore.ENTRY_DISCARD,
                                  entry.getBookmark());
                }
                if (_ring.discard(index))
                {
                    _parent._recentChanged = true;
                    _recoveryTimestamp = null;
                    if (_parent._adapter != null) {
                        _parent.adapterUpdate(_sub,
                                    (BookmarkField)getMostRecentList(false));
                    }
                }
            }
            finally {
                _lock.unlock();
            }
        }

        /**
         * Check to see if this message is older than the most recent one seen,
         * and if it is, then check to see if it is discarded.
         */
        public boolean isDiscarded(BookmarkField bookmark) throws IOException
        {
            _lock.lock();
            try {
                if (bookmark.isRange() || bookmark.isBookmarkList())
                    return false;
                Long recoveredIndex = _recovered.get(bookmark);
                long publisher = bookmark.getPublisherId();
                long sequence = bookmark.getSequenceNumber();
                if(!_publishers.containsKey(publisher) ||
                    unsignedLongLess(_publishers.get(publisher), sequence))
                {
                    _publishers.put(publisher, sequence);
                    if (recoveredIndex == null) {
                        return false;
                    }
                }

                if (recoveredIndex != null)
                {
                    long recoveredVal = recoveredIndex;
                    BookmarkRingBuffer.Entry entry = _ring.getByIndex(recoveredVal);
                    if (entry == null && recoveredVal < _ring.getStartIndex())
                    {
                        // If the ring buffer no longer has an entry for this index
                        // and the index is before the ring's start index, then
                        // this is a stale recovered map entry whose bookmark has
                        // already been discarded. Don't let it get logged again.
                        // Remove the stale mapping and return true (it's discarded).
                        _recovered.remove(bookmark);
                        return true;
                    }
                    // Need current active state
                    return (entry == null) ? false : !entry.isActive();
                }

                return !_parent._recovering;
            }
            finally {
                _lock.unlock();
            }
        }

        public Field getLastPersisted() {
            _lock.lock();
            try { return _lastPersisted; }
            finally {
                _lock.unlock();
            }
        }

        public BookmarkRangeField getRange() {
            _lock.lock();
            try { return _range; }
            finally {
                _lock.unlock();
            }
        }

        public Field getMostRecent()
        {
            _lock.lock();
            try {
                return getMostRecent(false);
            }
            finally {
                _lock.unlock();
            }
        }

        protected Field getMostRecent(boolean updateRecovery_)
        {
            _lock.lock();
            try {
                // when this is called, we'll take a moment to update the list of things
                // recovered, so we don't accidentally log anything we ought not to.
                if (updateRecovery_ && _parent._recentChanged) updateRecovery();
                return _ring.getLastDiscarded();
            }
            finally {
                _lock.unlock();
            }
        }

        /** publisherId_ and sequence are signed, but the actual id's are
         * unsigned, so we have to do some work to create a valid string.
         */
        private static String convertUnsignedLongToString(long publisherId_)
        {
            final BigInteger offset = BigInteger.valueOf(Long.MAX_VALUE)
                    .shiftLeft(1).add(BigInteger.valueOf(2));
            if(publisherId_<0)
            {
                return offset.add(BigInteger.valueOf(publisherId_)).toString();
            }
            else
            {
                return Long.toString(publisherId_);
            }
        }

        /** publisherId_ and sequence are signed, but the actual id's are
         * unsigned, so we have to do some work to compare them.
         */
        private static boolean unsignedLongLess(long seqLeft_, long seqRight_)
        {
            final BigInteger offset = BigInteger.valueOf(Long.MAX_VALUE)
                    .shiftLeft(1).add(BigInteger.valueOf(2));
            if(seqLeft_<0 || seqRight_<0)
            {
                BigInteger left = offset.add(BigInteger.valueOf(seqLeft_));
                BigInteger right = offset.add(BigInteger.valueOf(seqRight_));
                return left.compareTo(right) < 0;
            }
            else
            {
                return seqLeft_ < seqRight_;
            }
        }

        private static boolean unsignedLongLessEqual(long seqLeft_, long seqRight_)
        {
            final BigInteger offset = BigInteger.valueOf(Long.MAX_VALUE)
                    .shiftLeft(1).add(BigInteger.valueOf(2));
            if(seqLeft_<0 || seqRight_<0)
            {
                BigInteger left = offset.add(BigInteger.valueOf(seqLeft_));
                BigInteger right = offset.add(BigInteger.valueOf(seqRight_));
                return left.compareTo(right) <= 0;
            }
            else
            {
                return seqLeft_ <= seqRight_;
            }
        }

        public Field getMostRecentList(boolean useList)
        {
            _lock.lock();
            try {
                boolean rangeIsValid = _range.isValid();
                BookmarkField lastDiscarded = (BookmarkField)_ring.getLastDiscarded();
                boolean useLastDiscarded = (lastDiscarded != null &&
                                            !lastDiscarded.isNull());
                long lastDiscardedPub = 0;
                long lastDiscardedSeq = 0;
                boolean useLastPersisted = (_lastPersisted != null &&
                                            _lastPersisted.length > 1);
                long lastPersistedPub = 0;
                long lastPersistedSeq = 0;
                if (useLastPersisted)
                {
                    lastPersistedPub = _lastPersisted.getPublisherId();
                    lastPersistedSeq = _lastPersisted.getSequenceNumber();
                }
                if (useLastDiscarded)
                {
                    if (useLastPersisted && _ring.isEmpty()
                        && (!rangeIsValid || _range.getEnd().equals(_lastPersisted)))
                    {
                        useLastDiscarded = false;
                    }
                    else
                    {
                        lastDiscardedPub = lastDiscarded.getPublisherId();
                        lastDiscardedSeq = lastDiscarded.getSequenceNumber();
                        // Only use one if they are same publisher
                        if (useLastPersisted && (lastDiscardedPub == lastPersistedPub))
                        {
                            useLastDiscarded = (lastDiscardedSeq < lastPersistedSeq);
                            useLastPersisted = !useLastDiscarded;
                        }
                    }
                }

                StringBuilder recentStr = new StringBuilder();
                BookmarkField recentList = new BookmarkField();
                if (_recoveryTimestamp != null)
                {
                    recentStr.append(_recoveryTimestamp);
                }
                if (useLastDiscarded)
                {
                    if (recentStr.length() > 0) recentStr.append(',');
                    recentStr.append((lastDiscarded).getValue(_decoder));
                }
                // If we don't have a last persisted or a last discarded OR we are
                // expecting persisted acks but haven't received one yet, then we
                // should try to build a list of bookmarks based on publishers we
                // have seen so far, if any, or return EPOCH.
                if (useList &&
                    ((!useLastPersisted && !useLastDiscarded)
                    || (_lastPersisted != null &&
                        _lastPersisted.equals(EPOCH_FIELD))
                    ))
                {
                    if (_publishers.isEmpty() && !rangeIsValid)
                    {
                        // Set last persisted to EPOCH and return it
                        if (_lastPersisted == null) {
                            _lastPersisted = new BookmarkField();
                            _lastPersisted.copyFrom(EPOCH_FIELD);
                        }
                        return _lastPersisted;
                    }
                    // If an EPOCH lastDiscarded value was added, remove it.
                    if (useLastDiscarded && lastDiscarded.equals(EPOCH_FIELD))
                    {
                        int len = recentStr.length();
                        if (len == 1) // Only contains EPOCH
                            recentStr.setLength(0);
                        else if (len > 2) // Ends with ",0" (,EPOCH)
                            recentStr.setLength(len - 2);
                    }
                    Iterator it = _publishers.entrySet().iterator();
                    while (it.hasNext())
                    {
                        Map.Entry pairs = (Map.Entry)it.next();
                        long pubId = (Long)pairs.getKey();
                        if (pubId == 0) continue;
                        if (useLastDiscarded && pubId == lastDiscardedPub) continue;
                        long seq = (Long)pairs.getValue();
                        if (recentStr.length() > 0) recentStr.append(',');
                        recentStr.append(convertUnsignedLongToString(pubId))
                                 .append(BookmarkField.SEPARATOR_CHAR)
                                 .append(convertUnsignedLongToString(seq))
                                 .append(BookmarkField.SEPARATOR_CHAR);
                    }
                    recentList.setValue(recentStr.toString(), _encoder);
                    if (rangeIsValid) {
                        if (recentList.length > 1
                            && (_range.isStartInclusive()
                               || !recentList.equals(_range.getStart()))) {
                            _range.replaceStart(recentList, true);
                        }
                        return _range;
                    }
                    return recentList;
                }
                if (useLastPersisted)
                {
                    if (recentStr.length() > 0) recentStr.append(',');
                    recentStr.append(_lastPersisted.getValue(_decoder));
                }
                recentList.setValue(recentStr.toString(), _encoder);
                if (rangeIsValid) {
                    if (recentList.length > 1
                        && (_range.isStartInclusive()
                           || !recentList.equals(_range.getStart()))) {
                        _range.replaceStart(recentList, true);
                    }
                    return _range;
                }
                return recentList;
            }
            finally {
                _lock.unlock();
            }
        }

        private void updateRecovery()
        {
            _recovered.clear();
            long end = _ring.getEndIndex();
            for(long index = _ring.getStartIndex(); index< end; ++index)
            {
                BookmarkRingBuffer.Entry entry = _ring.getByIndex(index);
                if(entry != null && entry._bookmark != null && !entry._bookmark.isNull())
                {
                    _recovered.put(entry.getBookmark(), index);
                }
            }
        }

        private void getActiveEntries(ArrayList entryList_)
        {
            _ring.getRecoveryEntries(entryList_);
        }

        /**
         * Old style of setting a persisted bookmark no longer used.
         * @deprecated use {@link #setLastPersisted(BookmarkField)} instead.
         * @param bookmark Used to be the bookmark sequence number.
         */
        @Deprecated
        public void setLastPersisted(long bookmark) throws IOException
        {
            _lock.lock();
            try {
                BookmarkRingBuffer.Entry entry = _ring.getByIndex(bookmark);
                if (entry == null) return;
                BookmarkField bookmarkField = entry.getBookmark();
                if (bookmarkField == null || bookmarkField.isNull()) return;
                long publisherId = bookmarkField.getPublisherId();
                boolean lastPersistedNull = (_lastPersisted == null);
                if (!lastPersistedNull &&
                    publisherId == _lastPersisted.getPublisherId() &&
                    unsignedLongLessEqual(bookmarkField.getSequenceNumber(),
                                          _lastPersisted.getSequenceNumber()))
                {
                    return;
                }
                if (!lastPersistedNull) _lastPersisted.reset();
                _lastPersisted = bookmarkField.copy();
                if (!_parent._recovering) {
                    _parent.write(_sub, ENTRY_PERSISTED, bookmarkField);
                }
                if (lastPersistedNull
                    || publisherId == _ring.getLastDiscarded().getPublisherId())
                {
                    _parent._recentChanged = true;
                    _recoveryTimestamp = null;
                    if (!_parent._recovering && _parent._adapter != null) {
                        _parent.adapterUpdate(_sub,
                                    (BookmarkField)getMostRecentList(false));
                    }
                }
            }
            finally {
                _lock.unlock();
            }
        }

        public void setLastPersisted(BookmarkField bookmark) throws IOException
        {
            _lock.lock();
            try {
                if (bookmark == null || bookmark.isNull()
                    || bookmark.equals(_lastPersisted)
                    || bookmark.isRange())
                    return;

                if (bookmark.isTimestamp())
                {
                    if (_lastPersisted != null) _lastPersisted.reset();
                    _lastPersisted = bookmark.copy();
                    _parent.write(_sub, ENTRY_PERSISTED, bookmark);
                    _parent._recentChanged = true;;
                    if (_parent._adapter != null) {
                        _parent.adapterUpdate(_sub,
                                    (BookmarkField)getMostRecentList(false));
                    }
                    return;
                }

                long publisherId = bookmark.getPublisherId();
                boolean lastPersistedNull = (_lastPersisted == null);
                if (!lastPersistedNull &&
                    publisherId == _lastPersisted.getPublisherId() &&
                    unsignedLongLessEqual(bookmark.getSequenceNumber(),
                                          _lastPersisted.getSequenceNumber()))
                {
                    return;
                }
                if (!lastPersistedNull) _lastPersisted.reset();
                _lastPersisted = bookmark.copy();
                if (!_parent._recovering) {
                    _parent.write(_sub, ENTRY_PERSISTED, bookmark);
                }
                if (lastPersistedNull
                    || _ring.isEmpty()
                    || publisherId == _ring.getLastDiscarded().getPublisherId())
                {
                    _parent._recentChanged = true;
                    _recoveryTimestamp = null;
                    if (!_parent._recovering && _parent._adapter != null) {
                        _parent.adapterUpdate(_sub,
                                    (BookmarkField)getMostRecentList(false));
                    }
                }
            }
            finally {
                _lock.unlock();
            }
        }

        public long getOldestBookmarkSeq()
        {
            _lock.lock();
            try {
                return _ring.getStartIndex();
            }
            finally {
                _lock.unlock();
            }
        }

        /**
         * Call on a Subscription object just after recovery is performed to
         * convert logged entries into recovery entries and set the publishers
         * cache state to the earliest sequence seen for each publisher minus
         * one.
         *
         * NOTE: If after recovery lastDiscarded for this sub is null (i.e.
         * nothing was discarded) and lastPersisted is null or EPOCH (i.e.
         * no persisted ack was recorded in the log), then we can throw away
         * this subscription state and return this Subscription object to the
         * pool.
         *
         * @return Indicates whether this subscription's state was reset
         *      (because it's undeeded due to the lack of any discards and
         *      persisted acks) and this object should be returned to the
         *      Subscription pool.
         */
        public boolean justRecovered()
        {
            _lock.lock();
            try {
                BookmarkField ld = _ring.getLastDiscarded();
                if ((ld == null || ld.isNull() || ld.equals(EPOCH_FIELD))
                        && (_lastPersisted == null || _lastPersisted.isNull()
                                || _lastPersisted.equals(EPOCH_FIELD))
                        && !_range.isValid())
                {
                    // Reset this sub for reuse.
                    reset();
                    return true;
                }

                updateRecovery();
                ArrayList active = new ArrayList();
                getActiveEntries(active);
                setPublishersToDiscarded(active, _publishers);
            }
            finally {
                _lock.unlock();
            }

            return false;
        }

        public static void setPublishersToDiscarded(
                                    List active,
                                    Map publishers)
        {
            if (active == null || publishers == null || publishers.isEmpty()) return;
            Iterator it = active.iterator();
            while (it.hasNext()) {
                BookmarkRingBuffer.Entry entry = it.next();
                if (!entry.isActive()) continue;
                BookmarkField bf = entry.getBookmark();
                long seq = bf.getSequenceNumber();
                if (seq == 0) continue;
                long publisher = bf.getPublisherId();
                Long pubSeq = publishers.get(publisher);
                if (pubSeq != null && unsignedLongLessEqual(seq, pubSeq)) {
                    publishers.put(publisher, seq - 1);
                }
            }
        }

        public void setResizeHandler(BookmarkStoreResizeHandler handler, BookmarkStore store)
        {
            _ring.setResizeHandler(handler, store);
        }

        /**
         * Lock self's internal lock.
         * Used by LoggedBookmarkStore when gathering Subscriptions to prune.
         */
        void lock()
        {
            _lock.lock();
        }

        /**
         * Unlock self's internal lock.
         * Used by LoggedBookmarkStore when finishing a prune() operation.
         */
        void unlock()
        {
            _lock.unlock();
        }
    }

    HashMap _subs = new HashMap();
    RandomAccessFile _file;
    String _fileName;
    volatile boolean _recovering = false;
    volatile boolean _recentChanged = true;
    final int VERSION = 2;
    Pool _pool;
    BookmarkStoreResizeHandler _resizeHandler = null;
    private int _serverVersion = Client.MIN_MULTI_BOOKMARK_VERSION;
    final CharsetEncoder _encoder = StandardCharsets.UTF_8.newEncoder();
    final CharsetDecoder _decoder = StandardCharsets.UTF_8.newDecoder();

    // The Store lock
    final Lock       _lock           = new ReentrantLock();
    final Lock       _subsLock           = new ReentrantLock();
    RecoveryPointAdapter _adapter = null;
    RecoveryPointFactory _factory = null;

    /**
     * Same as calling LoggedBookmarkStore(path, 1, false).
     *
     * @see #LoggedBookmarkStore(String, int, boolean)
     * @param path The path to the backing bookmark log file.
     * @throws IOException If there is a problem creating, reading, or writing
     *         the backing file.
     */
    public LoggedBookmarkStore(String path) throws IOException
    {
        this(path, 1);
    }

    /**
     * Same as calling LoggedBookmarkStore(path, targetNumberOfSubscriptions, false).
     *
     * @see #LoggedBookmarkStore(String, int, boolean)
     * @param path The path to the backing bookmark log file.
     * @param targetNumberOfSubscriptions The initial capacity for the number
     * @throws IOException If there is a problem creating, reading, or writing
     *         the backing file.
     */
    public LoggedBookmarkStore(String path, int targetNumberOfSubscriptions) throws IOException
    {
        this(path, targetNumberOfSubscriptions, false);
    }

    /**
     * A file-backed bookmark store implementation that fully supports
     * discarding messages in an order different from the order they arrived
     * (i.e. out-of-order discards) and fail-over to a replicated server.
     * All messages must eventually be discarded, otherwise memory usage will
     * become proportional to the number of messages received on a
     * bookmark subscription since the first undiscarded message.
     * This implementation requires that the prune() method be called
     * periodically to removed unneeded discarded bookmarks from the
     * backing-file, otherwise it will continue to grow without bound.
     * The prune() method is thread-safe and can be called from any thread.
     *
     * @param path The path to the backing bookmark log file.
     * @param targetNumberOfSubscriptions The initial capacity for the number
     *        of bookmark subscriptions you anticipate creating on the AMPS
     *        client instance that this bookmark store is registered on. This
     *        will grow as needed if more subscriptions are created than
     *        anticipated.
     * @param useLastModifiedTime Indicates whether the recovery timestamp
     *        feature should be used. If true, the last-modified time of the
     *        backing file is included (as an AMPS timestamp bookmark in a
     *        comma-separated list of bookmarks) when getMostRecent() is called
     *        after recovering from a bookmark file. This feature could be
     *        useful if you have an infrequently run process that is run on a
     *        schedule that is longer than the AMPS server keeps messages in
     *        its transaction log. When this process is started and recovers
     *        a bookmark log full of old bookmarks that are no longer available
     *        using the MOST_RECENT bookmark indicator, the recovery timestamp
     *        will cause the bookmark subscription to begin at the start of the
     *        transaction log (e.g. EPOCH), rather than its tail (e.g. NOW).
     *
     * @throws IOException If there is a problem creating, reading, or writing
     *         the backing file.
     */
    public LoggedBookmarkStore(String path,
                               int targetNumberOfSubscriptions,
                               boolean useLastModifiedTime) throws IOException
    {
        File bmLogFile = new File(path);
        String recoveryTimestamp = null;
        if (useLastModifiedTime && bmLogFile.exists()) {
            Date lastMod = new Date(bmLogFile.lastModified());
            TimeZone tz = TimeZone.getTimeZone("UTC");
            DateFormat df = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
            df.setTimeZone(tz);
            recoveryTimestamp = df.format(lastMod);
        }

        _pool = new Pool(Subscription.class, targetNumberOfSubscriptions);
        _file = new RandomAccessFile(path, "rw");
        _fileName = path;

        try
        {
            recover();

            if (recoveryTimestamp != null)
            {
                // Initialize the recovery timestamp on each recovered sub.
                for (Subscription sub: _subs.values())
                {
                    sub.setRecoveryTimestamp(recoveryTimestamp);
                }
            }
        }
        catch(IOException ioex)
        {
            try
            {
                _file.close();
            }
            catch (IOException ignoreTheCloseException)
            {
            }
            finally
            {
                _file = null;
            }
            throw ioex;
        }
    }

    /**
     * A file-backed bookmark store implementation that fully supports
     * discarding messages in an order different from the order they arrived
     * (i.e. out-of-order discards) and fail-over to a replicated server.
     * All messages must eventually be discarded, otherwise memory usage will
     * become proportional to the number of messages received on a
     * bookmark subscription since the first undiscarded message.
     * This implementation requires that the prune() method be called
     * periodically to removed unneeded discarded bookmarks from the
     * backing-file, otherwise it will continue to grow without bound.
     * The prune() method is thread-safe and can be called from any thread.
     * The store also has a backup RecoveryPointAdapter used in case the file
     * is deleted.
     *
     * @param path The path to the backing bookmark log file.
     * @param targetNumberOfSubscriptions The initial capacity for the number
     *        of bookmark subscriptions you anticipate creating on the AMPS
     *        client instance that this bookmark store is registered on. This
     *        will grow as needed if more subscriptions are created than
     *        anticipated.
     * @param useLastModifiedTime Indicates whether the recovery timestamp
     *        feature should be used. If true, the last-modified time of the
     *        backing file is included (as an AMPS timestamp bookmark in a
     *        comma-separated list of bookmarks) when getMostRecent() is called
     *        after recovering from a bookmark file. This feature could be
     *        useful if you have an infrequently run process that is run on a
     *        schedule that is longer than the AMPS server keeps messages in
     *        its transaction log. When this process is started and recovers
     *        a bookmark log full of old bookmarks that are no longer available
     *        using the MOST_RECENT bookmark indicator, the recovery timestamp
     *        will cause the bookmark subscription to begin at the start of the
     *        transaction log (e.g. EPOCH), rather than its tail (e.g. NOW).
     * @param adapter The RecoveryPointAdapter backing up the store. The
     *        adapter will be sent FixedRecoveryPoints.
     *
     * @throws IOException If there is a problem creating, reading, or writing
     *         the backing file.
     */
    public LoggedBookmarkStore(String path,
                               int targetNumberOfSubscriptions,
                               boolean useLastModifiedTime,
                               RecoveryPointAdapter adapter) throws IOException
    {
        this(path, targetNumberOfSubscriptions, useLastModifiedTime, adapter, new FixedRecoveryPointFactory());
    }

    /**
     * A file-backed bookmark store implementation that fully supports
     * discarding messages in an order different from the order they arrived
     * (i.e. out-of-order discards) and fail-over to a replicated server.
     * All messages must eventually be discarded, otherwise memory usage will
     * become proportional to the number of messages received on a
     * bookmark subscription since the first undiscarded message.
     * This implementation requires that the prune() method be called
     * periodically to removed unneeded discarded bookmarks from the
     * backing-file, otherwise it will continue to grow without bound.
     * The prune() method is thread-safe and can be called from any thread.
     * The store also has a backup RecoveryPointAdapter used in case the file
     * is deleted.
     *
     * @param path The path to the backing bookmark log file.
     * @param targetNumberOfSubscriptions The initial capacity for the number
     *        of bookmark subscriptions you anticipate creating on the AMPS
     *        client instance that this bookmark store is registered on. This
     *        will grow as needed if more subscriptions are created than
     *        anticipated.
     * @param useLastModifiedTime Indicates whether the recovery timestamp
     *        feature should be used. If true, the last-modified time of the
     *        backing file is included (as an AMPS timestamp bookmark in a
     *        comma-separated list of bookmarks) when getMostRecent() is called
     *        after recovering from a bookmark file. This feature could be
     *        useful if you have an infrequently run process that is run on a
     *        schedule that is longer than the AMPS server keeps messages in
     *        its transaction log. When this process is started and recovers
     *        a bookmark log full of old bookmarks that are no longer available
     *        using the MOST_RECENT bookmark indicator, the recovery timestamp
     *        will cause the bookmark subscription to begin at the start of the
     *        transaction log (e.g. EPOCH), rather than its tail (e.g. NOW).
     * @param adapter The RecoveryPointAdapter backing up the store.
     * @param factory The RecoveryPointFactory used to craete RecoveryPoints
     *        that are sent to the adapter.
     *
     * @throws IOException If there is a problem creating, reading, or writing
     *         the backing file.
     */
    public LoggedBookmarkStore(String path,
                               int targetNumberOfSubscriptions,
                               boolean useLastModifiedTime,
                               RecoveryPointAdapter adapter,
                               RecoveryPointFactory factory) throws IOException
    {
        String recoveryTimestamp = null;
        File bmLogFile = new File(path);
        if (useLastModifiedTime && bmLogFile.exists()) {
            Date lastMod = new Date(bmLogFile.lastModified());
            TimeZone tz = TimeZone.getTimeZone("UTC");
            DateFormat df = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
            df.setTimeZone(tz);
            recoveryTimestamp = df.format(lastMod);
        }

        _pool = new Pool(Subscription.class, targetNumberOfSubscriptions);
        _file = new RandomAccessFile(path, "rw");
        _fileName = path;

        try
        {
            _recovering = true;
            Message m = new JSONMessage(_encoder, _decoder);
            for (RecoveryPoint rp : adapter) {
                if (rp == null) break;
                Field subId = rp.getSubId();
                m.reset();  
                m.setSubId(subId.buffer, subId.position, subId.length);
                BookmarkField bookmark = rp.getBookmark();
                if (bookmark.isRange()) {
                    m.setBookmark(bookmark.buffer, bookmark.position,
                                  bookmark.length);
                    try {
                        log(m);
                    } catch(AMPSException e) { } // Always a valid range
                }
                else {
                    try {
                        for (BookmarkField bm : bookmark.parseBookmarkList())
                        {
                            if (!bm.isTimestamp())
                            {
                                m.setBookmark(bm.buffer, bm.position,
                                              bm.length);
                                isDiscarded(m);
                                if (log(m) > 0)
                                    discard(m);
                            }
                            else
                            {
                                find(subId).setRecoveryTimestamp(bm.toString());
                            }
                        }
                    } catch(AMPSException e) { } // Always valid
                }
            }
        }
        finally {
            _recovering = false;
        }
        try
        {
            recover();
            if (recoveryTimestamp != null)
            {
                // Initialize the recovery timestamp on each recovered sub.
                for (Subscription sub: _subs.values()) {
                    sub.setRecoveryTimestamp(recoveryTimestamp);
                }
            }
        }
        catch(IOException ioex) {
            try
            {
                _file.close();
            }
            catch (IOException ignoreTheCloseException) {
            }
            finally {
                _file = null;
            }
            throw ioex;
        }
        // Set after recovery complete to avoid sending any updates
        // during recovery
        _adapter = adapter;
        _factory = factory;
    }

    /**
     *  Remove outdated entries in the bookmark store.
     *  This function creates a temporary file, copies current entries to
     *  that file, and then replaces the current file with the temporary
     *  file.
     *  @throws IOException Thrown when an operation on the file fails.
     *  @throws StoreException Thrown when any other operation fails with details on the failure.
     */
    public void prune() throws IOException, StoreException
    {
        String name = _fileName + ".tmp";
        prune(name);
    }

    /**
     *  Remove outdated entries in the bookmark store.
     *  This function creates a temporary file, copies current entries to
     *  that file, and then replaces the current file with the temporary
     *  file.
     *  @param tmpFileName_  The name of the temporary file.
     *  @throws IOException Thrown when an operation on the file fails.
     *  @throws StoreException Thrown when any other operation fails with details on the failure.
     */

    public void prune(String tmpFileName_) throws IOException, StoreException
    {
        _subsLock.lock();
        try
        {
            List> subs = _lockSubs();
            try
            {
                prune(tmpFileName_,subs);
            }
            finally
            {
                _unlockSubs(subs);
            }
        }
        finally
        {
            _subsLock.unlock();
        }
    }

    /**
     *  Remove outdated entries in the bookmark store.
     *  This function is used internally and processes a list of subscriptions
     *  whose locks are already held, so that the locking order of subscription
     *  then store can be preserved.
     *  @param tmpFileName_ The name of the temporary file.
     *  @param subs_        The list of Subscriptions to prune.
     */
    private void prune(String tmpFileName_,
                       List> subs_)
      throws IOException, StoreException
    {
        _lock.lock();
        try
        {
            if(_file == null)
            {
                throw new StoreException("Store not open.");
            }
            if (!_recentChanged) {
                return;
            }
            RandomAccessFile file = new RandomAccessFile(tmpFileName_, "rw");
            try
            {
                file.getChannel().force(false);
                file.writeInt(VERSION);
                file.writeByte((byte)'\n');
                StringBuilder bookmarkBuilder = new StringBuilder(64);
                Field bookmark = new Field();
                for (Map.Entry sub : subs_)
                {
                    Field subId = sub.getKey();
                    assert(subId != null);
                    Subscription subscription = sub.getValue();
                    BookmarkField recent = (BookmarkField)subscription.getMostRecent().copy();
                    if (recent.isNull()) {
                        recent.copyFrom(subscription.EPOCH_FIELD);
                    }
                    long recentPub = recent.getPublisherId();
                    // Check for the Subscription's range
                    if (subscription.getRange().isValid())
                    {
                        // Update and return the range
                        Field range = subscription.getMostRecentList(true);
                        // A range doesn't need to be discarded so only need log
                        writeBookmarkToFile(file, subId, range, ENTRY_BOOKMARK);
                        // Ignore recent after this because it's a range
                        recentPub = 0;
                        recent.reset();
                    }
                    HashMap publishers = new HashMap(subscription._publishers);
                    ArrayList recovered = new ArrayList();
                    subscription.getActiveEntries(recovered);
                    Subscription.setPublishersToDiscarded(recovered, publishers);

                    // First write the highest discard for each publisher other
                    // than most recent.
                    Iterator> pubIter = publishers.entrySet().iterator();
                    while (pubIter.hasNext()) {
                        Map.Entry publisher = pubIter.next();
                        long pubId = publisher.getKey();
                        long pubSeq = publisher.getValue();
                        if (pubId == 0 || pubSeq == 0 || pubId == recentPub)
                            continue;
                        bookmarkBuilder.setLength(0);
                        bookmarkBuilder.append(Subscription.convertUnsignedLongToString(pubId))
                                       .append(BookmarkField.SEPARATOR_CHAR)
                                       .append(Subscription.convertUnsignedLongToString(pubSeq))
                                       .append(BookmarkField.SEPARATOR_CHAR);
                        bookmark.set(bookmarkBuilder.toString().getBytes(StandardCharsets.UTF_8), 0, bookmarkBuilder.length());
                        writeBookmarkToFile(file, subId, bookmark, ENTRY_BOOKMARK);
                        writeBookmarkToFile(file, subId, bookmark, ENTRY_DISCARD);
                    }
                    // Now write the most recent
                    if (recent.length > 1) {
                        writeBookmarkToFile(file, subId, recent, ENTRY_BOOKMARK);
                        writeBookmarkToFile(file, subId, recent, ENTRY_DISCARD);
                    }
                    // Now write the last persisted
                    if (subscription._lastPersisted != null
                        && subscription._lastPersisted.length > 1)
                    {
                        writeBookmarkToFile(file, subId,
                                            subscription._lastPersisted,
                                            ENTRY_PERSISTED);
                    }
                    for (BookmarkRingBuffer.Entry entry : recovered)
                    {
                        BookmarkField entryBookmark = entry.getBookmark();
                        if (entryBookmark == null
                            || entryBookmark.isNull())
                        {
                            continue;
                        }
                        writeBookmarkToFile(file, subId, entryBookmark,
                                            ENTRY_BOOKMARK);
                        if (!entry.isActive())
                            writeBookmarkToFile(file, subId,
                                                entryBookmark,
                                                ENTRY_DISCARD);
                    }
                }
            }
            // If we end up in catch because of an IOException, need to clean tmp file
            catch (IOException e)
            {
                file.close();
                File tmp = new File(tmpFileName_);
                // We can ignore a failure of delete here, prune has failed
                tmp.delete();
                throw new StoreException("Failed attempting to prune file " + _fileName + " to " + tmpFileName_, e);
            }
            // Close the files, delete the original, move the pruned file
            file.close();
            _file.close();
            _file = null;
            int retries = 0;
            File origTmp = new File(_fileName);
            while (retries++ < 3)
            {
                if (!origTmp.delete())
                {
                    if (retries >= 3) throw new StoreException(
                            "Failed to delete original file " + _fileName
                            + " after completing prune to " + tmpFileName_);
                }
                else
                {
                    break;
                }
            }
            // Let file system catch up
            while (origTmp.exists())
            {
                try {
                    Thread.sleep(100L);
                } catch (InterruptedException e) {
                    // Ignore
                }
            }
            retries = 0;
            while (retries++ < 3)
            {
                File tmp = new File(tmpFileName_);
                if (!tmp.renameTo(origTmp))
                {
                    if (retries >= 3) throw new StoreException(
                            "Failed to rename pruned file " + tmpFileName_
                            + " to original file name: " + _fileName);
                    try { Thread.sleep(50); }
                    catch (InterruptedException ex) { }
                }
                else
                {
                    break;
                }
            }
            _file = new RandomAccessFile(_fileName, "rw");
            if (_file.length() > 0) _file.seek(_file.length());
            _recentChanged = false;
        }
        finally {
            _lock.unlock();
        }
    }

    private void writeBookmarkToFile(RandomAccessFile file_, Field subId_, Field bookmark_, byte entry_) throws IOException
    {
        file_.writeInt(subId_.length);
        file_.write(subId_.buffer, subId_.position, subId_.length);
        file_.writeByte(entry_);
        file_.writeInt(bookmark_.length);
        file_.write(bookmark_.buffer, bookmark_.position, bookmark_.length);
        file_.writeByte((byte)'\n');
    }

    void write(Field sub, byte entry, Field data) throws IOException
    {
        _lock.lock();
        try {
            if(!_recovering)
            {
                writeBookmarkToFile(_file, sub, data, entry);
            }
        }
        finally {
            _lock.unlock();
        }
    }

    void write(Field sub, byte entry, long data) throws IOException
    {
        _lock.lock();
        try {
            if(!_recovering)
            {
                _file.writeInt(sub.length);
                _file.write(sub.buffer, sub.position, sub.length);
                _file.writeByte(entry);
                _file.writeLong(data);
                _file.writeByte((byte)'\n');
            }
        }
        finally {
            _lock.unlock();
        }
    }

    /**
     * This method is used internally by this {@link BookmarkStore}
     * implementation to recover its state from the bookmark store file.
     * This allows it to know on restart what messages have been discarded by
     * a subscriber and what the most recent message a subscriber should receive
     * from a publisher for a bookmark subscription.
     *
     * IMPORTANT NOTE: When making changes to this method, please run the
     * manual test case in
     * gfs/sixty/amps-client-java/wip/manual_tests/LargeUnprunedBookmarkStore
     * to ensure proper function in certain hard to automate cases such
     * as ensuring reasonable memory footprint.
     *
     * @throws IOException If an unrecoverable problem is detected while
     *         processing the bookmark log file.
     */
    private final void recover() throws IOException
    {
        _recovering = true;
        if (_file.length() < 4) {
            try {
                _file.writeInt(VERSION);
                _file.writeByte((byte)'\n');
            }
            finally {
                _recovering = false;
            }
            return;
        }
        int version = 0;
        try {
            version = _file.readInt();
        }
        catch (IOException ex) {
            _recovering = false;
            throw ex;
        }
        boolean readInts = version == VERSION;
        if (readInts) {
            try {
                _file.readByte();
            }
            catch (IOException ex) {
                _recovering = false;
                throw ex;
            }
            if (_file.length() == 5) {
                _recovering = false;
                return;
            }
        }
        else {
            _file.seek(0);
        }
        HashMap> ids = new HashMap>();
        int maxSubLen = 255;
        byte[] sub = new byte[maxSubLen];
        int maxBookmarkLen = 255;
        byte[] bookmark = new byte[maxBookmarkLen];
        long lastGoodPosition = _file.getFilePointer();
        int subLen = 0;
        int bmLen = 0;
        Field subId = new Field();
        BookmarkField bookmarkField = new BookmarkField();
        Long zero = Long.valueOf(0);
        // Create temporary buffered stream for bookmark file. We use a new file
        // channel so that closing the stream later won't close _file.
        Path path = FileSystems.getDefault().getPath(_fileName);
        FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
        channel.position(lastGoodPosition);
        DataInputStream in = null;
        try
        {
            in = new DataInputStream(
                    new BufferedInputStream(Channels.newInputStream(channel),
                                            8192));
            long position = lastGoodPosition;
            long line = 0;
            while(lastGoodPosition < _file.length())
            {
                long entryStartPos = position;
                Field subIdCopy = null;
                if (readInts) {
                    subLen = in.readInt();
                    position+=4;
                }
                else {
                    subLen = in.readUnsignedByte();
                    position++;
                }
                if (position < 0 || position + subLen > _file.length())
                {
                    // truncated entry
                    _file.seek(entryStartPos);
                    throw new IOException("Invalid subid length " + subLen + " starting at position " + entryStartPos + " line " + line + " in file " + _fileName + " of size " + _file.length());
                }
                if (subLen > maxSubLen) {
                    do {
                        maxSubLen *= 2;
                    } while (subLen > maxSubLen);
                    sub = new byte[maxSubLen];
                }
                in.readFully(sub, 0, subLen);
                subId.set(sub, 0, subLen);
                position += subLen;
                HashMap subscriptionMap = ids.get(subId);
                if(subscriptionMap == null)
                {
                    subscriptionMap = new HashMap();
                    subIdCopy = subId.copy();
                    ids.put(subIdCopy, subscriptionMap);
                }
                Subscription subscription = find(subId);

                int entryType = in.readUnsignedByte();
                position++;
                if (readInts) {
                    bmLen = in.readInt();
                    position+=4;
                }
                else {
                    bmLen = in.readUnsignedByte();
                    position++;
                }
                if(bmLen < 0 || position + bmLen > _file.length())
                {
                    // truncated entry
                    _file.seek(entryStartPos);
                    throw new IOException("Invalid bookmark len " + bmLen + " starting at position " + entryStartPos + " line " + line + " in file " + _fileName + " of size " + _file.length());
                }
                if (bmLen > maxBookmarkLen) {
                    do {
                        maxBookmarkLen *= 2;
                    } while (bmLen > maxBookmarkLen);
                    bookmark = new byte[maxBookmarkLen];
                }
                in.readFully(bookmark, 0, bmLen);
                position += bmLen;
                if (in.readUnsignedByte() != (byte)'\n') {
                    // bad read
                    _file.seek(entryStartPos);
                    throw new IOException("Invalid record didn't end with newline starting at position " + entryStartPos + " line " + line + " in file " + _fileName + " of size " + _file.length());
                }
                position++;
                bookmarkField.set(bookmark, 0, bmLen);
                switch(entryType)
                {
                case ENTRY_BOOKMARK:
                    if (bookmarkField.isRange()) {
                        try {
                            // log does all we need
                            subscription.log(bookmarkField);
                        } catch (CommandException e) { } // range always valid
                    }
                    else {
                        String bmStr = bookmarkField.getValue(_decoder);
                        if(subscriptionMap.get(bmStr) != null)
                        {
                            subscription.getMostRecent(true);
                            subscriptionMap.clear();
                        }
                        if (!subscription.isDiscarded(bookmarkField))
                        {
                            try {
                                long addedIdx = subscription.log(bookmarkField);
                                subscriptionMap.put(bmStr, addedIdx);
                            } catch (CommandException e) { } // no range
                        }
                        else
                        {
                            subscriptionMap.put(bmStr, zero);
                        }
                    }
                    break;

                case ENTRY_DISCARD:
                    String bkmStr = bookmarkField.getValue(_decoder);
                    Long subscriptionMapEntry = subscriptionMap.get(bkmStr);
                    if(subscriptionMapEntry != null)
                    {
                        subscriptionMap.remove(bkmStr);
                        if (subscriptionMapEntry > 0)
                        {
                            subscription.discard(subscriptionMapEntry);
                        }
                    }
                    break;

                case ENTRY_PERSISTED:
                    subscription.setLastPersisted(bookmarkField);
                    break;

                default:
                    throw new IOException("Corrupt file found.");
                }
                lastGoodPosition = position;
                ++line;
            }
            if (_file.getFilePointer() != lastGoodPosition) {
                _file.seek(lastGoodPosition);
            }
            in.close();
            in = null;
        }
        catch (IOException ex)
        {
            if (in == null) { //Failed to create DataInputStream
                _recovering = false;
                throw ex;
            }
            boolean onLastTruncatedLine = true;
            try {
                // Try to determine if we are on the last line of the file,
                // which may have been corrupted due to truncation from an
                // abrupt exit.
                int len = 0;
                byte[] buffer = new byte[255];
                while ((len = in.read(buffer, 0, buffer.length)) != -1) {
                    // Scan for a newline char from the current position.
                    for (int i = 0; i < len; i++) {
                        // If we found a newline, then we're not on the
                        // last line of the file that has been truncated.
                        if ((byte)'\n' == buffer[i]) {
                            onLastTruncatedLine = false;
                            break;
                        }
                    }
                }

                // Now close the stream.
                in.close();
                in = null;
            }
            catch (IOException ex2) {
                ex.addSuppressed(ex2);
                _recovering = false;
                throw ex; // Something seems to be seriously wrong, so throw.
            }
            if (lastGoodPosition > 0 && onLastTruncatedLine)
            {
                // We only want to seek to the last good position if
                // we know we're on the last line of the file and it
                // has been truncated (due to an abrupt shutdown).
                try {
                    _file.seek(lastGoodPosition);
                }
                catch (IOException ex2) {
                    ex.addSuppressed(ex2);
                    _recovering = false;
                    throw ex;
                }
            }
            else
            {
                // The corruption seems to be somewhere earlier in the
                // file, so throw the exception.
                _recovering = false;
                throw ex;
            }
        }
        finally
        {
            try {
                if (in != null) in.close();
            }
            catch (IOException ex) {
            }
            // Because this is only called either in constructor or in
            // setServerVersion already under the _subsLock, there is
            // no reason to lock _subsLock here.
            Iterator> it = _subs.entrySet().iterator();
            while (it.hasNext())
            {
                Map.Entry pairs = it.next();
                boolean disposeSub = pairs.getValue().justRecovered();
                if (disposeSub)
                {
                    it.remove();
                    _pool.returnToPool(pairs.getValue());
                }
            }
            _recovering = false;
        }
        // If this was an older file version, rewrite it
        if (!readInts) {
            try {
                prune();
            }
            catch (StoreException ex) {
                throw new IOException("Failed to rewrite older file version, see inner exception", ex);
            }
        }
    }

    public long log(Message message) throws AMPSException
    {
        BookmarkField bookmark = (BookmarkField)message.getBookmarkRaw();
        if (bookmark.equals(Subscription.EPOCH_FIELD)) return 0;
        Subscription sub = (LoggedBookmarkStore.Subscription)message.getSubscription();
        Field subId = message.getSubIdRaw();
        if (subId == null || subId.isNull())
            subId = message.getSubIdsRaw();

        _lock.lock();
        try
        {
            if(!_recovering && _file == null)
            {
                throw new StoreException("Store not open.");
            }
        }
        finally
        {
            _lock.unlock();
        }

        long index = 0;
        try
        {
            if (sub == null)
            {
                sub = find(subId);
                message.setSubscription(sub);
            }
            index = sub.log(bookmark);
        }
        catch (IOException ioex)
        {
            throw new AMPSException("Error logging to bookmark store", ioex);
        }
        message.setBookmarkSeqNo(index);
        _lock.lock();
        try
        {
            // log the arrival of this bookmark.
            write(subId, LoggedBookmarkStore.ENTRY_BOOKMARK, bookmark);
        }
        catch (IOException ioex)
        {
            throw new AMPSException("Error logging to bookmark store", ioex);
        }
        finally
        {
            _lock.unlock();
        }
        return index;
    }

    public void discard(Field subId, long bookmarkSeqNo) throws AMPSException
    {
        _lock.lock();
        try
        {
            if(!_recovering && _file == null)
            {
                throw new StoreException("Store not open.");
            }
        }
        finally
        {
            _lock.unlock();
        }
        try
        {
            find(subId).discard(bookmarkSeqNo);
        }
        catch (IOException ioex)
        {
            throw new AMPSException("Error discarding from bookmark store", ioex);
        }
    }

    public void discard(Message message) throws AMPSException
    {
        _lock.lock();
        try
        {
            if(_file == null)
            {
                throw new StoreException("Store not open.");
            }
        }
        finally
        {
            _lock.unlock();
        }
        BookmarkField bookmarkField = message.getBookmarkRaw();
        if (bookmarkField.equals(Subscription.EPOCH_FIELD)
            || bookmarkField.isTimestamp() || bookmarkField.isRange()
            || bookmarkField.isBookmarkList())
        {
            return;
        }
        long bookmark = message.getBookmarkSeqNo();
        Subscription sub = (LoggedBookmarkStore.Subscription)message.getSubscription();
        if (sub == null)
        {
            Field subId = message.getSubIdRaw();
            if (subId == null || subId.isNull())
                subId = message.getSubIdsRaw();
            sub = find(subId);
            message.setSubscription(sub);
        }
        try
        {
            sub.discard(bookmark);
        }
        catch (IOException ioex)
        {
            throw new AMPSException("Error discarding from bookmark store", ioex);
        }
    }

    public Field getMostRecent(Field subId) throws AMPSException
    {
        return getMostRecent(subId, true);
    }

    public Field getMostRecent(Field subId, boolean useList) throws AMPSException
    {
        _lock.lock();
        try
        {
            if(_file == null)
            {
                throw new StoreException("Store not open.");
            }
        }
        finally
        {
            _lock.unlock();
        }
        return find(subId).getMostRecentList(useList).copy();
    }

    public boolean isDiscarded(Message message) throws AMPSException
    {
        _lock.lock();
        try
        {
            if(_file == null)
            {
                throw new StoreException("Store not open.");
            }
        }
        finally
        {
            _lock.unlock();
        }
        BookmarkField bookmark = (BookmarkField)message.getBookmarkRaw();
        if (bookmark.equals(Subscription.EPOCH_FIELD)) return true;
        if (bookmark.isTimestamp() || bookmark.isBookmarkList()) return false;
        Subscription sub = (LoggedBookmarkStore.Subscription)message.getSubscription();
        if (sub == null)
        {
            Field subId = message.getSubIdRaw();
            if (subId == null || subId.isNull())
                subId = message.getSubIdsRaw();
            sub = find(subId);
            message.setSubscription(sub);
        }
        try
        {
            return sub.isDiscarded(bookmark);
        }
        catch (IOException ioex)
        {
            throw new AMPSException("Error checking is discarded in bookmark store", ioex);
        }
    }

    /**
     * Old style of setting a persisted bookmark no longer used.
     * @deprecated use {@link #persisted(Field, BookmarkField)} instead.
     */
    @Deprecated
    public void persisted(Field subId, long bookmark) throws AMPSException
    {
        _lock.lock();
        try
        {
            if(_file == null)
            {
                throw new StoreException("Store not open.");
            }
        }
        finally
        {
            _lock.unlock();
        }
        try
        {
            find(subId).setLastPersisted(bookmark);
        }
        catch (IOException ioex)
        {
            throw new AMPSException("Error logging persisted to bookmark store", ioex);
        }
    }

    public void persisted(Field subId, BookmarkField bookmark) throws AMPSException
    {
        _lock.lock();
        try
        {
            if(_file == null)
            {
                throw new StoreException("Store not open.");
            }
        }
        finally
        {
            _lock.unlock();
        }
        if (bookmark.equals(Subscription.EPOCH_FIELD) || bookmark.isRange())
        {
            return;
        }
        try
        {
            find(subId).setLastPersisted(bookmark);
        }
        catch (IOException ioex)
        {
            throw new AMPSException("Error logging persisted to bookmark store", ioex);
        }
    }

    public long getOldestBookmarkSeq(Field subId) throws AMPSException
    {
        _lock.lock();
        try
        {
            if(_file == null)
            {
                throw new StoreException("Store not open.");
            }
        }
        finally
        {
            _lock.unlock();
        }
        return find(subId).getOldestBookmarkSeq();
    }

    public void setResizeHandler(BookmarkStoreResizeHandler handler)
    {
        // The _resizeHandler is only touched under _subsLock
        _subsLock.lock();
        try {
            _resizeHandler = handler;
            Iterator it = _subs.entrySet().iterator();
            while (it.hasNext())
            {
                Map.Entry pairs = (Map.Entry)it.next();
                ((Subscription)pairs.getValue()).setResizeHandler(handler, this);
            }
        }
        finally {
            _subsLock.unlock();
        }
    }

    /**
     * Finds and returns the Subscription object for the specified
     * subscription id (subId).
     * @param subId The subId to find or create a Subscription for.
     * @return The Subscription associated with the subId.
     */
    protected Subscription find(Field subId)
    {
        _subsLock.lock();
        try {
            Subscription s = _subs.get(subId);
            if(s==null)
            {
                s=_pool.get();
                s.init(subId, this);
                s.setResizeHandler(_resizeHandler, this);
                _subs.put(subId.copy(), s);
            }
            return s;
        }
        finally {
            _subsLock.unlock();
        }
    }

    /**
     *  Remove all entries in the bookmark store, completely
     *  clearing all record of messages received and discarded.
     */
    public void purge() throws AMPSException
    {
        if(_file == null)
        {
            throw new StoreException("Store not open.");
        }
        _purge();
        if (_adapter != null) {
            try {
                _adapter.purge();
            }
            catch (AMPSException e) {
                throw e;
            }
            catch (Exception e) {
                throw new StoreException("Exception in RecoveryPointAdapter.purge()", e);
            }
        }
    }

    public void _purge() throws AMPSException
    {
        _subsLock.lock();
        _lock.lock();
        try {
            _recentChanged = true;
            // delete the file on disk.
            try
            {
                _file.setLength(0);
                _file.writeInt(VERSION);
                _file.writeByte((byte)'\n');
            }
            catch(IOException ioex)
            {
                throw new StoreException("Error truncating file", ioex);
            }

            for (Subscription sub: _subs.values())
            {
                sub.reset();
                _pool.returnToPool(sub);
            }

            _subs.clear();
        }
        finally {
            _lock.unlock();
            _subsLock.unlock();
        }
    }

    /**
     *  Remove all entries in the bookmark store for a subId.
     *  @param subId_ The subId to remove from the store.
     */
    public void purge(Field subId_) throws AMPSException
    {
        if(_file == null)
        {
            throw new StoreException("Store not open.");
        }
        _purge(subId_);
        if (_adapter != null) {
            try {
                _adapter.purge(subId_);
            }
            catch (AMPSException e) {
                throw e;
            }
            catch (Exception e) {
                throw new StoreException("Exception in RecoveryPointAdapter.purge(" + subId_.toString() + ")", e);
            }
        }
    }

    public void _purge(Field subId_) throws AMPSException
    {
        _subsLock.lock();
        String name = "";
        try {
            // Need to acquire all the Subscription locks here before the
            // store lock, because prune(name) will acquire all of them below.
            // If we didn't acquire these and some other Subscription is active
            // (not the one we're purging) we could get a deadlock if that
            // active sub is executing a discard() or persisted() call.
            List> subs = _lockSubs();
            try {
                _lock.lock();
                try {
                    Subscription sub = _subs.remove(subId_);
                    if (sub == null) return;
                    sub.reset();
                    _pool.returnToPool(sub);
                    name = _fileName + ".tmp";
                    prune(name);
                }
                finally {
                    _lock.unlock();
                }
            }
            finally {
                _unlockSubs(subs);
            }
        }
        catch (IOException e) {
            throw new StoreException("Underlying IOException while pruning. "
                    + "temp bookmark log file = " + name, e);
        }
        finally {
            _subsLock.unlock();
        }
    }

    /**
     *  Change the RecoveryPointFactory used by this store for its adapter.
     *  @param factory_ The new RecoveryPointFactory
     *  @throws AMPSException If one of factory or adapter is null.
     */
    public void setRecoveryPointFactory(RecoveryPointFactory factory_) throws AMPSException
    {
        if (factory_ == null || _adapter == null) {
            throw new CommandException("Factory and Adapter must not be null.");
        }
        _factory = factory_;
    }

    /**
    * Closes the bookmark store. If it is already closed, a StoreException is thrown.
    * @throws StoreException if there is an error closing the file backing in the store,
    * or the store already closed.
    */
    public void close() throws AMPSException
    {
        _lock.lock();
        try {
            StoreException ex = null;
            if (_adapter != null) {
                try
                {
                    _adapter.close();
                }
                catch(Exception e)
                {
                    ex = new StoreException("Error closing adapter", e);
                }
            }
            if(_file != null) {
                try
                {
                    _file.close();
                }
                catch(IOException ioex)
                {
                    if (ex == null) {
                        throw new StoreException("Error closing file", ioex);
                    }
                    else {
                        throw new StoreException("Error closing adapter and file " + ioex.toString(), ex);
                    }
                }
            }
            else {
                throw new StoreException("Store not open.");
            }
            if (ex != null) throw ex;
        }
        finally {
            _adapter = null;
            _factory = null;
            _file = null;
            _lock.unlock();
        }
    }

    /**
     * Used to change the version of the AMPS server that this bookmark store's
     * client has connected to.
     *
     * @param version An AMPS server version integer of the form 03080000 for
     *        version 3.8.0.0.
     */
    public void setServerVersion(int version)
    {
        _serverVersion = version;
    }

    /**
    * Called by Client when connected to an AMPS server in order to retrieve
    * the version number of the server. Returns retrieved version number.
    * @return the server version, represented as an integer
    */
    public int getServerVersion()
    {
        return _serverVersion;
    }

    /**
     * Used inernally to update the RecoveryPointAdapter if there is one.
     * @param subId The subId to update.
     * @param bookmark The latest bookmark.
     * @throws IOException If there is an exception from the adapter.
     */
    protected void adapterUpdate(Field subId, BookmarkField bookmark) throws IOException {
        if (_adapter != null) {
            try {
                _adapter.update(_factory.createRecoveryPoint(subId, bookmark));
            }
            catch (Exception e) {
                throw new IOException("Exception in LoggedBookmarkStore updating the RecoveryPointAdapter", e);
            }
        }
    }

    /**
     * Locks and returns the current list of Subscriptions.
     * Used internally by prune() to gather and lock the list of subscriptions
     * that we need to prune. We hold self's lock here to get the subscription
     * list and then release it to lock up all of the subscriptions.
     */
    private List> _lockSubs()
    {
        // The _subsLock MUST already be held
        // No need to hold self's _lock here.
        List> subs
            = new Vector>(_subs.size());
        for(Map.Entry entry : _subs.entrySet())
        {
            subs.add(entry);
        }

        // Lock the subscriptions without self's lock held.
        for(Map.Entry entry : subs)
        {
            entry.getValue().lock();
        }
        return subs;
    }

    /**
     * Unlocks the provided list of Subscriptions.
     * Used internally by prune() to gather and lock the list of subscriptions
     * that we need to prune. We hold self's lock here to get the subscription
     * list and then release it to lock up all of the subscriptions.
     */
    private void _unlockSubs(List> subs_)
    {
        // The _subsLock MUST already be held
        // No need to hold self's _lock here.
        // We are just unlocking the individual Subscriptions that were
        // held for the duration of prune().
        for(Map.Entry entry : subs_)
        {
            entry.getValue().unlock();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy