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

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

///////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2010-2024 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.IOException;
import java.lang.Long;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
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.fields.StringField;
import com.crankuptheamps.client.exception.*;

/**
 * Implements a bookmark store useful for handling
 * server failover scenarios, but without a backing store to recover from
 * subscriber failure.
 * 
 * An optional {@link RecoveryPointAdapter} can be specified at construction
 * to persist bookmark subscription recovery state to an external store so the
 * subscription can be resumed if the subscriber fails.
 *
 */
public class MemoryBookmarkStore implements BookmarkStore 
{
    /**
     * 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;

        /**
         * The last-modified timestamp, if any, from a RecoveryPointAdapter
         * 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.
         */
        protected volatile String _recoveryTimestamp = null;

        // If this subscription uses a range, hold it here
        BookmarkRangeField _range = new BookmarkRangeField();

        // Represents the epoch field
        static final Field EPOCH_FIELD = new Field(Client.Bookmarks.EPOCH);

        BookmarkRingBuffer _ring = new BookmarkRingBuffer();

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

        // 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();

        MemoryBookmarkStore _parent;

        /**
         * Constructor that creates a Subscription object with the provided subscription
         * ID and initializes its state.
         *
         * @param subscriptionId The subscription ID to associate with this
         *                       Subscription.
         */
        public Subscription(Field subscriptionId)
        {
            _sub = subscriptionId.copy();
            _ring.setSubId(_sub);
            _lastPersisted = new BookmarkField();
            _lastPersisted.copyFrom(EPOCH_FIELD);
        }

        /**
         * Default constructor that creates a Subscription object and initializes its
         * state.
         */
        public Subscription() {
            _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() {
            _lock.lock();
            try {
                _sub.reset();
                _lastPersisted.copyFrom(EPOCH_FIELD);
                _ring.reset();
                _publishers.clear();
                _range.reset();
            }
            finally {
                _lock.unlock();
            }
        }

        /**
         * Initialize the Subscription with a new subscription ID and assign a reference
         * to the parent bookmark store.
         *
         * @param subId  The new subscription ID to associate with this Subscription.
         * @param parent The parent MemoryBookmarkStore to which this Subscription
         *               belongs.
         */
        public void init(Field subId, MemoryBookmarkStore parent) {
            _lock.lock();
            try {
                _sub = subId.copy();
                _ring.setSubId(_sub);
                _parent = parent;
            }
            finally {
                _lock.unlock();
            }
        }

        /**
         * Get the last-modified timestamp used for recovery.
         *
         * @return The last-modified timestamp used for recovery.
         */
        public String getRecoveryTimestamp() 
        {
            return _recoveryTimestamp;
        }

        /**
         * The last-modified timestamp, if any, from a RecoveryPointAdapter
         * 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.
         *
         * @param rts The recovery timestamp to set.
         */
        protected final void setRecoveryTimestamp(String rts) 
        {
            _recoveryTimestamp = rts;
        }

        /**
         * Log a bookmark, either a single bookmark or a list of bookmarks.
         *
         * @param bookmark The bookmark to log.
         * @return The sequence number associated with the logged bookmark.
         * @throws IOException      Thrown when an IO operation fails.
         * @throws CommandException Thrown when a command operation fails.
         */
        public long log(BookmarkField bookmark) throws IOException, CommandException
        {
            _lock.lock();
            try {
                if (!bookmark.isRange()) {
                    // 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()) {
                            // Check if the bookmark should be discarded
                            isDiscarded(bm);
                            // Log the bookmark and get its sequence
                            seq = _log(bm);
                            if (seq != 0) {
                                _ring.discard(seq); // Discard the logged sequence
                            }
                        }
                        // If a parent adapter exists, update it with the logged bookmark list
                        if (_parent._adapter != null) {
                            _parent.adapterUpdate(_sub, bookmark);
                        }
                        return 0;
                    }
                }
                else {
                    // Handle bookmark ranges
                    _range.copyFrom(bookmark);
                    if (!_range.isValid()) {
                        _range.reset();
                        throw new CommandException("Invalid bookmark range specified");
                    }
                    long seq = 0;
                    if (_range.isStartExclusive()) {
                        // Parse the start of the range and log/discard each
                        BookmarkField start = _range.getStart();
                        if (!start.isBookmarkList()) {
                            isDiscarded(start);
                            seq = _log(start);
                            if (seq != 0) {
                                _ring.discard(seq);
                            }
                        }
                        else {
                            for (BookmarkField bm : start.parseBookmarkList()) {
                                isDiscarded(bm);
                                seq = _log(bm);
                                if (seq != 0) {
                                    _ring.discard(seq);
                                }
                            }
                            seq = 0;
                        }
                    }
                    if (_parent._adapter != null) {
                        _parent.adapterUpdate(_sub, _range);
                    }
                    return seq;
                }
            }
            finally {
                _lock.unlock();
            }
        }

        /**
         * Log a single bookmark.
         *
         * @param bm The bookmark to log.
         * @return The sequence number associated with the logged bookmark.
         * @throws IOException Thrown when an IO operation fails.
         */
        private long _log(BookmarkField bm) throws IOException
        {
            // _lock is already acquired
            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;
        }

        /**
         * Discard a bookmark at the specified index.
         *
         * @param index The index of the bookmark to discard.
         * @throws IOException Thrown when an IO operation fails.
         */
        public void discard(long index) throws IOException
        {
            _lock.lock();
            try {
                if (_ring.discard(index)) 
                {
                    if (_parent._adapter != null) {
                        _parent.adapterUpdate(_sub,
                                    (BookmarkField) getMostRecentList(false));
                    }
                    _recoveryTimestamp = null;
                }
            }
            finally {
                _lock.unlock();
            }
        }

        /**
         * Check to see if this message is older than the most recent one seen,
         * and if it is, check if it discarded.
         * @param bookmark The bookmark containing the publisher ID and sequence number.
         * @return True if the message is discarded or in-flight, false otherwise.
         */
        public boolean isDiscarded(BookmarkField bookmark)
        {
            _lock.lock();
            try {
                long publisher = bookmark.getPublisherId();
                long sequence = bookmark.getSequenceNumber();
                if(!_publishers.containsKey(publisher) ||
                    BookmarkField.unsignedLongLess(_publishers.get(publisher), sequence))
                {
                    _publishers.put(publisher, sequence);
                    return false;
                }

                return true; // message is in-flight or discarded
            }
            finally {
                _lock.unlock();
            }
        }

        /**
         * Gets the range of bookmarks stored in this bookmark store.
         *
         * @return A copy of the bookmark range.
         */
        public BookmarkRangeField getRange() {
            _lock.lock();
            try { return _range.copy(); }
            finally {
                _lock.unlock();
            }
        }

        /**
         * Get the most recent bookmark.
         *
         * @return A copy of the most recent bookmark.
         */
        public Field getMostRecent()
        {
            _lock.lock();
            try {
                return _ring.getLastDiscarded().copy();
            }
            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_);
            }
        }

        public Field getMostRecentList(boolean useList)
        {
            _lock.lock();
            try {
                boolean rangeIsValid = _range.isValid();
                BookmarkField lastDiscarded = (BookmarkField)_ring.getLastDiscarded();
                boolean useLastDiscarded = (lastDiscarded != null &&
                                            !lastDiscarded.isNull());
                // Check if we haven't discarded the first message yet
                if (useLastDiscarded && lastDiscarded.equals(EPOCH_FIELD)) {
                    if (rangeIsValid) {
                        // Return the unmodified range
                        return _range;
                    }
                    else if (_recoveryTimestamp != null) {
                        BookmarkField ret = new BookmarkField();
                        ret.setValue(_recoveryTimestamp, _encoder);
                        return ret;
                    }
                    // Return EPOCH
                    return lastDiscarded;
                }
                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 (_ring.isEmpty() && useLastPersisted
                        && (!rangeIsValid || _range.getEnd().equals(_lastPersisted)))
                    {
                        useLastDiscarded = false;
                    }
                    else
                    {
                        lastDiscardedPub = lastDiscarded.getPublisherId();
                        lastDiscardedSeq = lastDiscarded.getSequenceNumber();
                        // Only use one if they are the same publisher
                        if (useLastPersisted &&
                            (lastPersistedPub == lastDiscardedPub))
                        {
                            useLastPersisted = (lastPersistedSeq < lastDiscardedSeq);
                            useLastDiscarded = !useLastPersisted;
                        }
                    }
                }

                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();
            }
        }

        /**
         * Old style of setting a persisted bookmark no longer used.
         *
         * @deprecated use {@link #setLastPersisted(BookmarkField)} instead.
         * @param bookmark The bookmark to be persisted.
         * @throws IOException if the bookmark was unable to be persisted to the
         *                     bookmark store.
         */
        @Deprecated
        public void setLastPersisted(long bookmark) throws IOException
        {
            _lock.lock();
            try {
                BookmarkRingBuffer.Entry entry = _ring.getByIndex(bookmark);
                if (entry == null) return;
                if (_lastPersisted != null) _lastPersisted.reset();
                _lastPersisted = entry.getBookmark().copy();
                _recoveryTimestamp = null;
                if (_parent._adapter != null) {
                    _parent.adapterUpdate(_sub,
                                    (BookmarkField) getMostRecentList(false));
                }
            }
            finally {
                _lock.unlock();
            }
        }

        /**
         * Set the last persisted bookmark.
         *
         * @param bookmark The bookmark to set as the last persisted.
         * @throws IOException If an I/O error occurs.
         */
        public void setLastPersisted(BookmarkField bookmark) throws IOException
        {
            _lock.lock();
            try {
                // If the bookmark is null, empty, equals the last persisted, or is a range,
                // return.
                if (bookmark == null || bookmark.isNull()
                    || bookmark.equals(_lastPersisted)
                    || bookmark.isRange()) {
                    return;
                }

                if (bookmark.isTimestamp())
                {
                    _recoveryTimestamp = bookmark.toString();
                    if (_parent._adapter != null) {
                        _parent.adapterUpdate(_sub,
                                    (BookmarkField) getMostRecentList(false));
                    }
                    return;
                }
                long publisherId = bookmark.getPublisherId();
                if (_lastPersisted != null &&
                    publisherId == _lastPersisted.getPublisherId() &&
                    BookmarkField.unsignedLongLessEqual(bookmark.getSequenceNumber(),
                                     _lastPersisted.getSequenceNumber()))
                {
                    return;
                }
                BookmarkField lastPersisted = _lastPersisted;
                _lastPersisted = bookmark.copy();
                _recoveryTimestamp = null;
                if ((lastPersisted == null || lastPersisted.equals(EPOCH_FIELD))
                    && _ring.isEmpty()
                    && _ring.getLastDiscarded().equals(EPOCH_FIELD)) {
                        discard(_log(bookmark));
                }
                else if (_parent._adapter != null) {
                    _parent.adapterUpdate(_sub,
                                      (BookmarkField) getMostRecentList(false));
                }
                if (lastPersisted != null) lastPersisted.reset();
            }
            finally {
                _lock.unlock();
            }
        }

        /**
         * Get the oldest bookmark sequence number.
         *
         * @return The sequence number of the oldest bookmark.
         */
        public long getOldestBookmarkSeq()
        {
            _lock.lock();
            try {
                return _ring.getStartIndex();
            }
            finally {
                _lock.unlock();
            }
        }

        /**
         * Set a resize handler for the bookmark store.
         *
         * @param handler The resize handler to set.
         * @param store   The bookmark store to associate with the handler.
         */
        public void setResizeHandler(BookmarkStoreResizeHandler handler, BookmarkStore store)
        {
            _ring.setResizeHandler(handler, store);
        }
    }

    HashMap _subs = new HashMap();
    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();

    /**
     * Optional recovery point adapter used to persist bookmark replay recovery
     * state for each subscription.
     */
    protected RecoveryPointAdapter _adapter = null;
    protected RecoveryPointFactory _factory = null;

    // Subscriptions take a while to initialize, so we avoid doing this in the
    // message handler.
    // number of subscriptions we pre-initialize.
    Pool _pool;

    /**
     * Initializes a new instance of the MemoryBookmarkStore class with default settings.
     */
    public MemoryBookmarkStore()
    {
        this(1);
    }

    /**
     * Constructor for MemoryBookmarkStore with a target number of subscriptions.
     *
     * @param targetNumberOfSubscriptions The target number of subscriptions to
     *                                    pre-initialize.
     */
    public MemoryBookmarkStore(int targetNumberOfSubscriptions)
    {
        _pool = new Pool(Subscription.class, targetNumberOfSubscriptions);
    }

    /**
     * Initialize self with a target number of subscriptions to pool and the
     * specified recovery point adapter.
     * 
     * @param targetNumberOfSubscriptions The number of subscriptions this store
     *                                    will initially create in its pool.
     * @param adapter                     Recovery point adapter this instance will
     *                                    call with recovery point state for each of
     *                                    its subscriptions.
     *                                    The adapter can persist this state to an
     *                                    external store and retrieve it on application
     *                                    restart so that bookmark replay subscriptions
     *                                    can be continued approximately where they left
     *                                    off (with the possible delivery of some
     *                                    duplicate messages). The adapter will be given
     *                                    FixedRecoveryPoints.
     * @throws AMPSException If the given adapter throws an exception during
     *                       recovery.
     */
    public MemoryBookmarkStore(int targetNumberOfSubscriptions,
                               RecoveryPointAdapter adapter) throws AMPSException
    {
        this(targetNumberOfSubscriptions, adapter, new FixedRecoveryPointFactory());
    }

    /**
     * Initialize self with a target number of subscriptions to pool and the
     * specified recovery point adapter.
     * 
     * @param targetNumberOfSubscriptions The number of subscriptions this store
     *                                    will initially create in its pool.
     * @param adapter                     Recovery point adapter this instance will
     *                                    call with recovery point state for each of
     *                                    its subscriptions.
     *                                    The adapter can persist this state to an
     *                                    external store and retrieve it on application
     *                                    restart so that bookmark replay subscriptions
     *                                    can be continued approximately where they
     *                                    left off (with the possible delivery of some
     *                                    duplicate messages).
     * @param factory                     RecoveryPointFactory that will produce
     *                                    RecoveryPoints for the adapter.
     * @throws AMPSException If the given adapter throws an exception during
     *                       recovery.
     */
    public MemoryBookmarkStore(int targetNumberOfSubscriptions,
                               RecoveryPointAdapter adapter,
                               RecoveryPointFactory factory) throws AMPSException
    {
        _pool = new Pool(Subscription.class, targetNumberOfSubscriptions);

        // Try to recover subscription state if a recovery point adapter
        // was specified.
        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);
                log(m);
            }
            else {
                for (BookmarkField bm : bookmark.parseBookmarkList()) {
                    if (!bm.isTimestamp()) {
                        try {
                            m.setBookmark(bm.buffer, bm.position, bm.length);
                            isDiscarded(m);
                            if (log(m) > 0)
                                discard(m);
                        } catch (AMPSException e) { } // Always valid
                    }
                    else {
                        find(subId).setRecoveryTimestamp(bm.toString());
                    }
                }
            }
        }
        // Set after recovery so no updates sent back to adapter
        _adapter = adapter;
        _factory = factory;
    }

    /**
     * Change the RecoveryPointFactory used by this store for its adaptor.
     *
     * @param factory_ The new RecoveryPointFactory
     * @throws AMPSException If the factory_ parameter is null or the recovery point
     *                       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_;
    }

    /**
     * Log a message and update the bookmark sequence number.
     *
     * @param message The message to log.
     * @return The updated sequence number.
     * @throws AMPSException If an error occurs during the log operation.
     */
    public long log(Message message) throws AMPSException
    {
        BookmarkField bookmark = (BookmarkField) message.getBookmarkRaw();
        if (bookmark.equals(Subscription.EPOCH_FIELD)) return 0;
        Subscription sub = (MemoryBookmarkStore.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 {
            long seqNo = sub.log(bookmark);
            message.setBookmarkSeqNo(seqNo);
            return seqNo;
        }
        catch (IOException e) {
            throw new StoreException("Failed to save range in RecoveryPointAdapter", e);
        }
    }

    /**
     * Discard a message based on the subscription ID and bookmark sequence number.
     *
     * @param subId         The subscription ID.
     * @param bookmarkSeqNo The bookmark sequence number to discard.
     * @throws AMPSException If an error occurs during the discard operation.
     */
    public void discard(Field subId, long bookmarkSeqNo) throws AMPSException
    {
        try {
            find(subId).discard(bookmarkSeqNo);
        }
        catch (IOException e) {
            throw new StoreException("Error discarding from bookmark store: " + e, e);
        }
    }

    /**
     * Discard a message based on the provided message.
     *
     * @param message The message to discard.
     * @throws AMPSException If an error occurs during the discard operation.
     */
    public void discard(Message message) throws AMPSException
    {
        long bookmark = message.getBookmarkSeqNo();
        Subscription sub = (MemoryBookmarkStore.Subscription) message.getSubscription();
        if (sub == null)
        {
            BookmarkField bmField = message.getBookmarkRaw();
            if (bmField.equals(Subscription.EPOCH_FIELD)
                || bmField.isTimestamp() || bmField.isRange()
                || bmField.isBookmarkList()) {
                return;
            }
            Field subId = message.getSubIdRaw();
            if (subId == null || subId.isNull())
                subId = message.getSubIdsRaw();
            sub = find(subId);
            message.setSubscription(sub);
        }
        try {
            sub.discard(bookmark);
        }
        catch (IOException e) {
            throw new StoreException("Error discarding from bookmark store: " + e, e);
        }
    }

    /**
     * Get the most recent bookmark for a specific subscription.
     *
     * @param subId The subscription ID.
     * @return The most recent bookmark.
     * @throws AMPSException If an error occurs during the operation.
     */
    public Field getMostRecent(Field subId) throws AMPSException
    {
        return getMostRecent(subId, true);
    }

    /**
     * Get the most recent bookmark for a specific subscription.
     *
     * @param subId   The subscription ID.
     * @param useList Indicates whether to use the bookmark list.
     * @return The most recent bookmark.
     * @throws AMPSException If an error occurs during the operation.
     */
    public Field getMostRecent(Field subId, boolean useList) throws AMPSException
    {
        return find(subId).getMostRecentList(useList).copy();
    }

    /**
     * Check if a message is discarded based on its bookmark.
     *
     * @param message The message to check.
     * @return True if the message is discarded, false otherwise.
     * @throws AMPSException If an error occurs during the operation.
     */
    public boolean isDiscarded(Message message) throws AMPSException
    {
        BookmarkField bookmark = (BookmarkField) message.getBookmarkRaw();
        if (bookmark.equals(Subscription.EPOCH_FIELD)) return true;
        if (bookmark.isTimestamp() || bookmark.isBookmarkList()) return false;
        Field subId = message.getSubIdRaw();
        if (subId == null || subId.isNull())
            subId = message.getSubIdsRaw();
        Subscription sub = find(subId);
        message.setSubscription(sub);
        return sub.isDiscarded(bookmark);
    }

    /**
     * Old style of setting a persisted bookmark no longer used.
     *
     * @deprecated use {@link #persisted(Field, BookmarkField)} instead.
     * @param subId    The subId associated with the bookmark to be persisted.
     * @param bookmark The bookmark to be persisted.
     * @throws AMPSException If the bookmark was unable to be persisted to the
     *                       bookmark store.
     */
    @Deprecated
    public void persisted(Field subId, long bookmark) throws AMPSException
    {
        try {
            find(subId).setLastPersisted(bookmark);
        }
        catch (IOException e) {
            throw new StoreException("Error logging persisted to bookmark store: " + e, e);
        }
    }

    /**
     * Persist a bookmark for a subscription.
     *
     * @param subId    The subId associated with the bookmark to be persisted.
     * @param bookmark The bookmark to be persisted.
     * @throws AMPSException If the bookmark was unable to be persisted to the
     *                       bookmark store.
     */
    public void persisted(Field subId, BookmarkField bookmark) throws AMPSException
    {
        try {
            find(subId).setLastPersisted(bookmark);
        }
        catch (IOException e) {
            throw new StoreException("Error logging persisted to bookmark store: " + e, e);
        }
    }

    /**
     * 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 for the given subId.
     */
    protected Subscription find(Field subId)
    {
        _lock.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 {
            _lock.unlock();
        }
    }

    /**
     * Remove all entries in the bookmark store, completely clearing all records of
     * messages received and discarded.
     *
     * @throws AMPSException If an error occurs during the purge operation.
     */
    public void purge() throws AMPSException
    {
        _lock.lock();
        try {
            for (Subscription sub : _subs.values())
            {
                sub.reset();
                _pool.returnToPool(sub);
            }
            _subs.clear();

            // Also purge recovery point adapter if it exists.
            if (_adapter != null) _adapter.purge();
        }
        catch (Exception e) {
            throw new StoreException("An error occurred while purging recovery"
                    + "state from recovery point adapter: " + e, e);
        }
        finally {
            _lock.unlock();
        }
    }

    /**
     * Remove entries for a specific subscription ID, clearing its records of
     * messages received and discarded.
     *
     * @param subId_ The subscription ID to purge.
     * @throws AMPSException If an error occurs during the purge operation.
     */
    public void purge(Field subId_) throws AMPSException
    {
        _lock.lock();
        try {
            Subscription sub = _subs.remove(subId_);
            if (sub == null) return;
            sub.reset();
            _pool.returnToPool(sub);

            // Also purge recovery point adapter if it exists.
            if (_adapter != null) _adapter.purge(subId_);
        }
        catch (Exception e) {
            throw new StoreException("An error occurred while purging recovery"
                            + "state from recovery point adapter for subId '"
                            + subId_.toString() + "': " + e, e);
        }
        finally {
            _lock.unlock();
        }
    }

    /**
     * Get the oldest bookmark sequence number for a specific subscription.
     *
     * @param subId The subscription ID.
     * @return The oldest bookmark sequence number.
     * @throws AMPSException If an error occurs during the operation.
     */
    public long getOldestBookmarkSeq(Field subId) throws AMPSException
    {
        _lock.lock();
        try {
            long retVal = 0;
            retVal = find(subId).getOldestBookmarkSeq();
            return retVal;
        }
        finally {
            _lock.unlock();
        }
    }

    /**
     * Set the resize handler for this bookmark store and all associated
     * subscriptions.
     *
     * @param handler The BookmarkStoreResizeHandler to set.
     */
    public void setResizeHandler(BookmarkStoreResizeHandler handler)
    {
        _resizeHandler = handler;
        Iterator it = _subs.entrySet().iterator();
        while (it.hasNext())
        {
            Map.Entry pairs = (Map.Entry) it.next();
            ((Subscription) pairs.getValue()).setResizeHandler(handler, this);
        }
    }

    /**
     * Set the version of the AMPS server.
     *
     * @param version The server version to set.
     */
    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;
    }

    /**
     * Closes down the bookmark store. If a recovery point adapter was
     * specified during construction, this will call close() on it.
     */
    public void close() throws AMPSException
    {
        _lock.lock();
        try {
            if (_adapter != null) {
                _adapter.close();
                _adapter = null;
            }
        }
        catch (Exception e) {
            throw new StoreException("An error occurred while closing the underlying "
                    + "recovery point adapter: " + e, e);
        }
        finally {
            _lock.unlock();
        }
    }

    /**
     * Used internally 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 {
            try {
                _adapter.update(_factory.createRecoveryPoint(subId, bookmark));
        }
        catch (Exception e) {
            throw new IOException("Exception in LoggedBookmarkStore updating the RecoveryPointAdapter", e);
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy