com.crankuptheamps.client.HAClient Maven / Gradle / Ivy
////////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2010-2021 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.io.IOException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import com.crankuptheamps.client.exception.*;
/**
*
* A highly-available AMPS Client object that automatically reconnects and
* re-subscribes to AMPS instances upon disconnect.
*
* An HAClient object provides failover and resubscription functionality by
* default. For reliable publish, set a {@link PublishStore} for the HAClient
* (and, in most cases, set a {@link FailedWriteHandler} as well so the
* application can detect any failed publishes). For managing transaction
* log replays, set a {@link BookmarkStore}.
*
* To connect to AMPS, you must provide a {@link ServerChooser}
* implementation to the HAClient. By default, the HAClient provides a
* {@link ExponentialDelayStrategy} with the default delay and backoff
* behavior for that class.
*
* By default, an HAClient object has the reconnect and resubscribe
* functionality provided by the {@link MemorySubscriptionManager} and the
* {@link HADisconnectHandler} classes. It is typically not necessary to
* replace the disconnect handler or subscription manager provided by
* default. Notice that replacing either of these defaults will change
* the failover, resubscription, and/or republish behavior of the
* HAClient.
*
* See the Developer Guide for more informantion on using the
* HAClient.
*/
public class HAClient extends Client
{
private volatile boolean _disconnected = false;
private final static int DefaultTimeout = 10000;
private Lock _haClientLock = new ReentrantLock();
private int _timeout = DefaultTimeout;
private ReconnectDelayStrategy _delayStrategy = null;
/**
* Sets the time delay between reconnect events. Calling this method
* creates and installs a new {@link FixedDelayStrategy } in this client.
* @param reconnectInterval The time waited between reconnect events, in milliseconds.
*/
public void setReconnectDelay(int reconnectInterval)
{
this._delayStrategy = new FixedDelayStrategy(reconnectInterval);
}
/**
* Sets the timeout used for logging on and resubscribing, once a connection
* is re-established.
*
* @param timeout_ The timeout value, in milliseconds, where 0 indicates no timeout.
*/
public void setTimeout(int timeout_)
{
_timeout = timeout_;
}
/**
* Returns the timeout used for logging on and resubscribing, once a
* connection is re-established.
*
* @return The timeout value in millseconds.
*/
public int getTimeout()
{
return _timeout;
}
/**
* Class used to handle dicsonnects for highly-available AMPS Clients.
* Catches any exceptions thrown by the HA Client.
*/
public static class HADisconnectHandler implements ClientDisconnectHandler, ClientDisconnectHandler2
{
public void invoke(Client client)
{
// This is only here to support subclasses of HADisconnectHandler,
// prior to the addition of the additional argument to invoke().
}
public void invoke(Client client_, Exception e_)
{
// a no-op, unless you've derived HADisconnectHandler.
invoke(client_);
HAClient haClient = (HAClient) client_;
try
{
haClient.getServerChooser().reportFailure(e_,
client_.getConnectionInfo());
haClient.connectAndLogonInternal();
} catch (Exception e)
{
if (client_.exceptionListener != null)
{
client_.exceptionListener.exceptionThrown(e);
}
return;
}
}
public HADisconnectHandler()
{
}
}
/**
* Returns the current {@link ServerChooser}.
*
* @return The current ServerChooser.
*/
public ServerChooser getServerChooser()
{
return _serverChooser;
}
/**
* Sets a {@link ServerChooser} for self.
*
* @param serverChooser_
* The ServerChooser that will be used by self to reconnect.
*/
public void setServerChooser(ServerChooser serverChooser_)
{
_haClientLock.lock();
try {
if (serverChooser_ != null) {
this._serverChooser = serverChooser_;
}
else {
this._serverChooser = new DefaultServerChooser();
}
}
finally {
_haClientLock.unlock();
}
}
ServerChooser _serverChooser;
/**
* Returns the current logon options string.
*
* @return The current String.
*/
public String getLogonOptions()
{
return _options;
}
/**
* Sets a logon options string for self.
*
* @param options_
* The String that will be used by self during logon.
*/
public void setLogonOptions(String options_)
{
this._options = options_;
}
String _options;
/**
* Constructs a new HAClient.
*
* @param name The client name, passed to the server to uniquely identify
* this client across sessions. 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 HAClient(String name)
{
super(name);
super.setDisconnectHandler(new HADisconnectHandler());
super.setSubscriptionManager(new MemorySubscriptionManager());
setReconnectDelayStrategy(new ExponentialDelayStrategy());
}
/**
* Creates a new memory-backed highly-available client. This client will
* automatically reconnect and re-subscribe, and uses memory to ensure
* messages are not lost and duplicates are processed appropriately.
* This convenience method is equivalent to creating an {@link HAClient }
* and setting a {@link MemoryBookmarkStore } and
* {@link MemoryPublishStore } for that client. The {@link MemoryPublishStore }
* has a capacity of 10000 blocks. If your application will not be both
* publishing to AMPS and using a bookmark replay, it is recommended that you
* construct an {@link HAClient } and then set the appropriate stores.
*
* @param name The client name, passed to the server to uniquely identify
* this client across sessions. 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.
* @return A new memory-backed HAClient instance.
* @throws StoreException Thrown when an operation fails with details of the failure.
*/
public static HAClient createMemoryBacked(String name) throws StoreException
{
HAClient client = new HAClient(name);
try
{
client.setBookmarkStore(new MemoryBookmarkStore());
client.setPublishStore(new MemoryPublishStore(10000));
}
catch(AlreadyConnectedException ex_)
{
// Declared on the set*Store functions, but can only happen if
// connected and we are definitely not connected yet.
}
catch(StoreException ex_)
{
client.close();
throw ex_;
}
return client;
}
/**
* Creates a new highly available client backed by disk. This client will
* automatically reconnect and re-subscribe, and uses disk to ensure
* messages are not lost and duplicates are processed appropriately.
* This convenience method is equivalent to creating an {@link HAClient }
* and setting a {@link LoggedBookmarkStore } and {@link PublishStore } for
* that client. If your application will not be both
* publishing to AMPS and using a bookmark replay, it is recommended that you
* construct an {@link HAClient } and then set the appropriate stores.
*
* @param name The client name, passed to the server to uniquely identify
* this client across sessions. 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 publishLog
* The path to a file used to store outgoing messages. This file
* will be created if it does not exist.
* @param initialPublishCapacity
* The initial size (in 2k blocks) of the publish store. Choosing
* an optimal value of initialPublishCapacity can prevent costly
* resizes, when servers or networks slow down.
* @param subscribeLog
* The path to a file used to log incoming message bookmarks.
* This file will be created if it does not exist.
*
* @return A new HAClient instance.
* @throws IOException Thrown if file creation fails.
* @throws StoreException Thrown when an operation fails with details of the failure.
*/
public static HAClient createFileBacked(String name, String publishLog,
int initialPublishCapacity, String subscribeLog) throws IOException, StoreException
{
HAClient client = new HAClient(name);
try
{
client.setBookmarkStore(new LoggedBookmarkStore(subscribeLog));
client.setPublishStore(new PublishStore(publishLog, initialPublishCapacity));
}
catch(AlreadyConnectedException ex_)
{
// Declared on the set*Store functions, but can only happen if
// connected and we are definitely not connected yet.
}
catch(IOException ex_)
{
client.close();
throw ex_;
}
catch(StoreException ex_)
{
client.close();
throw ex_;
}
return client;
}
/**
* Creates a new highly available client backed by disk. This client will
* automatically reconnect and re-subscribe, and uses disk to ensure
* messages are not lost and duplicates are processed appropriately.
* This convenience method is equivalent to creating an {@link HAClient }
* and setting a {@link LoggedBookmarkStore } and {@link PublishStore }
* that client. If your application will not be both
* publishing to AMPS and using a bookmark replay, it is recommended that you
* construct an {@link HAClient } and then set the appropriate stores.
*
* @param name The client name, passed to the server to uniquely identify
* this client across sessions. 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 publishLog
* The path to a file used to store outgoing messages. This file
* will be created if it does not exist.
* @param subscribeLog
* The path to a file used to log incoming message bookmarks.
* This file will be created if it does not exist.
* @return A new HAClient instance.
* @throws IOException Thrown when creating either the publish store or the bookmark store results in an IOException.
* @throws StoreException Thrown for any other failure. The exception will contain details of the operation that failed.
*/
public static HAClient createFileBacked(String name, String publishLog,
String subscribeLog) throws IOException, StoreException
{
HAClient client = new HAClient(name);
try
{
client.setBookmarkStore(new LoggedBookmarkStore(subscribeLog));
client.setPublishStore(new PublishStore(publishLog));
}
catch(AlreadyConnectedException ex_)
{
// Declared on the set*Store functions, but can only happen if
// connected and we are definitely not connected yet.
}
catch(IOException ex_)
{
client.close();
throw ex_;
}
catch(StoreException ex_)
{
client.close();
throw ex_;
}
return client;
}
/**
* Connects to the next server chosen for us by our {@link ServerChooser}.
* Will continue attempting to connect and logon to each successive
* server returned by the ServerChooser until the connection succeeds
* or the ServerChooser returns an empty URI.
*
* @throws ConnectionException
* An exception indicating no AMPS instance could be connected
* to.
*/
public void connectAndLogon() throws ConnectionException
{
_haClientLock.lock();
try {
_disconnected = false;
_delayStrategy.reset();
while(true)
{
connectAndLogonInternal();
// When manually called, we must re-subscribe manually as well.
try
{
this.getSubscriptionManager().resubscribe(this);
broadcastConnectionStateChanged(ConnectionStateListener.Resubscribed);
return;
} catch (AMPSException subException)
{
super.disconnect();
try {
_serverChooser.reportFailure(subException, getConnectionInfo());
} catch (Exception ex) {
throw new ConnectionException("ServerChooser.reportFailure exception", ex);
}
}
}
}
finally {
_haClientLock.unlock();
}
}
private void connectAndLogonInternal() throws ConnectionException
{
_haClientLock.lock();
try {
// get the next pair from the list
if (_serverChooser == null)
{
_disconnected = true;
throw new ConnectionException(
"No server chooser registered with this HAClient.");
}
// block any extra disconnect handling while we are in this loop.
ClientDisconnectHandler currentHandler = super.getDisconnectHandler();
super.setDisconnectHandler(new DefaultDisconnectHandler());
// Save current queue ack timeout value and temporarily set
// it to zero to prevent any queue acking until reconnect
// is complete.
long origAckTimeout = super.getAckTimeout();
super.setAckTimeout(0L);
try
{
while (!_disconnected)
{
String uri = _serverChooser.getCurrentURI();
if (uri == null)
{
_disconnected = true;
throw new ConnectionException(
"No AMPS instances available for connection. " + _serverChooser.getError());
}
int sleepTimeMillis = 0;
try
{
sleepTimeMillis =
_delayStrategy.getConnectWaitDuration(uri);
}
catch (Exception e)
{
_disconnected = true;
throw new ConnectionException(
"The reconnect delay strategy indicated that no " +
"more reconnect attempts should be made. The last " +
"connection error was: " + _serverChooser.getError(), e);
}
try
{
Thread.sleep(sleepTimeMillis);
Authenticator authenticator = _serverChooser
.getCurrentAuthenticator();
lock.lock();
try {
if (_disconnected) {
return;
}
super.connect(uri);
super.logon(_timeout, authenticator, _options);
}
finally {
lock.unlock();
}
_serverChooser.reportSuccess(getConnectionInfo());
_delayStrategy.reset();
if (_disconnected) {
super.disconnect();
}
return;
} catch (Exception exception)
{
// We need to substitute the "current" URI with the one
// returned by the serverChooser; the URI may not yet be set on the
// client.
ConnectionInfo ci = getConnectionInfo();
ci.put("client.uri", uri);
try {
_serverChooser.reportFailure(exception, ci);
} catch (Exception ex) {
throw new ConnectionException("ServerChooser.reportFailure exception", ex);
}
// Try disconnecting -- we might be partially connected?
try
{
super._disconnect(true);
} catch (Exception e)
{
if (exceptionListener != null)
{
exceptionListener.exceptionThrown(e);
}
}
}
}
}
finally
{
// Reinstate the original queue ack timeout to resume
// any pending batched queue acking.
super.setAckTimeout(origAckTimeout);
// Restore original disconnect handler.
super.setDisconnectHandler(currentHandler);
}
}
finally {
_haClientLock.unlock();
}
}
public void connect() throws ConnectionException
{
connectAndLogon();
}
@Override
public void connect(String uri) throws ConnectionException
{
connectAndLogon();
}
@Override
public CommandId logon(long timeout) throws ConnectionException
{
if (_disconnected) throw new DisconnectedException("Attempt to logon a disconnected HAClient. Call connectAndLogon() instead.");
throw new AlreadyConnectedException("Attempt to logon an HAClient. Call connectAndLogon() instead.");
}
@Override
public CommandId logon(String options) throws ConnectionException
{
if (_disconnected) throw new DisconnectedException("Attempt to logon a disconnected HAClient. Call connectAndLogon() instead.");
throw new AlreadyConnectedException("Attempt to logon an HAClient. Call connectAndLogon() instead.");
}
@Override
public CommandId logon(long timeout, String options)
throws ConnectionException
{
if (_disconnected) throw new DisconnectedException("Attempt to logon a disconnected HAClient. Call connectAndLogon() instead.");
throw new AlreadyConnectedException("Attempt to logon an HAClient. Call connectAndLogon() instead.");
}
@Override
public CommandId logon(long timeout, Authenticator authenticator)
throws ConnectionException
{
if (_disconnected) throw new DisconnectedException("Attempt to logon a disconnected HAClient. Call connectAndLogon() instead.");
throw new AlreadyConnectedException("Attempt to logon an HAClient. Call connectAndLogon() instead.");
}
@Override
public CommandId logon(long timeout, Authenticator authenticator,
String options) throws ConnectionException
{
if (_disconnected) throw new DisconnectedException("Attempt to logon a disconnected HAClient. Call connectAndLogon() instead.");
throw new AlreadyConnectedException("Attempt to logon an HAClient. Call connectAndLogon() instead.");
}
@Override
public CommandId logon() throws ConnectionException
{
if (_disconnected) throw new DisconnectedException("Attempt to logon a disconnected HAClient. Call connectAndLogon() instead.");
throw new AlreadyConnectedException("Attempt to logon an HAClient. Call connectAndLogon() instead.");
}
@Override
public void disconnect()
{
_disconnected = true;
super.disconnect();
}
/**
* Assembles a new ConnectionInfo with the state of this client and
* associated classes.
* @return A new ConnectionInfo object.
*/
@Override
public ConnectionInfo getConnectionInfo()
{
ConnectionInfo ci = super.getConnectionInfo();
ci.put("haClient.timeout", this._timeout);
return ci;
}
/**
* Sets the {@link ReconnectDelayStrategy} used to control sleeping
* behavior before connecting to a server.
*
* @param delayStrategy_ The delay strategy to use for connecting.
* @return Self.
*/
public final HAClient setReconnectDelayStrategy
(ReconnectDelayStrategy delayStrategy_)
{
_haClientLock.lock();
try {
if (delayStrategy_ != null) {
_delayStrategy = delayStrategy_;
}
else {
_delayStrategy = new ExponentialDelayStrategy();
}
}
finally {
_haClientLock.unlock();
}
return this;
}
/**
* Returns the {@link ReconnectDelayStrategy} used to control sleeping
* behavior before connecting to a server.
*
* @return The delay strategy to use for connecting.
*/
public ReconnectDelayStrategy getReconnectDelayStrategy()
{
return _delayStrategy;
}
}