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

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

////////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2010-2024 60East Technologies Inc., All Rights Reserved.
//
// This computer software is owned by 60East Technologies Inc. and is
// protected by U.S. copyright laws and other laws and by international
// treaties.  This computer software is furnished by 60East Technologies
// Inc. pursuant to a written license agreement and may be used, copied,
// transmitted, and stored only in accordance with the terms of such
// license agreement and with the inclusion of the above copyright notice.
// This computer software or any other copies thereof may not be provided
// or otherwise made available to any other person.
//
// U.S. Government Restricted Rights.  This computer software: (a) was
// developed at private expense and is in all respects the proprietary
// information of 60East Technologies Inc.; (b) was not developed with
// government funds; (c) is a trade secret of 60East Technologies Inc.
// for all purposes of the Freedom of Information Act; and (d) is a
// commercial item and thus, pursuant to Section 12.212 of the Federal
// Acquisition Regulations (FAR) and DFAR Supplement Section 227.7202,
// Government's use, duplication or disclosure of the computer software
// is subject to the restrictions set forth by 60East Technologies Inc..
//
////////////////////////////////////////////////////////////////////////////
package com.crankuptheamps.client;

import java.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. Most production applications
 * use the HAClient rather than Client.
 *
 * 

* 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 information on using the * HAClient. *

* *

* Example usage of the HAClient for publishing a JSON message: *

* *
 * 
 *
 * public class ConsolePublisher{
 *
 *   // The location of the AMPS server.
 *   private static final String uri_ = "tcp://127.0.0.1:9007/amps/json";
 *   
 *   public static void main(String[] args) {
 *      System.out.println("Starting the console publisher.");
 * 
 *      try (HAClient client = new HAClient("ConsolePublisher");) {
 *      // wait a bit to give a subscriber time to listen
 *      Thread.sleep(2000L);
 * 
 *      // Create a custom server chooser
 *      DefaultServerChooser serverChooser = new DefaultServerChooser();
 *      serverChooser.add(uri_);
 *   
 *      // Set the server chooser for the HAClient
 *      client.setServerChooser(serverChooser);
 *      // connect to the AMPS server and logon
 *      client.connectAndLogon();
 *      System.out.println("Publisher Connected");
 *   
 *
 *      client.publish("messages", "{\"message\" : \"Hello, World!\"}");
 *     }
 *     catch (Exception e)
 *     {
 *     System.err.println(e.getLocalizedMessage());
 *     e.printStackTrace(System.err);
 *     }
 *
 *   }
 *
 * }
 * 
 * 
*/ public class HAClient extends Client { private volatile boolean _disconnected = true; 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 milliseconds. */ public int getTimeout() { return _timeout; } /** * Class used to handle disconnects 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} for this client. * * @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; /** * Default constructor that creates a client without a name. This * constructor exists for use with frameworks that require managed * objects to have a default constructor. The client MUST still be * assigned a name using {@link #setName(String)} before connecting. * * @see #HAClient(String) * @see #setName(String) */ public HAClient() { this(null); } /** * 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 { String clientName = this.getName(); if (clientName == null || clientName.isEmpty()) { throw new ConnectionException("Client name must be set to a " + "non-empty value before connecting."); } if (!_disconnected) { throw new AlreadyConnectedException("HAClient is already connected. Call disconnect() first."); } _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); } catch (ConnectionException connEx) { Throwable exc = connEx.getCause(); // For the following exceptions, we need to disconnect // before relesing the lock as the bad connection could // still be open if it was a logon failure. if (exc != null && (exc instanceof AuthenticationException || exc instanceof DuplicateLogonException || exc instanceof NameInUseException || exc instanceof NotEntitledException || exc instanceof TimedOutException)) { try { super._disconnect(true); } catch (Exception ignoredEx) { if (exceptionListener != null) { exceptionListener.exceptionThrown(ignoredEx); } } } throw connEx; } 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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy