com.crankuptheamps.client.MessageStream 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.lang.AutoCloseable;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.*;
import java.util.NoSuchElementException;
import java.util.HashMap;
import com.crankuptheamps.client.exception.*;
import com.crankuptheamps.client.fields.*;
/**
* MessageStream provides an iteration abstraction over the results of an AMPS
* command such
* as a subscribe, a SOW query, or SOW delete. MessageStream is produced when
* calling Client.execute() and
* continues iterating over the results until the connection is closed, or the
* iterator is explicitly closed, or when the SOW query is ended.
*
* You can use a MessageStream as you would other Iterators, for example, using
* a for loop (Java 1.7):
*
*
* MessageStream stream = Client.execute(new Command("sow").setTopic("/orders")).timeout(1000);
* for(Message message : stream)
* {
* ...
* }
*
*/
public class MessageStream
implements Iterator, Iterable, MessageHandler, ConnectionStateListener, AutoCloseable {
static final int STATE_Unset = 0x0;
static final int STATE_Reading = 0x10;
static final int STATE_Subscribed = 0x11;
static final int STATE_SOWOnly = 0x12;
static final int STATE_StatsOnly = 0x13;
static final int STATE_AcksOnly = 0x13;
static final int STATE_Disconnected = 0x01;
static final int STATE_Complete = 0x02;
protected CommandId _commandId;
protected CommandId _queryId;
protected CommandId _subId;
protected Client _client;
protected ConcurrentLinkedQueue _q = new ConcurrentLinkedQueue();
protected HashMap _sowKeyMap = null;
protected Lock _sowKeyLock = new ReentrantLock();
protected Message _current = null;
protected volatile int _state = STATE_Unset;
protected int _timeout = 0;
protected boolean _timedOut = false;
protected volatile int _maxDepth = Integer.MAX_VALUE;
protected int _requestedAcks = Message.AckType.None;
protected Field _previousTopic = new Field();
protected Field _previousBookmark = new Field();
private static MessageStream _emptyMessageStream = new MessageStream();
private MessageStream() {
_commandId = null;
_queryId = null;
_subId = null;
_client = null;
_state = STATE_Complete;
_requestedAcks = Message.AckType.None;
}
/**
* Method to retrieve the empty MessageStream.
*
* @return the static, empty MessageStream
*/
public static MessageStream getEmptyMessageStream() {
return _emptyMessageStream;
}
/**
* Constructs an instance of message stream with the given client. The
* message stream will hold a reference to the provided client, and
* registers itself as a connection state listener for the client.
*
* @param client_ the client to use for this message stream
*/
protected MessageStream(Client client_) {
_client = client_;
_client.addConnectionStateListener(this);
}
/**
* When this message stream is the result of a subscription command, this gets
* called with the subscription ID.
*
* @param subId_ the CommandId to set as the SubscriptionId of this command
*/
protected void setSubscription(CommandId subId_) {
_subId = subId_;
// Don't want to automatically resubscribe when disconnected
if (_client != null) {
SubscriptionManager subMgr = _client.getSubscriptionManager();
if (subMgr != null)
subMgr.unsubscribe(_subId);
}
setState(STATE_Subscribed);
}
/**
* When this message stream is the result of a form of SOW command, this gets
* called with the query ID of the command.
*
* @param queryId_ the CommandId to set as the QueryId of this command
*/
protected void setQueryId(CommandId queryId_) {
if (_queryId == null &&
(_commandId == null || !_commandId.equals(queryId_)))
_queryId = queryId_;
}
/**
* When this message stream is the result of a form of subscribe command, this
* gets called with the command ID of the command.
*
* @param commandId_ the CommandId to set as the QueryId of this command
*/
protected void setCommandId(CommandId commandId_) {
if (_commandId == null)
_commandId = commandId_;
}
/**
* Changes the current state of the message stream to {@link #STATE_SOWOnly}.
*/
protected void setSOWOnly() {
setState(STATE_SOWOnly);
}
/**
* Changes the current state of the message stream to {@link #STATE_StatsOnly}.
*/
protected void setStatsOnly() {
setState(STATE_StatsOnly);
_requestedAcks = Message.AckType.Stats;
}
/**
* Changes the current state of the message stream to {@link #STATE_AcksOnly}.
*
* @param commandId_ The commandId of the command for the acks.
* @param ackTypes_ The types of acks that will be returned.
*/
protected void setAcksOnly(CommandId commandId_, int ackTypes_) {
setState(STATE_AcksOnly);
_commandId = commandId_;
_requestedAcks = ackTypes_;
}
/**
* Changes the current state of the message stream to {@link #STATE_Reading}.
*/
protected void setRunning() {
setState(STATE_Reading);
}
/**
* Sets a timeout on self.
* If no message is received in this timeout, next() returns 'null',
* but leaves the stream open.
*
* @param timeout_ The timeout in milliseconds, or 0 for infinite timeout.
* @return Returns self, so it can be used in a "builder" style.
*/
public MessageStream timeout(int timeout_) {
_timeout = timeout_;
return this;
}
/**
* Gets the currently set timeout value for self (zero if none).
*
* @return the timeout set on this command
*
* @see #timeout(int)
*/
public int getTimeout() {
return _timeout;
}
/**
* Causes messages to be conflated by SOW key.
* You call this method on your MessageStream to use client-side conflation and
* continue to use the MessageStream as usual to process the messages you're
* interested in. Client-side conflation works when you have messages with
* unique identifiers(SowKeys). When using it with delta subscriptions (where
* you receive only the changes between messages), be aware that client-side
* conflation replaces the whole message, potentially causing updates to be lost
* if messages are replaced. In such cases, server-side conflation(handled by
* AMPS on the server) might be a safer option, as it can merge deltas more
* effectively.
*
* @return Returns this instance so that various operations can be chained
* together.
*/
public MessageStream conflate() {
if (_sowKeyMap == null)
_sowKeyMap = new HashMap();
return this;
}
/**
* Indicates whether this message stream is conflating messages by SOW key.
*
* @see #conflate()
* @return true if this message stream is conflating messages
*/
public boolean isConflating() {
return _sowKeyMap != null;
}
/**
*
* Sets the maximum number of messages that will be buffered in memory
* by this stream before taking measures to slow reads. When this threshold
* is exceeded the message stream must push back on the client's background
* reader thread, delaying reads from the client's socket connection until
* the buffer depth reaches or falls below the maxDepth. Due to this
* behavior, there are several caveats that must be observed when using
* a max depth on a message stream:
*
*
*
* - You should not create and use more than one MessageStream instance
* at the same time from a given Client instance. It's also recommended not
* to use this and an asynchronous {@link MessageHandler} on the
* same client instance, since your asynchronous message handler could
* be starved.
*
* - Inside your message processing loop that iterates the MessageStream,
* you should not publish or execute AMPS commands using the same Client
* instance that created the MessageStream you are processing.
*
*
*
* Ignoring these caveats can potentially cause your AMPS client to hang
* because the background thread may never be able to read important
* internal acknowledgement control messages.
*
*
*
* If these caveats are unsuitable for your use case, alternative solutions
* include (1) using the pause/resume or rate options for a bookmark
* subscription; (2) using the top_n/skip_n options to page a large
* SOW query; (3) using message stream conflation with a SOW via
* {@link #conflate()}; (4) use your own asynchronous {@link MessageHandler}
* implementation instead of using this synchronous message stream, so that
* you can process one message at a time without needing to store it; or
* (5) creating an additional client instance so that its background
* reader thread isn't affected by max depth push-back by this
* message stream.
*
*
* @param maxDepth_ The maximum depth to set on the stream's internal buffer.
* The default value comes from
* {@link Client#setDefaultMaxDepth(int)},
* which defaults to {@link Integer#MAX_VALUE}.
*
* @return This message stream instance.
*/
public MessageStream maxDepth(int maxDepth_) {
_maxDepth = maxDepth_;
return this;
}
/**
* Gets the allowed maximum depth of the internal message buffer before
* this stream will push-back on reads from the server.
*
* @see #maxDepth(int)
* @return the current maximum depth
*/
public int getMaxDepth() {
return _maxDepth;
}
/**
* Gets the number of messages currently buffered by this message stream.
* This typically would be no more than maxDepth + 1, unless the maxDepth
* was lowered after messages were buffered.
*
* NOTE: If iterating this stream use hasNext() instead.
*
* @see #hasNext()
* @return the current depth of the message stream
*/
public int getDepth() {
return _q.size();
}
/**
* Used to indicate if there was a change in state with connection.
*
* @param newState_ integer to compare to Disconnected
*/
public void connectionStateChanged(int newState_) {
if (newState_ == Disconnected) {
setState(STATE_Disconnected);
close();
}
}
private void setState(int state_) {
if (_state != STATE_Disconnected)
_state = state_;
}
/**
* Implements {@link Iterator#hasNext} for a message stream. //might need a
* comment
*/
public boolean hasNext() {
if (_client != null
&& !_previousTopic.isNull() && !_previousBookmark.isNull()) {
try {
_client._ack(_previousTopic, _previousBookmark);
} catch (AMPSException e) {
_current = null;
_timedOut = false;
return false;
} finally {
_previousTopic.reset();
_previousBookmark.reset();
}
}
_timedOut = false;
if (_current != null)
return true;
_current = _q.poll();
if (_current != null)
return true;
if (_timeout == 0) {
while (((_state & STATE_Reading) != 0) && _current == null) {
try {
Thread.sleep(1);
} catch (InterruptedException ie) {
_timedOut = true;
return true;
}
_current = _q.poll();
if(_sowKeyMap != null && _current != null) {
_sowKeyLock.lock();
try {
_sowKeyMap.remove(_current.getSowKey());
}
finally { _sowKeyLock.unlock(); }
}
}
// NOTE: A DisconnectedException cannot be thrown from an iterator's hasNext.
return _current != null;
} else {
long startTime = System.currentTimeMillis();
while (((_state & STATE_Reading) != 0) && _current == null && !_timedOut) {
try {
Thread.sleep(1);
} catch (InterruptedException ie) {
_timedOut = true;
return true;
}
_current = _q.poll();
if(_sowKeyMap != null && _current != null) {
_sowKeyLock.lock();
try {
_sowKeyMap.remove(_current.getSowKey());
}
finally { _sowKeyLock.unlock(); }
}
else if(_current == null)
{
_timedOut = System.currentTimeMillis()-startTime > _timeout;
}
}
return (((_state & STATE_Reading) != 0) &&
_timedOut) || _current != null;
}
}
/**
* Implements {@link Iterator#next} for a message stream.
*/
public Message next() {
if (_timedOut) {
_timedOut = false;
return null;
}
if (_current == null) {
if (!hasNext()) {
throw new NoSuchElementException();
}
}
Message retVal = _current;
_current = null;
if (retVal != null) {
int command = retVal.getCommand();
if (_state == STATE_SOWOnly && command == Message.Command.GroupEnd) {
setState(STATE_Complete);
if (_client == null) {
_commandId = null;
_queryId = null;
_subId = null;
} else {
_client.removeConnectionStateListener(this);
if (_commandId != null) {
_client.removeMessageHandler(_commandId);
_commandId = null;
}
if (_queryId != null) {
_client.removeMessageHandler(_queryId);
_queryId = null;
}
if (_subId != null) {
_client.removeMessageHandler(_subId);
_subId = null;
}
}
} else if (_state == STATE_AcksOnly && command == Message.Command.Ack) {
_requestedAcks &= ~(retVal.getAckType());
if (_requestedAcks == 0) {
setState(STATE_Complete);
if (_client == null) {
_commandId = null;
_queryId = null;
_subId = null;
} else {
_client.removeConnectionStateListener(this);
if (_commandId != null) {
_client.removeMessageHandler(_commandId);
_commandId = null;
}
if (_queryId != null) {
_client.removeMessageHandler(_queryId);
_queryId = null;
}
if (_subId != null) {
_client.removeMessageHandler(_subId);
_subId = null;
}
}
}
} else if (command == Message.Command.Publish &&
_client != null && _client.getAutoAck() &&
!retVal.isBookmarkNull() &&
!retVal.isLeasePeriodNull()) {
_previousTopic = retVal.getTopicRaw().copy();
_previousBookmark = retVal.getBookmarkRaw().copy();
}
}
return retVal;
}
public void invoke(Message message) {
if (_sowKeyMap != null && message.getSowKeyRaw().length > 0) {
String sowKey = message.getSowKey();
Message prevMessage = null;
_sowKeyLock.lock();
try {
prevMessage = _sowKeyMap.get(sowKey);
if (prevMessage != null) {
message._copyTo(prevMessage);
}
} finally {
_sowKeyLock.unlock();
}
if (prevMessage == null) {
// No need to be locked here -- we are the only thread to do inserts
while (_q.size() > _maxDepth) {
try {
if (_client != null) {
_client.checkAndSendHeartbeat(false);
}
Thread.sleep(1);
} catch (InterruptedException iex) {
;
}
}
Message newMessage = message.copy();
_sowKeyLock.lock();
try {
_sowKeyMap.put(sowKey,newMessage);
}
finally { _sowKeyLock.unlock(); }
_q.add(newMessage);
}
} else {
while (_q.size() > _maxDepth) {
try {
if (_client != null) {
_client.checkAndSendHeartbeat(false);
}
Thread.sleep(1);
} catch (InterruptedException iex) {
;
}
}
_q.add(message.copy());
}
message.setIgnoreAutoAck();
}
/**
* 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;
}
/**
* Returns true if the connection to AMPS is still active, or false if a
* disconnect is detected.
*
* @return true if the connection is active, false if the {@link Client} has
* disconnected since the message stream was created
*/
public boolean isConnected() {
return _state != STATE_Disconnected;
}
/**
* Closes this MessageStream, unsubscribing from AMPS if applicable.
*/
public void close() {
if (_client == null) {
_commandId = null;
_queryId = null;
_subId = null;
} else {
_client.removeConnectionStateListener(this);
if (_commandId != null) {
if (_state == STATE_Subscribed) {
try {
_client.unsubscribe(_commandId);
} catch (AMPSException e) {
_client.absorbedException(e);
}
} else {
_client.removeMessageHandler(_commandId);
}
_commandId = null;
}
if (_queryId != null) {
if (_state >= STATE_Complete) {
try {
_client.unsubscribe(_queryId);
} catch (AMPSException e) {
_client.absorbedException(e);
}
} else {
_client.removeMessageHandler(_queryId);
}
_queryId = null;
}
if (_subId != null) {
if (_state >= STATE_Complete) {
try {
_client.unsubscribe(_subId);
} catch (AMPSException e) {
_client.absorbedException(e);
}
} else {
_client.removeMessageHandler(_subId);
}
_subId = null;
}
}
setState(STATE_Complete);
}
}