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

com.crankuptheamps.client.SOWRecoveryPointAdapter 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.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 (State of the World) topic as an external store to persist recovery * point state for bookmark replay subscriptions. *

* *

* This class allows you to use an AMPS SOW topic as a backup bookmark store for * bookmark replay. It serves as a recovery point adapter that serializes and * deserializes recovery points to and from the SOW topic. To use this class * effectively, you will need to configure your AMPS server to include a SOW * topic that matches the expected format used by this adapter. *

* *

* Server-side configuration for AMPS SOW topics is required for successful * integration with this class. Ensure that your AMPS server is properly * configured to handle SOW topics, including topics matching the expected * format used for recovery points. See the 'Java Developer Guide' for the * suggested configuration. *

*/ 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. * @throws StoreException If client name and trackedClientName are the same. */ public SOWRecoveryPointAdapter(Client client, String trackedClientName, boolean closeClient, boolean useTimestamp, boolean throwExceptions) throws StoreException { // It delegates to the full constructor by passing in the provided parameters // along with some default values for the rest. // The default values are as follows: // - timeoutMillis: 0L (no timeout) // - topic: "/ADMIN/bookmark_store" // - clientNameField: "clientName" // - subIdField: "subId" // - bookmarkField: "bookmark" 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. * @throws StoreException If client name and trackedClientName are the same. */ public SOWRecoveryPointAdapter(Client client, String trackedClientName, boolean closeClient, boolean useTimestamp, boolean throwExceptions, long timeoutMillis, String topic, String clientNameField, String subIdField, String bookmarkField) throws StoreException { if (client.getName().equals(trackedClientName)) { throw new StoreException("SOWRecoveryPointAdapter must use a different client than the one it tracks"); } // Assign provided parameters to instance variables _client = client; _trackedName = trackedClientName; _timeoutMillis = timeoutMillis; _closeClient = closeClient; _useTimestamp = useTimestamp; _throwNotListen = throwExceptions; _topic = topic; _clientNameField = clientNameField; _subIdField = subIdField; _bookmarkField = bookmarkField; // Set the topic for the internal command object _cmd.setTopic(_topic); // Initialize regex patterns for extracting subId and bookmark fields from JSON _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 { // Check if the internal client instance exists if (_client != null) { try { // Flush any pending updates to the server. This ensures that any recovery state // updates are sent to the server before closing _client.publishFlush(_timeoutMillis); } catch (DisconnectedException e) { // Handle disconnection exceptions gracefully. } catch (Exception e) { // Handle other exceptions that may occur during flushing if (_throwNotListen) { // If configured to throw exceptions, rethrow the exception throw new StoreException("SOWRecoveryPointAdapter error flushing to store during close", e); } else if (_exceptionListener != null) { // Otherwise, notify the exception listener if it's registered _exceptionListener.exceptionThrown( new StoreException("SOWRecoveryPointAdapter error flushing to store during close", e)); } } finally { // If configured to close the internal client, do so if (_closeClient) _client.close(); } } } /** * Set up the MessageStream for data recovery. */ protected void runQuery() { try { // Create a StringBuilder to build a filter for the SOW query StringBuilder builder = new StringBuilder(); builder.append('/').append(_clientNameField).append("=\"") .append(_trackedName).append('"'); // Create a new SOW (State of the World) command Command cmd = new Command("sow"); // Configure the SOW command with the topic and timeout cmd.setTopic(_topic).setTimeout(_timeoutMillis) .setFilter(builder.toString()); // Clear the StringBuilder for building options for the SOW command builder.setLength(0); // Build the select options for the SOW command, including subId and bookmark // fields builder.append("select=[-/,+/").append(_subIdField) .append(",+/").append(_bookmarkField).append(']'); // If useTimestamp is enabled, add "timestamp" to the select options if (_useTimestamp) { builder.append(",timestamp"); // Set the options for the SOW command cmd.setOptions(builder.toString()); } else { cmd.setOptions(builder.toString()); } // Execute the SOW command on the internal AMPS client and get the MessageStream _stream = _client.execute(cmd); } catch (Exception e) { // Handle exceptions that may occur during the SOW query r. // If _throwNotListen is false and an exception listener is set, // report the exception to the listener and set _stream to null if (!_throwNotListen && _exceptionListener != null) { _exceptionListener.exceptionThrown(new StoreException("Failed to execute SOW query for recovery", e)); _stream = null; } } // Indicate that the recovery process is not active (completed) _recovering = false; } /** * Implements {@link Iterator#hasNext}. */ public boolean hasNext() { if (_recovering && _stream == null) { // If recovery is in progress and the stream is null, trigger a SOW query runQuery(); } // Check if there is more data available in the stream return _stream != null && _stream.hasNext(); } /** * Implements {@link Iterator#next} for a message stream. */ public RecoveryPoint next() { if (_recovering && _stream == null) { runQuery(); } // Get the next message from the stream Message m = _stream.next(); if (m == null) { // If there are no more messages, set the stream to null and return null _stream = null; return null; } int command = m.getCommand(); if (command == Message.Command.GroupBegin) { // Skip GroupBegin messages and continue to the next message return next(); } if (command != Message.Command.SOW) { // Group_End, Ack, or something wacky _stream = null; return null; } try { // Deserialize the SOW message into a RecoveryPoint if (!deserialize(m)) { return next(); } } catch (Exception e) { return null; } // Return the current RecoveryPoint 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 instance as the 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 if serialization is successful 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()) { // If the subId field is not found in the SOW record, throw an exception throw new StoreException("SubId NOT FOUND in SOW" + " record during recovery: data = " + data); } Matcher bookmarkMatch = _bookmarkPattern.matcher(data); if (!bookmarkMatch.find()) { // If the recovery bookmark is not found in the SOW record, throw an exception 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) { // If the serialization buffer is not large enough, create a new buffer byte[] buffer = new byte[len]; _serializeField.set(buffer, 0, len); } // Copy the subId and bookmark data into the serialization buffer 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; } /** * Initializes the serialization buffer and an initial string for serialization * purposes. * This method prepares the serialization buffer and an initial JSON-like string * for later serialization tasks. * It handles buffer initialization, ensures sufficient space, and populates the * initial string if necessary. * * @throws Exception if there is an issue during initialization. */ protected void initSerialization() throws Exception { // Check if the buffer for serialization is null or too small if (_serializeField.buffer == null || _serializeField.buffer.length < JSON_INIT_LEN) { // If it is null or too small, create a new buffer with a specific size byte[] buffer = new byte[JSON_INIT_LEN]; _serializeField.set(buffer, 0, JSON_INIT_LEN); } if (_initStr == null) { // If the initial string is null, create a StringBuilder to build an initial // JSON-like string StringBuilder builder = new StringBuilder(JSON_INIT_LEN); builder.append("{\"").append(_clientNameField).append("\":\"") .append(_trackedName).append("\",\"") .append(_subIdField).append("\":\""); _initStr = builder.toString(); // Set the _serializeStart to the length of the initial string _serializeStart = _initStr.length(); } // Copy the initial string into the serialization buffer System.arraycopy(_initStr.getBytes("UTF8"), 0, _serializeField.buffer, 0, _initStr.length()); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy