com.crankuptheamps.client.MessageStream 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.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 commandId_ 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.
* @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 a 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.
*/
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;
}
if(_sowKeyMap != null)
{
_sowKeyLock.lock();
try {
_current = _q.poll();
if(_current != null)
_sowKeyMap.remove(_current.getSowKey());
}
finally { _sowKeyLock.unlock(); }
}
else
{
_current = _q.poll();
}
}
// 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;
}
if(_sowKeyMap != null)
{
_sowKeyLock.lock();
try {
_current = _q.poll();
if(_current != null)
_sowKeyMap.remove(_current.getSowKey());
}
finally { _sowKeyLock.unlock(); }
}
else
{
_current = _q.poll();
}
if(_current==null)
{
_timedOut = System.currentTimeMillis()-startTime > _timeout;
}
}
if(_sowKeyMap != null && _current != null)
{
_sowKeyMap.remove(_current.getSowKey());
}
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);
_sowKeyLock.unlock();
}
}
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();
_sowKeyMap.put(sowKey,newMessage);
_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);
}
}