
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.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:
*
* -
* {@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
*/
// 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 extends EMessage> mc =
(Class extends EMessage>)
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 extends EMessage> mc =
(Class extends EMessage>)
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