net.sf.eBus.client.EAbstractConnection Maven / Gradle / Ivy
//
// Copyright 2015, 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 java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SocketChannel;
import java.time.Duration;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static net.sf.eBus.client.EClient.sCoreExecutor;
import net.sf.eBus.client.sysmessages.KeyMessage;
import net.sf.eBus.client.sysmessages.SystemMessageType;
import net.sf.eBus.config.EConfigure.AbstractConfig;
import net.sf.eBus.config.EConfigure.ConnectionRole;
import net.sf.eBus.config.EConfigure.ConnectionType;
import net.sf.eBus.config.EConfigure.RemoteConnection;
import net.sf.eBus.config.EConfigure.Service;
import net.sf.eBus.messages.EMessage;
import net.sf.eBus.messages.EMessageHeader;
import net.sf.eBus.messages.ENotificationMessage;
import net.sf.eBus.messages.EReplyMessage;
import net.sf.eBus.messages.ESystemMessage;
import net.sf.eBus.messages.InvalidMessageException;
import net.sf.eBus.messages.UnknownMessageException;
import net.sf.eBus.messages.type.DataType;
import net.sf.eBus.messages.type.MessageType;
import net.sf.eBus.net.AsyncChannel;
import net.sf.eBus.timer.EScheduledExecutor.IETimer;
import net.sf.eBus.util.LazyString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for eBus TCP connections. Maintains the
* {@link AsyncChannel channel} connection and
* {@link AbstractMessageWriter} instance.
*
* @author Charles Rapp
*/
@SuppressWarnings({"java:S1192", "java:S3457"})
/* package */ abstract class EAbstractConnection
implements EObject
{
//---------------------------------------------------------------
// Member enums.
//
/**
* This enum defines the allowed connection states used by
* all eBus connection types.
*/
protected enum ConnectState
{
/**
* The eBus connection is closed.
*/
CLOSED,
/**
* eBus is in the process of connecting.
*/
CONNECTING,
/**
* eBus is in the process of establishing a secure
* UDP connection (that is, performing the DTLS opening
* handshake).
*/
UDP_SECURE_CONNECTING,
/**
* The eBus is connected. Note that this has nothing to
* do with logging on to the remote eBus.
*/
CONNECTED,
/**
* The eBus connection will be closed once all messages
* are sent.
*/
CLOSING,
/**
* The eBus connection is down and will be
* re-established.
*/
RECONNECTING
} // end of enum ConnectState
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* This value specifies that there is no limit to the
* transmit queue size.
*/
public static final int DEFAULT_QUEUE_SIZE = 0;
/**
* A message's serialized size may be at most this many
* bytes. Messages longer than that size result in
* eBus connection termination.
*/
public static final int MAX_MESSAGE_SIZE = Short.MAX_VALUE;
/**
* A zero bind port value means that the local end of the
* TCP connection will be bound to any available ephemeral
* port.
*/
public static final int ANY_PORT = 0;
/**
* Wait one minute before attempting to reconnect by default.
*/
public static final Duration DEFAULT_RECONNECT_DELAY =
Duration.ofMinutes(1);
/**
* The default heartbeat rate is 0 milliseconds - which means
* that heartbeating is off.
*/
public static final Duration DEFAULT_HEARTBEAT_DELAY =
Duration.ZERO;
/**
* Wait one minute for a heartbeat reply by default.
*/
public static final Duration DEFAULT_HEARTBEAT_REPLY_DELAY =
Duration.ofMinutes(1);
/**
* The message size takes {@value} bytes.
*/
public static final int MESSAGE_SIZE_SIZE = 4;
/**
* A message header size is {@value} bytes.
*/
protected static final int MESSAGE_HEADER_SIZE = 16;
/**
* Heartbeat and heartbeat reply messages are negative
* two-byte integers.
*/
protected static final int HEARTBEAT = -15000;
/**
* There is a separate reply message for a heartbeat.
*/
protected static final int HEARTBEAT_REPLY = -8000;
//
// Message callback MethodHandles.
//
/**
* Well known method names for processing messages.
*/
private static final String[] CB_METHOD_NAMES =
{
"remoteAd", // AdMessage.
"remoteCancelRequest", // CancelRequest
"remoteClassUpdate", // KeyMessage
"remoteFeedStatus", // FeedStatusMessage
"remoteLogoff", // LogoffMessage
"remoteLogonComplete", // LogonComplete
"remoteLogon", // Logon
"remoteLogonReply", // LogonReply
"remoteRequestAck", // RemoteAck
"remoteSubscribe", // SubscribeMessage
"remotePauseRequest", // PauseRequest
"remotePauseReply", // PauseReply
"remoteResumeRequest", // ResumeRequest
"remoteResumeReply", // ResumeReply
"remoteInvalidMessage", // UDPConnectRequest
"remoteInvalidMessage", // UDPConnectReply
"remoteInvalidMessage", // UDPDiconnectRequest
"remoteInvalidMessage", // UDPDisconnectReply
"remoteNotify", // Notification
"remoteRequest", // Request
"remoteReply" // Reply
};
/**
* Pass application notification messages to "remoteNotify".
*/
protected static final int NOTIFY_CB = 18;
/**
* Pass application request messages to "remoteRequest".
*/
protected static final int REQUEST_CB = 19;
/**
* Pass application reply messages to "remoteReply"
*/
protected static final int REPLY_CB = 20;
/**
* Message callback method handles.
*/
protected static final MethodHandle[] MESSAGE_CB;
/**
* The connection is open callback.
*/
protected static final int OPEN_CB = 0;
/**
* The connection is closed callback.
*/
protected static final int CLOSE_CB = 1;
/**
* Call this {@code ERemoteApp} methods to report changes in
* the connection state.
*/
protected static final String[] CONN_CB_METHOD_NAMES =
{
"handleOpen",
"handleClose"
};
/**
* Callbacks for reporting when the connection is up, down,
* or en error occurred.
*/
protected static final MethodHandle[] CONN_CB;
/**
* The message type is used to serialize and de-serialize an
* eBus message.
*/
protected static final DataType MESSAGE_TYPE =
DataType.findType(EMessage.class);
//-----------------------------------------------------------
// Statics.
//
/**
* Access point for logging.
*/
private static final Logger sLogger =
LoggerFactory.getLogger(EAbstractConnection.class);
/**
* The total inbound message count for all external clients.
*/
protected static int sTotalInCount = 0;
/**
* The total outbound message count for all external clients.
*/
protected static int sTotalOutCount = 0;
// Class static initialization
static
{
final MethodHandles.Lookup lookup = MethodHandles.lookup();
int size;
int index = 0;
size = CB_METHOD_NAMES.length;
MESSAGE_CB = new MethodHandle[size];
// Look up the message callback methods.
try
{
final MethodType mt =
MethodType.methodType(
void.class, EMessageHeader.class);
for (index = 0; index < size; ++index)
{
MESSAGE_CB[index] =
lookup.findVirtual(
ERemoteApp.class,
CB_METHOD_NAMES[index],
mt);
}
}
catch (NoSuchMethodException |
IllegalAccessException jex)
{
sLogger.error("{} lookup failed",
CB_METHOD_NAMES[index],
jex);
}
size = CONN_CB_METHOD_NAMES.length;
CONN_CB = new MethodHandle[size];
// Look up the connection callback methods.
try
{
final MethodType mt =
MethodType.methodType(
void.class, EAbstractConnection.class);
for (index = 0; index < size; ++index)
{
CONN_CB[index] =
lookup.findVirtual(
ERemoteApp.class,
CONN_CB_METHOD_NAMES[index],
mt);
}
}
catch (NoSuchMethodException |
IllegalAccessException jex)
{
sLogger.error("{} lookup failed",
CONN_CB_METHOD_NAMES[index],
jex);
}
} // end of class static initialization
//-----------------------------------------------------------
// Locals.
//
/**
* Forward connection events (which includes messages) to
* this listener.
*
* This data member is not {@code final} because
* when resuming a connection it needs to be reset to the
* previous {@link ERemoteApp} that was paused and not the
* {@code ERemoteApp} instance created when the connection
* was accepted.
*
*/
protected ERemoteApp mRemoteApp;
/**
* The asynchronous socket on which messages are sent and
* received. This field is not final because the channel
* constructor takes {@code this} as an argument. That means
* the socket must be created post construction.
*/
protected AsyncChannel mAsocket;
/**
* The underlying connection is of this type.
*/
protected final ConnectionType mConnectionType;
/**
* This connection was either initiated or accepted.
*/
protected final ConnectionRole mConnectRole;
/**
* Connecting to this remote address. This value does not
* change once set.
*/
protected SocketAddress mRemoteAddress;
/**
* Bind the local address to this port. This value is
* set when the socket is opened, so it cannot be final.
*/
protected SocketAddress mBindAddress;
/**
* Maps the class identifier to the reader responsible for
* de-serializing it and posting it to the appropriate
* callback.
*/
protected final Map mInputReaders;
/**
* The current connection state.
*/
protected final AtomicReference mState;
/**
* The message writer is responsible for serializing outbound
* messages directly to the {@link #mAsocket socket's}
* output buffer.
*
* Like {@link #mAsocket}, this field must be set post
* construction.
*
*/
protected AbstractMessageWriter mOutputWriter;
/**
* When this flag is true, we are expecting a heartbeat
* reply. When a heartbeat is received and this flag is
* true, don't send a heartbeat in reply. When this flag is
* false, send a heartbeat.
*
* This flag is {@code protected} because subclasses need
* to turn off the reply flag when input arrives.
*
*/
protected volatile boolean mHeartbeatReplyFlag;
/**
* The serialized heartbeat message.
*/
protected final byte[] mHeartbeatData;
/**
* The serialized heartbeat reply message.
*/
protected final byte[] mHeartbeatReplyData;
/**
* Set to {@code true} if the connect supports pause and
* {@code false} if the connection cannot be paused.
*/
private final boolean mPauseFlag;
/**
* When set to true, attempt to maintain the connection.
*/
private volatile boolean mReconnectFlag;
/**
* If the connection is lost, then reconnect after the
* specified millisecond delay.
*/
private final AtomicReference mReconnectDelay;
/**
* When this timer expires, attempt to connect.
*/
private final AtomicReference mReconnectTimer;
/**
* Send heartbeats at this millisecond frequency.
*/
private final AtomicReference mHeartbeatDelay;
/**
* The heartbeat timer task.
*/
private final AtomicReference mHeartbeatTimer;
/**
* If heartbeating is turned on, then send heartbeat
* messages at this millisecond rate.
*/
private final AtomicReference mHeartbeatReplyDelay;
/**
* The heart beat timer task.
*/
private final AtomicReference mHeartbeatReplyTimer;
//
// StatusReport statistics.
// Not meant to be strictly accurate.
//
/**
* Count up the number of messages received from the remote
* eBus engine.
*/
protected int mMsgInCount;
/**
* Count up the number of messages sent to the remote eBus
* engine.
*/
protected int mMsgOutCount;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new eBus connection instance.
* @param connType defines the connection type (TCP or UDP)
* and secure or insecure.
* @param connRole either accepted or initiated.
* @param remoteApp pass events to this eBus remote
* application interface.
* @param pauseFlag {@code true} if the connection supports
* pause.
*/
protected EAbstractConnection(final ConnectionType connType,
final ConnectionRole connRole,
final ERemoteApp remoteApp,
final boolean pauseFlag)
{
if (remoteApp == null)
{
throw (
new IllegalArgumentException(
"remoteApp is null"));
}
mRemoteApp = remoteApp;
mConnectionType = connType;
mConnectRole = connRole;
mAsocket = null;
mInputReaders = new HashMap<>();
mOutputWriter = null;
mBindAddress = null;
mPauseFlag = pauseFlag;
mReconnectFlag = false;
mReconnectDelay =
new AtomicReference<>(DEFAULT_RECONNECT_DELAY);
mReconnectTimer = new AtomicReference<>();
mHeartbeatTimer = new AtomicReference<>();
mHeartbeatDelay =
new AtomicReference<>(DEFAULT_HEARTBEAT_DELAY);
mHeartbeatReplyTimer = new AtomicReference<>();
mHeartbeatReplyFlag = false;
mHeartbeatReplyDelay =
new AtomicReference<>(DEFAULT_HEARTBEAT_REPLY_DELAY);
mHeartbeatData = new byte[Integer.BYTES];
mHeartbeatReplyData = new byte[Integer.BYTES];
mState = new AtomicReference<>(ConnectState.CLOSED);
} // end of EAbstractConnection(...)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Declarations.
//
/**
* Initializes a newly created remote connection with the
* given configuration.
* @param config initialize connection with this
* configuration.
*/
protected abstract void doInitialize(RemoteConnection config);
/**
* Returns output writer for this connection type.
* @param config remote connection configuration.
* @return connection output writer.
*/
protected abstract AbstractMessageWriter createWriter(AbstractConfig config);
/**
* Establishes a connection to the given address and bind
* port. Returns {@code true} if the connection is
* immediately established and {@code false} if the
* connection is in-progress. If {@code false} is returned,
* {@link ERemoteApp#handleOpen(EAbstractConnection)} is
* called when the connection is established.
* @param address connect to this remote address.
* @param bindAddress bind the local address to this address.
* @return {@code true} if the connection is successfully
* opened prior to returning.
* @throws IOException
* if an I/O error occurs when establishing the connection.
*
* @see #doOpen(SelectableChannel)
* @see #doClose()
* @see #doCloseNow()
*/
protected abstract boolean doOpen(final SocketAddress address,
final SocketAddress bindAddress)
throws IOException;
/**
* Initializes a newly accepted remote connection with the
* given service configuration.
* @param config initialize connection with this
* configuration.
*/
protected abstract void doInitialize(Service config);
/**
* Uses a previously accepted channel to "open" the
* connection.
* @param socket the accepted socket connection.
* @return {@code true} if the connection is successfully
* opened prior to returning.
* @throws IOException
* if {@code socket} is closed.
*
* @see #doOpen(InetSocketAddress, int)
* @see #doClose()
* @see #doCloseNow()
*/
protected abstract boolean doOpen(SelectableChannel socket)
throws IOException;
/**
* Performs the necessary post-connection configuration on
* the underlying socket.
* @throws IOException
* if an I/O failure occurs while configuring the socket.
*/
protected abstract void doConnected()
throws IOException;
/**
* Performs the actual work of closing the underlying socket
* connection. This call should perform a "slow" socket close
* which waits for all pending output to be transmitted
* before closing the socket.
*
* @see #close()
* @see #doCloseNow()
* @see #send(EMessageHeader)
*/
protected abstract void doClose();
/**
* Closes the eBus connection immediately. All messages
* posted for transmission but not completely sent are lost.
* No new messages may be sent after this point and no new
* messages will be received.
* @see #close
* @see #send(EMessageHeader)
*/
protected abstract void doCloseNow();
/**
* Returns {@code true} if data may be transmitted on this
* connection and {@code false} otherwise.
* @return {@code true} if clear to transmit a message.
*/
protected abstract boolean maySend();
/**
* Sends the messages stored in the
* {@link #mOutputWriter output buffer writer}.
* @param writer outbound messages are stored in this writer.
* @throws IOException
* if an I/O failure occurs during the transmit.
*/
protected abstract void doSend(AbstractMessageWriter writer)
throws IOException;
/**
* Sends the heartbeat bytes to the far-end.
* @throws IOException
* if an I/O failure occurs when transmitting the heartbeat
* bytes.
*/
protected abstract void doSendHeartbeat()
throws IOException;
//
// end of Abstract Method Declarations.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Timeout Handlers.
//
/**
* Sends a heartbeat message to the far-end.
*/
@SuppressWarnings({"java:S1172"})
private void heartbeatTimeout()
{
sLogger.debug("{}: heartbeat timer expired.",
mRemoteAddress);
mHeartbeatTimer.set(null);
mHeartbeatReplyFlag = true;
// If the send fails, what's the point in starting
// the reply timer?
try
{
sLogger.trace("{}: sending heartbeat.",
mRemoteAddress);
doSendHeartbeat();
startHeartbeatTimer();
}
catch (IOException ioex)
{
sLogger.warn("{}: heartbeat send failed.",
mRemoteAddress,
ioex);
disconnected(ioex);
}
} // end of heartbeatTimeout()
/**
* Closes the connect due to the remote eBus failing to
* respond to a heartbeat.
*/
@SuppressWarnings({"java:S1172"})
private void heartbeatReplyTimeout()
{
sLogger.debug("{}: heartbeat reply timer expired.",
mRemoteAddress);
mHeartbeatReplyTimer.set(null);
// We are no longer waiting for a reply.
mHeartbeatReplyFlag = false;
// We have timed out waiting for the far-end to reply
// to our heartbeat. Something is very wrong.
// Close down this connection.
mAsocket.closeNow();
disconnected(new IOException("no heartbeat reply"));
} // end of heartbeatReplyTimeout()
/**
* Attempts to reconnect to the far-end.
*/
@SuppressWarnings({"java:S1172"})
private void reconnectTimeout()
{
sLogger.debug("{}: reconnect timer expired.",
mRemoteAddress);
mReconnectTimer.set(null);
reconnect();
} // end of reconnectTimeout()
//
// end of Timeout Handlers.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get Methods.
//
/**
* Returns the connection type.
* @return connection type.
*/
public final ConnectionType connectionType()
{
return (mConnectionType);
} // end of connectionType()
/**
* Returns {@code true} if this connection automatically
* reconnects upon unexpected connection loss.
* @return {@code true} if automatic reconnection is set
* and {@code false} otherwise.
*/
public final boolean willReconnect()
{
return (mReconnectFlag);
} // end of willReconnect()
/**
* Returns {@code true} if this connection may be paused and
* {@code false} if it does not support pause.
* @return {@code true} if this connection may be paused.
*/
public final boolean willPause()
{
return (mPauseFlag);
} // end of willPause()
/**
* Returns {@code true} if this connection is in the process
* of connecting; {@code false} otherwise.
* @return {@code true} when connecting.
*/
/* package */ final boolean isConnecting()
{
final ConnectState state = mState.get();
return (state == ConnectState.CONNECTING ||
state == ConnectState.UDP_SECURE_CONNECTING);
} // end of isConnecting()
/**
* Returns {@code true} if fully connected to remote eBus
* and {@code false} otherwise.
* @return {@code true} if fully connected to remote eBus
* and {@code false} otherwise.
*/
public final boolean isOpen()
{
return (mAsocket.isOpen());
} // end of isOpen()
/**
* Returns remote eBus' far-end address.
* If not connected, then returns {@code null}.
* @return an {@code SocketAddress} value. Will be
* {@code null} if not connected.
* @see #localSocketAddress
*/
public final SocketAddress remoteSocketAddress()
{
return (mRemoteAddress);
} // end of remoteSocketAddress()
/**
* Returns this eBus connection's local address. If not
* connected, then returns {@code null}.
* @return the eBus connection's local address.
* @see #remoteSocketAddress
*/
public final SocketAddress localSocketAddress()
{
return (mAsocket.localSocketAddress());
} // end of localSocketAddress()
/**
* Returns the input buffer size in bytes.
* @return input buffer capacity.
*/
public final int inputBufferSize()
{
return (mAsocket.inputBufferSize());
} // end of inputBufferSize()
/**
* Returns the output buffer size in bytes.
* @return output buffer capacity.
*/
public final int outputBufferSize()
{
return (mAsocket.outputBufferSize());
} // end of outputBufferSize()
/**
* Returns the maximum message queue size.
* @return maximum queue size.
*/
public final int maxMessageQueueSize()
{
return (mOutputWriter.maximumSize());
} // end of maxMessageQueueSize()
/**
* Returns number of messages currently on the queue.
* @return message queue size.
*/
public final int messageQueueSize()
{
return (mOutputWriter.transmitQueueSize());
} // end of messageQueueSize()
/**
* Returns how long this {@code EConnection} will
* wait before attempting to reconnect.
* @return Reconnect delay in milliseconds.
*/
public Duration reconnectDelay()
{
return (mReconnectDelay.get());
} // end of reconnectDelay()
/**
* Returns how long this {@code EConnection} will
* wait before sending a heartbeat message.
* @return heartbeat frequency in milliseconds.
*/
public Duration heartbeatDelay()
{
return (mHeartbeatDelay.get());
} // end of heartbeatDelay()
/**
* Returns the heartbeat reply delay.
* @return heartbeat reply delay. in milliseconds.
*/
public Duration heartbeatReplyDelay()
{
return (mHeartbeatReplyDelay.get());
} // end of heartbeatReplyDelay()
/**
* Returns current outbound message sequence number.
* @return outbound message sequence number.
*/
public final int outboundSequenceNumber()
{
return (mOutputWriter.outboundSequenceNumber());
} // end of outboundSequenceNumber()
/**
* Returns a copy of the input readers map. This is done so
* that a paused connection may be restored when resumed.
* @return input readers map copy.
*/
/* package */ final Map readers()
{
return (new HashMap<>(mInputReaders));
} // end of readers()
//
// end of Get Methods.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Set Methods.
//
/**
* Sets the new connect state, logging the fact.
* @param state new connect state.
*/
protected final void setState(final ConnectState state)
{
sLogger.trace("{}: connect state is {}.",
mRemoteAddress,
state);
mState.set(state);
} // end of setState(ConnectState)
/**
* When a previously paused connection is resumed, this
* method is called to move this connection from its
* original remote application to the now resumed remote
* application instance.
* @param app resumed remote application connection.
* @param readers reset the readers map to the given values.
*/
/* package */ final void resumeConnection(final ERemoteApp app,
final Map readers)
{
mRemoteApp = app;
mInputReaders.clear();
mInputReaders.putAll(readers);
} // end of resumeConnection(ERemoteApp, Map<>)
/**
* Creates a new {@link #mInputReaders} entry based on the
* {@link KeyMessage} message. If the message contains a
* class name that is not found in this JVM, then
* store a {@code null} {@link MessageReader} in the map to
* signal that the message class is not supported.
* @param msg the class update message.
*/
/* package */ final void keyUpdate(final KeyMessage msg)
{
final int keyId = msg.keyId;
MessageReader reader = null;
try
{
@SuppressWarnings ("unchecked")
final Class extends EMessage> mc =
(Class extends EMessage>)
Class.forName(msg.messageClass);
final String subject = msg.messageSubject;
final MessageType mt =
(MessageType) DataType.findType(mc);
final MethodHandle mh;
final String mn;
// Is this fish, fowl, or flesh?
// Make that notification, request, or reply.
if ((ENotificationMessage.class).isAssignableFrom(mc))
{
// Notification.
mh = MESSAGE_CB[NOTIFY_CB];
mn = CB_METHOD_NAMES[NOTIFY_CB];
}
else if ((EReplyMessage.class).isAssignableFrom(mc))
{
// Reply.
mh = MESSAGE_CB[REPLY_CB];
mn = CB_METHOD_NAMES[REPLY_CB];
}
else
{
// That leaves request.
mh = MESSAGE_CB[REQUEST_CB];
mn = CB_METHOD_NAMES[REQUEST_CB];
}
reader = createReader(keyId, subject, mt, mh, mn);
}
catch (ClassNotFoundException classex)
{
// Ignore and put null into the readers map.
}
mInputReaders.put(keyId, reader);
} // end of keyUpdate(KeyMessage)
//
// end of Set Methods.
//-----------------------------------------------------------
/**
* Initializes a newly created remote connection with the
* given configuration.
* @param config initialize connection with this
* configuration.
*/
/* package */ final void initialize(final RemoteConnection config)
{
createHeartbeatData(config);
doInitialize(config);
mOutputWriter = createWriter(config);
initializeReaders();
} // end of initialize(RemoteConnection)
/**
* Establishes a connection to the address specified in
* {@code config}. Returns {@code true} if the connection is
* established and {@code false} if the connection is
* in-progress. If {@code false} is returned,
* {@link ERemoteApp#handleOpen(EAbstractConnection)} is
* called when the connection is established.
* @param config remote connection configuration.
* @return {@code true} if connection is established and
* {@code false} if the connection is in-progress.
* @exception IOException
* if socket connection attempt threw an exception and
* {@code reconnectFlag} is {@code false}.
*
* @see #open(SelectableChannel, Service)
* @see #close()
* @see #closeNow()
*/
@SuppressWarnings({"java:S2139"})
/* package */ final boolean open(final RemoteConnection config)
throws IOException
{
boolean retcode = false;
sLogger.debug("{}: connecting.", config.address());
try
{
setState(ConnectState.CONNECTING);
mRemoteAddress = config.address();
mBindAddress = config.bindAddress();
retcode = doOpen(mRemoteAddress, mBindAddress);
// Set these data members after calling
// AsyncSocket.open() in case the socket throws an
// IllegalStateException.
mReconnectFlag = config.reconnectFlag();
mReconnectDelay.set(config.reconnectTime());
mHeartbeatDelay.set(config.heartbeatDelay());
mHeartbeatReplyDelay.set(
config.heartbeatReplyDelay());
if (retcode)
{
setState(ConnectState.CONNECTED);
startHeartbeatTimer();
}
}
catch (IOException ioex)
{
sLogger.warn("{}: open failed.",
config.address(),
ioex);
// Do *not* attempt to reconnect if the initial
// connect attempt fails. Reconnection is do iff
// the first connect attempt succeeds.
throw (ioex);
}
return (retcode);
} // end of open(SocketAddress, ...)
/**
* Initializes a newly accepted remote connection with the
* given configuration.
* @param config initialize connection with this
* configuration.
*/
/* package */ final void initialize(final Service config)
{
doInitialize(config);
mOutputWriter = createWriter(config);
initializeReaders();
} // end of initialize(Service)
/**
* Encapsulates an already connected socket accept by an
* {@link EServer} instance. Will not reconnect.
* @param socket the accepted socket connection.
* @param config accept socket configuration.
* @exception NullPointerException
* if {@code socket} is {@code null}.
* @exception IOException
* if {@code socket} is closed.
*/
/* package */ final void open(final SelectableChannel socket,
final SocketAddress remoteAddress,
final Service config)
throws IOException
{
final boolean retcode;
Objects.requireNonNull(socket, "socket is null");
setState(ConnectState.CONNECTED);
mRemoteAddress = remoteAddress;
mReconnectFlag = false;
mReconnectDelay.set(Duration.ZERO);
mHeartbeatDelay.set(config.heartbeatDelay());
mHeartbeatReplyDelay.set(config.heartbeatReplyDelay());
sLogger.debug("{}: encapsulating socket.",
mRemoteAddress);
retcode = doOpen(socket);
// If this is a clear text TCP connection, then start
// the heartbeat timer. Otherwise, wait for the secure
// handshake to complete.
if (retcode)
{
startHeartbeatTimer();
}
} // end of open(SelectableChannel, SocketAddress, Service)
/**
* Closes the eBus connection immediately. All messages
* posted for transmission but not completely sent are lost.
* No new messages may be sent after this point and no new
* messages will be received.
*
* @see #open(SocketChannel, long, long)
* @see #open(SocketAddress, int, boolean, long, long, long)
* @see #close()
* @see #send(EMessageHeader)
*/
/* package */ final void closeNow()
{
if (mState.get() != ConnectState.CLOSED)
{
setState(ConnectState.CLOSING);
closeConnection();
doCloseNow();
// Discard any and all unsent messages.
mOutputWriter.closed();
}
} // end of closeNow()
/**
* Immediately closes the open socket connection and starts
* the reconnect process, but only if this connection is
* configured to reconnect. If this connection is not
* configured to reconnect, then closes the connection only.
* Does nothing if the connection is already closed.
* Messages posted for transmission but not completely sent
* are lost.
*/
/* package */ final void closeAndReconnect()
{
if (mAsocket.isOpen())
{
stopHeartbeatTimer();
stopReconnectTimer();
sLogger.debug("{}: reconnecting after {}.",
mRemoteAddress,
mReconnectDelay.get());
mAsocket.closeNow();
if (mReconnectFlag)
{
// Starting the reconnect timer also sets the
// connection state.
startReconnectTimer();
}
else
{
setState(ConnectState.CLOSED);
}
}
} // end of closeAndReconnect()
/**
* Immediately closes the open socket connection and starts
* the reconnect process after the specified delay. Caller
* has already determined that this connection may be paused.
* Does nothing if the connection is already closed. Messages
* posted for transmission but not completely sent are lost.
* @param delay connection pause delay.
*/
/* package */ final void closeAndPause(final Duration delay)
{
if (isOpen())
{
stopHeartbeatTimer();
stopReconnectTimer();
sLogger.debug(
"{}: pausing connection for {} milliseconds.",
mRemoteAddress,
delay.toMillis());
doCloseNow();
// Treat a pause just like a reconnect because at
// this layer it behaves just like a reconnect.
// Starting the pause timer also sets the connection
// state.
startPauseTimer(delay);
}
} // end of closeAndPause(Duration)
/**
* Resume the connection now rather than wait for the resume
* timer to expire.
*/
/* package */ final void resumeNow()
{
sLogger.debug("{}: resuming connection now.",
mRemoteAddress);
stopReconnectTimer();
reconnect();
} // end of resumeNow()
/**
* Sends {@code message} to remote eBus application. Since
* messages are sent asynchronously, then it is likely that
* the message was not transmitted prior to this
* method returning. If an application must be certain that
* all messages are transmitted prior to closing the
* connection, then {@link #close} should be used rather than
* {@link #closeNow} as {@link #close} waits for the pending
* outgoing messages to be transmitted before closing
* the socket connection. Once a connection is closed, no
* further messages may be sent.
* @param header send this header and message.
* @exception NullPointerException
* if {@code header} is {@code null}.
* @exception IllegalStateException
* if this eBus connection is not connected.
* @exception IOException
* if the connection is closed.
*/
@SuppressWarnings({"java:S2696"})
/* package */ final void send(final EMessageHeader header)
throws IOException
{
if (header == null)
{
throw (new NullPointerException("null header"));
}
if (!maySend())
{
throw (new IllegalStateException("not in a state to transmit"));
}
if (sLogger.isTraceEnabled())
{
sLogger.trace(
"{}: sending message to remote eBus: from ID={}, to ID={}, seq num={}\n{}",
mRemoteAddress,
header.fromFeedId(),
header.toFeedId(),
header.sequenceNumber(),
header.message());
}
else
{
sLogger.debug(
"{}: sending {} message to remote eBus: from ID={}, to ID={}, seq num={}.",
mRemoteAddress,
header.messageClass(),
header.fromFeedId(),
header.toFeedId(),
header.sequenceNumber());
}
// Give the message to the output writer and the output
// writer to the socket. If the output writer queue is
// full, allow the buffer overflow exception to pass up
// to the caller.
if (mOutputWriter.post(header))
{
// But if the async socket throws a buffer overflow
// exception, do NOT allow that to flow up to the
// caller because this is not an exceptional
// circumstance. Rather, it is the normal progression
// in socket transmissions.
try
{
doSend(mOutputWriter);
++mMsgOutCount;
++sTotalOutCount;
}
catch (BufferOverflowException bufex)
{
// Ignore.
}
}
} // end of send(EMessageHeader)
/**
* Informs the listener that the connection is open. Turns
* off Nagle's TCP delay algorithm and starts the heartbeat
* time along the way.
*/
protected final void connected()
{
sLogger.debug("{}: connected.", mRemoteAddress);
setState(ConnectState.CONNECTED);
// Do post connection work.
try
{
doConnected();
}
catch (IOException ioex)
{
sLogger.warn("{}: error completing connection.",
mRemoteAddress,
ioex);
}
// Is this connection still connected?
// Secure UDP connection goes to a UDP_SECURE_CONNECTING
// state.
if (mState.get() == ConnectState.CONNECTED)
{
// Yes, fully connected. Continue with the connected
// processing.
fullyConnected();
}
} // end of connected()
/**
* Immediately closes the underlying socket and informs the
* remote address that the initial connect attempt failed.
* @param address connection target address.
* @param reason text explaining why the connect attempt
* failed.
*/
protected final void connectFailed(final InetSocketAddress address,
final String reason)
{
doCloseNow();
mRemoteApp.connectFailed(address, reason);
} // end of connectFailed(InetSocketAddress, String)
/**
* Completes the connection process by starting the heartbeat
* timer and informing the listener that the connection is
* up.
*/
protected final void fullyConnected()
{
startHeartbeatTimer();
// Let the listener know that we are open and ready for
// business.
try
{
final EAbstractConnection abConn = this;
(CONN_CB[OPEN_CB]).invokeExact(mRemoteApp, abConn);
}
catch (Throwable tex)
{
sLogger.warn("{} exception",
CONN_CB_METHOD_NAMES[OPEN_CB],
tex);
}
} // end of fullyConnected()
/**
* {@code asocket} is disconnected. Stop all timers and
* inform the listener. If set to reconnect, then start
* the reconnect timer.
* @param t The disconnection cause.
*/
protected final void disconnected(final Throwable t)
{
sLogger.debug("{}: closed.", mRemoteAddress, t);
// Stop any and all timers.
stopReconnectTimer();
stopHeartbeatTimer();
// Discard any and all unsent messages.
mOutputWriter.closed();
// Tell the listener about this sorry state of affairs.
try
{
final EAbstractConnection abConn = this;
(CONN_CB[CLOSE_CB]).invokeExact(mRemoteApp, abConn);
}
catch (Throwable tex)
{
sLogger.warn("{} exception.",
CONN_CB_METHOD_NAMES[CLOSE_CB],
tex);
}
// Reconnect if configured to do so.
startReconnectTimer();
} // end of disconnected(Throwable)
/**
* Starts either the heartbeat or the heartbeat reply timer
* depending on whether we are expecting a heartbeat reply
* or not. It is also depends on whether heartbeating is
* turned on.
*/
protected final void startHeartbeatTimer()
{
Duration delay = mHeartbeatDelay.get();
final boolean isHBOn =
(delay.compareTo(Duration.ZERO) > 0);
IETimer hbTimer = mHeartbeatTimer.get();
IETimer hbReplyTimer = mHeartbeatReplyTimer.get();
// Turn on the heartbeat timer if:
// 1. heartbeating is on.
// 2. we are *not* waiting for a heartbeat reply.
// 3. the heartbeat timer is not already running.
if (isHBOn &&
!mHeartbeatReplyFlag &&
hbTimer == null)
{
mHeartbeatTimer.set(
sCoreExecutor.schedule(this::heartbeatTimeout,
this,
delay));
sLogger.trace(
"{}: started heartbeat timer, deleay {} millis.",
mRemoteAddress,
delay);
}
// Turn on the heartbeat reply timer if:
// 1. heartbeating is on.
// 2. we are waiting for a heartbeat reply.
// 3. the heartbeat reply timer is not already
// running
else if (isHBOn &&
mHeartbeatReplyFlag &&
hbReplyTimer == null)
{
delay = mHeartbeatReplyDelay.get();
mHeartbeatReplyTimer.set(
sCoreExecutor.schedule(this::heartbeatReplyTimeout,
this,
delay));
sLogger.trace(
"{}: started heartbeat reply timer, delay {}.",
mRemoteAddress,
delay);
}
} // end of startHeartbeatTimer()
/**
* Stops whichever heartbeat timer is running or both
* (which should never be the case).
*/
protected final void stopHeartbeatTimer()
{
closeTimer(mHeartbeatTimer);
closeTimer(mHeartbeatReplyTimer);
} // end of stopHeartbeatTimer()
/**
* Logs connection-associated exception at {@code FINE}
* level.
* @param t The exception to be passed to the listeners.
*/
protected final void logError(final Throwable t)
{
sLogger.debug("{}: input processing error",
mRemoteAddress,
t);
} // end of logError(Throwable)
/**
* Returns input reader for given message type and subject
* used to decode inbound messages.
* @param keyId unique message class identifier from remote
* eBus application.
* @param subject message key subject.
* @param mt message type, used for de-serialization.
* @param mh callback target method.
* @param mn callback method name.
* @return message input message decoder.
*/
protected MessageReader createReader(final int keyId,
final String subject,
final MessageType mt,
final MethodHandle mh,
final String mn)
{
return (new MessageReader(keyId, subject, mt, mh, mn));
} // end of createReader(...)
/**
* Initializes the input message readers for eBus system
* messages.
*/
private void initializeReaders()
{
final SystemMessageType[] types =
SystemMessageType.values();
final int size = types.length;
int keyId;
int index;
MessageType mt;
// Add non-multicast system message input readers to map.
for (index = 0; index < size; ++index)
{
if (!types[index].isMulticast())
{
keyId = types[index].keyId();
mt =
(MessageType) DataType.findType(
types[index].messageClass());
mInputReaders.put(
keyId,
createReader(keyId,
ESystemMessage.SYSTEM_SUBJECT,
mt,
MESSAGE_CB[index],
CB_METHOD_NAMES[index]));
}
}
} // end of initializeReaders()
/**
* Creates heartbeat and heartbeat reply messages using the
* configured byte order.
* @param config contains buffer byte order.
*/
private void createHeartbeatData(final RemoteConnection config)
{
final ByteBuffer buffer =
ByteBuffer.allocate(Integer.BYTES);
buffer.order(config.byteOrder());
// Create the heartbeat and heartbeat reply messages.
buffer.putInt(HEARTBEAT)
.flip()
.get(mHeartbeatData)
.clear();
buffer.putInt(HEARTBEAT_REPLY)
.flip()
.get(mHeartbeatReplyData)
.clear();
} // end of createHeartbeatData(RemoteConnection)
/**
* Performs the actual work of closing an eBus connection
* except the underlying socket connection.
*/
private void closeConnection()
{
stopHeartbeatTimer();
mReconnectFlag = false;
stopReconnectTimer();
} // end of closeConnection()
/**
* Attempts to connect to the remote eBus.
*/
private void reconnect()
{
// Has anything changed while waiting to reconnect?
if (mState.get() == ConnectState.RECONNECTING)
{
sLogger.debug("{}: attempting to reconnect.",
mRemoteAddress);
try
{
setState(ConnectState.CONNECTING);
if (doOpen(mRemoteAddress, mBindAddress))
{
connected();
}
}
catch (IOException ioex)
{
sLogger.debug("{}: connect attempt failed.",
mRemoteAddress,
ioex);
// Connect attempt failed. Set timer and retry
// connection later.
startReconnectTimer();
}
}
// Yes, something did change: the eBus connection was
// closed.
} // end of reconnect()
/**
* If there is no reconnect timer task running, start one.
*/
private void startReconnectTimer()
{
// If reconnecting is turned off or this eBus
// connection was in the process of closing, then
// don't bother reconnecting.
if (!mReconnectFlag ||
mState.get() == ConnectState.CLOSING)
{
// Mark this connection as closed and stop the
// connection thread - but only after delivering
// all remaining events.
setState(ConnectState.CLOSED);
}
else
{
final Duration delay = mReconnectDelay.get();
setState(ConnectState.RECONNECTING);
// Stop the current timer if running.
closeTimer(mReconnectTimer);
mReconnectTimer.set(
sCoreExecutor.schedule(this::reconnectTimeout,
this,
delay));
sLogger.trace(
"{}: started reconnect timer, delay {} millis.",
mRemoteAddress,
delay);
}
} // end of startReconnectTimer()
/**
* Starts the reconnect timer in response to a connection
* pause. The timer delay is provided in this case rather
* than using the configured reconnect time.
* @param delay connection pause delay.
*/
private void startPauseTimer(final Duration delay)
{
setState(ConnectState.RECONNECTING);
// Stop the current timer if running.
closeTimer(mReconnectTimer);
mReconnectTimer.set(
sCoreExecutor.schedule(this::reconnectTimeout,
this,
delay));
sLogger.debug("{}: started pause timer, delay {}.",
mRemoteAddress,
delay);
} // end of startPauseTimer(Duration)
/**
* Stop the reconnect timer if it is running.
*/
private void stopReconnectTimer()
{
closeTimer(mReconnectTimer);
} // end of stopReconnectTimer()
/**
* Closes given timer. If {@code IETimer.close} throws an
* exception, it is caught and ignored.
* @param timer close this timer and set to {@code null}.
*/
private static void closeTimer(final AtomicReference timer)
{
final IETimer t = timer.getAndSet(null);
if (t != null)
{
try
{
t.close();
}
catch (Exception jex)
{}
}
} // end of closeTimer(AtomicReference);
//---------------------------------------------------------------
// Inner classes.
//
/**
* This class combines two data points: the message type for
* de-serializing a particular message class and the handle
* for the method responsible for processing the message.
* This class extracts messages from a buffer and forwards
* them to the target.
*/
protected class MessageReader
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Constants.
//
/**
* Header size sans message size is {@value} bytes.
*/
private static final int HEADER_SIZE = 8; // bytes.
//-------------------------------------------------------
// Locals.
//
/**
* A unique message key identifier received from the
* remote eBus application.
*/
protected final int mKeyId;
/**
* The message key subject. Needed to construct inbound
* messages.
*/
private final String mSubject;
/**
* The message type for deserializing the message class.
*/
private final MessageType mMessageType;
/**
* Forward the message to this callback method.
*/
private final MethodHandle mCallback;
/**
* The callback method name (not included in the method
* handle).
*/
private final String mMethodName;
/**
* Message header size sans message size's size.
*/
private final int mHeaderSize;
//---------------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new message reader for the given parameters.
* @param keyId unique message class identifier from
* remote eBus application.
* @param subject message key subject.
* @param mt message type, used for de-serialization.
* @param mh callback target method.
* @param mn callback method name.
*/
protected MessageReader(final int keyId,
final String subject,
final MessageType mt,
final MethodHandle mh,
final String mn,
final int headerSize)
{
mKeyId = keyId;
mSubject = subject;
mMessageType = mt;
mCallback = mh;
mMethodName = mn;
mHeaderSize = headerSize;
} // end of MessageReader(..)
/**
* Creates a new message reader for the given parameters.
* @param keyId unique message class identifier from
* remote eBus application.
* @param subject message key subject.
* @param mt message type, used for de-serialization.
* @param mh callback target method.
* @param mn callback method name.
*/
private MessageReader(final int keyId,
final String subject,
final MessageType mt,
final MethodHandle mh,
final String mn)
{
this (keyId, subject, mt, mh, mn, HEADER_SIZE);
} // end of MessageReader(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Get methods.
//
/**
* Returns the message key identifier.
* @return message key identifier.
*/
public final int keyId()
{
return (mKeyId);
} // end of keyId()
//
// end of Get methods.
//-------------------------------------------------------
/**
* Returns the message header extracted from the given
* buffer starting at the buffer's current position.
* Note that the message size and message key identifier
* have already been extracted from {@code buffer}.
* @param buffer extract the message from this buffer.
* @param address data came from this source address.
* @return the extracted message header and message.
* @throws BufferUnderflowException
* if {@code buffer} contains an incomplete message.
* @throws UnknownMessageException
* if the de-serialized class identifier references an
* unknown message class.
* @throws InvalidMessageException
* if the message construction fails.
*/
@SuppressWarnings({"java:S2696"})
public final EMessageHeader extractMessage(final ByteBuffer buffer,
final SocketAddress address)
{
final EMessage msg;
final EMessageHeader retval;
// Skip over header bytes and extract message first.
buffer.mark()
.position(buffer.position() + mHeaderSize);
// Need to pass in the message subject explicitly
// because it is not longer serialized. Rather the
// subject is inferred from the message key
// identifier.
msg =
(EMessage)
mMessageType.deserialize(mSubject, buffer);
// Reset to header start position and extract the
// header.
buffer.reset();
retval = extractHeader(buffer, address, msg);
++mMsgInCount;
++sTotalInCount;
if (sLogger.isTraceEnabled())
{
sLogger.trace(
"{}: handling {} message:\n From ID: {}\n To ID: {}\n Seq Num: {}\n{}",
mRemoteAddress,
retval.messageClass(),
retval.fromFeedId(),
retval.toFeedId(),
retval.sequenceNumber(),
msg);
}
else
{
sLogger.debug(
"{}: handling {} message (from={}, to={}).",
mRemoteAddress,
retval.messageClass(),
retval.fromFeedId(),
retval.toFeedId());
}
return (retval);
} // end of extractMessage(ByteBuffer)
/**
* Returns eBus message header containing message key,
* from, and to feed identifiers, sender's socket
* address, and decode message.
* @param buffer extract header from this buffer at
* current location.
* @param address data came from this source address.
* @param msg decode message.
* @return decoded eBus message header.
*/
protected EMessageHeader extractHeader(final ByteBuffer buffer,
final SocketAddress address,
final EMessage msg)
{
final int fromFeedId = buffer.getInt();
final int toFeedId = buffer.getInt();
return (new EMessageHeader(mKeyId,
fromFeedId,
toFeedId,
address,
msg));
} // end of extractHeader(...)
/**
* Forwards the given message header to the specified
* eBus remote application instance.
* @param header forward this message header to
* {@code target}.
* @param target the eBus remote application instance
* to receive the inbound message.
*/
public final void forwardMessage(final EMessageHeader header,
final ERemoteApp target)
{
try
{
if (sLogger.isTraceEnabled())
{
sLogger.trace(
"{}",
new LazyString(
() ->
{
final StringBuilder output =
new StringBuilder();
final MethodType mt =
mCallback.type();
final Class>[] parms =
mt.parameterArray();
final int nParms = parms.length;
int index;
String sep;
output.append(mRemoteAddress)
.append(" forward ")
.append(header.messageKey())
.append(" to ")
.append((mt.returnType()).getName())
.append(' ')
.append(parms[0].getName())
.append('.')
.append(mMethodName);
for (index = 1, sep = "(";
index < nParms;
++index, sep = ", ")
{
output.append(sep)
.append(parms[index].getName());
}
output.append(')');
return (output.toString());
}));
}
mCallback.invokeExact(target, header);
}
catch (Throwable tex)
{
sLogger.warn(
"Error processing message header.", tex);
}
} // end of forwardMessage(EMessageHeader)
} // end of class MessageReader
/**
* This class is responsible for serializing messages
* directly to a given {@link AsyncChannel channel}
* {@link ByteBuffer output buffer}.
*/
protected abstract static class AbstractMessageWriter
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Statics.
//
/**
* Logging subsystem interface.
*/
private static final Logger sSublogger =
LoggerFactory.getLogger(AbstractMessageWriter.class);
//-------------------------------------------------------
// Locals.
//
/**
* This message writer works for this eBus connection.
*/
protected final EAbstractConnection mConnection;
/**
* The message queue maximum allowed size. If the value
* is ≤ zero, then the queue has no maximum. Once the
* maximum is reached, new messages are rejected.
*/
protected final int mMaxSize;
/**
* Post messages here until it is time to serialize them
* to the socket output buffer. Then serialize as many
* as possible.
*/
protected final Deque mTransmitQueue;
/**
* Tracks the {@link #mTransmitQueue transmit queue}
* approximate size. This is necessary
* because {@code ConcurrentLinkedDeque.size()} is a
* costly operation.
*/
protected final AtomicInteger mTransmitQueueSize;
/**
* Next outbound message will have this sequence number.
* This number is incremented by one after outbound
* message receipt is acknowledged.
*/
protected int mOutboundSequenceNumber;
/**
* Set to {@code true} when the parent eBus connection is
* doing a slow close.
*/
protected volatile boolean mClosingFlag;
/**
* Count up the number of messages transmitted. Meant for
* reporting purposes only.
*/
protected int mTransmitCount;
/**
* Count up the number of discarded application messages.
* Meant for reporting purposes only.
*/
protected int mDiscardCount;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a message writer instance with the given
* maximum outbound message queue size and eBus
* connection.
* @param config connection configuration.
* @param conn this message writer works for this eBus
* connection.
*/
protected AbstractMessageWriter(final AbstractConfig config,
final EAbstractConnection conn)
{
mConnection = conn;
mMaxSize = config.messageQueueSize();
mTransmitQueue = new ConcurrentLinkedDeque<>();
mTransmitQueueSize = new AtomicInteger();
mOutboundSequenceNumber = 0;
mClosingFlag = false;
mTransmitCount = 0;
mDiscardCount = 0;
} // end of AbstractMessageWriter(int, EConnection)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Get methods.
//
/**
* Returns the transmit queue's maximum size. If
* {@link #DEFAULT_QUEUE_SIZE} is returned, then the
* queue size is unlimited. If a value >
* {@code DEFAULT_QUEUE_SIZE} is returned, then when the
* queue reaches this limit and attempts to offer a new
* message, the new message is discarded and a buffer
* overflow exception thrown
* @return the transmit queue maximum size.
*/
public final int maximumSize()
{
return (mMaxSize);
} // end of maximumSize()
/**
* Returns the current message transmit count. Since this
* method is not synchronized the returned value is
* approximate.
* @return the current message transmit count.
*/
public final int transmitCount()
{
return (mTransmitCount);
} // end of transmitCount()
/**
* Returns the number of discarded user messages. Since
* this method is not synchronized the value is
* approximate.
* @return the number of discarded user messages.
*/
public final int discardCount()
{
return (mDiscardCount);
} // end of discardCount()
/**
* Returns the transmit queue current size. This
* is an approximate value and should not be
* used to determine if the queue is empty.
* @return approximate transmit queue size.
*/
public final int transmitQueueSize()
{
return (mTransmitQueueSize.get());
} // end of transmitQueueSize()
/**
* Returns {@code true} if this message writer has
* outbound messages pending and {@code false} otherwise.
* @return {@code true} if there are messages waiting to
* be transmitted.
*/
public final boolean hasMessages()
{
return (!mTransmitQueue.isEmpty());
} // end of hasMessages()
/**
* Returns current outbound sequence number.
* @return outbound sequence number.
*/
public int outboundSequenceNumber()
{
return (mOutboundSequenceNumber);
} // end of outboundSequenceNumber()
//
// end of Get methods.
//-------------------------------------------------------
/**
* Queues the message header. If the maximum queue
* size is reached, then the header is dropped and a
* buffer overflow exception is thrown.
*
* If the header contains a
* {@link net.sf.eBus.messages.ESystemMessage system message},
* then the message is queued despite exceeding the
* maximum queue size because system message must get
* through.
*
*
* Returns {@code true} if this is the first message on
* the queue. This means the caller is clear to send the
* queued message since no other thread is currently
* accessing the channel.
*
* @param h enqueue this message header.
* @return {@code true} if this message writer should be
* posted to the socket.
* @exception BufferOverflowException
* if the transmit queue maximum size is reached.
*/
public final boolean post(final EMessageHeader h)
{
// Increment the queue size if-and-only-if the
// message type is *not* system.
final EMessage.MessageType msgType = h.messageType();
final int queueSize =
(msgType == EMessage.MessageType.SYSTEM ?
(mTransmitQueueSize.get() + 1) :
mTransmitQueueSize.incrementAndGet());
final boolean retcode;
// If the queue is *not* of unlimited size and the
// queue has reached that limit, then let the caller
// know about this by throwing an overflow exception.
// Put always add system messages.
if (mMaxSize > DEFAULT_QUEUE_SIZE &&
queueSize >= mMaxSize)
{
final BufferOverflowException bufex =
new BufferOverflowException();
final IllegalStateException statex =
new IllegalStateException(
String.format(
"message queue maximum reached (%,d)",
mMaxSize));
bufex.initCause(statex);
sSublogger.debug(
"{} queue: queue maximum reached ({}).",
mConnection.remoteSocketAddress(),
mMaxSize);
++mDiscardCount;
// HEAVE!
throw (bufex);
}
// Place header on message queue.
retcode = enqueue(h, queueSize);
sSublogger.debug(
"{} queue: added message (size={}, transmited={}, discarded={}).",
mConnection.remoteSocketAddress(),
queueSize,
mTransmitCount,
mDiscardCount);
// Pass this message writer to the socket if this is
// the first message on the queue and the socket
// is not overflowing.
return (retcode);
} // end of post(EMessageHeader)
/**
* Returns {@code true} if this is the only message
* on the transmit queue. Places message at queue's end.
* @param header enqueue this message.
* @param queueSize message queue size as a result of
* adding message.
* @return {@code true} if clear to transmit.
*/
protected boolean enqueue(final EMessageHeader header,
final int queueSize)
{
mTransmitQueue.offer(header);
// Pass this message writer to the socket if this is
// the first message on the queue and the socket
// is not overflowing.
return (queueSize == 1);
} // end of enqueue(EMessageHeader, int)
/**
* Sets the closing flag to {@code true}. This is the
* only possible setting because, once set, it cannot be
* changed back.
*/
protected final void setClosing()
{
mClosingFlag = true;
} // end of setClosing()
/**
* Closes this message writer by discarding all unsent
* messages.
*/
protected void closed()
{
mTransmitQueue.clear();
mTransmitQueueSize.set(0);
} // end of closed()
} // end of class AbstractMessageWriter
} // end of class EAbstractConnection