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

com.crankuptheamps.client.Client Maven / Gradle / Ivy

There is a newer version: 5.3.4.0
Show newest version
////////////////////////////////////////////////////////////////////////////
//
// 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.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.lang.AutoCloseable;
import java.lang.ClassLoader;
import java.lang.reflect.Method;
import java.lang.Math;
import java.util.Collection;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Properties;
import java.util.Map;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import com.crankuptheamps.client.Store.StoreReplayer;
import com.crankuptheamps.client.exception.AMPSException;
import com.crankuptheamps.client.exception.AlreadyConnectedException;
import com.crankuptheamps.client.exception.AuthenticationException;
import com.crankuptheamps.client.exception.BadFilterException;
import com.crankuptheamps.client.exception.BadRegexTopicException;
import com.crankuptheamps.client.exception.BadSowKeyException;
import com.crankuptheamps.client.exception.CommandException;
import com.crankuptheamps.client.exception.ConnectionException;
import com.crankuptheamps.client.exception.ConnectionRefusedException;
import com.crankuptheamps.client.exception.DisconnectedException;
import com.crankuptheamps.client.exception.DuplicateLogonException;
import com.crankuptheamps.client.exception.InvalidBookmarkException;
import com.crankuptheamps.client.exception.InvalidOptionsException;
import com.crankuptheamps.client.exception.InvalidOrderByException;
import com.crankuptheamps.client.exception.InvalidSubIdException;
import com.crankuptheamps.client.exception.InvalidTopicException;
import com.crankuptheamps.client.exception.InvalidURIException;
import com.crankuptheamps.client.exception.LogonRequiredException;
import com.crankuptheamps.client.exception.NotEntitledException;
import com.crankuptheamps.client.exception.NameInUseException;
import com.crankuptheamps.client.exception.ProtocolException;
import com.crankuptheamps.client.exception.PublishFilterException;
import com.crankuptheamps.client.exception.RetryOperationException;
import com.crankuptheamps.client.exception.StoreException;
import com.crankuptheamps.client.exception.SubidInUseException;
import com.crankuptheamps.client.exception.SubscriptionAlreadyExistsException;
import com.crankuptheamps.client.exception.TimedOutException;
import com.crankuptheamps.client.exception.TransportTypeException;
import com.crankuptheamps.client.exception.UnknownException;
import com.crankuptheamps.client.fields.Field;
import com.crankuptheamps.client.fields.BookmarkField;
import com.crankuptheamps.client.fields.CommandField;
import com.crankuptheamps.client.fields.LongField;
import com.crankuptheamps.client.fields.OptionsField;
import com.crankuptheamps.client.fields.StringField;

/**
 *   The base AMPS client object used in AMPS applications. Each client object
 *   manages a single connection to AMPS. Each AMPS connection has a name,
 *   a specific transport (such as tcp), a protocol (used for framing messages
 *   to AMPS), and a message type (such as FIX or JSON).
 *   

* The client object creates and manages background threads. The object * provides both a synchronous interface for processing messages on the * calling thread and an asynchronous interface suitable for populating * a queue to be processed by worker threads. *

* The Client provides named convenience methods for many AMPS commands. * These methods provide control over commonly used parameters and support * most programs. For full control over AMPS, you build a * {@link com.crankuptheamps.client.Command} object and use * {@link Client#execute } or {@link Client#executeAsync} to run the command. *

* AMPS uses the client name to detect duplicate messages, so the name * of each instance of the client application should be unique. *

* An example of a simple Java program publishing the JSON message * { "message" : "Hello, World!"} is listed below. *


 *    public static void main(String[] args) {
 *    Client client = new Client("ConsolePublisher");
 *
 *   try
 *   {
 *     client.connect("tcp://127.0.0.1:9007/amps");
 *     System.out.println("connected..");
 *     client.logon();
 *    
 *     client.publish("messages", "{\"message\" : \"Hello, World!\"}");
 *
 *     System.exit(0);
 *    }
 *   catch (AMPSException e)
 *   {
 *     System.err.println(e.getLocalizedMessage());
 *     e.printStackTrace(System.err);
 *   }
 *
 *  }
 * 
* */ public class Client implements AutoCloseable { private volatile String name = null; private volatile String nameHash = null; private volatile String logonCorrelationData = null; private Transport transport = null; // Please do not change this name: there are tests that use reflection and depend on it. private long lastSentSequenceNumber = 0; private Store publishStore = null; private volatile boolean connected = false; private volatile boolean _isRetryOnDisconnect = true; private volatile int _defaultMaxDepth = Integer.MAX_VALUE; private volatile URI uri = null; private volatile ClientDisconnectHandler disconnectHandler = new DefaultDisconnectHandler(); protected volatile ExceptionListener exceptionListener = null; private Message message = null; private Command _command = new Command(); private ThreadLocal _publishCommand = new ThreadLocal() { @Override protected Command initialValue() { return new Command(Message.Command.Publish); } }; private ThreadLocal _bookmarksData = new ThreadLocal() { @Override protected byte[] initialValue() { return new byte[64]; } }; private Message beatMessage = null; // Client lock protected final Lock lock = new ReentrantLock(); private final Condition _reconnecting = lock.newCondition(); //private final Lock lock = new DebugLock("HAClient"); // Asynchronous->Synchronous ack handling private final Lock acksLock = new ReentrantLock(); private final Condition ackReceived = acksLock.newCondition(); protected volatile int _lastFailedConnectionVersion = -1; private final HashSet _connectionStateListeners = new HashSet(); // Queue Acking members private volatile long _queueAckTimeout = 0; private volatile boolean _isAutoAckEnabled = false; private volatile int _ackBatchSize = 0; private HashBox _hashBox = new HashBox(); private CommandId _queueAckCommandId = CommandId.nextIdentifier(); private static final OptionsField CANCEL_FIELD = new OptionsField(Message.Options.Cancel); static class QueueBookmarks { private byte[] _topic; private byte[] _data; private long _oldestTime; private volatile int _bookmarkCount; private int _length; } private Map _topicHashMap = new HashMap(); static class AckResponse { public volatile int connectionVersion; public VersionInfo serverVersion; public volatile boolean responded; public volatile boolean abandoned; public volatile int state; public volatile int reason; public volatile String reasonText; public volatile String username; public volatile String password; public volatile long sequence; public volatile String options; public volatile String bookmark; } private Map _acks = new ConcurrentHashMap(); private MessageRouter _routes = new MessageRouter(); private ClientHandler _handler = new ClientHandler(this); private int version = Version.AMPS_5; private static volatile String _version = null; private BookmarkStore bookmarkStore = new DefaultBookmarkStore(); private volatile String username; private volatile SubscriptionManager subscriptionManager = new DefaultSubscriptionManager(); private volatile boolean _reconnectingPublishStore = false; private volatile boolean _reconnectingSubscriptionManager = false; private boolean _defaultSubscriptionManager = true; private volatile FailedWriteHandler _failedWriteHandler = null; private VersionInfo serverVersion = new VersionInfo("default"); private StopWatch heartbeatTimer = new StopWatch(); private int _heartbeatInterval = 0; private int _readTimeout = 0; public static final int MIN_PERSISTED_BOOKMARK_VERSION = 3080000; public static final int MIN_MULTI_BOOKMARK_VERSION = 4000000; private static final int MIN_FLUSH_VERSION = 4000000; private ThreadCreatedHandler _threadCreatedHandler = null; private TransportFilter _transportFilter = null; /** * Specifies a type of message that is always sent to an associated * handler when one is set. All messages of this type will be sent to the * handler in addition to any others that are set based on the command, * sub, or query id in the message. */ public enum GlobalCommandTypeHandlers { Publish, SOW, GroupBegin, GroupEnd, Heartbeat, OOF, Ack, LastChance, DuplicateMessage, COUNT } private final MessageHandler[] _globalCommandTypeHandlers = new MessageHandler[ GlobalCommandTypeHandlers.COUNT.ordinal()]; /** * Class that holds supported versions of AMPS, those are: AMPS version 3, AMPS version 4 and * AMPS version 5. */ public static final class Version { // AMPS_1 and AMPS_2 are no longer supported. // public static final int AMPS_1 = 0x00010000; // public static final int AMPS_2 = 0x00020000; public static final int AMPS_3 = 0x00030000; public static final int AMPS_4 = 0x00040000; public static final int AMPS_5 = 0x00050000; } /** * Provides constants for special bookmark values. All bookmarks, including * these special values, are {@link java.lang.String Strings}. * Functions that take a bookmark such as * {@link Client#bookmarkSubscribe bookmarkSubscribe} can be passed a * bookmark ID, a timestamp with the format YYYYmmddTHHMMSS (as * described in the AMPS User Guide), or one of these special values. * *

For example, to begin a bookmark subscription at the beginning of the * journal, provide {@code Bookmarks.EPOCH} as the bookmark in the * {@link Command} object or the call to * bookmarkSubscribe.

*/ public static final class Bookmarks { /** * Start the subscription at the first undiscarded message in the * bookmark store, or at the end of the bookmark store if all * messages have been discarded. */ public static final String MOST_RECENT = "recent"; /** * Start the subscription at the beginning of the journal. */ public static final String EPOCH = "0"; /** * Start the subscription at the point in time when AMPS processes * the subscription. */ public static final String NOW = "0|1|"; } /** * Creates a client. * * @param name Name for the client. This name is used for duplicate * message detection and should be unique. AMPS does not enforce * specific restrictions on the character set used, however some * protocols (for example, XML) may not allow specific characters. * 60East recommends that the client name be meaningful, short, human * readable, and avoids using control characters, newline characters, * or square brackets. */ public Client(String name) { this(name, null); } /** * Creates a client. * * @param name Name for the client. This name is used for duplicate * message detection and should be unique.AMPS does not enforce * specific restrictions on the character set used, however some * protocols (for example, XML) may not allow specific characters. * 60East recommends that the client name be meaningful, short, human * readable, and avoids using control characters, newline characters, * or square brackets. * @param version Server version connecting to */ public Client(String name, int version) { this(name, null, version); } /** * Creates a client with a transport. * * @param name Name for the client. This name is used for duplicate * message detection and should be unique. * @param transport Transport to use for this client */ public Client(String name, Transport transport) { this(name, transport, Version.AMPS_5); } /** * Creates a client with a transport * * @param name Name for the client. This name is used for duplicate * message detection and should be unique. * @param transport Transport to use with the client * @param version Server version connecting to. Only AMPS_3 and greater are supported. */ public Client(String name, Transport transport, int version) { this.name = name; if (transport != null) { this.transport = transport; this.transport.setMessageHandler(this._handler); this.transport.setDisconnectHandler(this._handler); this.transport.setIdleRunnable(new AckFlusherRunnable()); this.message = this.transport.allocateMessage(); this.message._client = this; this.beatMessage = this.transport.allocateMessage(); this.beatMessage.setCommand(Message.Command.Heartbeat); this.beatMessage.setOptions("beat"); } this.version = version; this.replayer = new ClientStoreReplayer(this); for (int i=0; i < GlobalCommandTypeHandlers.COUNT.ordinal(); ++i) { _globalCommandTypeHandlers[i] = new DefaultMessageHandler(); } } /** * Return the name of the Client. * * @return String Client name */ public String getName() { return this.name; } /** * Return the name hash of the Client as returned by the server at logon. * * @return String Client name */ public String getNameHash() { return this.nameHash; } /** * Return the URI the Client is connected to. * * @return URI */ public URI getURI() { return this.uri; } /** * Return the server version retrieved during logon. If the client has * not logged on or is connected to a server whose version is less than * 3.8.0.0 this function will return 0. * The version uses 2 digits each for major minor maintenance and hotfix * i.e., 3.8.1.5 will return 3080105 * Versions of AMPS prior to 3.8.0.0 did not return the server version * to the client in the logon ack. * Version parts with more than 2 digits will be set to 99. This fuction will * be deprecated in a future version of the client. * @return The version of the server as a long. */ public int getServerVersion() { return this.serverVersion.getOldStyleVersion(); } /** * Return the server version retrieved during logon. If the client has * not logged on or is connected to a server whose version is less than * 3.8.0.0 this function will return a version "default". * Versions of AMPS prior to 3.8.0.0 did not return the server version * to the client in the logon ack. * @return The version of the server as a VersionInfo. */ public VersionInfo getServerVersionInfo() { return this.serverVersion; } /** * WARNING: @deprecated Use {@link VersionInfo#getVersion()} * Return the numeric value for the given version string with the pattern: * Major.Minor.Maintenance.Hotfix * The version uses 2 digits each for major minor maintenance and hotfix * i.e., 3.8.1.5 will return 3080105 * Version strings passed in can be shortened to not include all levels * so 3.8 will return 3080000. * Version parts with more than 2 digits will be set to 99. This fuction will * be deprecated in a future version of the client. * @param version The version string to convert. * @return The version as a int. * @throws CommandException The string doesn't represent a valid version. */ @Deprecated static public int getVersionAsInt(String version) throws CommandException { if (version == null || version.length() == 0) return 0; int retVersion = 0; int c = 0; int dots = 0; int lastDot = -1; byte[] bytes = version.getBytes(StandardCharsets.ISO_8859_1); for (int i=0; dots<4 && i 5) { throw new CommandException("Too many digits between dots found translating version string."); } else if (i-lastDot > 3) { retVersion *= 10; retVersion += 99; } else { if (i-lastDot == 3) { retVersion += bytes[i-2] - '0'; } retVersion *= 10; retVersion += bytes[i-1] - '0'; } lastDot = i; } else if (c < '0' || c > '9') { throw new CommandException("Invalid character found in version string."); } else if (i == version.length()-1) { ++dots; retVersion *= 10; if (i-lastDot > 4) { throw new CommandException("Too many digits between dots found translating version string."); } else if (i-lastDot > 2) { retVersion *= 10; retVersion += 99; } else { if (i-lastDot == 2) { retVersion += bytes[i-1] - '0'; } retVersion *= 10; retVersion += bytes[i] - '0'; } } } for (; dots<4; ++dots) retVersion *= 100; return retVersion; } /** * Return the underlying transport. * For advanced users, this method provides direct access to the transport. * Care needs to be taken to not modify the underlying transport in ways that * are incompatible with the Client. * * @return Transport Underlying transport instance */ public Transport getTransport() { try { lock.lock(); return this.transport; } finally { lock.unlock(); } } /** * Sets the underlying bookmark store, which is used to track * which messages the client has received and which messages have * been processed by the program. This method replaces any existing * bookmark store for the client, without transferring the state of * that store. * * @param val The new bookmark store. * @throws AlreadyConnectedException The store cannot be set while the client is connected. */ public void setBookmarkStore(BookmarkStore val) throws AlreadyConnectedException { try { lock.lock(); if (this.connected) { throw new AlreadyConnectedException("Setting a bookmark store on a connected client is undefined behavior."); } if (val != null) { this.bookmarkStore = val; } else { this.bookmarkStore = new DefaultBookmarkStore(); } } finally { lock.unlock(); } } /** * Returns the underlying bookmark store for this client. The bookmark store * is used to track which messages the client has received and which * messages have been processed by the program. * * @return BookmarkStore The bookmark store for this client. */ public BookmarkStore getBookmarkStore() { return this.bookmarkStore; } /** * Sets the underlying publish store, which is used to store * published messages until the AMPS instance has acknowledged * those messages. This method replaces any existing * publish store for the client, without transferring the state * of that store. * * @param store The new publish store. * @throws AlreadyConnectedException The store cannot be set while the client is connected. */ public void setPublishStore(Store store) throws AlreadyConnectedException { try { lock.lock(); if (this.connected) { throw new AlreadyConnectedException("Setting a publish store on a connected client is undefined behavior."); } this.publishStore = store; } finally { lock.unlock(); } } /** * Returns the underlying publish store for this client. The publish store * is used to store published messages until the AMPS instance has * acknowledged those messages. * * @return Store The publish store for this client. */ public Store getPublishStore() { return this.publishStore; } /** * Connects to the AMPS instance through the provided URI. *

The URI is a {@link String} with the format: * transport://user:password@host:port/protocol *

* *
    *
  • transport -- the network transport used for the connection
  • *
  • user -- the userId used to authenticate the connection
  • *
  • password -- the password used to authenticate the connection
  • *
  • host -- the hostname or IP address of the host where AMPS is running
  • *
  • port -- the port to connect to *
  • protocol -- the protocol used for the connection *
* *

Notice that the protocol can be independent of the message type. * 60East recommends using the amps protocol, although some * installations use one of the legacy protocols such as fix, * nvfix or xml. Contact your server * administrator for the correct URI for the instance.

* *

Authentication is optional if the system is using the default * authentication provided with AMPS. This default authentication allows * all users to authenticate, regardless of the user or password provided. * If your AMPS installation uses custom authentication, use the * Authenticator appropriate for that authentication mechanism. *

* * @param uri The URI string to connect to * @throws ConnectionRefusedException The connection could not be established * @throws AlreadyConnectedException The connection is already connected * @throws InvalidURIException The specified URI is invalid * @throws ProtocolException The protocol is invalid * @throws TransportTypeException The transport type is invalid */ public void connect(String uri) throws ConnectionException { lock.lock(); try { try { this.uri = new URI(uri); } catch (URISyntaxException urisex) { throw new InvalidURIException(urisex); } if(this.transport == null) { Properties props = new URIProperties(this.uri); String uriPath = this.uri.getPath(); String[] sPath = uriPath.substring(1, uriPath.length()).split("/"); String sProtocol = sPath[0]; if(sPath.length > 1) { if(!"amps".equals(sProtocol)) { throw new InvalidURIException("Specification of message type requires amps protocol"); } } Protocol messageType = ProtocolFactory.createProtocol(sProtocol, props); this.transport = TransportFactory .createTransport(this.uri.getScheme(), messageType, props); this.message = this.transport.allocateMessage(); this.message._client = this; this.beatMessage = this.transport.allocateMessage(); this.beatMessage.setCommand(Message.Command.Heartbeat); this.beatMessage.setOptions("beat"); if (publishStore != null) publishStore.setMessage(transport.allocateMessage()); this.transport.setMessageHandler(this._handler); this.transport.setDisconnectHandler(this._handler); this.transport.setIdleRunnable(new AckFlusherRunnable()); } if (_threadCreatedHandler != null) { this.transport.setThreadCreatedHandler(_threadCreatedHandler); } if (exceptionListener != null) { this.transport.setExceptionListener(exceptionListener); } if (_transportFilter != null) { this.transport.setTransportFilter(_transportFilter); } this.transport.connect(this.uri); broadcastConnectionStateChanged(ConnectionStateListener.Connected); this.connected = true; } finally { lock.unlock(); } } /** * Sets the {@link ClientDisconnectHandler}. In the event that the Client is * unintentionally disconnected from AMPS, the invoke method * from the ClientDisconnectHandler will be invoked. * * @param disconnectHandler_ The disconnect handler */ public void setDisconnectHandler(ClientDisconnectHandler disconnectHandler_) { if (disconnectHandler_ != null) { this.disconnectHandler = disconnectHandler_; } else { this.disconnectHandler = new DefaultDisconnectHandler(); } } /** * Returns the current {@link ClientDisconnectHandler} set on self. * * @return The current disconnect handler. */ public ClientDisconnectHandler getDisconnectHandler() { return this.disconnectHandler; } /** * * Sets the {@link MessageHandler} instance used when no other handler matches. * * @param messageHandler The message handler used when no other handler matches. * * @deprecated * Use {@link #setLastChanceMessageHandler } instead. */ @Deprecated public void setUnhandledMessageHandler(MessageHandler messageHandler) { setLastChanceMessageHandler(messageHandler); } /** * Sets the {@link MessageHandler} instance used when no other handler matches. * * @param messageHandler The message handler used when no other handler matches. */ public void setLastChanceMessageHandler(MessageHandler messageHandler) { if (messageHandler != null) { this._globalCommandTypeHandlers[GlobalCommandTypeHandlers.LastChance.ordinal()] = messageHandler; } else { this._globalCommandTypeHandlers[GlobalCommandTypeHandlers.LastChance.ordinal()] = new DefaultMessageHandler(); } } /** * Sets the {@link java.beans.ExceptionListener} instance used for communicating absorbed exceptions. * * @param exceptionListener The exception listener instance to invoke for exceptions. */ public void setExceptionListener(ExceptionListener exceptionListener) { this.exceptionListener = exceptionListener; if (this.transport != null && exceptionListener != null) { this.transport.setExceptionListener(exceptionListener); } } /** * Returns the {@link java.beans.ExceptionListener} instance used for * communicating absorbed exceptions. * * @return The current exception listener. */ public ExceptionListener getExceptionListener() { return this.exceptionListener; } /** * Sets the {@link SubscriptionManager} instance used for recording active subscriptions. * * @param subscriptionManager The subscription manager invoked when the subscriptions change. */ public void setSubscriptionManager(SubscriptionManager subscriptionManager) { if (subscriptionManager != null) { this.subscriptionManager = subscriptionManager; _defaultSubscriptionManager = subscriptionManager instanceof DefaultSubscriptionManager; } else { this.subscriptionManager = new DefaultSubscriptionManager(); _defaultSubscriptionManager = true; } } /** * Returns the {@link SubscriptionManager} instance used for recording active subscriptions. * * @return The current subscription manager. */ public SubscriptionManager getSubscriptionManager() { return this.subscriptionManager; } void absorbedException(Exception e) { try { if(this.exceptionListener != null) { this.exceptionListener.exceptionThrown(e); } } catch(Exception e2) { ; // Absorb exception from the exception listener } } /** * Sets the {@link MessageHandler} instance used for all messages of the * given command type. * @param command The type of command to send to this handler * @param messageHandler The message handler to invoke for duplicate messages * @throws CommandException If the command is not a supported command. */ public void setGlobalCommandTypeMessageHandler(String command, MessageHandler messageHandler) throws CommandException { MessageHandler handler = messageHandler; if (handler == null) { handler = new DefaultMessageHandler(); } switch (command.charAt(0)) { /* These types are not currently allowed to avoid extra branching * in message delivery. case 'p': setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.Publish, handler); break; case 's': setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.SOW, handler); break; case 'g': if (command.charAt(6) == 'b') { setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.GroupBegin, handler); } else if (command.charAt(6) == 'e') { setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.GroupEnd, handler); } else { throw new CommandException("Invalid command " + command + " passed to setGlobalCommandTypeMessageHandler"); } break; case 'o': setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.OOF, handler); break; */ case 'h': setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.Heartbeat, handler); break; case 'a': setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.Ack, handler); break; case 'l': case 'L': setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.LastChance, handler); break; case 'd': case 'D': setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.DuplicateMessage, handler); break; default: throw new CommandException("Invalid command " + command + " passed to setGlobalCommandTypeMessageHandler"); } } /** * Sets the {@link MessageHandler} instance used for all messages of the * given command type. * @param command The Message.Command value of the desired command to send * to this handler. * @param messageHandler The message handler to invoke for duplicate messages * @throws CommandException If the command is not a supported command. */ public void setGlobalCommandTypeMessageHandler(int command, MessageHandler messageHandler) throws CommandException { MessageHandler handler = messageHandler; if (handler == null) { handler = new DefaultMessageHandler(); } switch (command) { /* These types are not currently allowed to avoid extra branching * in message delivery. case Message.Command.Publish: setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.Publish, handler); break; case Message.Command.SOW: setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.SOW, handler); break; case Message.Command.GroupBegin: setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.GroupBegin, handler); break; case Message.Command.GroupEnd: setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.GroupEnd, handler); break; case Message.Command.OOF: setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.OOF, handler); break; */ case Message.Command.Heartbeat: setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.Heartbeat, handler); break; case Message.Command.Ack: setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers.Ack, handler); break; default: throw new CommandException("Invalid command " + command + " passed to setGlobalCommandTypeMessageHandler"); } } /** * Sets the {@link MessageHandler} instance used for all messages of the * given command type. * @param command The GlobalCommandTypeHandlers value of the desired * command to send to this handler. * @param messageHandler The message handler to invoke for duplicate messages * @throws CommandException If the command is not a supported command. */ public void setGlobalCommandTypeMessageHandler( GlobalCommandTypeHandlers command, MessageHandler messageHandler) throws CommandException { MessageHandler handler = messageHandler; if (handler == null) { handler = new DefaultMessageHandler(); } _globalCommandTypeHandlers[command.ordinal()] = handler; } /** * Sets the {@link MessageHandler} instance used for duplicate messages. * * @param messageHandler The message handler to invoke for duplicate messages */ public void setDuplicateMessageHandler(MessageHandler messageHandler) { MessageHandler handler = messageHandler; if (handler == null) { handler = new DefaultMessageHandler(); } this._globalCommandTypeHandlers[GlobalCommandTypeHandlers.DuplicateMessage.ordinal()] = handler; } /** * Returns the {@link MessageHandler} instance used for duplicate messages. * * @return The message handler to invoke for duplicate messages */ public MessageHandler getDuplicateMessageHandler() { return this._globalCommandTypeHandlers[GlobalCommandTypeHandlers.DuplicateMessage.ordinal()]; } /** * Sets the {@link FailedWriteHandler} instance used to report on failed * messages that have been written. * * @param handler_ The handler to invoke for published duplicates. */ public void setFailedWriteHandler(FailedWriteHandlerV4 handler_) { this._failedWriteHandler = new FailedWriteHandlerV4Compat(handler_); } /** * Sets the {@link FailedWriteHandler} instance used to report on failed * messages that have been written. * * @param handler_ The handler to invoke for published duplicates. */ public void setFailedWriteHandler(FailedWriteHandler handler_) { this._failedWriteHandler = handler_; } /** * Returns the {@link FailedWriteHandler} instance used to report on failed * messages that have been written. * * @return The handler to invoke for published duplicates. */ public FailedWriteHandler getFailedWriteHandler() { return this._failedWriteHandler; } /** * Adds a {@link ConnectionStateListener} instance that will be invoked when this client connects or disconnects. * @param listener_ The instance to invoke. * @since 4.0.0.0 */ public void addConnectionStateListener(ConnectionStateListener listener_) { lock.lock(); try { this._connectionStateListeners.add(listener_); } finally { lock.unlock(); } } /** * Removes a {@link ConnectionStateListener} from being invoked when this client connects or disconnects. * @param listener_ The instance to remove. * @since 4.0.0.0 */ public void removeConnectionStateListener(ConnectionStateListener listener_) { lock.lock(); try { this._connectionStateListeners.remove(listener_); } finally { lock.unlock(); } } protected void broadcastConnectionStateChanged(int newState_) { // Get a copy of the listeners under lock, then broadcast. // Allows listeners to remove themselves. ConnectionStateListener[] listeners = null; lock.lock(); try { listeners = new ConnectionStateListener[_connectionStateListeners.size()]; _connectionStateListeners.toArray(listeners); } finally { lock.unlock(); } for(ConnectionStateListener listener : listeners) { try { listener.connectionStateChanged(newState_); } catch(Exception ex_) { absorbedException(ex_); } } } /** * Disconnect from the AMPS server. * */ public void disconnect() { _disconnect(false); } /** * Disconnect from the AMPS server. * */ void _disconnect(boolean reconnecting) { Transport currentTransport = null; try { if(!reconnecting && getAckBatchSize() > 1) flushAcks(); } catch(AMPSException e) { absorbedException(e); } lock.lock(); try { currentTransport = this.transport; heartbeatTimer.setTimeout(0); if (this.connected) broadcastConnectionStateChanged(ConnectionStateListener.Disconnected); this.connected = false; } finally { lock.unlock(); } // don't disconnect the transport with the client lock held. if(currentTransport != null) currentTransport.disconnect(); lock.lock(); try { // only disconnect if a transport has been created _routes.clear(); // Signal and then clear the ack info cancelSynchronousWaiters(Integer.MAX_VALUE); // Only broadcast Shutdown if we're not reconnecting. if (!reconnecting) { broadcastConnectionStateChanged(ConnectionStateListener.Shutdown); } } catch(Exception e) { absorbedException(e); } finally { lock.unlock(); } } /** * Disconnect from the AMPS server. * */ public void close() { disconnect(); } /** * Creates a new {@link Message} appropriate for this client. This function should be * called rarely, since it does allocate a handful of small objects. Users sensitive * to garbage collection delays should cache the message object for later usage. * * @return A new {@link Message} instance * @throws DisconnectedException When the client has been closed or has never been connected. */ public Message allocateMessage() throws DisconnectedException { if (this.transport == null) { throw new DisconnectedException("Cannot allocate a message " + " while not connected."); } try { lock.lock(); return this.transport.allocateMessage(); } finally { lock.unlock(); } } /** * Send a {@link Message} to AMPS via the {@link Transport} used in the Client. * This method is provided for special cases. In general, you can get the * same results with additional error checking by using a {@link Command} * object with the execute or executeAsync methods. * * @param message The message to send * @throws DisconnectedException The connection was disconnected at time of send */ public void send(Message message) throws DisconnectedException { lock.lock(); try { sendInternal(message); } finally { lock.unlock(); } } /** * Adds a MessageHandler for a given CommandId to self. * @param commandId_ The command, query, or subid used to invoke the handler. * @param messageHandler_ The message handler to route to * @param requestedAcks_ The acks requested to be sent to the handler * @param isSubscribe_ True if this route is for an ongoing subscription */ public void addMessageHandler(CommandId commandId_, MessageHandler messageHandler_, int requestedAcks_, boolean isSubscribe_) { _routes.addRoute(commandId_, messageHandler_, requestedAcks_, 0, isSubscribe_); } /** * Remove a MessageHandler for a given CommandId from self. * @param commandId_ The commandId for the handler to remove * @return true if the handler was removed. */ public boolean removeMessageHandler(CommandId commandId_) { return _routes.removeRoute(commandId_); } private int sendWithoutRetry(Message message) throws DisconnectedException { if (this.transport != null) { int version = transport.getVersion(); transport.sendWithoutRetry(message); return version; } else throw new DisconnectedException("There is no current connection"); } private int sendInternal(Message message) throws DisconnectedException { return sendInternal(message, 0, false); } private int sendInternal(Message message, long haSeq) throws DisconnectedException { return sendInternal(message, haSeq, false); } /** Only messages stored in a publish store will set haSeq to non-zero * Only messages stored in a subscription manager will set isHASubscribe. */ private int sendInternal(Message message, long haSeq, boolean isSubscribe) throws DisconnectedException { assert ((ReentrantLock)lock).isHeldByCurrentThread(); while(true) { // It's not safe to HA publish while a reconnect is in progress. // When a reconnect is in progress, we might be able to sneak // a publish through, but if it's sequence number is higher // than the beginning of what we need to replay, the server will // take that message to mean that we're done replaying, and ignore // what we replay. If, once we have the lock, we discover // it's a bad time to HA publish, we'll spin waiting for the // reconnect to finish up, and then go. if (haSeq != 0 && _reconnectingPublishStore) { // If retry is disabled, give up if (!_isRetryOnDisconnect) throw new DisconnectedException(); // An interrupted exception is ignored to recheck try { _reconnecting.await(1000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { } } else { Transport currentTransport = this.transport; if (currentTransport == null) { throw new DisconnectedException("There is no current connection."); } int version = currentTransport.getVersion(); if ((haSeq != 0 && haSeq <= lastSentSequenceNumber) || (isSubscribe && _reconnectingSubscriptionManager)) return -1; // It's possible to get here out of sequence number order, // try sending lower sequence number messages first. while (haSeq > (lastSentSequenceNumber + 1)) { try { // This increments lastSentSequenceNumber if (!publishStore.replaySingle(replayer, lastSentSequenceNumber + 1)) { // An interrupted exception is ignored to recheck try {_reconnecting.await(1000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { } } } catch(AMPSException ex) { return version; } } try { currentTransport.sendWithoutRetry(message); if (haSeq != 0) { ++lastSentSequenceNumber; } return version; } catch(DisconnectedException d) { if (_isRetryOnDisconnect && message.getCommand() != Message.Command.Logon) { // attempt to reconnect. If we get anything // other than a Retry(meaning it succeeded), // let the exception flow upwards. lock.unlock(); try { currentTransport.handleCloseEvent(version, "Error occured while sending message", d); } catch(RetryOperationException r) { if (isSubscribe || haSeq != 0) return version; } finally { lock.lock(); } } else { throw d; } } } } } private boolean _registerHandler(CommandId qid, CommandId subid, CommandId cid, MessageHandler messageHandler, int requestedAcks, int systemAddedAcks, boolean isSubscribe) { boolean added = (qid != null || subid != null || cid != null); if(qid != null && _routes.findRoute(qid) == null) { _routes.addRoute(qid, messageHandler, requestedAcks, systemAddedAcks, isSubscribe); added = true; } if(subid!=null && _routes.findRoute(subid) == null) { if(qid == null || !subid.equals(qid)) { _routes.addRoute(subid, messageHandler, requestedAcks, systemAddedAcks, isSubscribe); } if (requestedAcks != Message.AckType.None && cid != subid) { _routes.addRoute(cid, messageHandler, requestedAcks, systemAddedAcks, isSubscribe); } added = true; } else if (cid!=null && (qid == null || !cid.equals(qid)) && _routes.findRoute(cid) == null) { _routes.addRoute(cid, messageHandler, requestedAcks, systemAddedAcks, isSubscribe); added = true; } return added; } /** * * Set whether or not messages being sent to the server should retry if the * client is disconnected. This is most useful if you are publishing data that * has a very short lifetime and so is no longer relevant after the time it * takes to reconnect. * * @param isRetryOnDisconnect False to disable default behavior of autmatic retry. * **/ public void setRetryOnDisconnect(boolean isRetryOnDisconnect) { _isRetryOnDisconnect = isRetryOnDisconnect; } /** * * Return whether or not messages being sent to the server will retry if the * client is disconnected. * * @return true if it will retry, false if not. * **/ public boolean getRetryOnDisconnect() { return _isRetryOnDisconnect; } /** *

* Sets the default value used for the max depth of {@link MessageStream} * instances created by this client. This value defaults to * {@link Integer#MAX_VALUE}. Note: changing this value will not affect * any message stream instances that have already been created by this * client; instead use {@link MessageStream#maxDepth(int)}. *

*

* WARNING: Before setting this, be sure to read important limitations * and restrictions of this feature discussed here: * {@link MessageStream#maxDepth(int)} *

* * @param md The default max depth of all message stream instances created * by this client instance. * * @see MessageStream#maxDepth(int) */ public void setDefaultMaxDepth(int md) { _defaultMaxDepth = md; } /** * Gets the currently set default value for the max depth of * {@link MessageStream} instances created by this client. * * @see #setDefaultMaxDepth(int) * @return The current default max depth. */ public int getDefaultMaxDepth() { return _defaultMaxDepth; } /** * Send a {@link Message} to AMPS and register the messageHandler for any messages * resulting from the command execution. * * This method is provided for special cases. In general, you can get the * same results with additional error checking by using a {@link Command} * object with the execute or executeAsync methods. * * @param messageHandler The message handler that'll receive messages for this command * @param message The message to send * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command, or null if none is assigned. * @throws AMPSException An exception occurred while sending or waiting for a response to this Message. */ @SuppressWarnings("fallthrough") public CommandId send(MessageHandler messageHandler, Message message, long timeout) throws AMPSException { CommandId id = null; String strId = message.getCommandId(); if (strId != null) { id = new CommandId(strId); } int requestedAcks = message.getAckTypeOutgoing(); int systemAddedAcks = 0; boolean isSubscribe = false; switch(message.getCommand()) { // Subscriptions need to have a sub id case Message.Command.Subscribe: case Message.Command.DeltaSubscribe: if (this.bookmarkStore != null && message.getBookmark() != null) { systemAddedAcks |= Message.AckType.Persisted; } // fall through case Message.Command.SOWAndSubscribe: case Message.Command.SOWAndDeltaSubscribe: if (id == null) { id = CommandId.nextIdentifier(); message.setCommandId(id); } if (message.getSubId() == null) { message.setSubId(id); } isSubscribe = true; // fall through. case Message.Command.SOW: // SOW queries just need a QueryId. if (id == null) { id = CommandId.nextIdentifier(); message.setCommandId(id); } if (message.getQueryId() == null) { message.setQueryId(id); } systemAddedAcks |= Message.AckType.Processed; // for SOW only, we get a completed ack so we know when to remove the handler. if (!isSubscribe) systemAddedAcks |= Message.AckType.Completed; message.setAckType(requestedAcks|systemAddedAcks); if (messageHandler != null) { CommandId subCmdId = new CommandId(message.getSubId()); CommandId qid = new CommandId(message.getQueryId()); // This will never return false, we set ids in this function. _registerHandler(qid, subCmdId, id, messageHandler, requestedAcks, systemAddedAcks, isSubscribe); } lock.lock(); try { // We aren't adding to subscription manager, so this isn't // an HA subscribe. this.syncAckProcessing(id, message, timeout, 0, false); } catch(TimedOutException e) { _routes.removeRoute(id); throw e; } finally { lock.unlock(); } break; // These are valid commands that are used as-is case Message.Command.Unsubscribe: case Message.Command.Heartbeat: case Message.Command.Logon: case Message.Command.StartTimer: case Message.Command.StopTimer: case Message.Command.DeltaPublish: case Message.Command.Publish: case Message.Command.SOWDelete: // if an ack is requested, it'll need a command ID. if(requestedAcks != Message.AckType.None) { if (id == null) { id = CommandId.nextIdentifier(); message.setCommandId(id); } if(messageHandler != null) { _routes.addRoute(id, messageHandler, requestedAcks, 0, false); } } this.send(message); break; // These are things that shouldn't be sent (not meaningful) case Message.Command.GroupBegin: case Message.Command.GroupEnd: case Message.Command.OOF: case Message.Command.Ack: case Message.Command.Unknown: default: throw new CommandException("Command type can not be sent directly to AMPS"); } return id; } /** * Execute the provided command and return messages received in response * in a {@link MessageStream}. * * This method creates a message based on the provided {@link Command}, * sends the message to AMPS, and receives the results. AMPS sends the * message and receives the results on a background thread. That thread * populates the MessageStream returned by this method. * * @param command The Command object containing the command to send to AMPS * @return A MessageStream that provides messages received in response to the command * @since 4.0.0.0 * @throws AMPSException Thrown when the command fails. */ public MessageStream execute(Command command) throws AMPSException { // If the command is sow and has a sub_id, OR // if the command has a replace option, return the existing // messagestream, don't create a new one. CommandId cid = null; CommandId subIdCmdId = command.getSubId(); int commandEnum = command.getCommand(); int ackType = command.getAckType(); String options = command.getOptions(); boolean useExistingHandler = subIdCmdId != null && ((options != null && options.contains("replace")) || commandEnum == Message.Command.SOW); if(useExistingHandler) { // Try to find the exist message handler. byte[] subId = subIdCmdId.id; if(subId != null) { MessageHandler existingHandler = _routes.findRoute(subIdCmdId); if (existingHandler != null) { // we found an existing handler. It might not be a message stream, but that's okay. executeAsync(command, existingHandler); // This will return null if handler was not a messagestream. return existingHandler instanceof MessageStream ? (MessageStream)existingHandler:null; } } // fall through; we'll use a new handler altogether. } else if((commandEnum & Message.Command.NoDataCommands) !=0 && (ackType == Message.AckType.Persisted || ackType == Message.AckType.None)) { // For publish/delta_publish with no acks, empty message stream executeAsync(command, null); return MessageStream.getEmptyMessageStream(); } MessageStream result = new MessageStream(this); // Set the default value for max depth on the message stream. result.maxDepth(_defaultMaxDepth); try { cid = executeAsync(command, result); } catch (AMPSException e) { // If any exception causes executeAsync() to exit abnormally, // be sure to close resources associated with the message stream. result.close(); throw e; } boolean statsAck = (commandEnum != Message.Command.Subscribe && commandEnum != Message.Command.DeltaSubscribe && commandEnum != Message.Command.SOWAndSubscribe && commandEnum != Message.Command.SOWAndDeltaSubscribe && (command._message.getAckTypeOutgoing() & Message.AckType.Stats) != 0); if(statsAck) { result.setStatsOnly(); result.setQueryId(command.getQueryId()); } else if (commandEnum == Message.Command.SOW) { result.setSOWOnly(); result.setQueryId(command.getQueryId()); } else if (cid!=null) { if (command.isSubscribe()) { result.setSubscription(cid); if (!cid.equals(command.getCommandId())) { result.setCommandId(command.getCommandId()); } if (commandEnum == Message.Command.SOWAndSubscribe || commandEnum == Message.Command.SOWAndDeltaSubscribe) { result.setQueryId(command.getQueryId()); } } else if (commandEnum == Message.Command.Publish || commandEnum == Message.Command.DeltaPublish || commandEnum == Message.Command.SOWDelete) { result.setAcksOnly(cid, command._message.getAckTypeOutgoing() & ~Message.AckType.Persisted); } else { result.setAcksOnly(cid, command._message.getAckTypeOutgoing()); } } else { result.close(); result = null; } return result; } /** * Execute the provided command and process responses on * the client receive thread using the provided handler. * * This method creates a message based on the provided {@link Command}, * sends the message to AMPS, and invokes the provided * {@link MessageHandler } to process messages returned in response * to the command. Rather than providing messages on the calling thread, * AMPS runs the MessageHandler directly on the client receive thread. * * When the provided MessageHandler is not null, this method blocks * until AMPS acknowledges that the command has been processed. The results * of the command after that acknowledgement are provided to the * MessageHandler. * * @param command The Command object containing the command to send to AMPS * @param handler The MessageHandler to invoke to process messages received * @return the CommandId for the command * @since 4.0.0.0 * @throws AMPSException Thrown when the command fails. */ public CommandId executeAsync(Command command, MessageHandler handler) throws AMPSException { lock.lock(); try { return executeAsyncNoLock(command, handler); } finally { lock.unlock(); } } // This is a bit complicated, but we're basically doing all the stuff we // need to do here to get the Client's state to reflect the command we're // about to send -- for example, record the subscription, set a client // sequence number, add a handler, and determine whether or not to wait for // a processed ack. private CommandId executeAsyncNoLock(Command command, MessageHandler messageHandler) throws AMPSException { CommandId cid = command.prepare(this); CommandId subid = command.getSubId(); CommandId qid = command.getQueryId(); int commandEnum = command.getCommand(); int systemAddedAcks = messageHandler == null ? Message.AckType.None : Message.AckType.Processed; int requestedAcks = command.getAckType(); final boolean isPublishStore = this.publishStore != null && command.needsSequenceNumber(); if(commandEnum == Message.Command.SOW) systemAddedAcks |= Message.AckType.Completed; // fixup bookmark and apply ack types as necessary. String bookmark = command.getBookmark(); if(bookmark != null) { if (this.bookmarkStore != null) { if(command.isSubscribe()) { systemAddedAcks |= Message.AckType.Persisted; } StringField subIdField = new StringField(); subIdField.setValue(subid); if(Client.Bookmarks.MOST_RECENT.equals(bookmark)) { Field mostRecent = bookmarkStore.getMostRecent(command._message.getSubIdRaw()); command._message.setBookmark(mostRecent.buffer, mostRecent.position, mostRecent.length); } else if (!Client.Bookmarks.EPOCH.equals(bookmark) && !Client.Bookmarks.NOW.equals(bookmark) && !bookmarkStore.getMostRecent(subIdField).toString().equals(bookmark)) { command._message.setBookmark(bookmark); if (!bookmarkStore.isDiscarded(command._message)) { if (bookmarkStore.log(command._message) > 0) { bookmarkStore.discard(command._message); bookmarkStore.persisted( command._message.getSubIdRaw(), command._message.getBookmarkRaw()); } } } } else if(Client.Bookmarks.MOST_RECENT.equals(bookmark)) { command._message.setBookmark(Bookmarks.EPOCH); } } // If this is destined for a publish store, make sure to request a persisted ack before we add routes if(isPublishStore) { systemAddedAcks |= Message.AckType.Persisted; } boolean isSubscribe = command.isSubscribe(); // Register any message handler supplied if not a subscribe command. // Subscription handlers will be registered later, when we're sure we // won't be interfering with resubscription by subscription manager. boolean isRegistered = false; if(messageHandler!=null && !isSubscribe) { if (!_registerHandler(qid, subid, cid, messageHandler, requestedAcks, systemAddedAcks, isSubscribe)) { if ((commandEnum & Message.Command.NoDataCommands) != 0) { cid = command.setCommandId(CommandId.nextIdentifier()) .getCommandId(); command._message.setCommandId(cid); _routes.addRoute(cid, messageHandler, requestedAcks, systemAddedAcks, isSubscribe); } else { throw new IllegalArgumentException("To use a messagehandler, you must also supply a command or subscription ID."); } } isRegistered = true; } final boolean useSyncSend = cid!=null && ((systemAddedAcks|requestedAcks)&Message.AckType.Processed)!=0; // if there's a command id & a processed ack type set, use syncAckProcessing. try { if(isPublishStore) { command._message.setAckType(systemAddedAcks|requestedAcks); while (true) { try { lock.unlock(); publishStore.store(command._message); } finally { lock.lock(); } command.setClientSequenceNumber(command._message.getSequence()); if(useSyncSend) syncAckProcessing(cid, command._message, command.getTimeout(), command._message.getSequence(), false); else sendInternal(command._message, command._message.getSequence()); break; } } else { if(isSubscribe) { // Cache values we'll need, since the command could be // invalid after syncAckProcessing. subid = new CommandId(subid.id, 0, subid.id.length); if (!_defaultSubscriptionManager) { boolean existingSub = (_routes.findRoute(subid) != null); try { Message message = command._message.copy(); if (cid != null) message.getCommandId(cid); lock.unlock(); try { this.subscriptionManager.subscribe(messageHandler, message); } finally { lock.lock(); } if (cid != null) message.setCommandId(cid); message.setAckType(systemAddedAcks|requestedAcks); if(useSyncSend) { if (!_registerHandler(qid, subid, cid, messageHandler, requestedAcks, systemAddedAcks, isSubscribe)) { command._message.setAckType(requestedAcks); throw new IllegalArgumentException("To use a messagehandler, you must also supply a command or subscription ID."); } isRegistered = !existingSub; syncAckProcessing(cid,message,command.getTimeout(), 0, !(messageHandler instanceof MessageStream)); } else { sendInternal(message, 0, !(messageHandler instanceof MessageStream)); } } catch (AMPSException ex) { if (!existingSub) { lock.unlock(); try { subscriptionManager.unsubscribe(subid); } finally { lock.lock(); } } throw ex; } } else { command._message.setAckType(systemAddedAcks|requestedAcks); if(useSyncSend) { if (!_registerHandler(qid, subid, cid, messageHandler, requestedAcks, systemAddedAcks, isSubscribe)) { command._message.setAckType(requestedAcks); throw new IllegalArgumentException("To use a messagehandler, you must also supply a command or subscription ID."); } isRegistered = true; syncAckProcessing(cid,command._message,command.getTimeout(), 0, false); } else { sendInternal(command._message, 0, true); } } } else { command._message.setAckType(systemAddedAcks|requestedAcks); if(useSyncSend) syncAckProcessing(cid,command._message,command.getTimeout()); else sendInternal(command._message); } } } catch (DisconnectedException ex) { // Need to remove any registered handlers due to failure // Unless it's saved in the publish store and will be // resent after a reconnect without restoring the handler. if (isRegistered && !isPublishStore) { _routes.removeRoute(cid); if (qid != null && !cid.equals(qid)) { _routes.removeRoute(qid); } if (subid != null && !cid.equals(subid)) { _routes.removeRoute(subid); } } throw ex; } catch (AMPSException ex) { // Need to remove any registered handlers due to failure // Unless it's saved in the publish store and will be // resent after a reconnect without restoring the handler. if (isRegistered && !isPublishStore) { _routes.removeRoute(cid); if (qid != null && !cid.equals(qid)) { _routes.removeRoute(qid); } if (subid != null && !cid.equals(subid)) { _routes.removeRoute(subid); } } throw ex; } command._message.setAckType(requestedAcks); if(subid != null) return subid; if(qid != null) return qid; return cid; } /** * Requests a server heartbeat, and configures the client to close the connection * if a heartbeat (or other activity) is not seen on the connection. * * @param intervalSeconds_ The time (seconds) between beats from the server. * @param timeoutSeconds_ The time (seconds) to allow silence on the connection before assuming it is dead. * @throws DisconnectedException The client became disconnected while attempting to send the heartbeat request. */ public void setHeartbeat(int intervalSeconds_, int timeoutSeconds_) throws DisconnectedException { _heartbeatInterval = intervalSeconds_; _readTimeout = Math.max(timeoutSeconds_, intervalSeconds_); _sendHeartbeat(); } /** * Requests a server heartbeat, and configures the client to close the connection * if a heartbeat (or other activity) is not seen on the connection after two heartbeat intervals. * * @param intervalSeconds_ The time (seconds) between beats from the server. * @throws DisconnectedException The client became disconnected while attempting to send the heartbeat request. */ public void setHeartbeat(int intervalSeconds_) throws DisconnectedException { setHeartbeat(intervalSeconds_, intervalSeconds_ * 2); } /** * Returns the heartbeat interval used by the Client. A value of 0 means heartbeats are disabled. * * @return The heartbeat inverval in seconds. */ public int getHeartbeatInterval() { return _heartbeatInterval; } /** * Returns the time (in seconds) to allow silence on the connection before assuming it is dead. * * @return The read timeout in seconds. */ public int getReadTimeout() { return _readTimeout; } /** * Sends the cached heartbeat settings to the server in the form of a * heartbeat command, and configures the read timeout on the socket. */ void _sendHeartbeat() throws DisconnectedException { if(this.transport != null) { lock.lock(); try { if(_heartbeatInterval > 0) { this.message.reset(); this.message.setCommand(Message.Command.Heartbeat); this.message.setOptions(String.format("start,%d",_heartbeatInterval)); this.heartbeatTimer.setTimeout(_heartbeatInterval * 1000L); this.heartbeatTimer.start(); this.transport.sendWithoutRetry(this.message); this.transport.setReadTimeout(_readTimeout * 1000); broadcastConnectionStateChanged(ConnectionStateListener.HeartbeatInitiated); } } finally { lock.unlock(); } } } /** * Publish a message to an AMPS topic. If the client * has a publish store set, then the client will store the message * before forwarding the message to AMPS. This method does not wait * for a response from the AMPS server. To detect failure, install * a failed write handler. If a DisconnectException occurs, * the message is still stored in the publish store. * * @param topic the topic to publish to * @param topicOffset the offset in topic where the topic begins * @param topicLength the length of the topic in the topic array * @param data the data to publish * @param dataOffset the offset in data where data begins * @param dataLength the length of the data * @throws DisconnectedException the client is not connected; the program needs to call connect() * @throws StoreException An error occurred writing to the local HA store. * @return the sequence number for the message when a persistent store is configured for the client, 0 if no store is configured */ public long publish( byte[] topic, int topicOffset, int topicLength, byte[] data , int dataOffset , int dataLength) throws AMPSException { if(topicLength ==0) { throw new InvalidTopicException("A topic must be specified."); } Command publishCommand = _publishCommand.get(); publishCommand.reset(Message.Command.Publish) .setTopic(topic,topicOffset,topicLength) .setData(data,dataOffset,dataLength); executeAsync(publishCommand, null); return publishCommand.getClientSequenceNumber(); } /** * Publish a message to an AMPS topic. If the client * has a publish store set, then the client will store the message * before forwarding the message to AMPS. This method does not wait * for a response from the AMPS server. To detect failure, install * a failed write handler. If a DisconnectException occurs, * the message is still stored in the publish store. * * @param topic the topic to publish the data to * @param data the data to publish to the topic * @throws DisconnectedException the client is not connected; the program needs to call connect() * @throws StoreException An error occurred writing to the local HA store. * @return the sequence number for the message when a persistent store is configured for the client, 0 if no store is configured */ public long publish( String topic, String data) throws AMPSException { if(topic==null || topic.length() ==0) { throw new InvalidTopicException("A topic must be specified."); } Command publishCommand = _publishCommand.get(); publishCommand.reset(Message.Command.Publish) .setTopic(topic).setData(data); executeAsync(publishCommand, null); return publishCommand.getClientSequenceNumber(); } /** * Publish a message to an AMPS topic. If the client * was created with a persistent store on construction, then the client will store * before forwarding the message to AMPS. In this store-and-forward case, the persistent * store will notify the user, when requested via callback, of the successful persistence * of the record within AMPS. If a DisconnectedException occurs, the message is still * stored in the publish store. * * @param topic the topic to publish to * @param topicOffset the offset in topic where the topic begins * @param topicLength the length of the topic in the topic array * @param data the data to publish * @param dataOffset the offset in data where data begins * @param dataLength the length of the data * @param expiration the number of seconds until the message expires * @throws DisconnectedException the client is not connected; the program needs to call connect() * @throws StoreException An error occurred writing to the local HA store. * @return the sequence number for the message when a persistent store is configured for the client, 0 if no store is configured */ public long publish( byte[] topic, int topicOffset, int topicLength, byte[] data , int dataOffset , int dataLength, int expiration) throws AMPSException { if(topicLength == 0) { throw new InvalidTopicException("A topic must be specified."); } Command publishCommand = _publishCommand.get(); publishCommand.reset(Message.Command.Publish) .setTopic(topic,topicOffset,topicLength) .setData(data,dataOffset,dataLength) .setExpiration(expiration); executeAsync(publishCommand, null); return publishCommand.getClientSequenceNumber(); } /** * Publish a message to an AMPS topic. If the client * was created with a persistent store on construction, then the client will store * before forwarding the message to AMPS. In this store-and-forward case, the persistent * store will notify the user, when requested via callback, of the successful persistence * of the record within AMPS. If a DisconnectedException occurs, the message is still * stored in the publish store. * * @param topic Topic to publish the data to * @param data Data to publish to the topic * @param expiration The number of seconds until the message expires * @throws DisconnectedException The client is not connected; the program needs to call connect() * @throws StoreException An error occurred writing to the local HA store. * @return the sequence number for the message when a persistent store is configured for the client, 0 if no store is configured */ public long publish( String topic, String data, int expiration) throws AMPSException { if(topic==null || topic.length() == 0) { throw new InvalidTopicException("A topic must be specified."); } Command publishCommand = _publishCommand.get(); publishCommand.reset(Message.Command.Publish) .setTopic(topic) .setData(data) .setExpiration(expiration); executeAsync(publishCommand, null); return publishCommand.getClientSequenceNumber(); } /** * Delta publish a message to an AMPS topic. If the client * has a publish store set, then the client will store the message * before forwarding the message to AMPS. This method does not wait * for a response from the AMPS server. To detect failure, install * a failed write handler. If a DisconnectException occurs, * the message is still stored in the publish store. * * @param topic Topic to publish the data to * @param topicOffset offset into topic array where the topic name begins * @param topicLength length of topic array * @param data Data to publish to the topic * @param dataOffset offset into data array where the data begins * @param dataLength length of data array * @throws DisconnectedException The client was disconnected at time of publish * @throws StoreException An error occurred writing to the local HA store. * @return the sequence number for the message when a persistent store is configured for the client, 0 if no store is configured */ public long deltaPublish( byte[] topic, int topicOffset, int topicLength, byte[] data, int dataOffset, int dataLength) throws AMPSException { if(topicLength == 0) { throw new InvalidTopicException("A topic must be specified."); } Command publishCommand = _publishCommand.get(); publishCommand.reset(Message.Command.DeltaPublish) .setTopic(topic,topicOffset,topicLength) .setData(data,dataOffset,dataLength); executeAsync(publishCommand, null); return publishCommand.getClientSequenceNumber(); } /** * Delta publish a message to an AMPS topic. If the client * has a publish store set, then the client will store the message * before forwarding the message to AMPS. This method does not wait * for a response from the AMPS server. To detect failure, install * a failed write handler. If a DisconnectException occurs, * the message is still stored in the publish store. * * @param topic Topic to publish the data to * @param data Data to publish to the topic * @throws DisconnectedException The client was disconnected at time of publish * @throws StoreException An error occurred writing to the local HA store. * @return the sequence number for the message when a persistent store is configured for the client, 0 if no store is configured */ public long deltaPublish( String topic, String data) throws AMPSException { if(topic==null || topic.length() == 0) { throw new InvalidTopicException("A topic must be specified."); } Command publishCommand = _publishCommand.get(); publishCommand.reset(Message.Command.DeltaPublish) .setTopic(topic) .setData(data); executeAsync(publishCommand,null); return publishCommand.getClientSequenceNumber(); } /** * Delta ublish a message to an AMPS topic. If the client * has a publish store set, then the client will store the message * before forwarding the message to AMPS. This method does not wait * for a response from the AMPS server. To detect failure, install * a failed write handler. If a DisconnectException occurs, * the message is still stored in the publish store. * * @param topic Topic to publish the data to * @param topicOffset offset into topic array where the topic name begins * @param topicLength length of topic array * @param data Data to publish to the topic * @param dataOffset offset into data array where the data begins * @param dataLength length of data array * @param expiration the number of seconds until the message expires * @throws DisconnectedException The client was disconnected at time of publish * @throws StoreException An error occurred writing to the local HA store. * @return the sequence number for the message when a persistent store is configured for the client, 0 if no store is configured */ public long deltaPublish( byte[] topic, int topicOffset, int topicLength, byte[] data, int dataOffset, int dataLength, int expiration) throws AMPSException { if(topicLength == 0) { throw new InvalidTopicException("A topic must be specified."); } Command publishCommand = _publishCommand.get(); publishCommand.reset(Message.Command.DeltaPublish) .setTopic(topic,topicOffset,topicLength) .setData(data,dataOffset,dataLength) .setExpiration(expiration); executeAsync(publishCommand, null); return publishCommand.getClientSequenceNumber(); } /** * Delta publish a message to an AMPS topic. If the client * has a publish store set, then the client will store the message * before forwarding the message to AMPS. This method does not wait * for a response from the AMPS server. To detect failure, install * a failed write handler. If a DisconnectException occurs, * the message is still stored in the publish store. * * @param topic Topic to publish the data to * @param data Data to publish to the topic * @param expiration the number of seconds until the message expires * @throws DisconnectedException The client was disconnected at time of publish * @throws StoreException An error occurred writing to the local HA store. * @return the sequence number for the message when a persistent store is configured for the client, 0 if no store is configured */ public long deltaPublish( String topic, String data, int expiration) throws AMPSException { if(topic==null || topic.length() == 0) { throw new InvalidTopicException("A topic must be specified."); } Command publishCommand = _publishCommand.get(); publishCommand.reset(Message.Command.DeltaPublish) .setTopic(topic) .setData(data) .setExpiration(expiration); executeAsync(publishCommand, null); return publishCommand.getClientSequenceNumber(); } /** * @deprecated No longer supported by the AMPS server. * Sends a start timer command to AMPS, which can be later stopped with * a stop timer command. * * @throws ConnectionException A connection error occurred while sending. */ @Deprecated public void startTimer() throws AMPSException { lock.lock(); try { if (serverVersion.compareTo(new VersionInfo("5.3.2")) >= 0) { throw new CommandException("The start_timer command has been deprecated"); } executeAsyncNoLock(_command.reset(Message.Command.StartTimer),null); } finally { lock.unlock(); } } /** * @deprecated No longer supported by the AMPS server. * Sends a stop timer command to AMPS * * @param handler_ A MessageHandler to process the acknowledgement returned by AMPS. * @return The command identifier. * @throws ConnectionException A connection error occurred while sending. */ @Deprecated public CommandId stopTimer(MessageHandler handler_) throws AMPSException { lock.lock(); try { if (serverVersion.compareTo(new VersionInfo("5.3.2")) >= 0) { throw new CommandException("The stop_timer command has been deprecated"); } return executeAsyncNoLock(_command.reset(Message.Command.StopTimer) .addAckType(Message.AckType.Stats) ,handler_); } finally { lock.unlock(); } } /** * Sets the uninterpreted logon correlation information a client sends * at logon to aid in searching server log files for specific clients. * * @param correlationData The base64 data to be used for logon correlation. */ public void setLogonCorrelationData(String correlationData) { logonCorrelationData = correlationData; } /** * Gets the uninterpreted logon correlation information currently set. * * @return The base64 data to be used for logon correlation. */ public String getLogonCorrelationData() { return logonCorrelationData; } /** * Logs into AMPS with the parameters provided in the connect method and the * logon correlation id, if any, that is set. * * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * * @return The command identifier. * @throws ConnectionException A connection error occurred while logging on. */ public CommandId logon(long timeout) throws ConnectionException { return logon(timeout, new DefaultAuthenticator(), null); } /** * Logs into AMPS with the parameters provided in the connect method and the * logon correlation id, if any, that is set. * * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @param options The options string to pass with the logon command * * @return The command identifier. * @throws ConnectionException A connection error occurred while logging on. */ public CommandId logon(long timeout, String options) throws ConnectionException { return logon(timeout, new DefaultAuthenticator(), options); } /** * Logs into AMPS with the parameters provided in the connect method and the * logon correlation id, if any, that is set. * * @param options The options string to pass with the logon command * * @return The command identifier. * @throws ConnectionException A connection error occurred while logging on. */ public CommandId logon(String options) throws ConnectionException { return logon(0, new DefaultAuthenticator(), options); } /** * Logs into AMPS with the parameters provided in the connect method and the * logon correlation id, if any, that is set. * * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @param authenticator The custom authenticator object to authenticate against * @return The command identifier * @throws ConnectionException A connection error occurred while logging on. */ public CommandId logon(long timeout, Authenticator authenticator) throws ConnectionException { return logon(timeout, authenticator, null); } /** * Logs into AMPS with the parameters provided in the connect method and the * logon correlation id, if any, that is set. * * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @param authenticator The custom authenticator object to authenticate against * @param options The options string to pass with the logon command * @return The command identifier * @throws ConnectionException A connection error occurred while logging on. */ public CommandId logon(long timeout, Authenticator authenticator, String options) throws ConnectionException { if (uri == null) throw new DisconnectedException("Not connected"); if (authenticator == null) { authenticator = new DefaultAuthenticator(); } CommandId id = CommandId.nextIdentifier(); lock.lock(); try { // Only set this true if we have a publish store this._reconnectingPublishStore = this.publishStore != null; this.message.reset(); this.message.setCommand(Message.Command.Logon); this.message.setCommandId(id); this.message.setClientName(this.name); if (options != null) { this.message.setOptions(options); } String uriUserInfo = this.uri.getUserInfo(); if(uriUserInfo != null) { // parse user info int upSep = uriUserInfo.indexOf(':'); if(upSep < 0) { this.username = uriUserInfo; this.message.setUserId(this.username); this.message.setPassword(authenticator.authenticate(this.username, null)); } else { this.username = uriUserInfo.substring(0,upSep); String passToken = uriUserInfo.substring(upSep+1); this.message.setUserId(this.username); this.message.setPassword( authenticator.authenticate(this.username, passToken)); } } String uriPath = this.uri.getPath(); String[] path = uriPath.substring(1, uriPath.length()).split("/"); if(path.length > 1) { this.message.setMessageType(path[path.length-1]); } URIProperties properties = new URIProperties(this.uri); String p = properties.getProperty("pretty"); if(p!=null && ("true".equalsIgnoreCase(p) || "t".equalsIgnoreCase(p) || "1".equals(p))) { this.message.setOptions("pretty"); } try { // Versions before 3.0 did not support the Processed ack // for a logon command. if(this.version >= Version.AMPS_3) { this.message.setAckType(Message.AckType.Processed); String version = Client.getVersion(); if (version != null && version.length() != 0) this.message.setVersion(version+":java"); else this.message.setVersion("notinmanifest:java"); // Set this after authenticate has been called so that // Authenticator can modify it. if (logonCorrelationData != null) this.message.setCorrelationId(logonCorrelationData); AckResponse response = null; while(true) { response = this.syncAckProcessing(id, this.message, timeout); if(response.state == Message.Status.Retry) { this.message.setPassword(authenticator.retry(response.username, response.password)); this.username = response.username; this.message.setUserId(this.username); } else { break; } } this.serverVersion = response.serverVersion; this.nameHash = response.bookmark.substring(0, response.bookmark.indexOf('|')); if (bookmarkStore != null) { bookmarkStore.setServerVersion(this.serverVersion.getOldStyleVersion()); } authenticator.completed(response.username, response.password, response.reason); broadcastConnectionStateChanged(ConnectionStateListener.LoggedOn); // heartbeat must be configured after logon completes. _sendHeartbeat(); } else { sendInternal(this.message); broadcastConnectionStateChanged(ConnectionStateListener.LoggedOn); } } catch(CommandException e) // Absorb exceptions { this.absorbedException(e); } catch (StoreException storeException) { throw new ConnectionException("A local store exception occurred while logging on.", storeException); } try { if(this.publishStore != null) { this.publishStore.replay(replayer); broadcastConnectionStateChanged(ConnectionStateListener.PublishReplayed); } } catch (StoreException storeException) { throw new ConnectionException("A local store exception occurred while replaying stored messages during logon.", storeException); } return id; } finally { this._reconnectingPublishStore = false; lock.unlock(); } } /** * Logs into AMPS with the parameters provided in the connect method. * * @return The command identifier * @throws ConnectionException A connection error occurred while logging on. */ public CommandId logon() throws ConnectionException { return logon(0); } /** * Places a bookmark subscription with AMPS. * Starts replay at the most recent message reported by the BookmarkStore. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param subId The subscription ID. You may optionally provide a subscription ID * to ease recovery scenarios, instead of having the system automatically * generate one for you. When used with the 'replace' option, this * is the subscription to be replaced. With a bookmark store, * this is the subscription ID used for recovery. So, when * using a persistent bookmark store, provide an explicit * subscription ID that is consistent across application restarts. * @param bookmark The timestamp or bookmark location at which to start the subscription. This parameter can be the bookmark assigned by AMPS to a message, one of the special values in {@link Client.Bookmarks}, or a string of the format YYYYmmddTHHMMSS, as described in the AMPS User's guide. * @param options A {@link Message.Options} value indicating desired options for this * subscription. Use Message.Options.None if no options are desired. * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId bookmarkSubscribe( MessageHandler messageHandler, String topic, String filter, CommandId subId, String bookmark, String options, long timeout) throws AMPSException { if(bookmark == null) { throw new AMPSException("A bookmark (or one of the Client.Bookmarks constants) is required to initiate a bookmark subscription."); } lock.lock(); try { _command.reset(Message.Command.Subscribe).setTopic(topic) .setFilter(filter).setOptions(options) .setBookmark(bookmark).setTimeout(timeout); if(subId != null) _command.setSubId(subId); return executeAsyncNoLock(_command, messageHandler); } finally { lock.unlock(); } } /** * Places a subscription with AMPS. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId subscribe( MessageHandler messageHandler, String topic, String filter, long timeout) throws AMPSException { return subscribe(messageHandler, topic, filter, Message.Options.None, timeout); } /** * Places a subscription with AMPS. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param options A value from Message.Options indicating additional processing options. * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId subscribe(MessageHandler messageHandler, String topic, String filter, String options, long timeout) throws AMPSException { return subscribe(messageHandler, topic, filter, options, timeout, null); } /** * Places a subscription with AMPS. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param options A value from Message.Options indicating additional processing options. * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @param subId The subscription id to use for the subscription. * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId subscribe(MessageHandler messageHandler, String topic, String filter, String options, long timeout, String subId) throws AMPSException { lock.lock(); try { if (subId == null && options != null && options.contains("replace")) throw new CommandException("Cannot issue a replacement subscription; a valid subscription id is required."); _command.reset(Message.Command.Subscribe).setTopic(topic) .setFilter(filter).setOptions(options) .setTimeout(timeout).setSubId(subId); return executeAsyncNoLock(_command, messageHandler); } finally { lock.unlock(); } } /** * Places a subscription with AMPS. * * @param topic The topic to subscribe to * @return a MessageStream to iterate over. * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public MessageStream subscribe(String topic) throws AMPSException { return execute(new Command("subscribe").setTopic(topic)); } /** * Places a subscription with AMPS. * * @param topic The topic to subscribe to * @param filter The filter * @return a MessageStream to iterate over. * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public MessageStream subscribe(String topic, String filter) throws AMPSException { return execute(new Command("subscribe").setTopic(topic).setFilter(filter)); } /** * Places a subscription with AMPS. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId subscribe( MessageHandler messageHandler, String topic, long timeout) throws AMPSException { return subscribe(messageHandler, topic, null, timeout); } /** * Places a delta subscription with AMPS. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param options A value from Message.Options indicating additional processing options. * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId deltaSubscribe( MessageHandler messageHandler, String topic, String filter, String options, long timeout) throws AMPSException { return deltaSubscribe(messageHandler, topic, filter, options, timeout, null); } /** * Places a delta subscription with AMPS. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param options A value from Message.Options indicating additional processing options. * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @param subId The subscription id to use for the subscription. * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId deltaSubscribe( MessageHandler messageHandler, String topic, String filter, String options, long timeout, String subId) throws AMPSException { lock.lock(); try { _command.reset(Message.Command.DeltaSubscribe) .setTopic(topic).setFilter(filter) .setOptions(options).setTimeout(timeout); if(subId != null) _command.setSubId(new CommandId(subId)); return executeAsyncNoLock(_command, messageHandler); } finally { lock.unlock(); } } /** * Places a delta subscription with AMPS. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId deltaSubscribe( MessageHandler messageHandler, String topic, String filter, long timeout) throws AMPSException { return deltaSubscribe(messageHandler, topic, filter, Message.Options.None, timeout); } /** * Places a delta subscription with AMPS. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId deltaSubscribe( MessageHandler messageHandler, String topic, long timeout) throws AMPSException { return deltaSubscribe(messageHandler, topic, null, timeout); } /** * Remove a subscription from AMPS. * * @param subscriptionId The subscription identifier to remove * * @throws DisconnectedException The client was disconnected at the time of * execution. This can still be considered a success because the * subscription was still removed from the client's subscription * manager (if any) and since the client is disconnected, * it is unsubscribed on the server-side. */ public void unsubscribe(CommandId subscriptionId) throws DisconnectedException { lock.lock(); try { unsubscribeInternal(subscriptionId); } finally { lock.unlock(); } } private void unsubscribeInternal(CommandId subscriptionId) throws DisconnectedException { CommandId id = CommandId.nextIdentifier(); if(subscriptionId != null) { lock.unlock(); try { this.subscriptionManager.unsubscribe(subscriptionId); } finally { lock.lock(); } _routes.removeRoute(subscriptionId); if (this.transport != null) { this.message.reset(); this.message.setCommand(Message.Command.Unsubscribe); this.message.setCommandId(id); this.message.setSubId(subscriptionId); // Try to send unsubscribe command without retrying. If it fails // that is fine because it means we will be unsubscribed by // the server. The background thread will need to notice the // disconnect for any HA reconnect. this.transport.sendWithoutRetry(this.message); } else { throw new DisconnectedException("There is no current connection."); } } } /** * Remove all of the client's subscriptions from AMPS. * * @throws DisconnectedException The client was disconnected at the time of * execution. This can still be considered a success because any * subscriptions were still removed from the client's subscription * manager (if any) and since the client is disconnected, * it is unsubscribed on the server-side. */ public void unsubscribe() throws DisconnectedException { this.subscriptionManager.clear(); CommandId id = CommandId.nextIdentifier(); lock.lock(); try { _routes.unsubscribeAll(); if (this.transport != null) { this.message.reset(); this.message.setCommand(Message.Command.Unsubscribe); this.message.setCommandId(id); this.message.setSubId("all"); // Try to send unsubscribe command without retrying. If it fails // that is fine because it means we will be unsubscribed by // the server. The background thread will need to notice the // disconnect for any HA reconnect. this.transport.sendWithoutRetry(this.message); } else { throw new DisconnectedException("There is no current connection."); } } finally { lock.unlock(); } } /** * Synchronously execute a SOW query. * * @param topic The topic to query * @param filter The filter * @return The command identifier assigned to this command * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public MessageStream sow(String topic, String filter) throws AMPSException { return execute(new Command("sow").setTopic(topic).setFilter(filter).setBatchSize(10)); } /** * Synchronously execute a SOW query. * * @param topic The topic to query * @return The command identifier assigned to this command * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public MessageStream sow(String topic) throws AMPSException { return execute(new Command("sow").setTopic(topic).setBatchSize(10)); } /** * Executes a SOW query. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param orderBy The ordering property * @param bookmark The timestamp or bookmark location for a historical query. This parameter can be the bookmark assigned by AMPS to a message, one of the special values in {@link Client.Bookmarks}, or a string of the format YYYYmmddTHHMMSS, as described in the AMPS User's guide. * @param batchSize The batching parameter to use for the results * @param topN The maximum number of records the server will return (default is all that match) * @param options The options string for the command, or an empty string for no options. * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sow(MessageHandler messageHandler, String topic, String filter, String orderBy, String bookmark, int batchSize, int topN, String options, long timeout) throws AMPSException { lock.lock(); try { _command.reset(Message.Command.SOW).setTopic(topic) .setFilter(filter).setOrderBy(orderBy) .setOptions(options).setBookmark(bookmark) .setBatchSize(batchSize).setTopN(topN) .setTimeout(timeout); return executeAsyncNoLock(_command, messageHandler); } finally { lock.unlock(); } } /** * Executes a SOW query. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param batchSize The batching parameter to use for the results * @param options The options string for the command, or an empty string for no options. * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sow(MessageHandler messageHandler, String topic, String filter, int batchSize, String options, long timeout) throws AMPSException { return sow(messageHandler,topic,filter,null,null,batchSize,-1,options,timeout); } /** * Executes a SOW query. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param batchSize The batching parameter to use for the results * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sow(MessageHandler messageHandler, String topic, String filter, int batchSize, long timeout) throws AMPSException { return sow(messageHandler, topic, filter, batchSize, Message.Options.None, timeout); } /** * Executes a SOW query. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param batchSize The batching parameter to use for the results * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sow(MessageHandler messageHandler, String topic, int batchSize, long timeout) throws AMPSException { return sow(messageHandler, topic, null, batchSize, timeout); } /** * Executes a SOW query. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sow(MessageHandler messageHandler, String topic, long timeout) throws AMPSException { // Default batchsize to 10 for good default performance return sow(messageHandler, topic, null, 10, timeout); } /** * Executes a SOW query and places a subscription. * * @param topic The topic to subscribe to * @param filter The filter * @return A MessageStream to iterate over. * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public MessageStream sowAndSubscribe(String topic, String filter) throws AMPSException { return execute(new Command("sow_and_subscribe").setTopic(topic).setFilter(filter).setBatchSize(10)); } /** * Executes a SOW query and places a subscription. * * @param topic The topic to subscribe to * @return A MessageStream to iterate over. * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public MessageStream sowAndSubscribe(String topic) throws AMPSException { return execute(new Command("sow_and_subscribe").setTopic(topic).setBatchSize(10)); } /** * Executes a SOW query and places a subscription. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param orderBy The ordering property * @param bookmark The timestamp or bookmark location for a historical query. This parameter can be the bookmark assigned by AMPS to a message, one of the special values in {@link Client.Bookmarks}, or a string of the format YYYYmmddTHHMMSS, as described in the AMPS User's guide. * @param batchSize The batching parameter to use for the SOW query results * @param topN The maximum number of records the server will return (default is all that match) * @param options A value from Message.Options indicating additional processing options. * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sowAndSubscribe(MessageHandler messageHandler, String topic, String filter, String orderBy, String bookmark, int batchSize, int topN, String options, long timeout) throws AMPSException { lock.lock(); try { _command.reset(Message.Command.SOWAndSubscribe).setTopic(topic) .setFilter(filter).setOrderBy(orderBy) .setOptions(options).setBookmark(bookmark) .setBatchSize(batchSize).setTopN(topN) .setTimeout(timeout); return executeAsyncNoLock(_command, messageHandler); } finally { lock.unlock(); } } /** * Executes a SOW query and places a subscription. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param batchSize The batching parameter to use for the SOW query results * @param options A value from Message.Options indicating additional processing options. * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sowAndSubscribe(MessageHandler messageHandler, String topic, String filter, int batchSize, String options, long timeout) throws AMPSException { return sowAndSubscribe(messageHandler,topic,filter,null,null,batchSize,-1,options,timeout); } /** * Executes a SOW query and places a subscription. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param batchSize The batching parameter to use for the SOW query results * @param oofEnabled Specifies whether or not Out-of-Focus processing is enabled * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sowAndSubscribe( MessageHandler messageHandler, String topic, String filter, int batchSize, boolean oofEnabled, long timeout) throws AMPSException { return sowAndSubscribe(messageHandler, topic, filter, null, null,batchSize,-1, oofEnabled?"oof":null, timeout); } /** * Executes a SOW query and places a subscription. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param batchSize The batching parameter to use for the SOW query results * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sowAndSubscribe( MessageHandler messageHandler, String topic, String filter, int batchSize, long timeout) throws AMPSException { return sowAndSubscribe(messageHandler, topic, filter, batchSize, false, timeout); } /** * Executes a SOW query and places a subscription. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param batchSize The batching parameter to use for the SOW query results * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sowAndSubscribe( MessageHandler messageHandler, String topic, int batchSize, long timeout) throws AMPSException { return sowAndSubscribe(messageHandler, topic, null, batchSize, false, timeout); } /** * Executes a SOW query and places a subscription. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sowAndSubscribe( MessageHandler messageHandler, String topic, long timeout) throws AMPSException { return sowAndSubscribe(messageHandler, topic, null, 10, false, timeout); } /** * Executes a SOW query and places a delta subscription. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param orderBy The ordering property * @param batchSize The batching parameter to use for the SOW query results * @param topN The maximum number of records the server will return (default is all that match) * @param options A value from Message.Options indicating additional processing options * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sowAndDeltaSubscribe(MessageHandler messageHandler, String topic, String filter, String orderBy, int batchSize, int topN, String options, long timeout) throws AMPSException { lock.lock(); try { _command.reset(Message.Command.SOWAndDeltaSubscribe) .setTopic(topic).setFilter(filter).setOrderBy(orderBy) .setOptions(options).setBatchSize(batchSize) .setTopN(topN).setTimeout(timeout); return executeAsyncNoLock(_command, messageHandler); } finally { lock.unlock(); } } /** * Executes a SOW query and places a delta subscription. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param batchSize The batching parameter to use for the SOW query results * @param oofEnabled Specifies whether or not Out-of-Focus processing is enabled * @param sendEmpties Specifies whether or not unchanged records are received on the delta subscription * @param options A value from Message.Options indicating additional processing options. * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sowAndDeltaSubscribe(MessageHandler messageHandler, String topic, String filter, int batchSize, boolean oofEnabled, boolean sendEmpties, String options, long timeout) throws AMPSException { String opts = options; if (oofEnabled) { opts = opts == null? "oof": opts+",oof"; } if (!sendEmpties) { opts = opts == null?"no_empties" : opts+",no_empties"; } return sowAndDeltaSubscribe(messageHandler,topic,filter,null,batchSize,-1,opts,timeout); } /** * Executes a SOW query and places a delta subscription. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param batchSize The batching parameter to use for the SOW query results * @param oofEnabled Specifies whether or not Out-of-Focus processing is enabled * @param sendEmpties Specifies whether or not unchanged records are received on the delta subscription * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sowAndDeltaSubscribe( MessageHandler messageHandler, String topic, String filter, int batchSize, boolean oofEnabled, boolean sendEmpties, long timeout) throws AMPSException { return sowAndDeltaSubscribe(messageHandler, topic, filter, batchSize, oofEnabled, sendEmpties, null, timeout); } /** * Executes a SOW query and places a delta subscription. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param filter The filter * @param batchSize The batching parameter to use for the SOW query results * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sowAndDeltaSubscribe( MessageHandler messageHandler, String topic, String filter, int batchSize, long timeout) throws AMPSException { return sowAndDeltaSubscribe(messageHandler, topic, filter, batchSize, false, true, timeout); } /** * Executes a SOW query and places a delta subscription. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param batchSize The batching parameter to use for the SOW query results * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sowAndDeltaSubscribe( MessageHandler messageHandler, String topic, int batchSize, long timeout) throws AMPSException { return sowAndDeltaSubscribe(messageHandler, topic, null, batchSize, false, true, timeout); } /** * Executes a SOW query and places a delta subscription. * * @param messageHandler The message handler to invoke with matching messages * @param topic The topic to subscribe to * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws SubscriptionAlreadyExistsException A subscription with this identifier is already registered * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sowAndDeltaSubscribe( MessageHandler messageHandler, String topic, long timeout) throws AMPSException { return sowAndDeltaSubscribe(messageHandler, topic, null, 10, false, true, timeout); } /** * Executes a SOW delete with filter. * * @param messageHandler The message handler to invoke with stats and completed acknowledgements * @param topic The topic to subscribe to * @param filter The filter. To delete all records, set a filter that is always true ('1=1') * @param options A value from Message.Options indicating additional processing options. * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sowDelete( MessageHandler messageHandler, String topic, String filter, String options, long timeout) throws AMPSException { Command publishCommand = _publishCommand.get(); publishCommand.reset(Message.Command.SOWDelete) .setTopic(topic).setFilter(filter) .addAckType(Message.AckType.Stats) .setOptions(options).setTimeout(timeout); return executeAsync(publishCommand, messageHandler); } /** * Executes a SOW delete with filter. * * @param messageHandler The message handler to invoke with stats and completed acknowledgements * @param topic The topic to subscribe to * @param filter The filter. To delete all records, set a filter that is always true ('1=1') * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sowDelete( MessageHandler messageHandler, String topic, String filter, long timeout) throws AMPSException { return this.sowDelete(messageHandler, topic, filter, null, timeout ); } /** * Executes a SOW delete with filter. * * @param topic The topic to subscribe to * @param filter The filter. To delete all records, set a filter that is always true ('1=1') * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The stats message returned by this delete. * @throws BadFilterException The provided filter is invalid * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public Message sowDelete(String topic, String filter, long timeout) throws AMPSException { MessageStream ms = null; ms = execute(_publishCommand.get().reset(Message.Command.SOWDelete) .addAckType(Message.AckType.Stats).setTopic(topic) .setFilter(filter).setTimeout(timeout)); return (ms == null) ? null : ms.next(); } /** * Executes a SOW delete using the SowKey assigned by AMPS to specify * the messages to delete. When you have the SowKeys for the messages, * this method is more efficient than using a filter to * delete messages from the SOW. * * For example, to efficiently delete a message that your program has * received from AMPS: *

     *  // DeleteMessageHandler receives a message containing
     *  // the results of the delete.
     *  DeleteMessageHandler dmh = new DeleteMessageHandler();
     *
     *  client.sowDeleteByKeys(dmh, message.getTopic(), message.getSowKey(), 1000); 
     * 
* * @param messageHandler The message handler to invoke with stats and completed acknowledgements * @param topic The topic to execute the SOW delete against * @param keys A comma separated list of SOW keys to be deleted * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws BadRegexTopicException The topic specified was an invalid regular expression * @throws TimedOutException The operation took longer than the timeout to execute * @throws DisconnectedException The client wasn't connected when the operation was executed */ public CommandId sowDeleteByKeys( MessageHandler messageHandler, String topic, String keys, long timeout) throws AMPSException { return executeAsync(_publishCommand.get() .reset(Message.Command.SOWDelete) .setTopic(topic).setSowKeys(keys) .addAckType(Message.AckType.Stats) .setTimeout(timeout) , messageHandler); } /** * Executes a SOW delete by data. AMPS uses key fields in the data to * find and delete a message with the same keys. * * @param messageHandler The message handler to invoke with stats and completed acknowledgements * @param topic The topic to execute the SOW delete against * @param data The message to match and delete in the SOW cache * @param timeout The number of milliseconds to wait for the client to receive and consume a processed ack for this command, where 0 indicates to wait indefinitely * @return The command identifier assigned to this command * @throws AMPSException An error occurred while waiting for this command to be processed. */ public CommandId sowDeleteByData( MessageHandler messageHandler, String topic, String data, long timeout) throws AMPSException { return executeAsync(_publishCommand.get() .reset(Message.Command.SOWDelete) .addAckType(Message.AckType.Stats) .setTopic(topic).setData(data) .setTimeout(timeout), messageHandler); } /** Ensures that AMPS messages are sent and have been processed by the * AMPS server. * When a publish store is present, waits until all of the messages * in the publish store at the time the command is called have been * acknowledged as persisted. *
* Otherwise, the client issues a flush command and waits for a processed * acknowledgement. This method blocks until messages have been * processed or until the client is disconnected, and is most useful when * the application reaches a point at which it is acceptable to block to * ensure that messages have been delivered to the AMPS server. For * example, an application might call publishFlush before exiting. * One thing to note is that if AMPS is unavailable (HA Client), publishFlush * needs to wait for a connection to come back up which may look like it's hanging. * @throws DisconnectedException Thrown if the client is shut down or has never been connected * @throws TimedOutException Not thrown from this overload: the method blocks until the operation completes. */ public void publishFlush() throws DisconnectedException, TimedOutException { if (publishStore == null) { try { lock.lock(); this.message.reset(); CommandId id = CommandId.nextIdentifier(); this.message.setCommandId(id); this.message.setAckType(Message.AckType.Processed); this.message.setCommand(Message.Command.Flush); this.syncAckProcessing(id, message, 0); return; } catch(NullPointerException e) { throw new DisconnectedException("not connected", e); } catch(AMPSException e) { absorbedException(e); } finally { lock.unlock(); } } else { publishStore.flush(); } } private static class FlushAckHandler implements MessageHandler, ConnectionStateListener { public static final int Waiting = 0; public static final int Acked = 1; public static final int Disconnected = 2; public volatile int _status = Waiting; public FlushAckHandler(Client client_) { client_.addConnectionStateListener(this); } public void connectionStateChanged(int state_) { if (state_ <= ConnectionStateListener.Shutdown) { _status = Disconnected; } } public void invoke(Message m) { _status = Acked; } } private static VersionInfo minPersistedFlushVersion = new VersionInfo("5.3.3"); /** Ensures that AMPS messages are sent and have been processed by the * AMPS server. * When a publish store is present, waits until all of the messages * in the publish store at the time the command is called have been * acknowledged as persisted. *
* In addition, the behavior depends on the server version. * the client issues a flush command. If the server * supports waiting for a persisted acknowledgement indicating that all * prior messages are also persisted and that was requested, it will do * that. Otherwise, it waits for a processed acknowledgement. * This method blocks until messages have been processed or until * a disconnect occures, and is most useful when the application reaches a * point at which it is acceptable to block to ensure that messages * have been delivered to the AMPS server. For example, an application * might call publishFlush before exiting. * One thing to note is that if AMPS is unavailable (HA Client), * publishFlush needs to wait for a connection to come back up which * may look like it's hanging. * @param ackType The acknowledgement type that the flush command should wait to receive, see {@link Message.AckType}. `processed` matches the no arguments version of this function. `persisted` ensures all previous messages have been persisted. * @throws CommandException Thrown if an invalid ack type is requested. * @throws DisconnectedException Thrown if the client is shut down or has never been connected * @throws TimedOutException Not thrown from this overload: the method blocks until the operation completes. */ public void publishFlush(String ackType) throws CommandException, DisconnectedException, TimedOutException { if (!"persisted".equals(ackType) && !"processed".equals(ackType)) { throw new CommandException("publishFlush only accepts 'processed' or 'persisted' ack types"); } if ("persisted".equals(ackType) && this.serverVersion.compareTo(minPersistedFlushVersion) < 0) { ackType = "processed"; } CommandId id = CommandId.nextIdentifier(); FlushAckHandler handler = new FlushAckHandler(this); try { lock.lock(); if (!this.connected) { throw new DisconnectedException("Not connected"); } this.message.reset(); this.message.setCommandId(id); this.message.setAckType(ackType); this.message.setCommand(Message.Command.Flush); if ("persisted".equals(ackType)) { this.addMessageHandler(id, handler, Message.AckType.Persisted, false); } else { this.addMessageHandler(id, handler, Message.AckType.Processed, false); } this.send(message); } catch(NullPointerException e) { throw new DisconnectedException("not connected", e); } finally { lock.unlock(); } try { if (publishStore != null) { publishStore.flush(); } while (handler._status == FlushAckHandler.Waiting) { try { Thread.sleep(10); } catch(InterruptedException e) { ; } } if (publishStore == null && handler._status == FlushAckHandler.Disconnected) { throw new DisconnectedException("Client disconnected waiting for flush acknowledgement"); } } finally { removeMessageHandler(id); removeConnectionStateListener(handler); } } /** Ensures that AMPS messages are sent and have been processed by the * AMPS server. * When a publish store is present, waits until all of the messages * in the publish store at the time the command is called have been * acknowledged as persisted. *
* Otherwise, the client issues a flush command and waits for a processed * acknowledgement. This method blocks until messages have been * processed, until the timeout occurs, or until the client is * disconnected, and is most useful when * the application reaches a point at which it is acceptable to block * to ensure that messages have been delivered to the AMPS server. For * example, an application might call publishFlush before exiting. * One thing to note is that if AMPS is unavailable (HA Client), publishFlush * needs to wait for a connection to come back up which may look like it's hanging. * * @param timeout Number of milliseconds to wait for flush, where 0 indicates to wait indefinitely * @throws DisconnectedException Thrown if the client is shut down or has never been connected * @throws TimedOutException Thrown if the timeout period expires before the method returns * */ public void publishFlush(long timeout) throws DisconnectedException, TimedOutException { if (publishStore == null) { try { lock.lock(); this.message.reset(); CommandId id = CommandId.nextIdentifier(); this.message.setCommandId(id); this.message.setAckType(Message.AckType.Processed); this.message.setCommand(Message.Command.Flush); this.syncAckProcessing(id, message, timeout); return; } catch(NullPointerException e) { throw new DisconnectedException("not connected", e); } catch(AMPSException e) { absorbedException(e); } finally { lock.unlock(); } } else { publishStore.flush(timeout); } } /** Ensures that AMPS messages are sent and have been processed by the * AMPS server. * When a publish store is present, waits until all of the messages * in the publish store at the time the command is called have been * acknowledged as persisted. *
* In addition, the behavior depends on the server version. * the client issues a flush command. If the server * supports waiting for a persisted acknowledgement indicating that all * prior messages are also persisted and that was requested, it will do * that. Otherwise, it waits for a processed acknowledgement. * This method blocks until messages have been processed, until the * timeout has expired, or until a disconnect occures, and is most useful * when the application reaches a point at which it is acceptable to block * to ensure that messages have been delivered to the AMPS server. For * example, an application might call publishFlush before exiting. * One thing to note is that if AMPS is unavailable (HA Client), * publishFlush needs to wait for a connection to come back up which * may look like it's hanging. * @param ackType The acknowledgement type that the flush command should wait to receive, see {@link Message.AckType}. `processed` matches the timeout only version of this function. `persisted` ensures all previous messages have been persisted. * @param timeout Number of milliseconds to wait for flush, where 0 indicates to wait indefinitely * @throws CommandException Thrown if an invalid ack type is requested. * @throws DisconnectedException Thrown if the client is shut down or has never been connected * @throws TimedOutException Not thrown from this overload: the method blocks until the operation completes. */ public void publishFlush(String ackType, long timeout) throws CommandException, DisconnectedException, TimedOutException { if (!"persisted".equals(ackType) && !"processed".equals(ackType)) { throw new CommandException("publishFlush only accepts processed or persisted ack types"); } if ("persisted".equals(ackType) && this.serverVersion.compareTo(minPersistedFlushVersion) < 0) { ackType = "processed"; } CommandId id = CommandId.nextIdentifier(); FlushAckHandler handler = new FlushAckHandler(this); try { lock.lock(); if (!this.connected) { throw new DisconnectedException("Not connected"); } this.message.reset(); this.message.setCommandId(id); this.message.setAckType(ackType); this.message.setCommand(Message.Command.Flush); if ("persisted".equals(ackType)) { this.addMessageHandler(id, handler, Message.AckType.Persisted, false); } else { this.addMessageHandler(id, handler, Message.AckType.Processed, false); } this.send(message); } catch(NullPointerException e) { throw new DisconnectedException("not connected", e); } finally { lock.unlock(); } long end = System.currentTimeMillis() + timeout; try { if (publishStore != null) { publishStore.flush(timeout); } while (handler._status == FlushAckHandler.Waiting && (timeout == 0 || System.currentTimeMillis() < end)) { try { Thread.sleep(10); } catch(InterruptedException e) { ; } } if (publishStore == null && handler._status == FlushAckHandler.Disconnected) { throw new DisconnectedException("Client disconnected waiting for flush acknowledgement"); } if (publishStore == null && handler._status != FlushAckHandler.Acked) { throw new TimedOutException("Timed out waiting for flush acknowledgement"); } } finally { removeMessageHandler(id); removeConnectionStateListener(handler); } } /** * @deprecated As of 5.3.0.0. Use (@link #publishFlush) instead. * Clear the queued messages which may be waiting in the transport. This is a no-op. * * @return long Number of messages or bytes queued after flush is completed or timed out * @throws DisconnectedException Thrown if the client is shut down or has never been connected * */ @Deprecated public long flush() throws DisconnectedException { // No-op return 0; } /** * @deprecated As of 5.3.0.0. Use (@link #publishFlush) instead. * Clear the queued messages which may be waiting in the transport. This is a no-op. * * @param timeout Number of milliseconds to wait for flush, where 0 indicates to wait indefinitely * @return long Number of messages or bytes queued after flush is completed or timed out * @throws DisconnectedException Thrown if the client is shut down or has never been connected * */ @Deprecated public long flush(long timeout) throws DisconnectedException { // No-op return 0; } /** * Return the build number for the client that is stored in the Manifest.mf of the jar file. * * @return String Build version number. */ static public String getVersion() { Manifest manifest; if (_version != null) return _version; // If it still exists, give precedence to the client version # from the // JAR manifest first, but use the ClientVersion class as a backup. // The JAR client version can be lost due to repackaging, such is // the case with "fat JARs". String version = "unknown"; try { // Allow ClientVersion not to exist. @SuppressWarnings("rawtypes") Class clientVersionCls = Class.forName( "com.crankuptheamps.client.ClientVersion"); @SuppressWarnings("unchecked") Method getImplementationVersion = clientVersionCls.getMethod( "getImplementationVersion"); version = (String) getImplementationVersion.invoke(null); } catch (Exception e) { // Do nothing. Keep using "unknown" as the backup version. } // Get the class loader and all jar manifests ClassLoader cl = (ClassLoader) Client.class.getClassLoader(); if (cl == null) { _version = version; return _version; } Enumeration urls; try { urls = cl.getResources("META-INF/MANIFEST.MF"); } catch (IOException e) { _version = version; return _version; } URL url = null; // Search for the amps_client.jar manifest and retrieve Release-Version while (urls.hasMoreElements()) { url = urls.nextElement(); if (url.toString().contains("amps_client")) { try { manifest = new Manifest(url.openStream()); } catch (IOException E) { _version = version; return _version; } Attributes attrs = manifest.getMainAttributes(); version = (String)(attrs.getValue("Implementation-Version")); break; } } _version = version; return _version; } /** * Set a handler that is invoked immediately by any thread created by * the transport. * @param handler_ A ThreadCreatedHandler to be invoked by new * transport threads. */ public void setThreadCreatedHandler(ThreadCreatedHandler handler_) { _threadCreatedHandler = handler_; if (transport != null) { transport.setThreadCreatedHandler(_threadCreatedHandler); } } /** * Set a filter that is used by the Client's transport for all incoming * and outgoing messages. * @param filter_ A TransportFilter to be invoked by the transport. */ public void setTransportFilter(TransportFilter filter_) { _transportFilter = filter_; if (transport != null) { if (filter_ != null) { transport.setTransportFilter(_transportFilter); } else { transport.setTransportFilter(new DefaultTransportFilter()); } } } /** * Assembles a new ConnectionInfo with the state of this client and * associated classes. * @return A new ConnectionInfo object. */ public ConnectionInfo getConnectionInfo() { ConnectionInfo ci = new ConnectionInfo(); ci.put("client.uri", this.uri == null ? null : this.uri.toString()); ci.put("client.name", this.name); ci.put("client.username", this.username); if(this.publishStore != null) { ci.put("publishStore.unpersistedCount", this.publishStore.unpersistedCount()); } return ci; } // Queue acking functions // private int _ack(QueueBookmarks bookmarks) throws AMPSException { //called with lock taken if(bookmarks._bookmarkCount > 0) { if (bookmarks._length == 0 || bookmarks._data == null) { bookmarks._length = 0; bookmarks._bookmarkCount = 0; return 0; } // Because of the unlock to call store(), we need to copy data from bookmarks // while the lock is taken. Other bookmarks could get added while the lock is // released, or another thread could call _ack and cause a double-send. We'll // reset length and count to 0 and get a copy of the buffer before we release // the lock. int bookmarksLength = bookmarks._length; bookmarks._length = 0; bookmarks._bookmarkCount = 0; Command publishCommand = _publishCommand.get(); if (publishCommand._message == null) publishCommand._message = allocateMessage(); publishCommand._message.reset(); publishCommand._message.setCommand(Message.Command.SOWDelete); publishCommand._message.setCommandId(_queueAckCommandId); publishCommand._message.getTopicRaw().set(bookmarks._topic, 0, bookmarks._topic.length); byte[] bookmarksData = _bookmarksData.get(); if (bookmarksData.length < bookmarksLength) { bookmarksData = new byte[bookmarksLength * 2]; _bookmarksData.set(bookmarksData); } System.arraycopy(bookmarks._data, 0, bookmarksData, 0, bookmarksLength); publishCommand._message.setBookmark(bookmarksData, 0, bookmarksLength); long sequence = 0; if (this.publishStore != null) { publishCommand._message.setAckType(Message.AckType.Persisted); try { lock.unlock(); publishStore.store(publishCommand._message); if (!publishCommand._message.isSequenceNull()) sequence = publishCommand._message.getSequence(); } finally { lock.lock(); } } sendInternal(publishCommand._message, sequence); return 1; } return 0; } /** * ACKs a message from a message queue with a byte-array topic and bookmark * @param topic The bytes of the topic to ack * @param topicPos The starting position in topic * @param topicLen The length of the topic in bytes * @param bookmark The bytes of the bookmark to ack * @param bookmarkPos The starting position in bookmark * @param bookmarkLen The length of the bookmark * @throws AMPSException An exception occurred while sending a sow_delete message. */ public void ack(byte[] topic, int topicPos, int topicLen, byte[] bookmark, int bookmarkPos, int bookmarkLen) throws AMPSException { if (_isAutoAckEnabled || bookmark == null || bookmarkLen == 0) return; if(_ackBatchSize < 2) { _ackImmediate(topic, topicPos, topicLen, bookmark, bookmarkPos, bookmarkLen, null, 0, 0); } else { _ackMany(topic, topicPos, topicLen, bookmark, bookmarkPos, bookmarkLen); } } private void _ackOne(byte[] topic, int topicPos, int topicLen, byte[] bookmark, int bookmarkPos, int bookmarkLen) throws AMPSException { try { lock.lock(); _hashBox.set(topic,topicPos,topicLen); QueueBookmarks bookmarks = _topicHashMap.get(_hashBox); if(bookmarks == null) { bookmarks = new QueueBookmarks(); bookmarks._topic = new byte[topicLen]; System.arraycopy(topic,topicPos,bookmarks._topic,0,topicLen); bookmarks._data = new byte[512]; _topicHashMap.put(_hashBox.clone(),bookmarks); } int bookmarksCapacity = bookmarks._data.length; int newLength = bookmarks._length + bookmarkLen + 1; if(bookmarksCapacity < newLength) { bookmarksCapacity = 512 * (1+(newLength/512)); byte[] newData = new byte[bookmarksCapacity]; System.arraycopy(bookmarks._data,0,newData,0,bookmarks._length); bookmarks._data = newData; } if(bookmarks._length > 0) { bookmarks._data[bookmarks._length] = (byte)','; ++bookmarks._length; } else { bookmarks._oldestTime = System.currentTimeMillis(); } System.arraycopy(bookmark, bookmarkPos, bookmarks._data, bookmarks._length, bookmarkLen); bookmarks._length += bookmarkLen; if(++bookmarks._bookmarkCount >= _ackBatchSize) _ack(bookmarks); } finally { lock.unlock(); } } private void _ackMany(byte[] topic, int topicPos, int topicLen, byte[] bookmark, int bookmarkPos, int bookmarkLen) throws AMPSException { try { lock.lock(); _hashBox.set(topic,topicPos,topicLen); QueueBookmarks bookmarks = _topicHashMap.get(_hashBox); if(bookmarks == null) { bookmarks = new QueueBookmarks(); bookmarks._topic = new byte[topicLen]; System.arraycopy(topic,topicPos,bookmarks._topic,0,topicLen); bookmarks._data = new byte[512]; _topicHashMap.put(_hashBox.clone(),bookmarks); } int start = bookmarkPos; int end = start+1; for (; end < bookmarkPos + bookmarkLen; ++end) { if (bookmark[end] == (byte)',') { if(++bookmarks._bookmarkCount >= _ackBatchSize) { int bookmarksCapacity = bookmarks._data.length; int newLength = bookmarks._length + end - start + 1; if(bookmarksCapacity < newLength) { bookmarksCapacity = 512 * (1+(newLength/512)); byte[] newData = new byte[bookmarksCapacity]; System.arraycopy(bookmarks._data,0,newData,0,bookmarks._length); bookmarks._data = newData; } if(bookmarks._length > 0) { bookmarks._data[bookmarks._length] = (byte)','; ++bookmarks._length; } else { bookmarks._oldestTime = System.currentTimeMillis(); } System.arraycopy(bookmark, start, bookmarks._data, bookmarks._length, end - start); bookmarks._length += end-start; _ack(bookmarks); start = ++end; } } } // Do we have uncopied bookmark(s)? if (end - start >= 4) { int bookmarksCapacity = bookmarks._data.length; int newLength = bookmarks._length + end - start + 1; if(bookmarksCapacity < newLength) { bookmarksCapacity = 512 * (1+(newLength/512)); byte[] newData = new byte[bookmarksCapacity]; System.arraycopy(bookmarks._data,0,newData,0,bookmarks._length); bookmarks._data = newData; } if(bookmarks._length > 0) { bookmarks._data[bookmarks._length] = (byte)','; ++bookmarks._length; } else { bookmarks._oldestTime = System.currentTimeMillis(); } System.arraycopy(bookmark, start, bookmarks._data, bookmarks._length, end-start); bookmarks._length += end-start; if(++bookmarks._bookmarkCount >= _ackBatchSize) _ack(bookmarks); } } finally { lock.unlock(); } } private void _ackImmediate(byte[] topic, int topicPos, int topicLen, byte[] bookmark, int bookmarkPos, int bookmarkLen, byte[] options, int optionsPos, int optionsLen) throws AMPSException { Command publishCommand = _publishCommand.get(); if (publishCommand._message == null) publishCommand._message = allocateMessage(); publishCommand._message.reset(); publishCommand._message.setCommand(Message.Command.SOWDelete); publishCommand._message.setCommandId(_queueAckCommandId); publishCommand._message.getTopicRaw().set(topic, topicPos, topicLen); publishCommand._message.getBookmarkRaw().set(bookmark, bookmarkPos, bookmarkLen); if (options != null && optionsLen > 0) { publishCommand._message.getOptionsRaw().set(options, optionsPos, optionsLen); } long sequence = 0; if (this.publishStore != null) { publishCommand._message.setAckType(Message.AckType.Persisted); publishStore.store(publishCommand._message); if (!publishCommand._message.isSequenceNull()) sequence = publishCommand._message.getSequence(); } if (sequence > 0 || options == null || !CANCEL_FIELD.equals(publishCommand._message.getOptionsRaw())) { try { lock.lock(); sendInternal(publishCommand._message, sequence); } finally { lock.unlock(); } } else { this.transport.sendWithoutRetry(publishCommand._message); } return; } /** * ACKs a message queue message using {@link Field} objects from a message. * @param topic The topic to ack * @param bookmark The bookmark to ack * @throws AMPSException An error occurred while sending a sow_delete message. */ public void ack(Field topic, Field bookmark) throws AMPSException { ack(topic.buffer,topic.position,topic.length, bookmark.buffer,bookmark.position,bookmark.length); } /** * ACKs a message queue message. * @param topic The topic to ack * @param bookmark The bookmark or comma-delimited bookmark list to ack. * @throws AMPSException An error occurred while sending a sow_delete message. */ public void ack(String topic, String bookmark) throws AMPSException { if (bookmark == null || bookmark.length() == 0 || _isAutoAckEnabled) return; if(_ackBatchSize < 2) { Command publishCommand = _publishCommand.get(); if (publishCommand._message == null) publishCommand._message = allocateMessage(); publishCommand._message.reset(); publishCommand._message.setCommand(Message.Command.SOWDelete); publishCommand._message.setCommandId(_queueAckCommandId); publishCommand._message.setTopic(topic); publishCommand._message.setBookmark(bookmark); long sequence = 0; if (this.publishStore != null) { publishCommand._message.setAckType(Message.AckType.Persisted); publishStore.store(publishCommand._message); if (!publishCommand._message.isSequenceNull()) sequence = publishCommand._message.getSequence(); } try { lock.lock(); sendInternal(publishCommand._message, sequence); } finally { lock.unlock(); } return; } try { lock.lock(); _hashBox.set(topic); QueueBookmarks bookmarks = _topicHashMap.get(_hashBox); if(bookmarks == null) { bookmarks = new QueueBookmarks(); bookmarks._topic = topic.getBytes(StandardCharsets.UTF_8); bookmarks._data = new byte[512]; _topicHashMap.put(_hashBox.clone(),bookmarks); } int bookmarksCapacity = bookmarks._data.length; int newLength = bookmarks._length + bookmark.length() + 1; if(bookmarksCapacity < newLength) { bookmarksCapacity = 512 * (1+(newLength/512)); byte[] newData = new byte[bookmarksCapacity]; System.arraycopy(bookmarks._data,0,newData,0,bookmarks._length); bookmarks._data = newData; } if(bookmarks._length > 0) { bookmarks._data[bookmarks._length] = (byte)','; ++bookmarks._length; } else { bookmarks._oldestTime = System.currentTimeMillis(); } for(int i = 0; i < bookmark.length(); ++i) { bookmarks._data[bookmarks._length] = (byte)bookmark.charAt(i); if (bookmarks._data[bookmarks._length++] == (byte)',') { if(++bookmarks._bookmarkCount >= _ackBatchSize) { _ack(bookmarks); } } } if(++bookmarks._bookmarkCount >= _ackBatchSize) { _ack(bookmarks); } } finally { lock.unlock(); } } /** * ACKs a message from a message queue with a byte-array topic and bookmark * @param topic The bytes of the topic to ack * @param topicPos The starting position in topic * @param topicLen The length of the topic in bytes * @param bookmark The bytes of the bookmark to ack * @param bookmarkPos The starting position in bookmark * @param bookmarkLen The length of the bookmark * @param options The bytes of the options for the ack * @param optionsPos The starting position in options * @param optionsLen The length of the options * @throws AMPSException An exception occurred while sending a sow_delete message. */ public void ack(byte[] topic, int topicPos, int topicLen, byte[] bookmark, int bookmarkPos, int bookmarkLen, byte[] options, int optionsPos, int optionsLen) throws AMPSException { if (_isAutoAckEnabled) return; if(_ackBatchSize < 2 || (options != null && optionsLen > 0)) { _ackImmediate(topic, topicPos, topicLen, bookmark, bookmarkPos, bookmarkLen, options, optionsPos, optionsLen); } else { _ackMany(topic, topicPos, topicLen, bookmark, bookmarkPos, bookmarkLen); } } protected void _ack(byte[] topic, int topicPos, int topicLen, byte[] bookmark, int bookmarkPos, int bookmarkLen, byte[] options, int optionsPos, int optionsLen) throws AMPSException { if (bookmark == null || bookmarkLen == 0) return; if(_ackBatchSize < 2 || (options != null && optionsLen > 0)) { _ackImmediate(topic, topicPos, topicLen, bookmark, bookmarkPos, bookmarkLen, options, optionsPos, optionsLen); } else { _ackOne(topic, topicPos, topicLen, bookmark, bookmarkPos, bookmarkLen); } } /** * ACKs a message queue message using {@link Field} objects from a message. * @param topic The topic to ack * @param bookmark The bookmark to ack * @param options The options for the ack * @throws AMPSException An error occurred while sending a sow_delete message. */ public void ack(Field topic, Field bookmark, Field options) throws AMPSException { if (_isAutoAckEnabled) return; ack(topic.buffer,topic.position,topic.length, bookmark.buffer,bookmark.position,bookmark.length, options.buffer,options.position,options.length); } /** * ACKs a message queue message. * @param topic The topic to ack * @param bookmark The bookmark or comma-delimited bookmark list to ack. * @param options The options for the ack * @throws AMPSException An error occurred while sending a sow_delete message. */ public void ack(String topic, String bookmark, String options) throws AMPSException { if (bookmark == null || bookmark.length() == 0 || _isAutoAckEnabled) return; if(_ackBatchSize < 2 || (options != null && options.length() > 0)) { Command publishCommand = _publishCommand.get(); if (publishCommand._message == null) publishCommand._message = allocateMessage(); publishCommand._message.reset(); publishCommand._message.setCommand(Message.Command.SOWDelete); publishCommand._message.setCommandId(_queueAckCommandId); publishCommand._message.setTopic(topic); publishCommand._message.setBookmark(bookmark); if(options != null && options.length() > 0) { publishCommand._message.setOptions(options); } long sequence = 0; if (this.publishStore != null) { publishCommand._message.setAckType(Message.AckType.Persisted); publishStore.store(publishCommand._message); if (!publishCommand._message.isSequenceNull()) sequence = publishCommand._message.getSequence(); } if (sequence > 0 || !Message.Options.Cancel.equals(options)) { try { lock.lock(); sendInternal(publishCommand._message, sequence); } finally { lock.unlock(); } } else // a cancel should only send during an active session { sendWithoutRetry(publishCommand._message); } return; } try { lock.lock(); _hashBox.set(topic); QueueBookmarks bookmarks = _topicHashMap.get(_hashBox); if(bookmarks == null) { bookmarks = new QueueBookmarks(); bookmarks._topic = topic.getBytes(StandardCharsets.UTF_8); bookmarks._data = new byte[512]; _topicHashMap.put(_hashBox.clone(),bookmarks); } int bookmarksCapacity = bookmarks._data.length; if(bookmarksCapacity < bookmarks._length + 1 + bookmark.length()) { byte[] newData = new byte[2*bookmarksCapacity]; System.arraycopy(bookmarks._data,0,newData,0,bookmarks._length); bookmarks._data = newData; } if(bookmarks._length > 0) { bookmarks._data[bookmarks._length] = (byte)','; ++bookmarks._length; } else { bookmarks._oldestTime = System.currentTimeMillis(); } for(int i = 0; i < bookmark.length(); ++i) { bookmarks._data[bookmarks._length] = (byte)bookmark.charAt(i); if (bookmarks._data[bookmarks._length++] == (byte)',') { if(++bookmarks._bookmarkCount >= _ackBatchSize) { _ack(bookmarks); } } } if(++bookmarks._bookmarkCount >= _ackBatchSize) { _ack(bookmarks); } } finally { lock.unlock(); } } /** * Used internally for autoacking and by Message. * @param topic The topic to ack * @param bookmark The bookmark to ack * @throws AMPSException An error occurred while sending a sow_delete message. */ protected void _ack(Field topic, Field bookmark) throws AMPSException { _ack(topic.buffer,topic.position,topic.length, bookmark.buffer,bookmark.position,bookmark.length,null,0,0); } /** * Used internally for autoacking and by Message. * @param topic The topic to ack * @param bookmark The bookmark to ack * @param options The options for the ack * @throws AMPSException An error occurred while sending a sow_delete message. */ protected void _ack(Field topic, Field bookmark, Field options) throws AMPSException { _ack(topic.buffer,topic.position,topic.length, bookmark.buffer,bookmark.position,bookmark.length, options.buffer,options.position,options.length); } /** * Enables or disables auto-acking. When auto-acking is enabled any successful * return from a message handler function will result in an ACK (sow_delete) message * sent to the server. Any thrown exception will result in a "cancel" ACK sent to the * server. * @param isAutoAckEnabled true to enable auto-acking; false to disable. (default: disabled). */ public void setAutoAck(boolean isAutoAckEnabled) { _isAutoAckEnabled = isAutoAckEnabled; } /** * Returns the current setting of auto-acking. * @return true if auto-acking is enabled, false if auto-acking is disabled. */ public boolean getAutoAck() { return _isAutoAckEnabled; } /** * Sets the current ACK batch size. The ACK batch size controls how many successful ACKs * (which are sow_delete messages) are batched together before sending to the server. When * combined with the "max_backlog" and MaxPerSubscriptionBacklog server configuration parameter, * greater network efficiency can be achieved when using message queues. Setting this parameter * causes calls to "ack" and successful auto-acks to be batched; unsuccessful/cancel acks * are sent immediately. * @param batchSize The batch size to use (default: 1) */ public void setAckBatchSize(int batchSize) { if (batchSize > 1 && _queueAckTimeout <= 0) _queueAckTimeout = 1000; _ackBatchSize = batchSize; } /** * Returns the current ACK batch size. See the documentation for {@link #setAckBatchSize} * for more information on this setting. * @return The current ACK batch size */ public int getAckBatchSize() { return _ackBatchSize; } /** * Sets the ack timeout -- the maximum time to let a success ack be cached before sending. * @param ackTimeout The maximum time in milliseconds to wait to acknowledge messages, where 0 indicates the client will wait until the batch size is full (defaults to 1000) */ public void setAckTimeout(long ackTimeout) { _queueAckTimeout = ackTimeout; } /** * Returns the current queue ack timeout in milliseconds. * @return The current queue ack timeout, in milliseconds. */ public long getAckTimeout() { return _queueAckTimeout; } /** * Send the current set of queue acknowledgements ("sow_delete" with the message bookmark) to the AMPS server. This method sends acknowledgements for messages that have been received and acknowledged: it does not acknowledge messages that the application has not yet processed. * * @throws AMPSException Thrown when the client is unable to flush acknowledgements. */ public void flushAcks() throws AMPSException { int sendCount = 0; // Only need the lock to send the acks lock.lock(); try { Collection entries = _topicHashMap.values(); for(Iterator iterator = entries.iterator(); iterator.hasNext();) { sendCount += _ack(iterator.next()); } } finally { lock.unlock(); } if(sendCount > 0) { // This locks as needed publishFlush(); } } /** * This is a helper function used to encapsulate the logic for waiting on a * sent message so that async commands like subscribe and * sow can have an * easier time using an exception interface for failed command execution. * * @param m Message to be sent * @param timeout Timeout in milliseconds to wait for the client to receive and consume a processed acknowledgement for this command, where 0 indicates to wait indefinitely * @throws DisconnectedException Connection was disconnected when trying to send * @throws BadFilterException Bad filter specified in query or subscription * @throws BadRegexTopicException Invalid regex topic specified in query or subscription * @throws SubscriptionAlreadyExistsException Subscription with the specified id already exists * @throws TimedOutException Operation took too long to execute * @throws NameInUseException Client name is already in use * @throws AuthenticationException Client was unable to be authenticated * @throws NotEntitledException Client not entitled to that command or topic * @throws SubidInUseException The specified subid is already used by the client and this is not a replace, pause, or resume of that subscription * @throws CommandException An error with the command was returned by the server * @throws StoreException There was an error saving the command to the publish store */ private AckResponse syncAckProcessing(CommandId id, Message m, long timeout) throws DisconnectedException, TimedOutException, NameInUseException, AuthenticationException, NotEntitledException, CommandException, StoreException { return syncAckProcessing(id, m, timeout, 0, false); } /** * This is a helper function used to encapsulate the logic for waiting on a * sent message so that async commands like subscribe and * sow can have an * easier time using an exception interface for failed command execution. * * @param m Message to be sent * @param timeout Timeout in milliseconds to wait for the client to receive and consume a processed acknowledgement for this command, where 0 indicates to wait indefinitely * @param haSeq The HA sequence number of the message. * @param isSubscribe If the command is an HA subscribe variant or not. * @throws DisconnectedException Connection was disconnected when trying to send * @throws BadFilterException Bad filter specified in query or subscription * @throws BadRegexTopicException Invalid regex topic specified in query or subscription * @throws InvalidTopicException Invalid topic specified, either the topic isn't a SOW topic for a SOW operation or the topic is not in the transaction log for a bookmark subscription * @throws SubscriptionAlreadyExistsException Subscription with the specified id already exists * @throws TimedOutException Operation took too long to execute * @throws NameInUseException Client name is already in use * @throws AuthenticationException Client was unable to be authenticated * @throws NotEntitledException Client not entitled to that command or topic * @throws SubidInUseException The specified subid is already used by the client and this is not a replace, pause, or resume of that subscription * @throws CommandException An error with the command was returned by the server * @throws StoreException There was an error saving the command to the publish store */ private AckResponse syncAckProcessing(CommandId id, Message m, long timeout, long haSeq, boolean isSubscribe) throws DisconnectedException, TimedOutException, NameInUseException, AuthenticationException, NotEntitledException, CommandException, StoreException { // NOTE: This is called with lock already held // Make an empty ack response and stick it in the map. // The reader thread takes care of setting it, removing it from the map, // and signaling everyone when an ack comes in. AckResponse ackResponse = new AckResponse(); ackResponse.responded = false; // This is the only place that should have both locks, but can't // release the client lock until after send to protect the message // from being changed. acksLock.lock(); try { _acks.put(id, ackResponse); } finally { acksLock.unlock(); } ackResponse.connectionVersion = sendInternal(m, haSeq, isSubscribe); if (ackResponse.connectionVersion == -1) { acksLock.lock(); try { _acks.remove(id); } finally { acksLock.unlock(); } return ackResponse; } lock.unlock(); try { long startTime = System.currentTimeMillis(); long now = startTime; acksLock.lock(); try { // First check to see that our connection version hasn't // already failed / been canceled by a client background // thread that detected a disconnect. if (!ackResponse.abandoned && ackResponse.connectionVersion <= _lastFailedConnectionVersion) { ackResponse.abandoned = true; _acks.remove(id); } while(!ackResponse.abandoned && !ackResponse.responded && (timeout == 0 || (now-startTime) < timeout)) { try { if(timeout > 0) { long remainingTime = timeout - (now-startTime); if(remainingTime > 0) { ackReceived.await(remainingTime, TimeUnit.MILLISECONDS); } } else { ackReceived.await(); } } catch(InterruptedException ie) { // It's OK to be interrupted, let's just continue } now = System.currentTimeMillis(); } } finally { acksLock.unlock(); } } finally { lock.lock(); } if(ackResponse.responded) { // We found the ack in the callback, therefore we have // the status and failure reason when they're set. if(ackResponse.state != Message.Status.Failure) { if(m.getCommand() == Message.Command.Logon) { // if we're a 'logon' command we need to extract the seq number, // if available if(this.publishStore != null) { try { this.publishStore.discardUpTo(ackResponse.sequence); } catch (AMPSException e) { absorbedException(e); } // On a fresh client, we need to adopt the sequence // number the server has for us or the last persisted value // from our store. long lastPersisted = publishStore.getLastPersisted(); if (lastSentSequenceNumber <= lastPersisted) { lastSentSequenceNumber = lastPersisted; } } else if (lastSentSequenceNumber < ackResponse.sequence) { lastSentSequenceNumber = ackResponse.sequence; } } // validate the max_backlog, if a queue subscribe ack; set our // batch size lower if need be so subscribers can make progress. else if(_ackBatchSize > 1 && ackResponse.options != null && ackResponse.options.length() > 0) { int maxBacklogReturned = extractMaxBacklog(ackResponse.options); if(maxBacklogReturned > 0 && maxBacklogReturned < _ackBatchSize) { _ackBatchSize = maxBacklogReturned; } } // all is good, return return ackResponse; } switch(ackResponse.reason) { case Message.Reason.BadFilter: throw new BadFilterException("Filter '" + m.getFilter() + "' is invalid."); case Message.Reason.BadRegexTopic: throw new BadRegexTopicException("Regular Expression Topic '" + m.getTopic() + "' is invalid."); case Message.Reason.InvalidTopic: throw new InvalidTopicException("Topic \"" + m.getTopic() + "\" is invalid."); case Message.Reason.SubscriptionAlreadyExists: throw new SubscriptionAlreadyExistsException("Subscription for command '" + m.getCommandId() + "' already exists."); case Message.Reason.SubidInUse: throw new SubidInUseException("Subscription with subscription id '" + m.getSubId() + "' already exists."); case Message.Reason.NameInUse: throw new NameInUseException("Client name \"" + m.getClientName() + "\" is already in use."); case Message.Reason.AuthFailure: throw new AuthenticationException("Logon failed for user \"" + this.username + "\""); case Message.Reason.NotEntitled: if(m.getCommand() == Message.Command.Logon) { throw new NotEntitledException("User \"" + this.username + "\" not entitled to logon."); } else { throw new NotEntitledException("User \"" + this.username + "\" not entitled to topic \"" + m.getTopic() + "\"."); } case Message.Reason.InvalidBookmark: throw new InvalidBookmarkException("Bookmark \"" + m.getBookmark() + "\" is not valid."); case Message.Reason.InvalidOrderBy: throw new InvalidOrderByException("OrderBy \"" + m.getOrderBy() + "\" is not valid."); case Message.Reason.LogonRequired: throw new LogonRequiredException(); case Message.Reason.InvalidTopicOrFilter: throw new InvalidTopicException("Invalid topic \"" + m.getTopic() + "\" or filter \"" + m.getFilter() + "\""); case Message.Reason.InvalidSubId: throw new InvalidSubIdException("SubId \"" + m.getSubId() + "\" is not valid."); case Message.Reason.BadSowKey: { if (m.isSowKeyNull()) throw new BadSowKeyException("Bad sow key \"" + m.getSowKeys() + "\" is not valid."); else throw new BadSowKeyException("Bad sow key \"" + m.getSowKey() + "\" is not valid."); } case Message.Reason.DuplicateLogon: throw new DuplicateLogonException(); case Message.Reason.InvalidOptions: throw new InvalidOptionsException("Options \"" + m.getOptions() + "\" is not valid."); case Message.Reason.OrderByTooLarge: throw new InvalidOrderByException("OrderBy \"" + m.getOrderBy() + "\" is too large."); case Message.Reason.PublishFilterNoMatch: throw new PublishFilterException(m.getFilter(), m.getData()); default: throw new CommandException("Error from server: " + ackResponse.reasonText); } } else { // No ack found, must've timed out or disconnected waiting. if(!ackResponse.abandoned) { throw new TimedOutException("timed out waiting for operation"); } else { throw new DisconnectedException("Connection closed while waiting for operation."); } } } private int extractMaxBacklog(String optionsString) { // Parse options correctly even though, at the moment, only max_backlog is returned. int optionsLength = optionsString.length(); int keyStart = 0; int data=0; boolean inData = false; for(int i = 0; i < optionsLength; ++i) { char c = optionsString.charAt(i); switch(c) { case '=': if(!inData) { data = 0; inData = true; } break; case ' ': if(!inData) ++keyStart; break; case ',': if(optionsString.regionMatches(keyStart,"max_backlog=",0,12)) { return data; } data =0; keyStart = i+1; inData = false; break; default: if(inData) { data = (data * 10) + ((int)c - 48); } } } if(optionsString.regionMatches(keyStart,"max_backlog=",0,12)) { return data; } return 0; } class ClientHandler implements MessageHandler, TransportDisconnectHandler { Client client = null; CommandId key = new CommandId(); ClientHandler(Client client) { this.client = client; } public void preInvoke(int connectionVersion) { client.cancelSynchronousWaiters(connectionVersion); } /** * The MessageHandler implementation */ public void invoke(Message m) { final int SOWMask = Message.Command.SOW | Message.Command.GroupBegin | Message.Command.GroupEnd; final int PublishMask = Message.Command.OOF | Message.Command.Publish | Message.Command.DeltaPublish; try { final int commandType = m.getCommand(); if ((commandType & SOWMask) != 0) { /* Not currently implemented to avoid extra branch // A small cheat here using knowledge of Message.Commands // values for SOW(8), GroupBegin(8192), GroupEnd(16384) // and their GlobalCommandTypeHandlers values of 1, 2, 3 client._globalCommandTypeHandlers[commandType/8192+1] .invoke(m); */ m.getQueryId(key); _routes.deliverData(m, key); } else if ((commandType & PublishMask) != 0) { /* Not currently implemented to avoid extra branch client._globalCommandTypeHandlers[ (commandType==Message.Commands.SOW ? GlobalCommandTypeHandlers.SOW : GlobalCommandTypeHandler.Publish).invoke(m); */ Field subIds = m.getSubIdsRaw(); BookmarkField bookmark = m.getBookmarkRaw(); // Publish messages coming through on a subscription int index = 0; while( index < subIds.length ) { int end = index; for (; end < subIds.length && subIds.buffer[subIds.position + end] != (byte)','; ++end) ; Field subId = new Field(subIds.buffer, subIds.position + index, end-index); key.set(subId.buffer, subId.position, subId.length); MessageHandler handler = _routes.findRoute(key); if(handler != null) { m.setSubId(subId.buffer, subId.position, subId.length); m.setSubscription(null); boolean isQueueMessage = m.getLeasePeriodRaw().length > 0; if(!isQueueMessage && !bookmark.isNull()) { // send it on to the BookmarkStore for logging, // only log those messages we're really delivering. // check to see if the log says its a duplicate if(bookmarkStore.isDiscarded(m)) { // yes it is: don't send it try { this.client._globalCommandTypeHandlers[GlobalCommandTypeHandlers.DuplicateMessage.ordinal()].invoke(m); } catch (Exception e) { absorbedException(e); } } else // not a dupe, log it. { bookmarkStore.log(m); try { handler.invoke(m); } catch (Exception e) { absorbedException(e); } } } else { boolean thrown = false; if(isQueueMessage) m._client = this.client; try { handler.invoke(m); } catch (Exception e) { thrown = true; absorbedException(e); if(isQueueMessage && client._isAutoAckEnabled && !m.isIgnoreAutoAck()) { _ack(m.getTopicRaw(),m.getBookmarkRaw(),CANCEL_FIELD); } } if(isQueueMessage && !thrown && client._isAutoAckEnabled && !m.isIgnoreAutoAck()) { try { this.client._ack(m.getTopicRaw(),m.getBookmarkRaw()); } catch (Exception e) { absorbedException(e); } } } // if !bookmark.isNull() }// if handler!=null index = end + 1; } // while index < subIds.length } else if (commandType == Message.Command.Ack) { client._globalCommandTypeHandlers[GlobalCommandTypeHandlers.Ack.ordinal()].invoke(m); int ackType = m.getAckType(); // call any handlers registered for this ack _routes.deliverAck(m, ackType); switch(ackType) { // Persisted acks are handled internally. case Message.AckType.Persisted: persistedAck(m); return; // Processed acks are used to respond to waiting synchronous commands. case Message.AckType.Processed: processedAck(m); return; default: break; } } else if (m.getCommand() == Message.Command.Heartbeat) { client._globalCommandTypeHandlers[GlobalCommandTypeHandlers.Heartbeat.ordinal()].invoke(m); if(client.heartbeatTimer.getTimeout() != 0) { checkAndSendHeartbeat(true); } else { client._globalCommandTypeHandlers[GlobalCommandTypeHandlers.LastChance.ordinal()].invoke(m); } return; } else if (_routes.deliverData(m) == 0) { client._globalCommandTypeHandlers[GlobalCommandTypeHandlers.LastChance.ordinal()].invoke(m); } checkAndSendHeartbeat(false); } catch(Exception e) { this.client.absorbedException(e); } } private void checkAndSendHeartbeat(boolean force) { if (force || client.heartbeatTimer.check()) { try { client.heartbeatTimer.start(); client.sendWithoutRetry(client.beatMessage); } catch(Exception e) { absorbedException(e); } } } private void processedAck(Message message) { if(message.getCommandId(key)) { // If this command is awaiting a Processed ack, let's notify it // that it's arrived acksLock.lock(); try { AckResponse response = null; response = client._acks.remove(key); if (response != null) { response.state = message.getStatus(); response.reason = message.getReason(); response.reasonText = message.getReasonText(); response.username = message.getUserId(); response.password = message.getPassword(); response.serverVersion = new VersionInfo(message.getVersion()); response.sequence = message.getSequenceRaw().isNull()?0:message.getSequence(); response.responded = true; // Reset after setting Status/Reason response.options = message.getOptions(); response.bookmark = message.getBookmark(); ackReceived.signalAll(); return; } } catch (Exception e) { this.client.absorbedException(e); } finally { acksLock.unlock(); } try { _globalCommandTypeHandlers[GlobalCommandTypeHandlers.LastChance.ordinal()].invoke(message); } catch (Exception e) { this.client.absorbedException(e); } return; } } private void persistedAck(Message message) { /* * Best Practice: If you don't care about the dupe acks that occur during * failover or rapid disconnect/reconnect, then just ignore them. * We could discard each duplicate from the persisted store, but the * storage costs of doing 1 record discards is heavy. In most scenarios * we'll just quickly blow through the duplicates and get back to * processing the non-dupes. */ boolean handled=false; int reason = message.getReason(); if (!message.isSequenceNull()) { if (publishStore != null) { long sequence = ((LongField)message.getSequenceRaw()).getValue(); if (client._failedWriteHandler != null && (reason == Message.Reason.Duplicate || reason == Message.Reason.NotEntitled || message.getStatus() == Message.Status.Failure)) { try { FailedWriteStoreReplayer replayer = new FailedWriteStoreReplayer(reason); publishStore.replaySingle(replayer, sequence); } catch (Exception e) { this.client.absorbedException(e); } } handled = true; try { publishStore.discardUpTo(sequence); } catch (Exception e) { this.client.absorbedException(e); } } else if (client._failedWriteHandler != null && (reason == Message.Reason.Duplicate || reason == Message.Reason.NotEntitled || message.getStatus() == Message.Status.Failure)) { try { LongField sequenceField = (LongField) message.getSequenceRaw(); long sequence = (sequenceField != null && !sequenceField.isNull()) ? sequenceField.getValue(): 0L; message.reset(); message.setSequence(sequence); client._failedWriteHandler.failedWrite(message, reason); } catch (Exception e) { this.client.absorbedException(e); } handled = true; } } if (!handled && bookmarkStore != null) { BookmarkField bookmark = message.getBookmarkRaw(); if (bookmark != null && bookmark.length>1 && !bookmark.isNull()) { message.getSubId(key); if (_routes.hasRoute(key)) { try { handled = true; bookmarkStore.persisted(message.getSubIdRaw(), bookmark); } catch (AMPSException e) { this.client.absorbedException(e); } } } } if(!handled) { try { _globalCommandTypeHandlers[GlobalCommandTypeHandlers.LastChance.ordinal()].invoke(message); } catch(Exception e) { this.client.absorbedException(e); } } } // This is our way of "spying" on the activity of the user's disconnect handler. // We need to know if the user successfully connected to the underlying at least once, // to tell if they've "given up" on reconnecting or not. class TransportConnectionMeasurer implements Transport { Transport _t; boolean _successfullyConnected = false; public TransportConnectionMeasurer(Transport underlying) { _t = underlying; } public void clear() { _successfullyConnected = false; } public boolean successfullyConnected() { return _successfullyConnected; } public void connect(URI uri)throws ConnectionRefusedException, AlreadyConnectedException, InvalidURIException { _t.connect(uri); _successfullyConnected = true; } public void close() throws Exception { _t.close(); } public void disconnect() { _t.disconnect(); } public void setMessageHandler(MessageHandler ml) { _t.setMessageHandler(ml); } public void setDisconnectHandler(TransportDisconnectHandler dh) { _t.setDisconnectHandler(dh); } public void setExceptionListener(ExceptionListener exceptionListener) { _t.setExceptionListener(exceptionListener); } public void setThreadCreatedHandler(ThreadCreatedHandler tch_) { _t.setThreadCreatedHandler(tch_); } public void send(Message message) throws DisconnectedException { _t.send(message); } public void sendWithoutRetry(Message message) throws DisconnectedException { _t.sendWithoutRetry(message); } public Message allocateMessage() { return _t.allocateMessage(); } public long writeQueueSize() throws DisconnectedException { return _t.writeQueueSize(); } public long readQueueSize() throws DisconnectedException { return _t.readQueueSize(); } public long flush() throws DisconnectedException { return _t.flush(); } public long flush(long timeout) throws DisconnectedException { return _t.flush(timeout); } public void handleCloseEvent(int failedVersion_, String message, Exception e) throws DisconnectedException, RetryOperationException { _t.handleCloseEvent(failedVersion_, message, e); } public int getVersion() { return _t.getVersion(); } public void setReadTimeout(int readTimeoutMillis_) { _t.setReadTimeout(readTimeoutMillis_); } public void setTransportFilter(TransportFilter tracer_) { _t.setTransportFilter(tracer_); } public void setIdleRunnable(AMPSRunnable runnable) { _t.setIdleRunnable(runnable); } } /** * The TransportDisconnectHandler implementation */ public void invoke(Transport newTransport, Exception e_) { Transport oldTransport = null; try { lock.lock(); if (this.client.connected) broadcastConnectionStateChanged(ConnectionStateListener.Disconnected); oldTransport = transport; TransportConnectionMeasurer measurer = new TransportConnectionMeasurer(newTransport); transport = measurer; while(true) { boolean connectComplete = false; try { // Only set this true if we have a publish store _reconnectingPublishStore = publishStore != null; // Subscriptions aren't saved by default sub manager // Only set this to true if we have a real sub manager _reconnectingSubscriptionManager = !_defaultSubscriptionManager; measurer.clear(); try { lock.unlock(); if (disconnectHandler instanceof ClientDisconnectHandler2) { ((ClientDisconnectHandler2)disconnectHandler).invoke(client, e_); } else { disconnectHandler.invoke(client); } } finally { lock.lock(); } connectComplete = true; } finally { if (!connectComplete) _reconnectingSubscriptionManager = false; _reconnectingPublishStore = false; _reconnecting.signalAll(); } // If the disconnect handler successfully connected to // something, we keep going, even if the connection isn't // alive now. if(!measurer.successfullyConnected()) { broadcastConnectionStateChanged(ConnectionStateListener.Shutdown); this.client.connected = false; this.client.absorbedException(new DisconnectedException("reconnect failed.")); _reconnectingSubscriptionManager = false; _reconnecting.signalAll(); return; } try { lock.unlock(); this.client.subscriptionManager.resubscribe(client); broadcastConnectionStateChanged(ConnectionStateListener.Resubscribed); break; } catch (TimedOutException ex) { // timed out attempting to resubscribe, pass this.client.absorbedException(ex); } catch (DisconnectedException ex) { // disconnected exception while attempting to // resubscribe, pass. this.client.absorbedException(ex); } finally { lock.lock(); _reconnectingSubscriptionManager = false; _reconnecting.signalAll(); } } } catch(Exception e) { broadcastConnectionStateChanged(ConnectionStateListener.Shutdown); this.client.connected = false; this.client.absorbedException(e); } finally { _reconnectingSubscriptionManager = false; transport = oldTransport; lock.unlock(); } } } /** * Package-access helper method that allows a MessageStream to still * send heartbeats when it is pushing back on the receive socket * while the max depth threshold is exceeded. */ void checkAndSendHeartbeat(boolean force) { _handler.checkAndSendHeartbeat(force); } /** * Implementation of {@link StoreReplayer} to replay messages which were * recorded as published, but did not receive an ack. This is performed as * part of the logon function by default. * */ static class ClientStoreReplayer implements StoreReplayer { Client client; public ClientStoreReplayer(Client c) { this.client = c; } public void execute(Message m) throws DisconnectedException { if (!m.isCommandNull() && (m.isOptionsNull() || !this.client._reconnectingPublishStore || !m.getOptions().contains("cancel"))) { m.setAckType(Message.AckType.Persisted|m.getAckTypeOutgoing()); client.sendWithoutRetry(m); } long sequence = m.getSequence(); if (sequence > client.lastSentSequenceNumber) { client.lastSentSequenceNumber = sequence; } } } private ClientStoreReplayer replayer = null; static class FailedWriteHandlerV4Compat implements FailedWriteHandler { FailedWriteHandlerV4 _handler; public FailedWriteHandlerV4Compat(FailedWriteHandlerV4 handler_) { _handler = handler_; } public void failedWrite(Message m, int reason) { Field topic = m.getTopicRaw(); Field data = m.getDataRaw(); Field corId = m.getCorrelationIdRaw(); if (m.isDataNull()) { data = (!m.isFilterNull() ? m.getFilterRaw() : m.getSowKeysRaw()); } long sequence = m.isSequenceNull() ? 0 : m.getSequence(); _handler.failedWrite(sequence, m.getCommand(), topic.buffer, topic.position, topic.length, data.buffer, data.position, data.length, corId.buffer, corId.position, corId.length, reason); } } class FailedWriteStoreReplayer implements StoreReplayer { int _reason; int _replayCount; public FailedWriteStoreReplayer(int reason) { _reason = reason; _replayCount = 0; } public void execute(Message m) { ++_replayCount; _failedWriteHandler.failedWrite(m, _reason); } public int replayCount() { return _replayCount; } } private void cancelSynchronousWaiters(int connectionVersion) { ArrayList removeList = new ArrayList(); acksLock.lock(); try { // _disconnect() calls this with Integer.MAX_VALUE, so don't save that if (connectionVersion < Integer.MAX_VALUE) { _lastFailedConnectionVersion = connectionVersion; } for(Entry e : _acks.entrySet()) { // collect items to remove and mark them abandoned. AckResponse response = e.getValue(); if(response.connectionVersion <= connectionVersion) { response.abandoned = true; removeList.add(e.getKey()); } } // remove abandoned items from the list for(CommandId commandId : removeList) { _acks.remove(commandId); } ackReceived.signalAll(); } finally { acksLock.unlock(); } } static private class StopWatch { private long _timeout; private long _start; public StopWatch() { _timeout = 0; _start = 0; } public boolean check() { if (_timeout == 0) return false; long current = System.currentTimeMillis(); return ((current - _start) > _timeout); } public void start() { _start = System.currentTimeMillis(); } public void setTimeout(long timeout) { _start = System.currentTimeMillis(); _timeout = timeout; } public long getTimeout() { return _timeout; } } private class AckFlusherRunnable implements AMPSRunnable { public void run() throws AMPSException { if (_queueAckTimeout <= 0) return; lock.lock(); try { if (_topicHashMap.size() > 0) { long currentTimeMillis = System.currentTimeMillis(); for(QueueBookmarks q : _topicHashMap.values()) { if(q._bookmarkCount > 0 && currentTimeMillis - q._oldestTime >= _queueAckTimeout) { _ack(q); } } } } finally { lock.unlock(); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy