
net.sf.eBus.client.ERemoteApp Maven / Gradle / Ivy
//
// 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.lang.management.ManagementFactory;
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.Timer;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.eBus.client.ConnectionMessage.ConnectionState;
import net.sf.eBus.client.EClient.ClientLocation;
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.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.messages.type.MessageType;
import net.sf.eBus.util.MultiKey2;
import net.sf.eBus.util.TimerTask;
import net.sf.eBus.util.logging.StatusReport;
import net.sf.eBus.util.logging.StatusReporter;
/**
* This class provides communication between eBus applications.
* eBus remote application connections are established in one of
* four ways:
*
* -
* {@link #openConnection(EConfigure.RemoteConnection) openConnection}:
* Establishes a connection from this process to a remote
* application at the specified Internet address.
*
* -
* {@link EServer#openServer(EConfigure.Service) EServer.openServer}:
* Accepts connections from remote eBus applications and
* encapsulates the accepted
* {@link java.nio.channels.SocketChannel socket}.
*
* -
* {@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.
*
* -
* 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.
*
*
* 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
*/
public final class ERemoteApp
implements EPublisher,
ESubscriber,
EReplier,
ERequestor
{
//---------------------------------------------------------------
// 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 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);
/**
* The unique identifier for this JVM.
*/
private static final String JVM_ID =
(ManagementFactory.getRuntimeMXBean()).getName();
//-----------------------------------------------------------
// 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 =
Logger.getLogger(ERemoteApp.class.getName());
/**
* Java timer thread used for resuming paused connection.
*/
private static final Timer mTimer =
new Timer("PauseTimer", true);
// 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(
new ERemoteStatusReporter());
sConnPublisher = new ConnectionPublisher();
EFeed.register(sConnPublisher);
EFeed.startup(sConnPublisher);
} // end of static initialization block.
//-----------------------------------------------------------
// Locals.
//
/**
* The connection finite state machine.
*/
private final ERemoteAppContext mFSM;
/**
* 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 a message key.
*/
private final Map 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 TimerTask 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 TimerTask 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.
*/
private ERemoteApp()
{
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;
mPendingLock = new ReentrantLock();
mPendingMessages = Collections.emptyList();
mPendingQueueLimit = 0;
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)
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: connected to remote eBus.",
mAddress));
}
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(
String.format("%s: 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
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);
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s: %s publish status is %s.",
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:
// + 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-key feeds cannot be transmitted to remote
* applications.
*/
@Override
public void feedStatus(final EFeedState feedState,
final IESubscribeFeed feed)
{
final int feedId = feed.feedId();
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s: %s feed status is %s.",
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-key
* 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))
{
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: forwarding message%n%s",
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.
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s: request step 4: %d -> %d has %d 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 extends EMessage> mc =
(Class extends EMessage>)
Class.forName(subMsg.messageClass);
final EMessageKey key =
new EMessageKey(mc, subMsg.messageSubject);
int toFeedId = header.toFeedId();
final ESubscribeFeed feed;
// Adding or removing.
if (subMsg.feedState == EFeedState.UP)
{
// Adding.
feed =
ESubscribeFeed.open(this,
key,
FeedScope.LOCAL_ONLY,
null,
ClientLocation.REMOTE,
false);
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());
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: subscribing to feed %s.",
mAddress,
feed));
}
// Now put the feed in place.
feed.subscribe();
}
// 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);
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: unsubscribing from feed %s.",
mAddress,
feed));
}
}
}
catch (ClassNotFoundException classex)
{
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: subscribe message %s unknown class %s, ignored.",
mAddress,
subMsg.feedState,
subMsg.messageClass));
}
}
catch (IllegalArgumentException jex)
{
sLogger.log(
Level.WARNING,
String.format(
"%s: ad message to %s %s:%s 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();
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: from=%d, to=%d, status=%s, feed=%s.",
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();
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: feed %s (from=%d, to=%d) received message:%n%s",
mAddress,
feed,
header.fromFeedId(),
toFeedId,
message));
}
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())));
}
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 (sLogger.isLoggable(Level.FINER))
{
final StringBuilder output = new StringBuilder();
output.append(mAddress)
.append(": feed %d remote cancel: ");
if (request == null)
{
output.append("unknown request feed");
}
else
{
output.append(request.key())
.append(" is ")
.append(request.requestState())
.append('.');
}
sLogger.finer(output.toString());
}
// 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);
}
// 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();
if (!mToFromMap.containsKey(toFeedId))
{
mToFromMap.put(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.warning(
String.format(
"%s: received unexpected message:%n%s",
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 {@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 ? false : remote.isConnected());
} // end of isConnected(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.
*/
/* package */ EServer acceptingServer()
{
return (mServer);
} // end of acceptingServer()
/**
* Returns the current connect attempt status.
* @return connect attempt status.
*/
/* package */ int connectStatus()
{
return (mConnectStatus);
} // end of connectStatus()
/**
* Returns the configured pause delay.
* @return pause delay.
*/
/* package */ Duration pauseDelay()
{
return (mPauseConfig.duration());
} // end of pauseDelay()
/**
* Returns the eBus remote application instance for the given
* socket address. May return {@code null}.
* @param type address is for this connection type.
* @param a the remote Internet address.
* @return an eBus remote application instance.
*/
/* package */ 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)
/**
* 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.
retval =
ERequestFeed.open(this,
key,
FeedScope.LOCAL_ONLY,
ClientLocation.REMOTE,
false);
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 to
* {@link net.sf.eBus.config.EConfigure.RemoteConnection#bindPort() local port}.
* 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();
// 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.
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"Opening connection to %s:%s:%n%s",
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 serverPort.
*/
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)
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"Closing connection to %s:%s",
connType,
address));
}
connection.close();
}
} // end of closeConnection(InetSocketAddress)
/**
* Closes all currently open connections.
*/
public static void closeAllConnections()
{
sConnections.values().stream().
forEach((conn) ->
{
conn.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();
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.
*/
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 EMessageKey key)
{
EFeed retval = mFeeds.get(feedId);
if (retval == null)
{
retval = mKeys.get(key);
}
return (retval);
} // end of findFeed(int, EMessageKey)
//-----------------------------------------------------------
// 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.
*/
/* package */ 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)
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format("%s: 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.log(Level.WARNING,
String.format(
"%s: 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());
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: remote eBus connected, max queue size: %,d, current queue size: %,d.",
mAddress,
mConnection.maxMessageQueueSize(),
mConnection.messageQueueSize()));
}
try
{
mConnection.open(channel, address, config);
}
catch (IOException ioex)
{
sLogger.log(Level.WARNING,
String.format(
"%s: 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.warning(
String.format(
"%s: connect failed, %s.", 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())
{
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();
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format("%s: sending logon:%n%s",
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();
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: sending logon reply:%n%s",
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(
String.format(
"%s: 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 long connectTime =
(mPauseConfig.maxConnectTime()).toMillis();
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: starting pause timer for %s millis.",
mAddress,
connectTime));
}
mPauseTimer =
new TimerTask(
task -> maxConnectTimer(mPauseTimer));
mTimer.schedule(mPauseTimer, connectTime);
}
} // end of startPauseTimer()
/**
* Stops the maximum connection timer.
*/
/* package */ void stopPauseTimer()
{
if (mPauseTimer != null)
{
mPauseTimer.cancel();
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 long idleTime =
(((mPauseConfig.idleTime()).toMillis()) / 2L);
mIdleTimer =
new TimerTask(task -> idleTimer(mIdleTimer));
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: starting idle timer for %s millis.",
mAddress,
idleTime));
}
mTimer.schedule(mIdleTimer, idleTime);
}
} // end of startIdleTimer()
/**
* Stops the idle connection timer.
*/
/* package */ void stopIdleTimer()
{
if (mIdleTimer != null)
{
mIdleTimer.cancel();
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 long resumeTime =
(mPauseDelay.plus(RESUME_OFFSET)).toMillis();
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: starting resume timer for %s millis.",
mAddress,
resumeTime));
}
// Reuse mPauseTimer for resume timer.
mPauseTimer =
new TimerTask(task -> resumeTimer(mPauseTimer));
mTimer.schedule(mPauseTimer, resumeTime);
} // end of startResumeTimer()
/**
* Cancels the wait-for-resume timer if running.
*/
/* package */ void stopResumeTimer()
{
if (mPauseTimer != null)
{
mPauseTimer.cancel();
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();
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format("%s: sending pause:%n%s",
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;
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: sending pause reply for user %s: status=%s, reason=%s.",
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(
String.format(
"%s: pausing user %s 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);
if (sLogger.isLoggable(Level.INFO))
{
sLogger.info(
String.format(
"%s: pausing remote user %s 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);
if (sLogger.isLoggable(Level.INFO))
{
sLogger.info(
String.format(
"%s: resuming remote user %s 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.
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s: posting %,d 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();
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format("%s: sending resume reply:%n%s",
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();
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format("%s: sending resume request:%n%s",
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);
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"Removing connection to %s:%s",
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.log(
Level.WARNING,
"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 extends EMessage> mc =
(Class extends EMessage>)
Class.forName(adMsg.messageClass);
final EMessageKey key =
new EMessageKey(mc, adMsg.messageSubject);
final EFeed feed;
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: received ad message:%n%s",
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(key, 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.
else if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s: redundant ad for %s received, ignored.",
mAddress,
key));
}
}
// Removing.
else if ((feed = mKeys.remove(key)) != null)
{
removeAdvertisement(key, feed);
}
}
catch (ClassNotFoundException classex)
{
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: ad message to %s unknown class %s, ignored.",
mAddress,
adMsg.adStatus,
adMsg.messageClass));
}
}
catch (IllegalArgumentException |
IllegalStateException jex)
{
sLogger.log(
Level.WARNING,
String.format(
"%s: ad message to %s %s:%s 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()
{
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()
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format("%s: shutting down.", mAddress));
}
} // end of doShutdown()
/**
* Logs the given message at the specified level.
* @param level
* @param msg
*/
/* package */ void log(final Level level,
final String msg)
{
sLogger.log(
level, String.format("%s: %s", mAddress, msg));
} // end of log(Level, String)
/**
* 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)
{
sConnPublisher.publish(mConnection.connectionType(),
mAddress,
(mServer == null ?
0 :
mServer.port()),
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()
//
// 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;
// That leaves UDP.
default:
retval = EUDPConnection.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;
// That leaves UDP, both secure and unsecure.
default:
retval =
EUDPConnection.create(address, config, this);
}
return (retval);
} // end of createConnection(InetSocketAddress, Service)
/**
* This timer expires when it is time to pause this
* connection.
* @param task expired maximum connect timer task.
*/
@SuppressWarnings({"java:S1172"})
private void maxConnectTimer(final TimerTask task)
{
mPauseTimer = null;
mEClient.dispatch(mFSM::pause);
} // end of maxConnectTimer(TimerTask)
/**
* 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 TimerTask task)
{
final Duration idleTime =
Duration.between(mBusyTimestamp, Instant.now());
final Duration maxIdleTime = mPauseConfig.idleTime();
mIdleTimer = null;
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: idle time is %s, max is %s.",
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(final TimerTask task)
/**
* 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.
* @param task resume timer task.
*/
@SuppressWarnings({"java:S1172"})
private void resumeTimer(final TimerTask task)
{
mPauseTimer = null;
mEClient.dispatch(mFSM::disconnected);
} // end of resumeTimer(TimerTask)
/**
* 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.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s: paused connection transmit queue at maximum (%d); 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.log(
Level.WARNING,
String.format(
"%s: failed to send %s, disconnecting",
mAddress,
(header.messageClass()).getName()),
jex);
mLogoffException = jex;
// Note: it is safe to issue a transition now because
// either this method is called from outside the FSM
// or by the last action in an FSM state entry block.
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.
*/
private void doPendingSend(final EMessageHeader header)
{
if (mRole == ConnectionRole.INITIATOR &&
!header.isSystemMessage() &&
mPendingQueueLimit > 0 &&
mPendingMessageCount >= mPendingQueueLimit &&
!mConnection.isConnecting())
{
sLogger.fine(
String.format(
"%s: pending message queue at limit (%,d); resuming connection.",
mAddress,
mPendingQueueLimit));
mConnection.resumeNow();
}
} // end of doPendingSend(EMessageHeader)
/**
* Adds a either a notification or request feed matching the
* given advertisement.
* @param key message key.
* @param adMsg advertisement message.
*/
private void addAdvertisement(final EMessageKey key,
final AdMessage adMsg)
{
final EFeed feed;
// But adding what? Publisher or replier?
if (key.isNotification())
{
// Publisher.
feed = EPublishFeed.open(this,
key,
FeedScope.LOCAL_ONLY,
ClientLocation.REMOTE,
false);
++mPubCount;
}
else
{
// Replier.
final MessageType dataType =
(MessageType)
DataType.findType(key.messageClass());
feed = EReplyFeed.open(this,
key,
FeedScope.LOCAL_ONLY,
null,
ClientLocation.REMOTE,
dataType,
false);
++mReplierCount;
}
// Map the message key and feed identifier to the
// feed instance.
mKeys.put(key, feed);
mFeeds.put(feed.feedId(), feed);
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format("%s: added %s feed %s (state: %s).",
mAddress,
(key.isNotification() ?
"publish" :
"reply"),
key,
adMsg.feedState));
}
// Now put the feed in place.
if (key.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 key feed message key.
* @param feed advertised feed.
*/
private void removeAdvertisement(final EMessageKey key,
final EFeed feed)
{
// Retract the feed and remove from the feeds map.
mFeeds.remove(feed.feedId());
if (key.isNotification())
{
((IEPublishFeed) feed).unadvertise();
--mPubCount;
}
else
{
((IEReplyFeed) feed).unadvertise();
--mReplierCount;
}
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format("%s: removed %s feed %s.",
mAddress,
(key.isNotification() ?
"publish" :
"reply"),
key));
}
} // end of removeAdvertisement(EMessageKey, EFeed)
/**
* Appends this remote application connection status to the
* report.
* @param report append the status to this report.
* @param index the 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 class adds the status of all remote application
* connections to the status report.
*/
private static final class ERemoteStatusReporter
implements StatusReporter
{
//-----------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// StatusReporter Interface Implementation.
//
/**
* Add the remote application connection status to the
* report.
* @param report the logged status report.
*/
@Override
public void reportStatus(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 reportStatus(PrintWriter)
//
// end of StatusReporter Interface Implementation.
//-------------------------------------------------------
} // end of class ERemoteStatusReporter
/**
* 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)
{
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s feed is %s.", 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)
.serverPort((conn.mServer).port())
.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 serverPort 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 int serverPort,
final ConnectionState state,
final String reason)
{
if (mStateFeed.isFeedUp())
{
final ConnectionMessage.Builder builder =
ConnectionMessage.builder();
mStateFeed.publish(
builder.connectionType(connType)
.remoteAddress(address)
.serverPort(serverPort)
.state(state)
.reason(reason)
.build());
}
} // end of publish(...)
} // end of class ConnectionPublisher
} // end of class ERemoteApp