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

com.crankuptheamps.client.SOWRecoveryPointAdapter 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.beans.ExceptionListener;
import java.lang.Iterable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.crankuptheamps.client.exception.AMPSException;
import com.crankuptheamps.client.exception.DisconnectedException;
import com.crankuptheamps.client.exception.StoreException;
import com.crankuptheamps.client.fields.Field;
import com.crankuptheamps.client.fields.BookmarkField;

/**
 * An implementation of the recovery point adapter interface that uses an
 * AMPS SOW topic as an external store to persist recovery point state for
 * bookmark replay subscriptions.
 */
public class SOWRecoveryPointAdapter implements RecoveryPointAdapter
{
    protected String _trackedName;
    protected String _topic;
    protected String _clientNameField;
    protected String _subIdField;
    protected String _bookmarkField;
    protected Client _client = null;
    protected ExceptionListener _exceptionListener = null;
    protected Field _serializeField = new Field();
    protected long _timeoutMillis = 0;
    protected boolean _closeClient = false;
    protected boolean _useTimestamp = false;
    protected boolean _throwNotListen = false;
    protected Command _cmd = new Command(Message.Command.Publish);
    protected MessageStream _stream = null;
    protected boolean _recovering = true;
    protected static final int JSON_START = 11; // {, 7", 2:, 1,
    protected static final int JSON_END = 8; // }, 5", 1:, 1,
    protected static final int JSON_LEN = JSON_START + JSON_END;
    protected static final int SUBID_LEN = 64; // rough typical max
    // Allow for a range with 2 ',' 1 ':' and begin/end markers
    protected static final int JSON_INIT_LEN = JSON_LEN + SUBID_LEN + 5
                                   + (4*BookmarkField.MAX_BOOKMARK_LENGTH);
    protected String _initStr = null;
    protected int _serializeStart = 0;
    protected RecoveryPoint _current = new FixedRecoveryPoint();
    protected Field EPOCH_FIELD = new Field("0");

    
    /**
     * Regular expression pattern to pull the bookmark string from subscription
     * state saved in the AMPS SOW.
     */
    protected Pattern _bookmarkPattern;

    /**
     * Regular expression pattern to pull the subscription id from subscription
     * state saved in the AMPS SOW.
     */
    protected Pattern _subIdPattern;

    /**
     * A concurrent hash map of subscription id's (known to this store)
     * mapped to the subscription's most-recent-for-recovery value
     * that was last persisted to the SOW. By caching the last
     * value, we can prevent unneeded writes to the SOW.
     */
    protected final ConcurrentHashMap _lastValue =
            new ConcurrentHashMap();

    /**
     * A recovery point adapter implementation that uses an AMPS SOW topic
     * as an external store to persist recovery point state for bookmark
     * replay subscriptions. See full constructor for details on all options.
     * 
     * @see #SOWRecoveryPointAdapter(Client, String, boolean, boolean, boolean,
     *                               long, String, String, String, String)
     * @param client The internal AMPS client instance used to write bookmark
     *        state to the AMPS SOW. This MUST NOT be the client whose
     *        registered bookmark store is associated with this recovery point
     *        adapter. In other words, it must not be the AMPS client used to
     *        place the bookmark replay subscriptions whose recovery state is
     *        being persisted by this adapter.
     * @param trackedClientName The unique name of the subscriber AMPS client
     *        we're tracking bookmark recovery state for. If your client names
     *        change with each run, this should be a unique session name that
     *        is stable across application restarts. For example, such a name
     *        could be formed by concatenating an application name with a
     *        logical functional area (e.g. "MyApp_OrderReplay"). This name will
     *        be used as a key in the SOW for tracking the subscriber client's
     *        bookmark state. For guidance on choosing stable unique AMPS client
     *        names or session names for use with this adapter, please see our
     *        support FAQ article entitled Unique Client Naming and AMPS.
     * @param closeClient Indicates whether this adapter instance should close
     *        its internal AMPS client when this adapter is closed. Defaults to
     *        true. If this flag is true, this adapter considers the internal
     *        client passed to it during construction as being owned by this
     *        adapter, such that its life-cycle should end when this adapter is
     *        closed. If false, this adapter will consider it the responsibility
     *        of the caller to close the internal client after this adapter is
     *        closed, allowing that client to be shared, possibly among multiple
     *        adapters (keeping in mind potential performance impacts of
     *        multiple threads publishing using the same AMPS client instance).
     * @param useTimestamp Indicates if the last updated timestamp of each
     *        entries should be included in the RecoveryPoint. This is similar
     *        to useLastModifiedTime in {@link LoggedBookmarkStore}.
     * @param throwExceptions Indicates whether exceptions should be thrown
     *        to the caller (usually the bookmark store), or should instead be
     *        delivered to this adapter's registered exception listener
     *        callback. This flag lets the user decide their error handling 
     *        strategy for recovery state write failures. The default is false,
     *        meaning the exception is absorbed, and sent to the exception
     *        listener (if any). A value of true means errors in the update()
     *        method throw exceptions. NOTE: Since this adapter implementation
     *        publishes to an AMPS SOW and most AMPS publishing failures have
     *        asynchronous notification, users should consider registering a
     *        {@link FailedWriteHandler} and publish store on the internal
     *        client passed above.
     */
    public SOWRecoveryPointAdapter(Client client, String trackedClientName,
                                   boolean closeClient, boolean useTimestamp,
                                   boolean throwExceptions) {
        this(client, trackedClientName, closeClient, useTimestamp,
             throwExceptions, 0L, "/ADMIN/bookmark_store", "clientName",
             "subId", "bookmark");
    }

    /**
     * A recovery point adapter implementation that uses an AMPS SOW topic
     * as an external store to persist recovery point state for bookmark
     * replay subscriptions.
     * 
     * @param client The internal AMPS client instance used to write bookmark
     *        state to the AMPS SOW. This MUST NOT be the client whose
     *        registered bookmark store is associated with this recovery point
     *        adapter. In other words, it must not be the AMPS client used to
     *        place the bookmark replay subscriptions whose recovery state is
     *        being persisted by this adapter.
     * @param trackedClientName The unique name of the subscriber AMPS client
     *        we're tracking bookmark recovery state for. If your client names
     *        change with each run, this should be a unique session name that
     *        is stable across application restarts. For example, such a name
     *        could be formed by concatenating an application name with a
     *        logical functional area (e.g. "MyApp_OrderReplay"). This name will
     *        be used as a key in the SOW for tracking the subscriber client's
     *        bookmark state. For guidance on choosing stable unique AMPS client
     *        names or session names for use with this adapter, please see our
     *        support FAQ article entitled Unique Client Naming and AMPS.
     * @param closeClient Indicates whether this adapter instance should close
     *        its internal AMPS client when this adapter is closed. Defaults to
     *        true. If this flag is true, this adapter considers the internal
     *        client passed to it during construction as being owned by this
     *        adapter, such that its life-cycle should end when this adapter is
     *        closed. If false, this adapter will consider it the responsibility
     *        of the caller to close the internal client after this adapter is
     *        closed, allowing that client to be shared, possibly among multiple
     *        adapters (keeping in mind potential performance impacts of
     *        multiple threads publishing using the same AMPS client instance).
     * @param useTimestamp Indicates if the last updated timestamp of each
     *        entries should be included in the RecoveryPoint. This is similar
     *        to useLastModifiedTime in {@link LoggedBookmarkStore}.
     * @param throwExceptions Indicates whether exceptions should be thrown
     *        to the caller (usually the bookmark store), or should instead be
     *        delivered to this adapter's registered exception listener
     *        callback. This flag lets the user decide their error handling 
     *        strategy for recovery state write failures. The default is false,
     *        meaning the exception is absorbed, and sent to the exception
     *        listener (if any). A value of true means errors in the update()
     *        method throw exceptions. NOTE: Since this adapter implementation
     *        publishes to an AMPS SOW and most AMPS publishing failures have
     *        asynchronous notification, users should consider registering a
     *        {@link FailedWriteHandler} and publish store on the internal
     *        client passed above.
     * @param timeoutMillis The number of milliseconds to wait for the initial
     *        sow query to complete and for publish_flush to complete during
     *        {@link #close()}. The default value is 0, which means no timeout.
     * @param topic The AMPS SOW topic configured to persist bookmark recovery
     *        state for this adapter. Usually defaults to "/ADMIN/bookmark_store",
     *        but this allows users to customize their usage for non-default SOW
     *        configs. NOTE: The message type of this topic in the AMPS config
     *        must be "json" unless overriding the operation of this adapter in
     *        a subclass.
     * @param clientNameField The name of the SOW topic field we use for
     *        persisting the tracked client name or session name. Usually just
     *        "clientName", but this allows users to customize their usage for
     *        non-default SOW configs. NOTE: This field must be one of two key
     *        fields configured on the SOW topic.
     * @param subIdField The name of the SOW topic field we use for persisting
     *        the subscription id. Usually just "subId", but this allows users
     *        to customize their usage for non-default SOW configs. NOTE: This
     *        field must be one of two key fields configured on the SOW topic.
     * @param bookmarkField The name of the SOW topic field we use for
     *        persisting the bookmark. Usually just "bookmark", but this
     *        allows users to customize their usage for non-default SOW configs.
     */
    public SOWRecoveryPointAdapter(Client client, String trackedClientName,
                                   boolean closeClient,
                                   boolean useTimestamp,
                                   boolean throwExceptions,
                                   long timeoutMillis,
                                   String topic,
                                   String clientNameField,
                                   String subIdField,
                                   String bookmarkField) {
        _client = client;
        _trackedName = trackedClientName;
        _timeoutMillis = timeoutMillis;
        _closeClient = closeClient;
        _useTimestamp = useTimestamp;
        _throwNotListen = throwExceptions;
        _topic = topic;
        _clientNameField = clientNameField;
        _subIdField = subIdField;
        _bookmarkField = bookmarkField;
        _cmd.setTopic(_topic);

        _subIdPattern = Pattern.compile("\"" + _subIdField + "\" *: *\"([^\"]+)\"");
        _bookmarkPattern = Pattern.compile("\"bookmark\" *: *\"([^\"]*)\"");
    }

    /**
     * Sets the {@link java.beans.ExceptionListener} instance used for
     * communicating absorbed exceptions.
     *
     * @param exceptionListener The exception listener instance to invoke for
     *        internal exceptions.
     */
    public void setExceptionListener(ExceptionListener exceptionListener) {
        this._exceptionListener = exceptionListener;
    }

    /**
     * Sends an update to the underlying SOW.
     * @param recoveryPoint The recovery state to persist in the SOW.
     * @throws Exception StoreException wrapper of exception thrown while
     *         clearing the SOW of recovery information.
     */
    public void update(RecoveryPoint recoveryPoint) throws Exception {
        try {
            // Don't write to the SOW if the current value equals
            // the prev value we wrote (i.e. there's been no change)
            Field subId = recoveryPoint.getSubId();
            BookmarkField currValue = recoveryPoint.getBookmark();
            BookmarkField prevValue = _lastValue.get(subId);
            if (prevValue == null || !currValue.equals(prevValue)) {
                if (!serialize(recoveryPoint)) return;
                _cmd.setData(_serializeField.buffer, _serializeField.position,
                             _serializeField.length);
                _client.execute(_cmd);
                // Update last value.
                if (prevValue == null) {
                    _lastValue.put(subId.copy(), currValue.copy());
                } else {
                    _lastValue.put(subId, currValue.copy());
                }
            }
        } catch (Exception e) {
            if (_throwNotListen) {
                throw new StoreException("Sow update exception " + e, e);
            } else {
                if (this._exceptionListener != null) {
                    this._exceptionListener.exceptionThrown(new StoreException("Sow update exception " + e.toString(), e));
                }
            }
        }
    }

    /**
     * Purge all recovery information stored in the SOW.
     * @throws StoreException Wrapper of exception thrown while clearing the
     *         SOW of recovery information.
     */
    public void purge() throws StoreException {
        try {
            String filter = String.format("/%s = '%s'", _clientNameField, _trackedName);
            _client.sowDelete(_topic, filter, _timeoutMillis);
        } catch (AMPSException e) {
            throw new StoreException("Error purging recovery state from SOW: " + e, e);
        }
    }

    /**
     * Purge all recovery information stored in the SOW.
     * @param subId The subId of the subscription to remove.
     * @throws StoreException Wrapper of exception thrown while clearing the
     *         SOW of recovery information.
     */
    public void purge(Field subId) throws StoreException {
        try {
            String filter = String.format("/%s = '%s' AND /%s = '%s'",
                    _clientNameField, _trackedName, _subIdField, subId);
            _client.sowDelete(_topic, filter, _timeoutMillis);
        } catch (Exception e) {
            throw new StoreException("Error purging recovery state from SOW for subId " + subId + ": " + e, e);
        }
    }

    /**
     * Close this adapter making sure all updates are at the server and close
     * the internal client if set up to do so.
     */
    public void close() throws Exception {
        if (_client != null) {
            try {
                _client.publishFlush(_timeoutMillis);
            }
            catch (DisconnectedException e) { }
            catch (Exception e) {
                if (_throwNotListen) {
                    throw new StoreException("SOWRecvoeryPointAdapter error flushing to store during close", e);
                }
                else if (_exceptionListener != null) {
                    _exceptionListener.exceptionThrown(new StoreException("SOWRecvoeryPointAdapter error flushing to store during close", e));
                }
            }
            finally {
                if (_closeClient) _client.close();
            }
        }
    }

    /**
     * Set up the MessageStream for recovery.
     */
    protected void runQuery() {
        try
        {
            StringBuilder builder = new StringBuilder();
            builder.append('/').append(_clientNameField).append("=\"")
                   .append(_trackedName).append('"');
            Command cmd = new Command("sow");
            cmd.setTopic(_topic).setTimeout(_timeoutMillis)
               .setFilter(builder.toString());
            builder.setLength(0);
            builder.append("select=[-/,+/").append(_subIdField)
                   .append(",+/").append(_bookmarkField).append(']');
            if (_useTimestamp) {
                builder.append(",timestamp");
                cmd.setOptions(builder.toString());
            }
            else {
                cmd.setOptions(builder.toString());
            }
            _stream = _client.execute(cmd);
        }
        catch (Exception e) {
            if (!_throwNotListen && _exceptionListener != null) {
                _exceptionListener.exceptionThrown(new StoreException("Failed to execute SOW query for recovery", e));
                _stream = null;
            }
        }
        _recovering = false;
    }

    /**
     * Implements {@link Iterator#hasNext}.
     */
    public boolean hasNext()
    {
        if (_recovering && _stream == null) {
            runQuery();
        }
        return _stream != null && _stream.hasNext();
    }

    /**
     * Implements {@link Iterator#next} for a message stream.
     */
    public RecoveryPoint next()
    {
        if (_recovering && _stream == null) {
            runQuery();
        }
        Message m = _stream.next();
        if (m == null) {
            _stream = null;
            return null;
        }
        int command = m.getCommand();
        if (command == Message.Command.GroupBegin) {
            return next();
        }
        if (command != Message.Command.SOW) {
            // Group_End, Ack, or something wacky
            _stream = null;
            return null;
        }
        try {
            if (!deserialize(m)) {
                return next();
            }
        }
        catch(Exception e) {
            return null;
        }
        return _current;
    }

    /**
     * Operation not supported. Implements {@link Iterator#remove} to throw UnsupportedOperationException.
     */
    public void remove()
    {
        throw new UnsupportedOperationException();
    }

    /**
     * Implements {@link Iterable#iterator} to return this instance.
     */
    public Iterator iterator()
    {
        return this;
    }

    /**
     * Serializes a {@link RecoveryPoint} to JSON in the internal buffer for
     * publishing to a JSON SOW topic. Used by the
     * {@link #update(RecoveryPoint)} method.
     *
     * This method provides an override point where a recovery point can be
     * serialized another message type into the buffer.
     * 
     * @param recoveryPoint The recovery point to serialize.
     * @return true upon successful serialization
     * @throws Exception If an error occurs during serialization.
     */
    protected boolean serialize(RecoveryPoint recoveryPoint)
            throws Exception {
        BookmarkField bookmark = recoveryPoint.getBookmark();
        if (bookmark.equals(EPOCH_FIELD)) {
            return false;
        }
        Field subId = recoveryPoint.getSubId();
        int endLength = subId.length + _bookmarkField.length()
                        + bookmark.length + JSON_END;
        int length = _clientNameField.length() + _trackedName.length()
                     + _subIdField.length() + JSON_START + endLength;
        if (_serializeField.buffer == null
            || _serializeField.buffer.length < length) {
            // Need a new buffer
            int newLength = length + (128-(length%128));
            byte[] buffer = new byte[newLength];
            _serializeField.set(buffer, 0, length);
            initSerialization();
        }
        System.arraycopy(subId.buffer, subId.position, _serializeField.buffer,
                         _serializeStart, subId.length);
        int pos = _serializeStart + subId.length;
        _serializeField.buffer[pos++] = (byte)'"';
        _serializeField.buffer[pos++] = (byte)',';
        _serializeField.buffer[pos++] = (byte)'"';
        System.arraycopy(_bookmarkField.getBytes("UTF8"), 0,
                         _serializeField.buffer, pos, _bookmarkField.length());
        pos += _bookmarkField.length();
        _serializeField.buffer[pos++] = (byte)'"';
        _serializeField.buffer[pos++] = (byte)':';
        _serializeField.buffer[pos++] = (byte)'"';
        System.arraycopy(bookmark.buffer, bookmark.position,
                         _serializeField.buffer, pos, bookmark.length);
        pos += bookmark.length;
        _serializeField.buffer[pos++] = (byte)'"';
        _serializeField.buffer[pos] = (byte)'}';
        _serializeField.length = length;
        return true;
    }

    /**
     * Deserializes a JSON string into a {@link RecoveryPoint} instance for use
     * by {@link #next()} during bookmark store creation/initialization.
     *
     * This method provides an override point where another message type
     * can be deserialized into a {@link RecoveryPoint}. This method is only
     * called via recover() during bookmark store creation/initialization by a
     * single thread.
     * 
     * @param m The `sow` Message from the server to deserialize.
     * @return true if the Message was successfully deserialized into the
     *         _current RecoveryPoint.
     * @throws Exception If an error occurs during deserialization.
     */
    protected boolean deserialize(Message m) throws Exception {
        int subIdIdx = 0;
        int bookmarkIdx = 0;
        String data = m.getData();
        Matcher subIdMatch = _subIdPattern.matcher(data);
        if (!subIdMatch.find()) {
            throw new StoreException("SubId NOT FOUND in SOW"
                + " record during recovery: data = " + data);
        }
        Matcher bookmarkMatch = _bookmarkPattern.matcher(data);
        if(!bookmarkMatch.find()) {
            throw new StoreException("Recovery bookmark NOT FOUND in SOW"
                + " record during recovery: data = " + data);
        }

        // Get the subId and the bookmark
        String subId = subIdMatch.group(1);
        String bookmark = bookmarkMatch.group(1);
        if (_useTimestamp) {
            String timestamp = m.getTimestamp();
            if (timestamp != null && !timestamp.isEmpty()
                && bookmark.charAt(0) != '[' && bookmark.charAt(0) != '(') {
                bookmark = bookmark + "," + timestamp;
            }
        }
        int subIdLen = subId.length();
        int bookmarkLen = bookmark.length();
        int len = subIdLen + bookmarkLen;
        if (_serializeField.buffer == null
            || _serializeField.buffer.length < len) {
            byte[] buffer = new byte[len];
            _serializeField.set(buffer, 0, len);
        }
        System.arraycopy(subId.getBytes("UTF8"), 0, _serializeField.buffer, 0,
                         subIdLen);
        System.arraycopy(bookmark.getBytes("UTF8"), 0, _serializeField.buffer,
                         subIdLen, bookmarkLen);
        _current.getSubId().set(_serializeField.buffer, 0, subIdLen);
        _current.getBookmark().set(_serializeField.buffer, subIdLen,
                                   bookmarkLen);
        return true;
    }

    protected void initSerialization() throws Exception {
        if (_serializeField.buffer == null
            || _serializeField.buffer.length < JSON_INIT_LEN) {
            byte[] buffer = new byte[JSON_INIT_LEN];
            _serializeField.set(buffer, 0, JSON_INIT_LEN);
        }
        if (_initStr == null)
        {
            StringBuilder builder = new StringBuilder(JSON_INIT_LEN);
            builder.append("{\"").append(_clientNameField).append("\":\"")
                   .append(_trackedName).append("\",\"")
                   .append(_subIdField).append("\":\"");
            _initStr = builder.toString();
            _serializeStart = _initStr.length();
        }
        System.arraycopy(_initStr.getBytes("UTF8"), 0,
                         _serializeField.buffer, 0, _initStr.length());
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy