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

net.sf.eBus.client.ERemoteApp Maven / Gradle / Ivy

The newest version!
//
// Copyright 2011 - 2016, 2020 Charles W. Rapp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package net.sf.eBus.client;

import com.google.common.base.Strings;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.nio.BufferOverflowException;
import java.nio.channels.SelectableChannel;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.Nullable;
import net.sf.eBus.client.ConnectionMessage.ConnectionState;
import net.sf.eBus.client.EClient.ClientLocation;
import static net.sf.eBus.client.EClient.sCoreExecutor;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.ERequestFeed.RequestState;
import net.sf.eBus.client.sysmessages.AdMessage;
import net.sf.eBus.client.sysmessages.CancelRequest;
import net.sf.eBus.client.sysmessages.FeedStatusMessage;
import net.sf.eBus.client.sysmessages.KeyMessage;
import net.sf.eBus.client.sysmessages.LogoffMessage;
import net.sf.eBus.client.sysmessages.LogonCompleteMessage;
import net.sf.eBus.client.sysmessages.LogonMessage;
import net.sf.eBus.client.sysmessages.LogonReply;
import net.sf.eBus.client.sysmessages.McastKeyMessage;
import net.sf.eBus.client.sysmessages.McastSubscribeMessage;
import net.sf.eBus.client.sysmessages.PauseReply;
import net.sf.eBus.client.sysmessages.PauseRequest;
import net.sf.eBus.client.sysmessages.RemoteAck;
import net.sf.eBus.client.sysmessages.ResumeReply;
import net.sf.eBus.client.sysmessages.ResumeRequest;
import net.sf.eBus.client.sysmessages.SubscribeMessage;
import net.sf.eBus.client.sysmessages.SystemMessageType;
import net.sf.eBus.client.sysmessages.UdpConnectReply;
import net.sf.eBus.client.sysmessages.UdpConnectRequest;
import net.sf.eBus.client.sysmessages.UdpDisconnectReply;
import net.sf.eBus.client.sysmessages.UdpDisconnectRequest;
import net.sf.eBus.config.EConfigure;
import net.sf.eBus.config.EConfigure.ConnectionRole;
import net.sf.eBus.config.EConfigure.ConnectionType;
import static net.sf.eBus.config.EConfigure.ConnectionType.SECURE_TCP;
import static net.sf.eBus.config.EConfigure.ConnectionType.TCP;
import net.sf.eBus.config.EConfigure.DiscardPolicy;
import net.sf.eBus.config.EConfigure.RemoteConnection;
import net.sf.eBus.config.EConfigure.Service;
import net.sf.eBus.config.ENetConfigure;
import net.sf.eBus.logging.AsyncLoggerFactory;
import net.sf.eBus.messages.EMessage;
import net.sf.eBus.messages.EMessageHeader;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ENotificationMessage;
import net.sf.eBus.messages.EReplyMessage;
import net.sf.eBus.messages.EReplyMessage.ReplyStatus;
import net.sf.eBus.messages.ERequestMessage;
import net.sf.eBus.messages.type.DataType;
import net.sf.eBus.timer.EScheduledExecutor.IETimer;
import net.sf.eBus.util.MultiKey2;
import net.sf.eBus.util.logging.StatusReport;
import org.slf4j.Logger;

/**
 * This class provides communication between eBus applications.
 * eBus remote application connections are established in one of
 * four ways:
 * 
    *
  1. * {@link #openConnection(EConfigure.RemoteConnection) openConnection}: * Establishes a connection from this process to a remote * application at the specified Internet address. *
  2. *
  3. * {@link EServer#openServer(EConfigure.Service) EServer.openServer}: * Accepts connections from remote eBus applications and * encapsulates the accepted * {@link java.nio.channels.SocketChannel socket}. *
  4. *
  5. * {@link #configure(EConfigure) ERemoteApp.configure} and * {@link EServer#configure(EConfigure) EServer.configure}: * Create an {@code EConfigure} instance containing all * connections and services opening them all in one go. *
  6. *
  7. * Specified in the * -Dnet.sf.eBus.config.jsonFile=<conf file path> * configuration file. This technique creates remote * connections and/or servers automatically upon start. This * is similar to the third option. *
  8. *
* See {@link EConfigure} for detailed explanation on how to use * API or configuration file to set up eBus connections and * servers. *

* An {@code ERemoteApp} instance is responsible for forwarding * advertisements, subscriptions, requests and replies to the * remote application. When {@code ERemoteApp} loses its * connection, the advertisements and subscriptions from the * remote eBus application are retracted on the local eBus. This * means that all subscribers, publishers, requestors, and * repliers will see their respective feeds go down. The * peer eBus will do the same with respect to the advertisements * and subscriptions the local eBus sent to the peer. *

*

* An application can track {@code ERemoteApp} events for all * connections by subscribing to the message key * {@link net.sf.eBus.client.ConnectionMessage#MESSAGE_KEY net.sf.eBus.client.ConnectionMessage:/eBus}. * The {@link ConnectionMessage} contains the remote eBus * {@link java.net.InetSocketAddress socket address} and whether * the remote eBus is * {@link ConnectionMessage.ConnectionState#LOGGED_ON logged on} * or * {@link ConnectionMessage.ConnectionState#LOGGED_OFF logged off}. * If logged off, an optional reason is provided for why the * eBus logged off. Unsubscribe to stop receiving these updates. * Note: this message key is published locally and cannot be * accessed by remote eBus applications. *

*

* Only one connection may exist between any two eBus processes, * independent of which process established the first connection. * This means that an eBus process may not connect to * itself. *

* * @see EConfigure * @see ENetConfigure * * @author Charles Rapp */ // Note: IDE or Sonar may note these methods are not used. // This is incorrect. They are referenced by the FSM // transitions. public final class ERemoteApp implements EPublisher, ESubscriber, EReplier, ERequestor { //--------------------------------------------------------------- // Enums. // /** * A remote feed is either a subscription or an * advertisement. This needed since subscriptions and * advertisements can reference the same message key. */ private enum RemoteFeedType { /** * Remote feed subscription. */ SUBSCRIPTION, /** * Remote feed advertisement. */ ADVERTISEMENT } // end of enum RemoteFeedType //--------------------------------------------------------------- // Member data. // //----------------------------------------------------------- // Constants. // /** * Used to specify that the feed identifier is not set. */ public static final int NO_ID = -1; /** * When a remote eBus disconnects by sending a * {@link net.sf.eBus.client.sysmessages.LogoffMessage logoff message}, * then the * {@link ConnectionMessage#reason reason} is set to * "logged off". */ public static final String NORMAL_LOGOFF = "logged off"; /** * The unique identifier for this JVM which is a random * UUID. */ public static final String JVM_ID = (UUID.randomUUID()).toString(); /** * The connection is down. */ /* package */ static final int CONNECT_DOWN = 0; /** * The remote TCP connection completed synchronously. */ /* package */ static final int CONNECT_COMPLETE = 1; /** * The remote TCP session is in progress. */ /* package */ static final int CONNECT_INCOMPLETE = 2; /** * The remote TCP connect attempt failed and will never * complete. */ /* package */ static final int CONNECT_FAILED = 3; /** * Add this offset to {@link #mPauseDelay} to allow far-end * extra time to re-establish connection. */ private static final Duration RESUME_OFFSET = Duration.ofMillis(500L); //----------------------------------------------------------- // Statics. // /** * Tracks all existing remote eBus connections by mapping * the host and port to the remote eBus instance. */ private static final Map, ERemoteApp> sConnections = new ConcurrentHashMap<>(); /** * Tracks all paused remote eBus connections, mapping the * {@link #mRemoteId} to the connection. */ private static final Map sPausedConnections = new ConcurrentHashMap<>(); /** * Stores the accepted logon identifiers. */ private static final Set sLogonIds = new TreeSet<>(); /** * Protects the connection map, listener collection, and * logon ID set. */ private static final Lock sConnectionMutex = new ReentrantLock(true); /** * Singleton responsible for publishing * {@link ConnectionMessage} to subscribers. */ private static final ConnectionPublisher sConnPublisher; /** * Java logging access point. */ private static final Logger sLogger = AsyncLoggerFactory.getLogger(ERemoteApp.class); // Static initialization block. static { // Add this application's JVM identifier into the logon // IDs in order to prevent self connection. sLogonIds.add(JVM_ID); // Create the system messages data types for better // performance. DataType.findType(AdMessage.class); DataType.findType(CancelRequest.class); DataType.findType(FeedStatusMessage.class); DataType.findType(KeyMessage.class); DataType.findType(LogoffMessage.class); DataType.findType(LogonCompleteMessage.class); DataType.findType(LogonMessage.class); DataType.findType(LogonReply.class); DataType.findType(McastKeyMessage.class); DataType.findType(McastSubscribeMessage.class); DataType.findType(PauseReply.class); DataType.findType(PauseRequest.class); DataType.findType(RemoteAck.class); DataType.findType(ResumeReply.class); DataType.findType(ResumeRequest.class); DataType.findType(SubscribeMessage.class); DataType.findType(UdpConnectReply.class); DataType.findType(UdpConnectRequest.class); DataType.findType(UdpDisconnectReply.class); DataType.findType(UdpDisconnectRequest.class); (StatusReport.getsInstance()).register( ERemoteApp::reportRemoteStatus); sConnPublisher = new ConnectionPublisher(); EFeed.register(sConnPublisher); EFeed.startup(sConnPublisher); } // end of static initialization block. //----------------------------------------------------------- // Locals. // /** * The connection finite state machine. */ private final ERemoteAppContext mFSM; /** * Contains configuration defining this remote connection. * May be either {@code RemoteConnection} for initiator * connections or {@code Service} for acceptor connections. */ private final EConfigure.AbstractConfig mConfiguration; /** * This is the eBus client for this remote connection. The * client contains the executor used to run eBus tasks meant * for this object. */ private EClient mEClient; /** * Specifies whether this client initiated or accepted the * connection. */ private ConnectionRole mRole; /** * This connection's endpoint. */ private InetSocketAddress mAddress; /** * {@link EServer eBus service} which accepted this * connection. If not so accepted, then this value is * {@code null}. */ private EServer mServer; /** * The actual eBus connection to the remote engine. */ private EAbstractConnection mConnection; /** * The connect attempt status. */ private int mConnectStatus; /** * Set to {@code true} when successfully logged on and * {@code false} otherwise. */ private volatile boolean mLoggedOn; /** * This instance creation timestamp. */ private final Date mCreated; /** * The remote application identifier. */ private String mRemoteId; /** * The optional exception behind a logoff. Will be * {@code null} if the remote eBus correctly logged off. */ private Throwable mLogoffException; /** * Maps a message key to the feed. This map is * used to find to a feed referenced by remote feed type and * message key. */ private final Map, EFeed> mKeys; /** * Maps the local feed identifier to the local feed instance. */ private final Map mFeeds; /** * Maps a locally-generated request identifier to the request * instance. This request identifier is used instead of the * feed identifier for request/reply. */ private final Map mLocalRequests; /** * When a remote request is received, a local request feed is * needed to create a local request feed. This map tracks the * extant request feeds used for the purpose of making local * requests. There is one request feed per message key, all * having a {@link FeedScope#LOCAL_ONLY} scope. */ private final Map mRequestFeeds; /** * Tracks the active, remote requests. Used to terminate * requests in case of disconnect. Maps the remote feed * identifier to the local request. */ private final Map mRemoteRequests; /** * Links the local to remote feed identifier. The key is the * local feed identifier and the value is the remote feed * identifier. */ private final Map mToFromMap; /** * Obtain unique message class identifiers from here. */ private final MessageKeyStore mKeyStore; /** * Store the advertisements received during logon here and * process them all once logon is successfully completed. */ private List mLogonAds; /** * Set to {@code true} if this remote connection may be * paused. */ private boolean mCanPause; /** * Connection pause configuration. For connection initiator, * this is the requested pause parameters. For connection * acceptor, this is the allowed pause parameters. The actual * pause duration and backlog size is the minimum of the * initiator and acceptor values. *

* This value is {@code null} if {@link #mCanPause} is * {@code false}. *

*/ private EConfigure.PauseConfig mPauseConfig; /** * Negotiated pause delay. */ private Duration mPauseDelay; /** * Negotiated maximum backlog size. */ private int mMaxBacklogSize; /** * Negotiated message discard policy. */ private EConfigure.DiscardPolicy mDiscardPolicy; /** * This timer is used in two different ways by connection * acceptor and initiator. The initiator uses this timer to * for the maximum connect time. When the timer expires, the * initiator requests a connection pause. The acceptor uses * this timer to wait for a paused connection to resume. When * the timer expires in this scenario, the acceptor closes * the paused connection because the initiator has not * resumed the connection in time. */ private IETimer mPauseTimer; /** * This timer is used by connection initiator only. It is * used to detect when a connection is idle long enough to * cause a connection pause. */ private IETimer mIdleTimer; /** * This timestamp is updated each time a message is sent or * received. */ private Instant mBusyTimestamp; /** * Set to {@code true} when the connection is paused. */ private volatile boolean mIsPaused; /** * The input readers from a paused accepted connection. Used * to restore the input readers when the resumed connection * is accepted. */ private Map mPausedReaders; /** * Acquire this lock when working with pending message list. */ private final Lock mPendingLock; /** * When connection is paused, messages to be sent to the * far-end are stored here. These messages will be sent when * the connection is successfully resumed. */ private List mPendingMessages; /** * Tracks the number of non-system messages in * {@link #mPendingMessages}. System messages are always * added to the pending messages list. */ private int mPendingMessageCount; /** * When {@link #mPendingMessageCount} is ≥ to this value, * automatically resume the client connection. If this value * is set to zero, then automatic resumption is disabled. */ private int mPendingQueueLimit; // // StatusReport statistics. // Not meant to be strictly accurate. // /** * Track the number of subscriber feeds. */ private int mSubCount; /** * Track the number of publisher feeds. */ private int mPubCount; /** * Track the number of replier feeds. */ private int mReplierCount; /** * Track the number of request feeds. */ private int mRequestorCount; //--------------------------------------------------------------- // Member methods. // //----------------------------------------------------------- // Constructors. // /** * Creates a new ERemoteApp instance for the specified * remote eBus engine. * @param config configuration defining this connection. */ private ERemoteApp(final EConfigure.AbstractConfig config) { mConfiguration = config; mAddress = null; mServer = null; mRemoteId = null; mConnection = null; mConnectStatus = CONNECT_DOWN; mLoggedOn = false; mCreated = new Date(); mFSM = new ERemoteAppContext(this); mFeeds = new HashMap<>(); mKeys = new HashMap<>(); mLocalRequests = new HashMap<>(); mRequestFeeds = new HashMap<>(); mRemoteRequests = new HashMap<>(); mToFromMap = new HashMap<>(); mKeyStore = new MessageKeyStore(this); mCanPause = false; mPauseConfig = null; mPauseDelay = Duration.ZERO; mPendingLock = new ReentrantLock(); mPendingMessages = Collections.emptyList(); mPendingQueueLimit = 0; // TODO: requires SMC be moved to slf4j. // mFSM.setDebugLogger(sLogger); // mFSM.setDebugLoggerLevel(Level.FINEST); // mFSM.setDebugFlag(sLogger.isLoggable(Level.FINEST)); } // end of ERemoteApp(...) // // end of Constructors. //----------------------------------------------------------- //----------------------------------------------------------- // EConnection Callback Methods. // /** * The connection is successfully established. * @param c the now connected {@link EAbstractConnection} * source. */ // Parameter required by functional interface. @SuppressWarnings({"java:S1172"}) /* package */ void handleOpen(final EAbstractConnection c) { sLogger.debug("{}: connected to remote eBus.", mAddress); // There is a race condition in SSLEngine's handshake // protocol: the client declares the handshaking as // finished after sending the final handshake message to // the server side. That means the server's SSL handshake // process is not finished when the eBus logon message is // sent. // Result: the server side will ignore the logon message // because it is still in handshake state. // Solution: Perform a deliberate sleep for a few // milliseconds to give server side a chance to process // the final handshake message and declare handshaking // finished. Then server side will be ready for the logon // message. if (connectionType() == ConnectionType.SECURE_TCP) { mEClient.dispatch( () -> { try { // HACK ALERT!!! // Sleep necessary to resolve SSL // handshake race condition. Thread.sleep(50L); mFSM.connected(); } catch (InterruptedException interrupt) {} }); } else { mEClient.dispatch(mFSM::connected); } } // end of handleConnect(EAbstractConnection) /** * The {@link EAbstractConnection} is now closed. * If an exception caused this closure, it is forwarded to * the listener via {@code ex}. * If {@link EAbstractConnection} is set to do automatically * reconnect, then {@link #handleOpen(EAbstractConnection)} * will be called when the connection has been * re-established. * @param c this connection is the now closed. */ // Parameter required by functional interface. @SuppressWarnings({"java:S1172"}) /* package */ void handleClose(final EAbstractConnection c) { sLogger.info("{}: disconnected.", mAddress); mEClient.dispatch(mFSM::disconnected); } // end of handleDisconnect(EAbstractConnection) // // end of EConnection Callback Methods. //----------------------------------------------------------- //----------------------------------------------------------- // EObject Interface Implementation. // /** * Returns eBus object name. May not be unique. * @return eBus object name. */ @Override public String name() { return (mAddress.toString()); } // end of name() // // end of EObject Interface Implementation. //----------------------------------------------------------- //----------------------------------------------------------- // EPublisher Interface Implementation. // /** * This callback means that there is or is not a local * subscriber for the remote publisher. Convert this into * a remote subscription message, passing along the given * feed state. * @param feedState subscribe to or unsubscribe from the * remote publisher. * @param feed the publish feed. */ @Override @SuppressWarnings ({"java:S1067"}) public void publishStatus(final EFeedState feedState, final IEPublishFeed feed) { final int fromFeedId = feed.feedId(); final Integer id = mToFromMap.get(fromFeedId); final int toFeedId = (id == null ? NO_ID : id); sLogger.debug("{}: {} publish status is {}.", mAddress, feed, feedState); // Is the feed still around? It might have been // retracted just prior to this method call. // Did the feed state change? // The feed state is changed if: // + from feed identifier is know AND EITHER // + the feed state is UP and to feed ID == NO_ID OR // + the feed state is DOWN and to feed ID != NO_ID. if (mFeeds.containsKey(fromFeedId) && ((feedState == EFeedState.UP && toFeedId == NO_ID) || (feedState == EFeedState.DOWN && toFeedId != NO_ID))) { // Yes. Forward the corresponding subscription // message to the remote eBus app. // If this is an unsubscribe, then mark the feed as // down and clear out the from->to map entry. if (feedState == EFeedState.DOWN) { ((EPublishFeed) feed).clearFeedState(); mToFromMap.remove(fromFeedId); } send( new EMessageHeader( (SystemMessageType.SUBSCRIBE).keyId(), fromFeedId, toFeedId, (SubscribeMessage.builder()).messageKey(feed.key()) .feedState(feedState) .build())); } } // end of publishStatus(EFeedState, IEPublishFeed) // // end of EPublisher Interface Implementation. //----------------------------------------------------------- //----------------------------------------------------------- // ESubscriber Interface Implementation. // /** * Forwards the subscribe feed state to the remote eBus * application if the feed is still in place. * @param feedState the updated subscription state. * @param feed the state change applies to this subscribe * feed. This is an {@link ESubscribeFeed} instance since * multi-subject feeds cannot be transmitted to remote * applications. */ @Override public void feedStatus(final EFeedState feedState, final IESubscribeFeed feed) { final int feedId = feed.feedId(); sLogger.debug("{}: {} feed status is {}.", mAddress, feed, feedState); // Is the feed still around? if (mFeeds.containsKey(feedId)) { // Yes. Forward the feed state to the remote eBus. // If putting the subscription is place, then there // is no remote feed yet. send( new EMessageHeader( (SystemMessageType.FEED_STATUS).keyId(), feedId, mToFromMap.get(feedId), (FeedStatusMessage.builder()).feedState(feedState) .build())); } } // end of feedStatus(EFeedState, ESubscribeFeed) /** * Forwards the message to the remote eBus application if the * subscription is still in place. * @param msg forward this message. * @param feed subscription feed posting this callback. This * is an {@link ESubscribeFeed} instance since multi-subject * feeds cannot be transmitted to remote applications. */ @Override public void notify(final ENotificationMessage msg, final IESubscribeFeed feed) { final int feedId = feed.feedId(); // Is the feed still around? It might have been retracted // just prior to this method call. if (mFeeds.containsKey(feedId)) { sLogger.trace("{}: forwarding message\n{}", mAddress, msg); // Yes, still here. Forward the notification message // to the remote eBus. send(new EMessageHeader(mKeyStore.findOrCreate(msg.key()), feedId, mToFromMap.get(feedId), msg)); } } // end of notify(ENotification, ESubscribeFeed) // // end of ESubscriber Interface Implementation. //----------------------------------------------------------- //----------------------------------------------------------- // EReplier Interface Implementation. // /** * Assigns a locally unique identifier to this request and * forwards the request to the remote eBus application. The * remote eBus application uses the assigned request * identifier to match this request to the remote request * feed. * @param request the request instance. */ @Override public void request(final EReplyFeed.ERequest request) { final ERequestMessage msg = request.request(); final int keyId = mKeyStore.findOrCreate(msg.key()); final int requestId = request.feedId(); // Map the request identifier to the request. mLocalRequests.put(requestId, request); // Forward the request message using the request // identifier as the from feed ID. send(new EMessageHeader(keyId, requestId, NO_ID, msg)); } // end of request(ERequest) /** * Forward this cancel request to the remote eBus application * if that remote application acknowledged the request. If * not yet acknowledged, then sends the cancel request * message upon acknowledgment. * @param request cancel this request. * @param mayRespond set to {@code true} if replier is * allowed to respond to the cancel request. This allows the * replier to accept or reject the request cancellation. */ @Override public void cancelRequest(final EReplyFeed.ERequest request, final boolean mayRespond) { final int feedId = request.feedId(); // Is the request acknowledged? if (mToFromMap.containsKey(feedId)) { // Yes. Forward the cancel request to the remote // eBus application. final CancelRequest.Builder builder = CancelRequest.builder(); send( new EMessageHeader( (SystemMessageType.CANCEL_REQUEST).keyId(), feedId, mToFromMap.get(feedId), builder.mayRespond(mayRespond).build())); // Remove from the request map. mLocalRequests.remove(feedId); } // No, not yet acknowledged. Send the cancel request // when the ack arrives. } // end of cancelRequest(ERequest) // // end of EReplier Interface Implementation. //----------------------------------------------------------- //----------------------------------------------------------- // ERequestor Interface Implementation. // /** * Ignores request feed state changes. * @param feedState the latest request feed state. * @param feed feed state applies to this feed. */ // This callback does nothing and so the method body is // empty. @SuppressWarnings({"java:S1186"}) @Override public void feedStatus(final EFeedState feedState, final IERequestFeed feed) {} /** * Forwards the remaining replier count and reply message to * the remote eBus application. *

* This method and {@link #remoteRequest(EMessageHeader)} are * synchronized to protect against the reply delivery prior * to storing the request information. If not synchronized, * the reply would appear to be for an unknown request. *

* @param remaining number of repliers still actively * replying. * @param reply the reply message. * @param request reply is for this request. */ @Override public synchronized void reply(final int remaining, final EReplyMessage reply, final ERequestFeed.ERequest request) { final int fromFeedId = request.feedId(); // Is the request still around? if (mToFromMap.containsKey(fromFeedId)) { final int toFeedId = mToFromMap.get(fromFeedId); // Yes, the request is still around. // Is this a final reply? if (reply.isFinal()) { // Yes. That means the remaining count changed. // Send a new request ack with the updated // remaining count. sLogger.debug( "{}: request step 4: {} -> {} has {} remaining replies.", mAddress, fromFeedId, toFeedId, remaining); send( new EMessageHeader( (SystemMessageType.REMOTE_ACK).keyId(), fromFeedId, toFeedId, (RemoteAck.builder()).remaining(remaining) .build())); } // Now forward teh reply to the remote eBus // application. send( new EMessageHeader( mKeyStore.findOrCreate(reply.key()), fromFeedId, toFeedId, reply)); // Is this remote request finished? if (remaining == 0) { // Yes. Clean up the request. mToFromMap.remove(fromFeedId); mRemoteRequests.remove(request.feedId()); } } } // end of reply(int, EReplyMessage, ERequestFeed.ERequest) // // end of ERequestor Interface Implementation. //----------------------------------------------------------- //----------------------------------------------------------- // Object Method Overrides. // @Override public String toString() { return ("ERemoteApp " + mAddress); } // end of toString() // // end of Object Method Overrides. //----------------------------------------------------------- //----------------------------------------------------------- // From Local JVM to Remote JVM. // /** * Forwards the given message to all {@code ERemoteApp} * instances by wrapping message in a {@link SendTask} and * posting it to the {@link EClient} task queue. * @param h forward this message to remote eBus * instances. */ /* package */ static void forwardAll(final EMessageHeader h) { sConnections.values() .stream() .forEach( conn -> (conn.mEClient).dispatch(() -> conn.send(h))); } // end of forwardAll(AdMessage) // // end of From Local JVM to Remote JVM. //----------------------------------------------------------- //----------------------------------------------------------- // From Remote JVM to Local JVM // /** * Passes the logon message to the finite state machine. * @param header contains the logon message. */ /* package */ void remoteLogon(final EMessageHeader header) { // Currently on the selector thread. Move this FSM // transition over to the run queue thread. mEClient.dispatch( () -> mFSM.logon((LogonMessage) header.message())); } // end of remoteLogon(EMessageHeader) /** * Passes the logon reply to the finite state machine. * @param header contains the logon reply message. */ /* package */ void remoteLogonReply(final EMessageHeader header) { // Currently on the selector thread. Move this FSM // transition over to the run queue thread. mEClient.dispatch( () -> mFSM.logonReply((LogonReply) header.message())); } // end of remoteLogonReply(EMessageHeader) /** * Passes the logon complete message to the finite state * machine. * @param header contains the logon complete message. */ /* package */ void remoteLogonComplete(final EMessageHeader header) { mBusyTimestamp = Instant.now(); // Currently on the selector thread. Move this FSM // transition over to the run queue thread. mEClient.dispatch( () -> mFSM.logonComplete( (LogonCompleteMessage) header.message())); } // end of remoteLogonComplete(EMessageHeader) /** * Passes the logoff message to the finite state machine. * @param header contains the logoff message. */ /* package */ void remoteLogoff(final EMessageHeader header) { // Currently on the selector thread. Move this FSM // transition over to the run queue thread. mEClient.dispatch( () -> mFSM.logoff((LogoffMessage) header.message())); } // end of remoteLogoff(EMessageHeader) /** * Passes the pause request message to the finite state * machine. * @param header contains the pause request message. */ /* package */ void remotePauseRequest(final EMessageHeader header) { // Currently on the selector thread. Move this FSM // transition over to the run queue thread. mEClient.dispatch( () -> mFSM.pause((PauseRequest) header.message())); } // end of remotePauseRequest(EMessageHeader) /** * Passes the pause reply message to the finite state * machine. * @param header contains the pause reply message. */ /* package */ void remotePauseReply(final EMessageHeader header) { // Currently on the selector thread. Move this FSM // transition over to the run queue thread. mEClient.dispatch( () -> mFSM.pauseReply((PauseReply) header.message())); } // end of remotePauseReply(EMessageHeader) /** * Passes the resume request message to the finite state * machine. * @param header contains the resume request message. */ /* package */ void remoteResumeRequest(final EMessageHeader header) { // Currently on the selector thread. Move this FSM // transition over to the run queue thread. mEClient.dispatch( () -> mFSM.resume((ResumeRequest) header.message())); } // end of remoteResumeRequest(EMessageHeader) /** * Passes the resume reply message to the finite state * machine. * @param header contains the resume reply message. */ /* package */ void remoteResumeReply(final EMessageHeader header) { // Currently on the selector thread. Move this FSM // transition over to the run queue thread. mEClient.dispatch( () -> mFSM.resumeReply((ResumeReply) header.message())); } // end of remoteResumeReply(EMessageHeader) /** * Turns around and passes this message right back to * {@link EAbstractConnection#keyUpdate(KeyMessage)}. * @param header contains the class update message. */ /* package */ void remoteClassUpdate(final EMessageHeader header) { mBusyTimestamp = Instant.now(); mConnection.keyUpdate((KeyMessage) header.message()); } // end of remoteClassUpdate(EMessageHeader) /** * Routes the advertisement message through the state * machine. If the connection is open, then processes the * advertisement immediately. If logging on, then stores the * advertisement for processing when logon is completed. * @param header contains the advertise message. */ /* package */ void remoteAd(final EMessageHeader header) { mBusyTimestamp = Instant.now(); // Currently on the selector thread. Move this FSM // transition over to the run queue thread. mEClient.dispatch( () -> mFSM.adMessage((AdMessage) header.message())); } // end of remoteAd(EMessageHeader) /** * Either adds are removes a local subscribe feed based on * the remote eBus feed state. * @param header contains the subscribe message. */ /* package */ void remoteSubscribe(final EMessageHeader header) { final SubscribeMessage subMsg = (SubscribeMessage) header.message(); mBusyTimestamp = Instant.now(); try { @SuppressWarnings ("unchecked") final Class mc = (Class) Class.forName(subMsg.messageClass); final EMessageKey messageKey = new EMessageKey(mc, subMsg.messageSubject); final MultiKey2 key = new MultiKey2<>( RemoteFeedType.SUBSCRIPTION, messageKey); int toFeedId = header.toFeedId(); final ESubscribeFeed feed; // Adding or removing. if (subMsg.feedState == EFeedState.UP) { // Adding. final ESubscribeFeed.Builder builder = ESubscribeFeed.builder(); feed = builder.target(this) .messageKey(messageKey) .location(ClientLocation.REMOTE) .scope(FeedScope.LOCAL_ONLY) .statusCallback(this::feedStatus) .notifyCallback(this::notify) .build(); toFeedId = feed.feedId(); // Map the message key and feed identifier to the // feed instance. mKeys.put(key, feed); mFeeds.put(toFeedId, feed); // Link the remote, publish feed to the local // subscribe feed. mToFromMap.put(toFeedId, header.fromFeedId()); sLogger.debug("{}: subscribing to feed {}.", mAddress, feed); // Now put the feed in place. feed.subscribe(); ++mSubCount; } // Removing. else if ( (feed = (ESubscribeFeed) findFeed( toFeedId, key)) != null) { // Retract the feed and remove from the feeds // map. feed.unsubscribe(); mKeys.remove(key); mFeeds.remove(toFeedId); // Disconnect the local and remote feeds. mToFromMap.remove(toFeedId); sLogger.debug("{}: unsubscribing from feed {}.", mAddress, feed); --mSubCount; } } catch (ClassNotFoundException classex) { sLogger.trace( "{}: subscribe message {} unknown class {}, ignored.", mAddress, subMsg.feedState, subMsg.messageClass); } catch (IllegalArgumentException jex) { sLogger.warn("{}: ad message to {} {}:{} failed.", mAddress, subMsg.feedState, subMsg.messageClass, subMsg.messageSubject, jex); } } // end of remoteSubscribe(EMessageHeader) /** * Converts the remote subscribe feed state to a local * publish feed state if the publish feed is still active. * @param header contains the subscribe feed state message. */ /* package */ void remoteFeedStatus(final EMessageHeader header) { final int toFeedId = header.toFeedId(); final int fromFeedId = header.fromFeedId(); final FeedStatusMessage fsMsg = (FeedStatusMessage) header.message(); final EFeed feed = mFeeds.get(toFeedId); mBusyTimestamp = Instant.now(); sLogger.trace("{}: from={}, to={}, status={}, feed={}.", mAddress, fromFeedId, toFeedId, fsMsg.feedState, (feed == null ? "(unknown)" : feed)); // Is the feed still around? // Is it still active? if (feed != null && feed.isActive()) { // Yes and yes. Convert the feed state to a publish // state. if (feed instanceof EPublishFeed) { ((IEPublishFeed) feed).updateFeedState( fsMsg.feedState); } else { ((IEReplyFeed) feed).updateFeedState( fsMsg.feedState); } // Link this publish feed to the remote subscribe // feed. mToFromMap.put(toFeedId, fromFeedId); } } // end of remoteFeedStatus(EMessageHeader) /** * Forwards a notification from a remote publisher to local * subscribers. * @param header contains the local publish feed identifier * and notification message. */ /* package */ void remoteNotify(final EMessageHeader header) { final int toFeedId = header.toFeedId(); final EPublishFeed feed = (EPublishFeed) mFeeds.get(toFeedId); final ENotificationMessage message = (ENotificationMessage) header.message(); mBusyTimestamp = Instant.now(); sLogger.trace( "{}: feed {} (from={}, to={}) received message:\n{}", mAddress, feed, header.fromFeedId(), toFeedId, message); if (feed != null) { try { feed.publish(message); } catch (IllegalArgumentException | IllegalStateException jex) { // Ignore. } } } // end of remoteNotify(EMessageHeader) /** * Converts the remote request message into a local * {@link ERequestFeed}, posting the inbound request message * to that feed. Then sends a {@link RemoteAck} message * which contains the request feed identifier and the remote * request identifier. This allows the far-end which sent the * request to link the feeds together. *

* This method and * {@link #reply(int, EReplyMessage, ERequestFeed.ERequest)} * are synchronized because replies may be delivered before * the request information is stored away. If not * synchronized, then the reply would appear to be for an * unknown request. *

* @param header contains the remote request message. */ /* package */ synchronized void remoteRequest(final EMessageHeader header) { final int fromFeedId = header.fromFeedId(); final ERequestMessage reqMsg = (ERequestMessage) header.message(); final EMessageKey key = reqMsg.key(); final ERequestFeed reqFeed = findRequestFeed(key); final ERequestFeed.ERequest request = reqFeed.request(reqMsg); final int toFeedId = request.feedId(); mBusyTimestamp = Instant.now(); try { // Create a new local request and store it away using // the remote feed identifier. mRemoteRequests.put(fromFeedId, request); // Link the two feeds together ... mToFromMap.put(toFeedId, fromFeedId); mFeeds.put(toFeedId, request); // ... and send an acknowledgement back to the remote // application which contains the local feed // identifier. But do this asynchronously since the // remote connection should be accessed from a // dispatcher thread only. mEClient.dispatch( () -> send( new EMessageHeader( (SystemMessageType.REMOTE_ACK).keyId(), toFeedId, fromFeedId, (RemoteAck.builder()).remaining(request.repliersRemaining()) .build()))); ++mRequestorCount; } catch (IllegalArgumentException | IllegalStateException jex) { final EMessageKey replyKey = new EMessageKey( EReplyMessage.class, key.subject()); final int keyId = mKeyStore.findOrCreate(replyKey); final EReplyMessage.ConcreteBuilder replyBuilder = (EReplyMessage.ConcreteBuilder) EReplyMessage.builder(); // If the request failed, then close the request and // clear the maps. request.close(); mToFromMap.remove(toFeedId); mFeeds.remove(toFeedId); // Send the reply first and then the updated request // acknowledgement. mEClient.dispatch( () -> send( new EMessageHeader( keyId, toFeedId, fromFeedId, replyBuilder.subject(key.subject()) .replyStatus(ReplyStatus.ERROR) .replyReason(jex.getMessage()) .build()))); // Send an acknowledgement stating that there will be // no replies except the failure reply. mEClient.dispatch( () -> send( new EMessageHeader( (SystemMessageType.REMOTE_ACK).keyId(), toFeedId, fromFeedId, (RemoteAck.builder()).remaining(0) .build()))); } } // end of remoteRequest(EMessageHeader) /** * Forward the request cancellation to the replier if still * in place. * @param header contains the request cancel message. */ /* package */ void remoteCancelRequest(final EMessageHeader header) { final int toFeedId = header.toFeedId(); final ERequestFeed.ERequest request = (ERequestFeed.ERequest) mFeeds.get(toFeedId); mBusyTimestamp = Instant.now(); if (request == null) { sLogger.debug( "{}: remote cancel: unknown request feed", mAddress); } else { sLogger.debug( "{}: remote cancel: {} is {}.", mAddress, request.key(), request.requestState()); } // Is the request still around? // Is the request still active? if (request != null && request.requestState() == RequestState.ACTIVE) { // No. Clean up the defunct request. request.close(); // Clean up the dead request. mToFromMap.remove(toFeedId); mRemoteRequests.remove(toFeedId); --mRequestorCount; } // The request is either done, terminated, or a cancel is // already in progress. } // end of remoteCancelRequest(EMessageHeader) /** * The inbound message contains the remote feed identifier * associated with the request, linking the local feed with * the remote. Allows the request to be canceled before the * first reply is received. * @param header contains the inbound request acknowledgment. */ /* package */ void remoteRequestAck(final EMessageHeader header) { final int fromFeedId = header.fromFeedId(); final int toFeedId = header.toFeedId(); final RemoteAck msg = (RemoteAck) header.message(); final EReplyFeed.ERequest request = mLocalRequests.get(toFeedId); final RequestState reqState = request.state(); mBusyTimestamp = Instant.now(); mToFromMap.putIfAbsent(toFeedId, fromFeedId); request.remoteRemaining(msg.remaining); // Was a cancel attempt made before this acknowledgment // was received? if (reqState == RequestState.CANCELED) { // Yes. Send the cancel request message now. final CancelRequest.Builder builder = CancelRequest.builder(); send(new EMessageHeader( (SystemMessageType.CANCEL_REQUEST).keyId(), toFeedId, fromFeedId, builder.mayRespond(false).build())); // Remove from the request map. mLocalRequests.remove(toFeedId); } } // end of remoteRequestAck(EMessageHeader) /** * Handles a {@link RemoteAck} message which is used to * link local and remote requests together. If the request * was canceled prior to this message receipt, then a * {@link CancelRequest} message is immediately sent back to * the remote eBus application. * @param header contains the system remote reply message. */ /* package */ void remoteReply(final EMessageHeader header) { final int toFeedId = header.toFeedId(); final int fromFeedId = header.fromFeedId(); final EReplyFeed.ERequest request = mLocalRequests.get(toFeedId); mBusyTimestamp = Instant.now(); // Is the request still around? if (request == null) { // No. Ignore this message. } // Yes, the request is still here. But is it still // breathing? else if (request.state() == RequestState.CANCELED) { // Nope, its dead. Tell the other side to terminate // its request. final CancelRequest.Builder builder = CancelRequest.builder(); send(new EMessageHeader( (SystemMessageType.CANCEL_REQUEST).keyId(), fromFeedId, toFeedId, builder.mayRespond(false).build())); // Remove from the request map as well. mLocalRequests.remove(toFeedId); } // The request is both known and alive. // Its alive, I tell you! Alive! else { final EReplyMessage replyMsg = (EReplyMessage) header.message(); final RequestState reqState = request.state(); // So link the request and remote feed by the hip. mToFromMap.put(toFeedId, fromFeedId); // Forward the reply to the requester. request.remoteReply(replyMsg); // Is a request cancellation in progress or // terminated? if (reqState == RequestState.CANCELED) { // Then send the cancel request to the far end. send(new EMessageHeader( (SystemMessageType.CANCEL_REQUEST).keyId(), fromFeedId, toFeedId, (CancelRequest.builder()).build())); } } } // end of remoteReply(EMessageHeader) /** * Logs the receipt of an eBus message which should not be * forwarded to {@code ERemoteApp}. * @param header unexpected message. */ /* package */ void remoteInvalidMessage(final EMessageHeader header) { sLogger.warn("{}: received unexpected message:\n{}", mAddress, header); } // end of remoteInvalidMessage(EMessageHeader) // // end of EPublisher Method Implementation. //----------------------------------------------------------- //----------------------------------------------------------- // Get methods // /** * Returns the underlying connection type. * @return underlying connection type. */ public ConnectionType connectionType() { return (mConnection.connectionType()); } // end of connectionType() /** * Returns the host and port of the remote eBus engine to * which this client is connected. * @return the host and port of the remote eBus engine to * which this client is connected. */ public InetSocketAddress address() { return (mAddress); } // end of address() /** * Returns eBus remote connection configuration. * @return eBus remote connection configuration. */ public EConfigure.AbstractConfig configuration() { return (mConfiguration); } // end of configuration() /** * Returns {@code true} if the connection is automatically * re-established when lost; {@code false} otherwise. * @return {@code true} if the connection is automatically * re-established when lost; {@code false} otherwise. */ public boolean willReconnect() { return (mConnection.willReconnect()); } // end of willReconnect() /** * Returns {@code true} if connected and logged on to the * remote eBus application and {@code false} otherwise. * @return {@code true} if connected and logged on to the * remote eBus application and {@code false} otherwise. */ public boolean isConnected() { return (mLoggedOn); } // end of isConnected() /** * Returns the number of remote connections. * @return the number of remote connections. */ public static int connectionCount() { return (sConnections.size()); } // end of connectionCount() /** * Returns a copy of the existing remote eBus application * connections. * @return a copy of the existing remote eBus application * connections. */ public static Collection> connections() { final Collection> retval = new ArrayList<>(); retval.addAll(sConnections.keySet()); return (retval); } // end of connections() /** * Returns {@code true} if there is a connection to the * remote eBus application at the given Internet address * and {@code false} otherwise. * @param type address is for this connection type. * @param a find a connection to this Internet address. * @return {@code true} if there is a connection to the * remote eBus application at the given Internet address * and {@code false} otherwise. */ public static boolean isConnected(final ConnectionType type, final InetSocketAddress a) { final MultiKey2 key = new MultiKey2<>(type, a); final ERemoteApp remote = sConnections.get(key); return (remote != null && remote.isConnected()); } // end of isConnected(ConnectionType, InetSocketAddress) /** * Returns eBus remote application instance for given * connection type and socket address. May return * {@code null}. * @param type address is for this connection type. * @param a the remote Internet address. * @return eBus remote application instance. */ @Nullable public static ERemoteApp connection(final ConnectionType type, final InetSocketAddress a) { final MultiKey2 key = new MultiKey2<>(type, a); return (sConnections.get(key)); } // end of connection(ConnectionType, InetSocketAddress) /** * If this connection is the result of accepting an inbound * connection, then returns the {@link EServer} instance * which accepted this connection. If not accepted, then * returns {@code null}. * @return {@code EServer} instance which accepted this * connection or {@code null} if this connection is the * initiator. */ public EServer acceptingServer() { return (mServer); } // end of acceptingServer() /** * Returns the current connect attempt status. * @return connect attempt status. */ public int connectStatus() { return (mConnectStatus); } // end of connectStatus() /** * Returns the configured pause delay. * @return pause delay. */ public Duration pauseDelay() { return (mPauseConfig.duration()); } // end of pauseDelay() /** * Returns the request feed for the given message key. If no * such request feed exists, then a new request feed is * opened and stored into the request feeds map. * @param key request message key. * @return request feed for the given key. */ private ERequestFeed findRequestFeed(final EMessageKey key) { ERequestFeed retval = mRequestFeeds.get(key); // Is there a request feed for this message key? if (retval == null) { // No. Create one now and store it away. final ERequestFeed.Builder builder = ERequestFeed.builder(); retval = builder.target(this) .messageKey(key) .location(ClientLocation.REMOTE) .scope(FeedScope.LOCAL_ONLY) .statusCallback(this::feedStatus) .replyCallback(this::reply) .mayClose(false) .build(); retval.subscribe(); mRequestFeeds.put(key, retval); } return (retval); } // end of findRequestFeed(EMessageKey) // // end of Get methods. //----------------------------------------------------------- /** * Opens a client socket connection to the remote eBus at * the specified * {@link net.sf.eBus.config.EConfigure.RemoteConnection#address() host and port} * and binding the local Internet address and port to * {@link net.sf.eBus.config.EConfigure.RemoteConnection#bindAddress() local address}. * If a connection to the given host and type already exists * then throws an {@code IllegalStateException} and does * nothing else. *

* If the bind port is zero, then the local Internet address * is bound to any available port. *

*

* Only one connection is allowed between eBus engines * regardless of which engine initiated the connection. * An eBus engine is also precluded from connecting to * itself. *

* @param config remote connection configuration. * @return the remote eBus connection instance. * @throws NullPointerException * if {@code config} is {@code null}. */ public static ERemoteApp openConnection(final RemoteConnection config) { final ConnectionType connType = config.connectionType(); final InetSocketAddress inetAddress = config.address(); final MultiKey2 key = new MultiKey2<>(connType, inetAddress); final ERemoteApp retval; Objects.requireNonNull(config, "config is null"); retval = new ERemoteApp(config); // If a non-null value is returned, that this remote // connection was previously established. if (sConnections.putIfAbsent(key, retval) != null) { throw ( new IllegalStateException( String.format( "already connected to %s/%s", connType, inetAddress))); } // This is a new remote connection. Continue // processing. sLogger.debug("Opening connection to {}:{}:\n{}", connType, inetAddress, config); retval.open(config); return (retval); } // end of openConnection(RemoteConnection) /** * Closes a currently open remote eBus client referenced by * the Internet address. Does nothing if no such client * currently exists. * @param connType address references this connection type. * @param address close the connection to this eBus host * and port. */ public static void closeConnection(final ConnectionType connType, final InetSocketAddress address) { final MultiKey2 key = new MultiKey2<>(connType, address); final ERemoteApp connection = sConnections.remove(key); if (connection != null) { sLogger.debug("Closing connection to {}:{}", connType, address); connection.close(); } } // end of closeConnection(InetSocketAddress) /** * Closes all currently open connections. */ public static void closeAllConnections() { sConnections.values() .stream() .forEach(ERemoteApp::close); } // end of closeAllConnections() /** * Opens the remote eBus connections as per * {@link EConfigure#remoteConnections()}. * @param config contains the remote eBus connection * configuration. */ public static void configure(final EConfigure config) { final Collection conns = (config.remoteConnections()).values(); conns.forEach(ERemoteApp::openConnection); } // end of configure(EConfigure) /** * Returns a {@code ERemoteApp} instance for the newly * accepted TCP socket. * @param server eBus server which accepted this connection. * @param address connecting to this far end address. * @param channel the accepted socket. * @param config accepted socket configuration. * @return the eBus remote application instance encapsulating * {@code socket}. */ /* package */ static ERemoteApp openConnection(final EServer server, final ConnectionType connType, final InetSocketAddress address, final SelectableChannel channel, final EConfigure.Service config) { final MultiKey2 key = new MultiKey2<>(connType, address); ERemoteApp retval = new ERemoteApp(config); sConnections.put(key, retval); retval.open(address, server, channel, config); return (retval); } // end of openConnection(int, SelectableChannel, Service) /** * Opens the connection to the remote eBus application as per * the given connection configuration. * @param config remote connection configuration. */ private void open(final RemoteConnection config) { // Create the eBus client instance for this remote // connection and then post the open task to it. mAddress = config.address(); mRole = ConnectionRole.INITIATOR; mEClient = EClient.findOrCreateClient( this, ClientLocation.REMOTE); // Perform the socket opening asynchronously. mEClient.dispatch(() -> mFSM.open(config)); } // end of open(RemoteConnection) /** * Opens the connection to the remote eBus application as per * the given parameters. Note that accepted socket * connections are not reconnected when lost. * @param addr remote application address. * @param server eBus server which accepted this connection. * @param channel the accepted socket. * @param config configure the channel as per these settings. */ private void open(final InetSocketAddress addr, final EServer server, final SelectableChannel channel, final Service config) { // Create the eBus client instance for this remote // connection and then post the open task to it. mAddress = addr; mRole = ConnectionRole.ACCEPTOR; mEClient = EClient.findOrCreateClient( this, ClientLocation.REMOTE); mEClient.dispatch(() -> mFSM.open(addr, server, channel, config)); } // end of open(...) /** * Closes an open remote connection indirectly by dispatching * the FSM close transition. */ private void close() { mEClient.dispatch(mFSM::close); } // end of close() /** * Returns the feed instance by first looking for it using * the unique feed identifier and, if not found, then the * message key. May return {@code null}. * @param feedId unique feed identifier. * @param key message class and subject key. * @return feed instance for the given identifier and message * key. */ private EFeed findFeed(final int feedId, final MultiKey2 key) { EFeed retval = mFeeds.get(feedId); if (retval == null) { retval = mKeys.get(key); } return (retval); } // end of findFeed(int, MultiKey2<>) //----------------------------------------------------------- // FSM Conditions. // /** * Returns {@code true} if the given logon identifier is * already logged on and {@code false} otherwise. * @param logonId check if this logon identifier is already * logged on. * @return {@code true} if the given logon identifier is * already logged on and {@code false} otherwise. */ /* package */ boolean isLoggedOn(final String logonId) { boolean retcode = false; sConnectionMutex.lock(); try { retcode = sLogonIds.contains(logonId); } finally { sConnectionMutex.unlock(); } return (retcode); } // end of isLoggedOn(String) /** * Returns {@code true} if this {@code ERemoteApp} instance * initiated the connection and {@code false} if it accepted * the connection. * @return {@code true} if this is the connection initiator. */ public boolean isInitiator() { return (mRole == ConnectionRole.INITIATOR); } // end of isInitiator() /** * Returns {@code true} if this {@code ERemoteApp} instance * accepted the connection and {@code false} if it initiated * the connection. * @return {@code true} if this is an accepted connection. */ /* package */ boolean isAcceptor() { return (mRole == ConnectionRole.ACCEPTOR); } // end of isAcceptor() /** * Returns {@code true} if this remote application connection * supports pause. * @return {@code true} if this connection can be paused. */ /* package */ boolean canPause() { return (mCanPause); } // end of canPause() /** * Returns {@code true} if {@code eid} references a paused * connection; otherwise returns {@code false}. * @param eid eBus identifier. * @return {@code true} if {@code eid} is a paused * connection. */ /* package */ boolean isPausedConnection(final String eid) { return (sPausedConnections.containsKey(eid)); } // end of isPauseConnection(String) // // end of FSM Conditions. //----------------------------------------------------------- //----------------------------------------------------------- // FSM Actions. // /** * Establishes a connection the remote eBus application at * the given IP address. * @param config the remote connection configuration. * @return {@code true} if the remote eBus connection was * successfully established and {@code false} otherwise. * Note: {@code true} does not mean the connection * successfully logged on. */ /* package */ int connect(final RemoteConnection config) { sLogger.debug("{}: connecting to remote eBus.", config.address()); mConnection = createConnection(config); // Combine the configured pause flag with the underlying // connection's ability to pause. mCanPause = (config.canPause() && mConnection.willPause()); mPauseConfig = config.pauseConfiguration(); if (mCanPause) { mPendingMessages = new ArrayList<>(mPauseConfig.maxBacklogSize()); mPendingQueueLimit = mPauseConfig.resumeOnQueueLimit(); } try { if (mConnection.open(config)) { mConnectStatus = CONNECT_COMPLETE; } else { mConnectStatus = CONNECT_INCOMPLETE; } } catch (IOException ioex) { mConnectStatus = CONNECT_FAILED; sLogger.warn("{}: connect failed.", config.address(), ioex); } return (mConnectStatus); } // end of connect(...) /** * Creates a remote application for the accepted connection. * Note that accepted connections are not reconnected by the * server. If the connection is lost, it is up to the client * to reconnect. * @param server eBus server which accepted this connection. * @param bindPort socket accepted on this service port. * @param channel the accepted socket channel. * @param config accepted socket connection configuration. */ /* package */ void connect(final InetSocketAddress address, final EServer server, final SelectableChannel channel, final Service config) { mAddress = address; mServer = server; mConnection = createConnection(address, config); mCanPause = (config.canPause() && mConnection.willPause()); mPauseConfig = config.pauseConfiguration(); mPendingMessages = (mCanPause ? new ArrayList<>(mPauseConfig.maxBacklogSize()) : Collections.emptyList()); sLogger.debug( "{}: remote eBus connected, max queue size: {}, current queue size: {}.", mAddress, mConnection.maxMessageQueueSize(), mConnection.messageQueueSize()); try { mConnection.open(channel, address, config); } catch (IOException ioex) { sLogger.warn("{}: connection open failed.", address, ioex); } } // end of connect(...) /** * Reports the initial connect attempt as failed. This * connection will not be re-tried. * @param address connection to this address failed. * @param reason text explaining why the connect attempt * failed. */ /* package */ void connectFailed(final InetSocketAddress address, final String reason) { mConnectStatus = CONNECT_FAILED; sLogger.warn("{}: connect failed, {}.", address, reason); } // end of connectFailed(String) /** * Disconnects the remote eBus application connection if * open. All queued messages not yet transmitted are * discarded. */ /* package */ void disconnect() { if (mConnection != null && mConnection.isOpen()) { sLogger.trace("{}: disconnecting.", mAddress); mConnection.closeNow(); } } // end of disconnect() /** * Sends a logon message to the remote eBus application. This * message is sent directly, by-passing the transmit queue. */ /* package */ void logon() { final EMessage logon = (LogonMessage.builder()).eid(JVM_ID).build(); sLogger.trace("{}: sending logon:\n{}", mAddress, logon); send(new EMessageHeader((SystemMessageType.LOGON).keyId(), NO_ID, NO_ID, logon)); } // end of logon() /** * Sends a logon reply message to the remote eBus * application. * @param status {@code true} if the remote eBus application * successfully logged on and {@code false} otherwise. * @param reason the reason for a logon failure. */ /* package */ void logonReply(final ReplyStatus status, final String reason) { final EMessage reply = (LogonReply.builder()).eid(JVM_ID) .logonStatus(status) .reason(reason) .build(); sLogger.trace("{}: sending logon reply:\n{}", mAddress, reply); send( new EMessageHeader( (SystemMessageType.LOGON_REPLY).keyId(), NO_ID, NO_ID, reply)); } // end of logonReply(boolean, String) /** * Sends a logoff message to the remote eBus application. */ /* package */ void logoff() { final EMessage logoff = (LogoffMessage.builder()).eid(JVM_ID).build(); sLogger.info("{}: logging off from remote eBus.", mAddress); send( new EMessageHeader( (SystemMessageType.LOGOFF).keyId(), NO_ID, NO_ID, logoff)); } // end of logoff() /** * Starts the pause timer if this is an initiator connection * which supports connection pause. */ /* package */ void startPauseTimer() { // Note: this method is called regardless of the // connection role and ability to support pause. if (mRole == ConnectionRole.INITIATOR && mCanPause) { final Duration connectTime = mPauseConfig.maxConnectTime(); sLogger.trace( "{}: starting pause timer for {} millis.", mAddress, connectTime.toMillis()); mPauseTimer = sCoreExecutor.schedule(this::maxConnectTimeout, this, mPauseDelay); } } // end of startPauseTimer() /** * Stops the maximum connection timer. */ /* package */ void stopPauseTimer() { stopTimer(mPauseTimer); mPauseTimer = null; } // end of stopPauseTimer() /** * Starts the idle timer if this is an initiator connection * which supports connection pause. */ /* package */ void startIdleTimer() { // Note: this method is called regardless of the // connection role and ability to support pause. if (mRole == ConnectionRole.INITIATOR && mCanPause) { // Have the idle timer check for an idle connection // at 1/2 the time. final Duration idleTime = (mPauseConfig.idleTime()).dividedBy(2L); sLogger.trace( "{}: starting idle timer for {} millis.", mAddress, idleTime); mIdleTimer = sCoreExecutor.schedule(this::idleTimer, this, idleTime); } } // end of startIdleTimer() /** * Stops the idle connection timer. */ /* package */ void stopIdleTimer() { stopTimer(mIdleTimer); mIdleTimer = null; } // end of stopIdleTimer() /** * Starts the wait-for-resume timer. The scheduled time is * the negotiated pause delay plus an offset to allow for * the actual effort to resume. */ /* package */ void startResumeTimer() { // Note: this method is called only if this is an // accepted connection which supports pausing. // Added some additional time to the resume time to // give the far-end a chance to reconnect. final Duration resumeTime = mPauseDelay.plus(RESUME_OFFSET); sLogger.trace( "{}: starting resume timer for {} millis.", mAddress, resumeTime.toMillis()); // Reuse mPauseTimer for resume timer. mPauseTimer = sCoreExecutor.schedule(this::resumeTimer, this, resumeTime); } // end of startResumeTimer() /** * Cancels the wait-for-resume timer if running. */ /* package */ void stopResumeTimer() { stopTimer(mPauseTimer); mPauseTimer = null; } // end of stopResumeTimer() /** * Sends a pause request message to the remote eBus * application. */ /* package */ void pause() { final EMessage pause = (PauseRequest.builder()).eid(JVM_ID) .pauseTime(mPauseConfig.duration()) .maximumBacklogSize(mPauseConfig.maxBacklogSize()) .discardPolicy(mPauseConfig.discardPolicy()) .build(); sLogger.trace("{}: sending pause:\n{}", mAddress, pause); send(new EMessageHeader((SystemMessageType.PAUSE_REQUEST).keyId(), NO_ID, NO_ID, pause)); } // end of pause() /** * Sends a pause reply as per the given reply status. If * accepted, then returns the * @param status pause request is either accepted * ({@link ReplyStatus#OK_FINAL}) or rejected * ({@link ReplyStatus#ERROR}). * @param reason reason for a reject response. * @param request responding to this pause request. */ /* package */ void pauseReply(final ReplyStatus status, final String reason, final PauseRequest request) { final PauseReply reply; sLogger.debug( "{}: sending pause reply for user {}: status={}, reason={}.", mAddress, mRemoteId, status, reason); // Was the request accepted or rejected? if (status == ReplyStatus.OK_FINAL) { // Accepted. Send back the support pause time and // backlog size if less than the requested size. mPauseDelay = ((mPauseConfig.duration()).compareTo( request.pauseTime) < 0 ? mPauseConfig.duration() : request.pauseTime); mMaxBacklogSize = (mPauseConfig.maxBacklogSize() < request.maximumBacklogSize ? mPauseConfig.maxBacklogSize() : request.maximumBacklogSize); mDiscardPolicy = request.discardPolicy; } // Rejected. else { mPauseDelay = Duration.ZERO; mMaxBacklogSize = 0; mDiscardPolicy = null; } reply = (PauseReply.builder()).eid(JVM_ID) .replyStatus(status) .replyReason(reason) .pauseTime(mPauseDelay) .maximumBacklogSize(mMaxBacklogSize) .build(); send( new EMessageHeader( (SystemMessageType.PAUSE_REPLY).keyId(), NO_ID, NO_ID, reply)); } // end of pauseReply(ReplyStatus, String, PauseRequest) /** * Pause the connection for the given delay. * @param reply pause reply message. */ /* package */ void closeAndPause(final PauseReply reply) { sLogger.info("{}: pausing user {} connection.", mAddress, mRemoteId); mIsPaused = true; mMaxBacklogSize = reply.maximumBacklogSize; mConnection.closeAndPause(reply.pauseTime); } // end of closeAndPause(PauseReply) /** * Moves this connection to the paused connection list. */ /* package */ void pauseConnection() { final MultiKey2 key = new MultiKey2<>( mConnection.connectionType(), mAddress); sLogger.info("{}: pausing remote user {} connection.", mAddress, mRemoteId); mIsPaused = true; // Get the input readers map which is restored when the // resumed connection is accepted. mPausedReaders = mConnection.readers(); sConnections.remove(key); sPausedConnections.put(mRemoteId, this); } // end of pauseConnection() /** * Removes the paused connection from the list and issues the * resumed connection to that connection. * @param msg send reply to this connection first * before posting the pending messages. */ /* package */void resumeConnection(final ResumeRequest msg) { final MultiKey2 key = new MultiKey2<>( mConnection.connectionType(), mAddress); final ERemoteApp connection = sPausedConnections.remove(msg.eid); sLogger.info("{}: resuming remote user {} connection.", mAddress, mRemoteId); // Replace the paused connection's with this connection's // address. connection.mAddress = mAddress; connection.mConnection = mConnection; connection.mConnectStatus = mConnectStatus; connection.mLoggedOn = true; // Move the accepted connection back to the paused // remote app. mConnection.resumeConnection( connection, connection.mPausedReaders); mPausedReaders = null; // Replace this remote connection with the paused // connection in the connections map. sConnections.replace(key, this, connection); (connection.mEClient).dispatch( () -> (connection.mFSM).resumed(msg)); } // end of resumeConection(ResumeRequest) /** * Marks this connection as active. */ /* package */ void connectionResumed() { // Lock up the pending message queue first, then turn off // the pause flag. mPendingLock.lock(); try { // Paused no longer. Back to work. mIsPaused = false; // Now send the pending messages on their way. sLogger.debug("{}: posting {} pending messages.", mAddress, mPendingMessages.size()); mPendingMessages.forEach(this::doSend); mPendingMessages.clear(); } finally { mPendingLock.unlock(); } } // end of connectionResumed() /** * Sends a connection resume reply to the far-end. * @param eid eBus identifier. * @param status resume status. * @param msg associated status message. */ /* package */ void resumeReply(final String eid, final ReplyStatus status, final String msg) { final EMessage reply = (ResumeReply.builder()).eid(eid) .replyStatus(status) .replyReason(msg) .build(); sLogger.trace("{}: sending resume reply:\n{}", mAddress, reply); send(new EMessageHeader((SystemMessageType.RESUME_REPLY).keyId(), NO_ID, NO_ID, reply)); } // end of resumeReply(String, ReplyStatus, String) /** * Sends a connection resume request to the connection * far-end. */ /* package */ void resume() { final EMessage resume = (ResumeRequest.builder()).eid(JVM_ID).build(); sLogger.trace("{}: sending resume request:\n{}", mAddress, resume); send(new EMessageHeader((SystemMessageType.RESUME_REQUEST).keyId(), NO_ID, NO_ID, resume)); } // end of resume() /** * Closes the connection and waits for it to reconnect later. */ /* package */ void closeAndReconnect() { mConnection.closeAndReconnect(); } // end of closeAndReconnect() /** * Clears all pending messages to the far-end. */ /* package */ void clearPendingMessages() { mPendingMessages.clear(); } // end of clearPendingMessages() /** * Stores the remote client logon identifier. * @param id the remote client logon identifier. */ /* package */ void storeRemoteId(final String id) { mRemoteId = id; sConnectionMutex.lock(); try { sLogonIds.add(id); } finally { sConnectionMutex.unlock(); } } // end of storeRemoteId(String) /** * Stores the advertisement message for later processing once * the logon process is complete. * @param msg store this advertisement message. */ /* package */ void storeAd(final AdMessage msg) { mLogonAds.add(msg); } // end of storeAd(AdMessage) /** * Removes this connection from the static connections list. */ /* package */ void removeConnection() { final MultiKey2 key = new MultiKey2<>( mConnection.connectionType(), mAddress); sLogger.debug("Removing connection to {}:{}", mConnection.connectionType(), mAddress); sConnections.remove(key); } // end of removeConnection() /** * When the connection comes up, retrieve all local * advertisements and forward item to the far-end. */ /* package */ void sendAds() { // Store *received* ads here and process when logon // completes. mLogonAds = new LinkedList<>(); mEClient.dispatch( () -> // Send the local client advertisements. (ESubject.localAds(AdMessage.AdStatus.ADD)) .stream() .forEach( header -> { try { mConnection.send(header); } catch (IOException ioex) { sLogger.warn( "Failed to send advertisement", ioex); } })); } // end of sendAds() /** * Sends a logon complete message to the far-end. */ /* package */ void sendLogonComplete() { mEClient.dispatch(() -> { final LogonCompleteMessage logonMsg = (LogonCompleteMessage.builder()).eid(JVM_ID) .build(); // Do this after retrieving the local advertisements // to prevent localAd() from posting a redundant // advertisement. mLoggedOn = true; send(new EMessageHeader( (SystemMessageType.LOGON_COMPLETE).keyId(), NO_ID, NO_ID, logonMsg)); }); } // end of sendLogonComplete() /** * Processes the advertisements received during logon. Once * completed, clears the stored advertisement list and drops * the reference to that list. */ /* package */ void processLogonAds() { mLogonAds.forEach(msg -> processAd(msg)); mLogonAds.clear(); mLogonAds = null; } // end of processLogonAds() /** * Either adds or removes a publish feed based on the message * advertise status. If the advertise message references an * unknown message class, then the advertise message is * ignored. * @param adMsg the advertisement message. */ /* package */ void processAd(final AdMessage adMsg) { try { @SuppressWarnings ("unchecked") final Class mc = (Class) Class.forName(adMsg.messageClass); final EMessageKey messageKey = new EMessageKey(mc, adMsg.messageSubject); final MultiKey2 key = new MultiKey2<>( RemoteFeedType.ADVERTISEMENT, messageKey); final EFeed feed; sLogger.trace("{}: received ad message:\n{}", mAddress, adMsg); // Adding or removing? if (adMsg.adStatus == AdMessage.AdStatus.ADD) { // Adding. // Is the message key unique? if (!mKeys.containsKey(key)) { // Yes it is unique. Put the ad in place. addAdvertisement(messageKey, adMsg); } // Not unique and that's wrong - but not terribly // wrong. It can happen when an ad is placed at // the same time as a remote connection is // established. sLogger.debug( "{}: redundant ad for {} received, ignored.", mAddress, key); } // Removing. else if ((feed = mKeys.remove(key)) != null) { removeAdvertisement(messageKey, feed); } } catch (ClassNotFoundException classex) { sLogger.trace( "{}: ad message to {} unknown class {}, ignored.", mAddress, adMsg.adStatus, adMsg.messageClass); } catch (IllegalArgumentException | IllegalStateException jex) { sLogger.warn("{}: ad message to {} {}:{} failed.", mAddress, adMsg.adStatus, adMsg.messageClass, adMsg.messageSubject, jex); } } // end of processAd(AdMessage) /** * When the connection goes down, retract and dispose of all * remote feeds. This means retracting all notification and * reply advertisements, and failing all remote requests. */ /* package */ void remoteDisconnect() { sLogger.debug("{}: remote disconnect, logged on: {}.", mAddress, mLoggedOn); if (mLoggedOn) { mLoggedOn = false; // Remove the remote ID and clear it. sConnectionMutex.lock(); try { sLogonIds.remove(mRemoteId); } finally { sConnectionMutex.unlock(); } mRemoteId = null; // Retract the remote ads. mFeeds.values() .stream() .filter(feed -> feed.inPlace()) // Close the feed rather than retract because // the feeds will be thrown away. .forEach(feed -> feed.close()); // Cancel the remote requests. mRemoteRequests.values() .stream() .forEach(request -> request.close()); // Clear out all the maps. mKeys.clear(); mFeeds.clear(); mLocalRequests.clear(); mRemoteRequests.clear(); mToFromMap.clear(); mLogoffException = null; } } // end of remoteDisconnect() /** * Log the connection shutdown. */ /* package */ void doShutdown() { sLogger.debug("{}: shutting down.", mAddress); } // end of doShutdown() /** * Publishes a connection message notifying subscribers of * the given state change and the reason for that change. * @param state new connection state. * @param reason reason for the state change. */ /* package */ void publishStateUpdate(final ConnectionState state, final String reason) { sLogger.debug("{}: connection state is {}, reason: {}.", mAddress, state, reason); sConnPublisher.publish(mConnection.connectionType(), mAddress, (mServer == null ? null : mServer.address()), state, reason); } // end of publishStateUpdate(ConnectionState, String) /** * Returns the reason for logging off from the remote * application base on the current logoff exception. * @return text explaining why a logoff happened. */ /* package */ String logoffReason() { String retval; if (mLogoffException == null) { retval = NORMAL_LOGOFF; } else { retval = mLogoffException.getLocalizedMessage(); if (Strings.isNullOrEmpty(retval)) { retval = (mLogoffException.getClass()).getName(); } } return (retval); } // end of logoffReason() /** * If timer is not {@code null} then closes timer. Any * exception thrown by close is caught and ignored. * @param timer close this timer. */ private static void stopTimer(final IETimer timer) { if (timer != null) { try { timer.close(); } catch (Exception jex) {} } } // end fo stopTimer(IETimer) // // end of FSM Actions. //----------------------------------------------------------- /** * Sends the message to the remote application. Takes the * appropriate action if the outgoing message queue * overflows. *

* If the message transmit fails for any reason, this * remote connection is dropped. *

* @param header send the message header and its encapsulated * message. */ /* package */ void send(final EMessageHeader header) { final Class mc = header.messageClass(); // Is the connection paused? // Is this a resume request/reply system message? if (mIsPaused && !mc.equals(ResumeRequest.class) && !mc.equals(ResumeReply.class) && storePending(header)) { doPendingSend(header); } else { doSend(header); } } // end of send(EMessage) /** * Returns the eBus connection based on the * {@link EConfigure.ConnectionType connection type} in * {@code config}. * @param config eBus connection configuration. * @return eBus connection instance. */ private EAbstractConnection createConnection(final RemoteConnection config) { final EAbstractConnection retval; switch (config.connectionType()) { case TCP: case SECURE_TCP: retval = ETCPConnection.create(config, this); break; case UDP: case SECURE_UDP: retval = EUDPConnection.create(config, this); break; // That leaves reliable UDP. default: retval = EReliableUDPConnection.create(config, this); } return (retval); } // end of createConnection(RemoteConnection) private EAbstractConnection createConnection(final InetSocketAddress address, final Service config) { final EAbstractConnection retval; switch (config.connectionType()) { case TCP: case SECURE_TCP: retval = ETCPConnection.create(config, this); break; case UDP: case SECURE_UDP: retval = EUDPConnection.create(address, config, this); break; // That leaves reliable UDP, both secure and // unsecure. default: retval = EReliableUDPConnection.create( address, config, this); } return (retval); } // end of createConnection(InetSocketAddress, Service) /** * This timer expires when it is time to pause this * connection. */ @SuppressWarnings({"java:S1172"}) private void maxConnectTimeout() { mPauseTimer = null; mEClient.dispatch(mFSM::pause); } // end of maxConnectTimeout() /** * Checks if the idle connection duration has been reached. * If so, issues a pause transition. * @param task idle timer task. */ @SuppressWarnings({"java:S1172"}) private void idleTimer() { final Duration idleTime = Duration.between(mBusyTimestamp, Instant.now()); final Duration maxIdleTime = mPauseConfig.idleTime(); mIdleTimer = null; sLogger.trace("{}: idle time is {}, max is {}.", mAddress, idleTime, maxIdleTime); // Twiddling our thumbs for long enough? if (idleTime.compareTo(maxIdleTime) >= 0) { mEClient.dispatch(mFSM::pause); } // No, check again later. else { startIdleTimer(); } } // end of idleTimer() /** * This timer expires when a client has failed to resume a * connection in time. When this happens the connection is * shut down as if the user logged out. */ @SuppressWarnings({"java:S1172"}) private void resumeTimer() { mPauseTimer = null; mEClient.dispatch(mFSM::disconnected); } // end of resumeTimer() /** * Places the message into the pending message queue. If the * queue is at the maximum allowed size, then non-system * messages are discarded as per {@link #mDiscardPolicy}. * @param header add this message to the list. * @return {@code true} if the connection is still paused; * {@code false} if the connection resumed while trying to * store this message. */ private boolean storePending(final EMessageHeader header) { boolean dropFlag = true; mPendingLock.lock(); try { final boolean systemFlag = header.isSystemMessage(); // Check the pause flag again. The connection may // have resumed prior to acquiring the pending // message queue. if (!mIsPaused) { // no-op. Message may be sent immediately } else if (systemFlag || mMaxBacklogSize == 0 || mPendingMessageCount < mMaxBacklogSize) { dropFlag = false; mPendingMessages.add(header); // System messages do *not* affect the pending // message count since system messages are always // sent. if (!systemFlag) { ++mPendingMessageCount; } } // Maximum backlog size is reached. Discard a message. else if (mDiscardPolicy == DiscardPolicy.OLDEST_FIRST) { final Iterator mIt = mPendingMessages.iterator(); boolean flag = false; // Find the first non-system message and discard it. while (!flag && mIt.hasNext()) { flag = !(mIt.next()).isSystemMessage(); if (flag) { mIt.remove(); } } mPendingMessages.add(header); // Do *not* increment the pending message count 'cuz // we are already at the maximum. } // Else drop the youngest message - which is the method // argument. } finally { mPendingLock.unlock(); } if (dropFlag && sLogger.isDebugEnabled()) { sLogger.debug( "{}: paused connection transmit queue at maximum ({}); application message dropped.", mAddress, mMaxBacklogSize); } return (mIsPaused); } // end of storePending(EMessageHeader) /** * Does the actual work of sending a message to the far-end. * @param header send this message on its way. */ private void doSend(final EMessageHeader header) { try { mConnection.send(header); mBusyTimestamp = Instant.now(); } catch (IllegalStateException statex) { // Ignore if not connected. This will happen if the // far-end disconnects at the same time as this send. } catch (BufferOverflowException | IOException jex) { sLogger.warn("{}: failed to send {}, disconnecting", mAddress, (header.messageClass()).getName(), jex); mLogoffException = jex; mEClient.dispatch(mFSM::disconnected); } } // end of doSend(EMessageHeader) /** * If this is the client, an application message is being * sent, resume-on-queue-limit is configured and breached, * and the connection is *not* reconnecting, then tell the * underlying connection to reconnect now. * @param header transmit this message header. */ @SuppressWarnings ({"java:S1067"}) private void doPendingSend(final EMessageHeader header) { if (mRole == ConnectionRole.INITIATOR && !header.isSystemMessage() && mPendingQueueLimit > 0 && mPendingMessageCount >= mPendingQueueLimit && !mConnection.isConnecting()) { sLogger.debug( "{}: pending message queue at limit ({}); resuming connection.", mAddress, mPendingQueueLimit); mConnection.resumeNow(); } } // end of doPendingSend(EMessageHeader) /** * Adds a either a notification or request feed matching the * given advertisement. * @param messageKey message key. * @param adMsg advertisement message. */ private void addAdvertisement(final EMessageKey messageKey, final AdMessage adMsg) { final boolean isNotification = messageKey.isNotification(); final MultiKey2 key = new MultiKey2<>( RemoteFeedType.ADVERTISEMENT, messageKey); final EFeed feed; // But adding what? Publisher or replier? if (isNotification) { // Publisher. final EPublishFeed.Builder builder = EPublishFeed.builder(); feed = builder.target(this) .messageKey(messageKey) .location(ClientLocation.REMOTE) .scope(FeedScope.LOCAL_ONLY) .statusCallback(this::publishStatus) .build(); ++mPubCount; } else { // Replier. final EReplyFeed.Builder builder = EReplyFeed.builder(); feed = builder.target(this) .messageKey(messageKey) .location(ClientLocation.REMOTE) .scope(FeedScope.LOCAL_ONLY) .cancelRequestCallback(this::cancelRequest) .requestCallback(this::request) .build(); ++mReplierCount; } // Map the message key and feed identifier to the // feed instance. mKeys.put(key, feed); mFeeds.put(feed.feedId(), feed); sLogger.debug("{}: added {} feed {} (state: {}).", mAddress, (isNotification ? "publish" : "reply"), messageKey, adMsg.feedState); // Now put the feed in place. if (isNotification) { ((IEPublishFeed) feed).advertise(); ((IEPublishFeed) feed).updateFeedState( adMsg.feedState); } else { ((IEReplyFeed) feed).advertise(); ((IEReplyFeed) feed).updateFeedState( adMsg.feedState); } } // end of addAdvertisement(EMessageKey, AdMessage) /** * Retracts an in place advertisement. * @param messageKey feed message key. * @param feed advertised feed. */ private void removeAdvertisement(final EMessageKey messageKey, final EFeed feed) { // Retract the feed and remove from the feeds map. mFeeds.remove(feed.feedId()); if (messageKey.isNotification()) { ((IEPublishFeed) feed).unadvertise(); --mPubCount; } else { ((IEReplyFeed) feed).unadvertise(); --mReplierCount; } sLogger.debug("{}: removed {} feed {}.", mAddress, (messageKey.isNotification() ? "publish" : "reply"), messageKey); } // end of removeAdvertisement(EMessageKey, EFeed) /** * Add the remote application connection status to report. * @param report logged status report. */ private static void reportRemoteStatus(final PrintWriter report) { final int appCount = sConnections.size(); report.print("ERemote: "); if (appCount == 0) { report.println("there are no remote connections."); } else { final Collection apps = new ArrayList<>(sConnections.values()); int index = 0; report.format( "there %s %,d remote application %s.%n", (appCount == 1 ? "is" : "are"), appCount, (appCount == 1 ? "connection" : "connections")); for (ERemoteApp remoteApp : apps) { remoteApp.reportStatus(report, index); report.println(); ++index; } } } // end of reportRemoteStatus(PrintWriter) /** * Appends this remote application connection status to the * report. * @param report append the status to this report. * @param index connection index in the map. */ private void reportStatus(final PrintWriter report, final int index) { report.format(" [%,d] address: %s%n", index, mAddress) .format( " created on %1$tY-%1$tm-%1$td @ %1$tH:%1$tM:%1$tS.%1$tL%n", mCreated) .format(" logged in: %b%n", mLoggedOn) .format(" subscribers: %,d%n", mSubCount) .format(" publishers: %,d%n", mPubCount) .format(" repliers: %,d%n", mReplierCount) .format(" requestors: %,d%n", mRequestorCount); } // end of reportStatus(PrintWriter, int, int) //--------------------------------------------------------------- // Inner classes. // /** * This singleton is responsible for locally publishing * {@link ERemoteApp} connection state updates to * subscribers. This must be done since {@code ERemoteApp} is * a remote client by definition and * {@link ConnectionMessage} has local scope. */ private static final class ConnectionPublisher implements EPublisher { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Constants. // /** * eBus object name is {@value}. */ public static final String OBJECT_NAME = "ConnectionPublisher"; //------------------------------------------------------- // Locals. // /** * The {@link ConnectionMessage} notification feed. */ private EPublishFeed mStateFeed; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a new connection publisher instance. The * connection state feed is initially {@code null} and * is instantiated when {@link #startup()} is called. * @see #startup() */ private ConnectionPublisher() {} // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // EObject Interface Implementation. // /** * Returns {@link #OBJECT_NAME} as eBus object name. * @return eBus object name. */ @Override public String name() { return (OBJECT_NAME); } // end of name() /** * Opens the {@link ConnectionMessage} notification feed. */ @Override public void startup() { final EPublishFeed.Builder builder = EPublishFeed.builder(); // Open the connect state feed and put its // advertisement in place. mStateFeed = builder.target(this) .messageKey(ConnectionMessage.MESSAGE_KEY) .scope(FeedScope.LOCAL_ONLY) .build(); mStateFeed.advertise(); mStateFeed.updateFeedState(EFeedState.UP); } // end of startup() // // end of EObject Interface Implementation. //------------------------------------------------------- //------------------------------------------------------- // EPublisher Interface Implementation. // /** * If {@code feedState} is {@link EFeedState#UP}, then * sends the current connection state for each known * connection. * @param feedState either up or down. * @param feed the connection state feed. */ @Override public void publishStatus(final EFeedState feedState, final IEPublishFeed feed) { sLogger.debug( "{} feed is {}.", feed.key(), feedState); // Is the feed now up? if (feedState == EFeedState.UP) { // Yes. Send the current feed state for each of // the known remote eBus connections. final ConnectionMessage.Builder builder = ConnectionMessage.builder(); sConnections.values() .forEach(conn -> mStateFeed.publish( builder.connectionType(conn.connectionType()) .remoteAddress(conn.mAddress) .serverAddress( (conn.mServer == null ? null : (conn.mServer).address())) .state(conn.mLoggedOn ? ConnectionState.LOGGED_ON : ConnectionState.LOGGED_OFF) .build())); } // The feed is down. Nothing to send. } // end of publishStatus(EFeedState, IEPublishFeed) // // end of EPublisher Interface Implementation. //------------------------------------------------------- /** * Publishes the {@link ConnectionMessage} with the given * information, if the feed is up. * @param connType connection type. * @param address the connection state change applies to * this address. * @param serverAddress socket accepted on this service port. * @param state the new connection state. * @param reason human-readable text explaining why the * state changed. May be {@code null}. */ private void publish(final ConnectionType connType, final InetSocketAddress address, final InetSocketAddress serverAddress, final ConnectionState state, final String reason) { if (mStateFeed.isFeedUp()) { final ConnectionMessage.Builder builder = ConnectionMessage.builder(); mStateFeed.publish( builder.connectionType(connType) .remoteAddress(address) .serverAddress(serverAddress) .state(state) .reason(reason) .build()); } } // end of publish(...) } // end of class ConnectionPublisher } // end of class ERemoteApp




© 2015 - 2025 Weber Informatics LLC | Privacy Policy