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

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy