All connections are now buffered - in the event there is a network issue, or a quick reconnect, and messages are sent DURING this disconnected phase, these messages will be resent on the new connection once it is connected
This commit is contained in:
parent
27b4b0421e
commit
fe98763712
|
@ -25,14 +25,14 @@ import dorkbox.network.aeron.AeronDriver
|
|||
import dorkbox.network.aeron.EventActionOperator
|
||||
import dorkbox.network.aeron.EventCloseOperator
|
||||
import dorkbox.network.aeron.EventPoller
|
||||
import dorkbox.network.connection.*
|
||||
import dorkbox.network.connection.Connection
|
||||
import dorkbox.network.connection.ConnectionParams
|
||||
import dorkbox.network.connection.EndPoint
|
||||
import dorkbox.network.connection.IpInfo.Companion.formatCommonAddress
|
||||
import dorkbox.network.connection.ListenerManager.Companion.cleanStackTrace
|
||||
import dorkbox.network.connection.ListenerManager.Companion.cleanStackTraceInternal
|
||||
import dorkbox.network.connection.session.SessionClient
|
||||
import dorkbox.network.connection.session.SessionConnection
|
||||
import dorkbox.network.connection.session.SessionManagerFull
|
||||
import dorkbox.network.connection.session.SessionManagerNoOp
|
||||
import dorkbox.network.connection.PublicKeyValidationState
|
||||
import dorkbox.network.connection.buffer.BufferManager
|
||||
import dorkbox.network.exceptions.*
|
||||
import dorkbox.network.handshake.ClientConnectionDriver
|
||||
import dorkbox.network.handshake.ClientHandshake
|
||||
|
@ -148,6 +148,14 @@ open class Client<CONNECTION : Connection>(config: ClientConfiguration = ClientC
|
|||
@Volatile
|
||||
private var stopConnectOnShutdown = false
|
||||
|
||||
/**
|
||||
* Different connections (to the same client) can be "buffered", meaning that if they "go down" because of a network glitch -- the data
|
||||
* being sent is not lost (it is buffered) and then re-sent once the new connection is established. References to the old connection
|
||||
* will also redirect to the new connection.
|
||||
*/
|
||||
@Volatile
|
||||
internal var bufferedManager: BufferManager<CONNECTION>? = null
|
||||
|
||||
// is valid when there is a connection to the server, otherwise it is null
|
||||
@Volatile
|
||||
private var connection0: CONNECTION? = null
|
||||
|
@ -420,7 +428,6 @@ open class Client<CONNECTION : Connection>(config: ClientConfiguration = ClientC
|
|||
require(port1 < 65535) { "port1 must be < 65535" }
|
||||
require(port2 < 65535) { "port2 must be < 65535" }
|
||||
}
|
||||
|
||||
require(connectionTimeoutSec >= 0) { "connectionTimeoutSec '$connectionTimeoutSec' is invalid. It must be >=0" }
|
||||
|
||||
// only try to connect via IPv4 if we have a network interface that supports it!
|
||||
|
@ -550,7 +557,7 @@ open class Client<CONNECTION : Connection>(config: ClientConfiguration = ClientC
|
|||
|
||||
|
||||
// the handshake connection is closed when the handshake has an error, or it is finished
|
||||
var handshakeConnection: ClientHandshakeDriver? = null
|
||||
var handshakeConnection: ClientHandshakeDriver?
|
||||
|
||||
try {
|
||||
// always start the aeron driver inside the restart loop.
|
||||
|
@ -699,6 +706,9 @@ open class Client<CONNECTION : Connection>(config: ClientConfiguration = ClientC
|
|||
handshakeTimeoutNs = handshakeTimeoutNs
|
||||
)
|
||||
|
||||
bufferedManager = BufferManager(config, listenerManager, aeronDriver, connectionInfo.sessionTimeout)
|
||||
|
||||
|
||||
// VALIDATE:: check to see if the remote connection's public key has changed!
|
||||
val validateRemoteAddress = if (handshakeConnection.pubSub.isIpc) {
|
||||
PublicKeyValidationState.VALID
|
||||
|
@ -720,13 +730,6 @@ open class Client<CONNECTION : Connection>(config: ClientConfiguration = ClientC
|
|||
// is rogue, we do not want to carelessly provide info.
|
||||
|
||||
|
||||
// NOTE: this can change depending on what the server specifies!
|
||||
// should we queue messages during a reconnect? This is important if the client/server connection is unstable
|
||||
if (connectionInfo.enableSession && sessionManager is SessionManagerNoOp) {
|
||||
sessionManager = SessionManagerFull(config, listenerManager as ListenerManager<SessionConnection>, aeronDriver, connectionInfo.sessionTimeout)
|
||||
} else if (!connectionInfo.enableSession && sessionManager is SessionManagerFull) {
|
||||
sessionManager = SessionManagerNoOp()
|
||||
}
|
||||
|
||||
///////////////
|
||||
//// RMI
|
||||
|
@ -772,12 +775,12 @@ open class Client<CONNECTION : Connection>(config: ClientConfiguration = ClientC
|
|||
)
|
||||
|
||||
val pubSub = clientConnection.connectionInfo
|
||||
val logInfo = pubSub.getLogInfo(logger)
|
||||
val connectionType = if (this is SessionClient) "session connection" else "connection"
|
||||
|
||||
val logInfo = pubSub.getLogInfo(logger.isDebugEnabled)
|
||||
if (logger.isDebugEnabled) {
|
||||
logger.debug("Creating new $connectionType to $logInfo")
|
||||
logger.debug("Creating new buffered connection to $logInfo")
|
||||
} else {
|
||||
logger.info("Creating new $connectionType to $logInfo")
|
||||
logger.info("Creating new buffered connection to $logInfo")
|
||||
}
|
||||
|
||||
val newConnection = newConnection(ConnectionParams(
|
||||
|
@ -788,17 +791,16 @@ open class Client<CONNECTION : Connection>(config: ClientConfiguration = ClientC
|
|||
connectionInfo.secretKey
|
||||
))
|
||||
|
||||
sessionManager.onNewConnection(newConnection)
|
||||
bufferedManager?.onConnect(newConnection)
|
||||
|
||||
if (!handshakeConnection.pubSub.isIpc) {
|
||||
// NOTE: Client can ALWAYS connect to the server. The server makes the decision if the client can connect or not.
|
||||
val connType = if (newConnection is SessionConnection) "Session connection" else "Connection"
|
||||
if (logger.isTraceEnabled) {
|
||||
logger.trace("[${handshakeConnection.details}] (${handshake.connectKey}) $connType (${newConnection.id}) adding new signature for [$addressString -> ${connectionInfo.publicKey.toHexString()}]")
|
||||
logger.trace("[${handshakeConnection.details}] (${handshake.connectKey}) Buffered connection (${newConnection.id}) adding new signature for [$addressString -> ${connectionInfo.publicKey.toHexString()}]")
|
||||
} else if (logger.isDebugEnabled) {
|
||||
logger.debug("[${handshakeConnection.details}] $connType (${newConnection.id}) adding new signature for [$addressString -> ${connectionInfo.publicKey.toHexString()}]")
|
||||
logger.debug("[${handshakeConnection.details}] Buffered connection (${newConnection.id}) adding new signature for [$addressString -> ${connectionInfo.publicKey.toHexString()}]")
|
||||
} else if (logger.isInfoEnabled) {
|
||||
logger.info("[${handshakeConnection.details}] $connType adding new signature for [$addressString -> ${connectionInfo.publicKey.toHexString()}]")
|
||||
logger.info("[${handshakeConnection.details}] Buffered connection adding new signature for [$addressString -> ${connectionInfo.publicKey.toHexString()}]")
|
||||
}
|
||||
|
||||
storage.addRegisteredServerKey(address!!, connectionInfo.publicKey)
|
||||
|
@ -822,25 +824,18 @@ open class Client<CONNECTION : Connection>(config: ClientConfiguration = ClientC
|
|||
// finished with the handshake, so always close these!
|
||||
handshakeConnection.close(this)
|
||||
|
||||
val connType = if (newConnection is SessionConnection) "Session connection" else "Connection"
|
||||
if (logger.isTraceEnabled) {
|
||||
logger.debug("[${handshakeConnection.details}] (${handshake.connectKey}) $connType (${newConnection.id}) done with handshake.")
|
||||
logger.debug("[${handshakeConnection.details}] (${handshake.connectKey}) Buffered connection (${newConnection.id}) done with handshake.")
|
||||
} else if (logger.isDebugEnabled) {
|
||||
logger.debug("[${handshakeConnection.details}] $connType (${newConnection.id}) done with handshake.")
|
||||
logger.debug("[${handshakeConnection.details}] Buffered connection (${newConnection.id}) done with handshake.")
|
||||
}
|
||||
|
||||
connection0 = newConnection
|
||||
newConnection.setImage()
|
||||
|
||||
|
||||
// in the specific case of using sessions, we don't want to call 'init' or `connect` for a connection that is resuming a session
|
||||
// when applicable - we ALSO want to restore RMI objects BEFORE the connection is fully setup!
|
||||
val newSession = sessionManager.onInit(newConnection)
|
||||
|
||||
// before we finish creating the connection, we initialize it (in case there needs to be logic that happens-before `onConnect` calls
|
||||
if (newSession) {
|
||||
listenerManager.notifyInit(newConnection)
|
||||
}
|
||||
|
||||
// this enables the connection to start polling for messages
|
||||
addConnection(newConnection)
|
||||
|
@ -882,7 +877,7 @@ open class Client<CONNECTION : Connection>(config: ClientConfiguration = ClientC
|
|||
}
|
||||
|
||||
val mustRestartDriverOnError = aeronDriver.internal.mustRestartDriverOnError
|
||||
val dirtyDisconnectWithSession = (this@Client is SessionClient) && !shutdownEventPoller && connection.isDirtyClose()
|
||||
val dirtyDisconnectWithSession = !shutdownEventPoller && connection.isDirtyClose()
|
||||
|
||||
autoReconnect = mustRestartDriverOnError || dirtyDisconnectWithSession
|
||||
|
||||
|
@ -950,9 +945,7 @@ open class Client<CONNECTION : Connection>(config: ClientConfiguration = ClientC
|
|||
|
||||
listenerManager.notifyConnect(newConnection)
|
||||
|
||||
if (!newSession) {
|
||||
(newConnection as SessionConnection).sendPendingMessages()
|
||||
}
|
||||
newConnection.sendBufferedMessages()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1050,6 +1043,7 @@ open class Client<CONNECTION : Connection>(config: ClientConfiguration = ClientC
|
|||
*/
|
||||
fun close(closeEverything: Boolean = true) {
|
||||
stopConnectOnShutdown = true
|
||||
bufferedManager?.close()
|
||||
close(closeEverything = closeEverything, sendDisconnectMessage = true, releaseWaitingThreads = true)
|
||||
}
|
||||
|
||||
|
|
|
@ -70,12 +70,12 @@ class ServerConfiguration : dorkbox.network.Configuration() {
|
|||
}
|
||||
|
||||
/**
|
||||
* If a connection is in a temporal state (in the middle of a reconnect) and sessions are enabled -- then how long should we consider
|
||||
* a new connection from the same client as part of the same session.
|
||||
* If a connection is in a temporal state (in the middle of a reconnect) and a buffered connection is in use -- then how long should we consider
|
||||
* a new connection from the same client as part of the same "session".
|
||||
*
|
||||
* The session timeout cannot be shorter than 60 seconds, and the server will send this configuration to the client
|
||||
*/
|
||||
var sessionTimeoutSeconds = TimeUnit.MINUTES.toSeconds(2)
|
||||
var bufferedConnectionTimeoutSeconds = TimeUnit.MINUTES.toSeconds(2)
|
||||
set(value) {
|
||||
require(!contextDefined) { errorMessage }
|
||||
field = value
|
||||
|
@ -118,7 +118,7 @@ class ServerConfiguration : dorkbox.network.Configuration() {
|
|||
config.listenIpAddress = listenIpAddress
|
||||
config.maxClientCount = maxClientCount
|
||||
config.maxConnectionsPerIpAddress = maxConnectionsPerIpAddress
|
||||
config.sessionTimeoutSeconds = sessionTimeoutSeconds
|
||||
config.bufferedConnectionTimeoutSeconds = bufferedConnectionTimeoutSeconds
|
||||
config.settingsStore = settingsStore
|
||||
|
||||
super.copy(config)
|
||||
|
@ -134,7 +134,7 @@ class ServerConfiguration : dorkbox.network.Configuration() {
|
|||
if (listenIpAddress != other.listenIpAddress) return false
|
||||
if (maxClientCount != other.maxClientCount) return false
|
||||
if (maxConnectionsPerIpAddress != other.maxConnectionsPerIpAddress) return false
|
||||
if (sessionTimeoutSeconds != other.sessionTimeoutSeconds) return false
|
||||
if (bufferedConnectionTimeoutSeconds != other.bufferedConnectionTimeoutSeconds) return false
|
||||
if (settingsStore != other.settingsStore) return false
|
||||
|
||||
return true
|
||||
|
@ -145,7 +145,7 @@ class ServerConfiguration : dorkbox.network.Configuration() {
|
|||
result = 31 * result + listenIpAddress.hashCode()
|
||||
result = 31 * result + maxClientCount
|
||||
result = 31 * result + maxConnectionsPerIpAddress
|
||||
result = 31 * result + sessionTimeoutSeconds.hashCode()
|
||||
result = 31 * result + bufferedConnectionTimeoutSeconds.hashCode()
|
||||
result = 31 * result + settingsStore.hashCode()
|
||||
return result
|
||||
}
|
||||
|
@ -175,7 +175,6 @@ class ClientConfiguration : dorkbox.network.Configuration() {
|
|||
super.validate()
|
||||
|
||||
// have to do some basic validation of our configuration
|
||||
|
||||
if (port != -1) {
|
||||
// this means it was configured!
|
||||
require(port > 0) { "Client listen port must be > 0" }
|
||||
|
|
|
@ -20,9 +20,7 @@ import dorkbox.network.aeron.*
|
|||
import dorkbox.network.connection.*
|
||||
import dorkbox.network.connection.IpInfo.Companion.IpListenType
|
||||
import dorkbox.network.connection.ListenerManager.Companion.cleanStackTrace
|
||||
import dorkbox.network.connection.session.SessionConnection
|
||||
import dorkbox.network.connection.session.SessionManagerFull
|
||||
import dorkbox.network.connection.session.SessionServer
|
||||
import dorkbox.network.connection.buffer.BufferManager
|
||||
import dorkbox.network.connectionType.ConnectionRule
|
||||
import dorkbox.network.exceptions.ServerException
|
||||
import dorkbox.network.handshake.ServerHandshake
|
||||
|
@ -30,6 +28,7 @@ import dorkbox.network.handshake.ServerHandshakePollers
|
|||
import dorkbox.network.ipFilter.IpFilterRule
|
||||
import dorkbox.network.rmi.RmiSupportServer
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.net.InetAddress
|
||||
import java.util.concurrent.*
|
||||
|
||||
/**
|
||||
|
@ -103,16 +102,19 @@ open class Server<CONNECTION : Connection>(config: ServerConfiguration = ServerC
|
|||
@Volatile
|
||||
internal lateinit var handshake: ServerHandshake<CONNECTION>
|
||||
|
||||
/**
|
||||
* Different connections (to the same client) can be "buffered", meaning that if they "go down" because of a network glitch -- the data
|
||||
* being sent is not lost (it is buffered) and then re-sent once the new connection is established. References to the old connection
|
||||
* will also redirect to the new connection.
|
||||
*/
|
||||
internal val bufferedManager: BufferManager<CONNECTION>
|
||||
|
||||
private val string0: String by lazy {
|
||||
"EndPoint [Server: ${storage.publicKey.toHexString()}]"
|
||||
}
|
||||
|
||||
init {
|
||||
if (this is SessionServer) {
|
||||
// only set this if we need to
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sessionManager = SessionManagerFull(config, listenerManager as ListenerManager<SessionConnection>, aeronDriver, config.sessionTimeoutSeconds)
|
||||
}
|
||||
bufferedManager = BufferManager(config, listenerManager, aeronDriver, config.bufferedConnectionTimeoutSeconds)
|
||||
}
|
||||
|
||||
final override fun newException(message: String, cause: Throwable?): Throwable {
|
||||
|
@ -392,6 +394,7 @@ open class Server<CONNECTION : Connection>(config: ServerConfiguration = ServerC
|
|||
* @param closeEverything if true, all parts of the server will be closed (listeners, driver, event polling, etc)
|
||||
*/
|
||||
fun close(closeEverything: Boolean = true) {
|
||||
bufferedManager.close()
|
||||
close(closeEverything = closeEverything, sendDisconnectMessage = true, releaseWaitingThreads = true)
|
||||
}
|
||||
|
||||
|
|
|
@ -907,20 +907,10 @@ class AeronDriver(config: Configuration, val logger: Logger, val endPoint: EndPo
|
|||
listenerManager.notifyError(exception)
|
||||
return false
|
||||
}
|
||||
else if (connection.endPoint.sessionManager.enabled()) {
|
||||
// if we are a SESSION, then the message will be placed into a queue to be re-sent once the connection comes back
|
||||
// no extra actions required by us. Returning a "false" here makes sure that the session manager picks-up this message to
|
||||
// re-broadcast (eventually) on the updated connection
|
||||
return false
|
||||
} else {
|
||||
logger.info("[${publication.sessionId()}] Connection disconnected while sending data, closing connection.")
|
||||
internal.mustRestartDriverOnError = true
|
||||
|
||||
// publication was actually closed or the server was closed, so no bother throwing an error
|
||||
connection.closeImmediately(
|
||||
sendDisconnectMessage = false, closeEverything = false
|
||||
)
|
||||
|
||||
else {
|
||||
// by default, we BUFFER data on a connection -- so the message will be placed into a queue to be re-sent once the connection comes back
|
||||
// no extra actions required by us.
|
||||
// Returning a "false" here makes sure that the session manager picks-up this message to e-broadcast (eventually) on the updated connection
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -947,15 +937,6 @@ class AeronDriver(config: Configuration, val logger: Logger, val endPoint: EndPo
|
|||
// this can happen when we use RMI to close a connection. RMI will (in most cases) ALWAYS send a response when it's
|
||||
// done executing. If the connection is *closed* first (because an RMI method closed it), then we will not be able to
|
||||
// send the message.
|
||||
|
||||
if (!endPoint.shutdownInProgress.value && !endPoint.sessionManager.enabled()) {
|
||||
// we already know the connection is closed. we closed it (so it doesn't make sense to emit an error about this)
|
||||
// additionally, if we are managing pending messages, don't show an error (since the message will be queued to send again)
|
||||
val exception = endPoint.newException(
|
||||
"[${publication.sessionId()}] Unable to send message. (Connection is closed, aborted attempt! ${errorCodeName(result)})"
|
||||
).cleanStackTrace(5)
|
||||
listenerManager.notifyError(exception)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -1053,15 +1034,6 @@ class AeronDriver(config: Configuration, val logger: Logger, val endPoint: EndPo
|
|||
// this can happen when we use RMI to close a connection. RMI will (in most cases) ALWAYS send a response when it's
|
||||
// done executing. If the connection is *closed* first (because an RMI method closed it), then we will not be able to
|
||||
// send the message.
|
||||
|
||||
if (!endPoint.shutdownInProgress.value && !endPoint.sessionManager.enabled()) {
|
||||
// we already know the connection is closed. we closed it (so it doesn't make sense to emit an error about this)
|
||||
// additionally, if we are managing pending messages, don't show an error (since the message will be queued to send again)
|
||||
val exception = endPoint.newException(
|
||||
"[${publication.sessionId()}] Unable to send message. (Connection is closed, aborted attempt! ${errorCodeName(result)})"
|
||||
)
|
||||
listenerManager.notifyError(exception)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -148,7 +148,6 @@ internal class AeronDriverInternal(endPoint: EndPoint<*>?, config: Configuration
|
|||
|
||||
// this is bad! We must close this connection. THIS WILL BE CALLED AS FAST AS THE CPU CAN RUN (because of how aeron works).
|
||||
if (!mustRestartDriverOnError) {
|
||||
|
||||
var restartNetwork = false
|
||||
|
||||
// if the network interface is removed (for example, a VPN connection).
|
||||
|
|
|
@ -19,7 +19,7 @@ import dorkbox.network.Client
|
|||
import dorkbox.network.Server
|
||||
import dorkbox.network.aeron.AeronDriver.Companion.sessionIdAllocator
|
||||
import dorkbox.network.aeron.AeronDriver.Companion.streamIdAllocator
|
||||
import dorkbox.network.connection.session.SessionConnection
|
||||
import dorkbox.network.connection.buffer.BufferedSession
|
||||
import dorkbox.network.ping.Ping
|
||||
import dorkbox.network.rmi.RmiSupportConnection
|
||||
import io.aeron.Image
|
||||
|
@ -32,10 +32,15 @@ import org.agrona.DirectBuffer
|
|||
import javax.crypto.SecretKey
|
||||
|
||||
/**
|
||||
* This connection is established once the registration information is validated, and the various connect/filter checks have passed
|
||||
* This connection is established once the registration information is validated, and the various connect/filter checks have passed.
|
||||
*
|
||||
* Connections are also BUFFERED, meaning that if the connection between a client-server goes down because of a network glitch, then the
|
||||
* data being sent is not lost (it is buffered) and then re-sent once a new connection has the same UUID within the timout period.
|
||||
*
|
||||
* References to the old connection will also redirect to the new connection.
|
||||
*/
|
||||
open class Connection(connectionParameters: ConnectionParams<*>) {
|
||||
private var messageHandler: FragmentHandler
|
||||
private val messageHandler: FragmentHandler
|
||||
|
||||
/**
|
||||
* The specific connection details for this connection!
|
||||
|
@ -106,6 +111,11 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
|
|||
*/
|
||||
val isNetwork = !isIpc
|
||||
|
||||
/**
|
||||
* used when the connection is buffered
|
||||
*/
|
||||
private val bufferedSession: BufferedSession
|
||||
|
||||
/**
|
||||
* The largest size a SINGLE message via AERON can be. Because the maximum size we can send in a "single fragment" is the
|
||||
* publication.maxPayloadLength() function (which is the MTU length less header). We could depend on Aeron for fragment reassembly,
|
||||
|
@ -164,11 +174,17 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
|
|||
endPoint.dataReceive(buffer, offset, length, header, this@Connection)
|
||||
}
|
||||
|
||||
bufferedSession = when (endPoint) {
|
||||
is Server -> endPoint.bufferedManager.onConnect(this)
|
||||
is Client -> endPoint.bufferedManager!!.onConnect(this)
|
||||
else -> throw RuntimeException("Unable to determine type, aborting!")
|
||||
}
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
rmi = endPoint.rmiConnectionSupport.getNewRmiSupport(this)
|
||||
|
||||
// For toString() and logging
|
||||
toString0 = info.getLogInfo(logger)
|
||||
toString0 = info.getLogInfo(logger.isDebugEnabled)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -202,7 +218,7 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
|
|||
*
|
||||
* @return true if the message was successfully sent, false otherwise. Exceptions are caught and NOT rethrown!
|
||||
*/
|
||||
internal open fun send(message: Any, abortEarly: Boolean): Boolean {
|
||||
internal fun send(message: Any, abortEarly: Boolean): Boolean {
|
||||
if (logger.isTraceEnabled) {
|
||||
// The handshake sessionId IS NOT globally unique
|
||||
// don't automatically create the lambda when trace is disabled! Because this uses 'outside' scoped info, it's a new lambda each time!
|
||||
|
@ -210,7 +226,28 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
|
|||
logger.trace("[$toString0] send: ${message.javaClass.simpleName} : $message")
|
||||
}
|
||||
}
|
||||
return endPoint.write(message, publication, sendIdleStrategy, this@Connection, maxMessageSize, abortEarly)
|
||||
|
||||
val success = endPoint.write(message, publication, sendIdleStrategy, this@Connection, maxMessageSize, abortEarly)
|
||||
|
||||
return if (!success && message !is DisconnectMessage) {
|
||||
// queue up the messages, because we couldn't write them for whatever reason!
|
||||
// NEVER QUEUE THE DISCONNECT MESSAGE!
|
||||
bufferedSession.queueMessage(this@Connection, message, abortEarly)
|
||||
} else {
|
||||
success
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendNoBuffer(message: Any): Boolean {
|
||||
if (logger.isTraceEnabled) {
|
||||
// The handshake sessionId IS NOT globally unique
|
||||
// don't automatically create the lambda when trace is disabled! Because this uses 'outside' scoped info, it's a new lambda each time!
|
||||
if (logger.isTraceEnabled) {
|
||||
logger.trace("[$toString0] send: ${message.javaClass.simpleName} : $message")
|
||||
}
|
||||
}
|
||||
|
||||
return endPoint.write(message, publication, sendIdleStrategy, this@Connection, maxMessageSize, false)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -282,6 +319,17 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
|
|||
return listenerManager.value?.notifyOnMessage(this, message) ?: false
|
||||
}
|
||||
|
||||
internal fun sendBufferedMessages() {
|
||||
// now send all buffered/pending messages
|
||||
if (logger.isDebugEnabled) {
|
||||
logger.debug("Sending pending messages: ${bufferedSession.pendingMessagesQueue.size}")
|
||||
}
|
||||
|
||||
bufferedSession.pendingMessagesQueue.forEach {
|
||||
sendNoBuffer(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this connection has had close() called
|
||||
*/
|
||||
|
@ -380,8 +428,20 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
|
|||
}
|
||||
|
||||
// make sure to save off the RMI objects for session management
|
||||
if (!closeEverything && endPoint.sessionManager.enabled()) {
|
||||
endPoint.sessionManager.onDisconnect(this as SessionConnection)
|
||||
if (!closeEverything) {
|
||||
when (endPoint) {
|
||||
is Server -> endPoint.bufferedManager.onDisconnect(this)
|
||||
is Client -> endPoint.bufferedManager!!.onDisconnect(this)
|
||||
else -> throw RuntimeException("Unable to determine type, aborting!")
|
||||
}
|
||||
}
|
||||
|
||||
if (!closeEverything) {
|
||||
when (endPoint) {
|
||||
is Server -> endPoint.bufferedManager.onDisconnect(this)
|
||||
is Client -> endPoint.bufferedManager!!.onDisconnect(this)
|
||||
else -> throw RuntimeException("Unable to determine type, aborting!")
|
||||
}
|
||||
}
|
||||
|
||||
// on close, we want to make sure this file is DELETED!
|
||||
|
|
|
@ -24,9 +24,6 @@ import dorkbox.network.ServerConfiguration
|
|||
import dorkbox.network.aeron.AeronDriver
|
||||
import dorkbox.network.aeron.BacklogStat
|
||||
import dorkbox.network.aeron.EventPoller
|
||||
import dorkbox.network.connection.session.SessionConnection
|
||||
import dorkbox.network.connection.session.SessionManager
|
||||
import dorkbox.network.connection.session.SessionManagerNoOp
|
||||
import dorkbox.network.connection.streaming.StreamingControl
|
||||
import dorkbox.network.connection.streaming.StreamingData
|
||||
import dorkbox.network.connection.streaming.StreamingManager
|
||||
|
@ -182,14 +179,6 @@ abstract class EndPoint<CONNECTION : Connection> private constructor(val type: C
|
|||
|
||||
private val streamingManager = StreamingManager<CONNECTION>(logger, config)
|
||||
|
||||
/**
|
||||
* By default, this is a NO-OP version! We do not want if/else checks for every message!
|
||||
* this can change of the lifespan of the CLIENT, depending on which/what server a single client connects to.
|
||||
*/
|
||||
@Volatile
|
||||
internal var sessionManager: SessionManager<SessionConnection> = SessionManagerNoOp()
|
||||
|
||||
|
||||
/**
|
||||
* The primary machine port that the server will listen for connections on
|
||||
*/
|
||||
|
@ -1021,16 +1010,12 @@ abstract class EndPoint<CONNECTION : Connection> private constructor(val type: C
|
|||
// Connections MUST be closed first, because we want to make sure that no RMI messages can be received
|
||||
// when we close the RMI support objects (in which case, weird - but harmless - errors show up)
|
||||
// IF CLOSED VIA RMI: this will wait for RMI timeouts if there are RMI in-progress.
|
||||
if (sessionManager.enabled()) {
|
||||
if (closeEverything) {
|
||||
// only close out RMI if we are using session management AND we are closing everything!
|
||||
responseManager.close(logger)
|
||||
}
|
||||
} else {
|
||||
// no session management, so always clear this out.
|
||||
// only close out RMI if we are closing everything!
|
||||
responseManager.close(logger)
|
||||
}
|
||||
|
||||
|
||||
// don't do these things if we are "closed" from a client connection disconnect
|
||||
// if there are any events going on, we want to schedule them to run AFTER all other events for this endpoint are done
|
||||
if (closeEverything) {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dorkbox.network.connection.session
|
||||
package dorkbox.network.connection.buffer
|
||||
|
||||
import dorkbox.bytes.ByteArrayWrapper
|
||||
import dorkbox.collections.LockFreeHashMap
|
||||
|
@ -30,24 +30,23 @@ import net.jodah.expiringmap.ExpiringMap
|
|||
import org.slf4j.LoggerFactory
|
||||
import java.util.concurrent.*
|
||||
|
||||
internal open class SessionManagerFull<CONNECTION: SessionConnection>(
|
||||
internal open class BufferManager<CONNECTION: Connection>(
|
||||
config: Configuration,
|
||||
listenerManager: ListenerManager<CONNECTION>,
|
||||
val aeronDriver: AeronDriver,
|
||||
aeronDriver: AeronDriver,
|
||||
sessionTimeout: Long
|
||||
): SessionManager<CONNECTION> {
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(SessionManagerFull::class.java.simpleName)
|
||||
private val logger = LoggerFactory.getLogger(BufferManager::class.java.simpleName)
|
||||
}
|
||||
|
||||
|
||||
private val sessions = LockFreeHashMap<ByteArrayWrapper, Session<CONNECTION>>()
|
||||
|
||||
|
||||
private val expiringSessions: ExpiringMap<ByteArrayWrapper, Session<CONNECTION>>
|
||||
private val sessions = LockFreeHashMap<ByteArrayWrapper, BufferedSession>()
|
||||
private val expiringSessions: ExpiringMap<ByteArrayWrapper, BufferedSession>
|
||||
|
||||
init {
|
||||
require(sessionTimeout >= 60) { "The buffered connection timeout 'bufferedConnectionTimeoutSeconds' must be greater than 60 seconds!" }
|
||||
|
||||
// ignore 0
|
||||
val check = TimeUnit.SECONDS.toNanos(sessionTimeout)
|
||||
val lingerNs = aeronDriver.lingerNs()
|
||||
|
@ -62,7 +61,7 @@ internal open class SessionManagerFull<CONNECTION: SessionConnection>(
|
|||
expiringSessions = ExpiringMap.builder()
|
||||
.expiration(sessionTimeout, timeUnit)
|
||||
.expirationPolicy(ExpirationPolicy.CREATED)
|
||||
.expirationListener<ByteArrayWrapper, Session<CONNECTION>> { publicKeyWrapped, sessionConnection ->
|
||||
.expirationListener<ByteArrayWrapper, BufferedSession> { publicKeyWrapped, sessionConnection ->
|
||||
// this blocks until it fully runs (which is ok. this is fast)
|
||||
logger.debug("Connection session expired for: ${publicKeyWrapped.bytes.toHexString()}")
|
||||
|
||||
|
@ -72,82 +71,60 @@ internal open class SessionManagerFull<CONNECTION: SessionConnection>(
|
|||
.build()
|
||||
}
|
||||
|
||||
override fun enabled(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* this must be called when a new connection is created
|
||||
*
|
||||
* @return true if this is a new session, false if it is an existing session
|
||||
*/
|
||||
override fun onNewConnection(connection: Connection) {
|
||||
require(connection is SessionConnection) { "The new connection does not inherit a SessionConnection, unable to continue. " }
|
||||
|
||||
fun onConnect(connection: Connection): BufferedSession {
|
||||
val publicKeyWrapped = ByteArrayWrapper.wrap(connection.uuid)
|
||||
|
||||
synchronized(sessions) {
|
||||
return synchronized(sessions) {
|
||||
// always check if we are expiring first...
|
||||
val expiring = expiringSessions.remove(publicKeyWrapped)
|
||||
if (expiring != null) {
|
||||
// we must always set this session value!!
|
||||
connection.session = expiring
|
||||
expiring.connection = connection
|
||||
expiring
|
||||
} else {
|
||||
val existing = sessions[publicKeyWrapped]
|
||||
if (existing != null) {
|
||||
// we must always set this session value!!
|
||||
connection.session = existing
|
||||
existing.connection = connection
|
||||
existing
|
||||
} else {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val newSession = (connection.endPoint as SessionEndpoint<CONNECTION>).newSession(connection as CONNECTION)
|
||||
val newSession = BufferedSession(connection)
|
||||
sessions[publicKeyWrapped] = newSession
|
||||
|
||||
// we must always set this when the connection is created, and it must be inside the sync block!
|
||||
connection.session = newSession
|
||||
|
||||
sessions[publicKeyWrapped] = newSession
|
||||
newSession
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* this must be called when a new connection is created AND when the internal `reconnect` occurs (as a result of a network error)
|
||||
*
|
||||
* @return true if this is a new session, false if it is an existing session
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun onInit(connection: Connection): Boolean {
|
||||
// we know this will always be the case, because if this specific method can be called, then it will be a sessionConnection
|
||||
connection as SessionConnection
|
||||
|
||||
val session: Session<CONNECTION> = connection.session as Session<CONNECTION>
|
||||
session.restore(connection as CONNECTION)
|
||||
|
||||
// the FIRST time this method is called, it will be true. EVERY SUBSEQUENT TIME, it will be false
|
||||
return session.isNewSession
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Always called when a connection is disconnected from the network
|
||||
*/
|
||||
override fun onDisconnect(connection: CONNECTION) {
|
||||
fun onDisconnect(connection: Connection) {
|
||||
try {
|
||||
val publicKeyWrapped = ByteArrayWrapper.wrap(connection.uuid)
|
||||
|
||||
val session = synchronized(sessions) {
|
||||
synchronized(sessions) {
|
||||
val sess = sessions.remove(publicKeyWrapped)
|
||||
// we want to expire this session after XYZ time
|
||||
expiringSessions[publicKeyWrapped] = sess
|
||||
sess
|
||||
}
|
||||
|
||||
|
||||
session!!.save(connection)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
logger.error("Unable to run save data for the session!", e)
|
||||
logger.error("Unable to run session expire logic!", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun close() {
|
||||
synchronized(sessions) {
|
||||
sessions.clear()
|
||||
expiringSessions.clear()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2023 dorkbox, llc
|
||||
*
|
||||
* 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 dorkbox.network.connection.buffer
|
||||
|
||||
import dorkbox.network.connection.Connection
|
||||
import java.util.concurrent.*
|
||||
|
||||
open class BufferedSession(@Volatile var connection: Connection) {
|
||||
/**
|
||||
* Only used when configured. Will re-send all missing messages to a connection when a connection re-connects.
|
||||
*/
|
||||
val pendingMessagesQueue: LinkedTransferQueue<Any> = LinkedTransferQueue()
|
||||
|
||||
fun queueMessage(connection: Connection, message: Any, abortEarly: Boolean): Boolean {
|
||||
if (this.connection != connection) {
|
||||
connection.logger.trace("[{}] message received on old connection, resending", connection)
|
||||
|
||||
// we received a message on an OLD connection (which is no longer connected ---- BUT we have a NEW connection that is connected)
|
||||
// this can happen on RMI object that are old
|
||||
val success = this.connection.send(message, abortEarly)
|
||||
if (success) {
|
||||
connection.logger.trace("[{}] successfully resent message", connection)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (!abortEarly) {
|
||||
// this was a "normal" send (instead of the disconnect message).
|
||||
pendingMessagesQueue.put(message)
|
||||
connection.logger.trace("[{}] queueing message", connection)
|
||||
}
|
||||
else if (connection.endPoint.aeronDriver.internal.mustRestartDriverOnError) {
|
||||
// the only way we get errors, is if the connection is bad OR if we are sending so fast that the connection cannot keep up.
|
||||
|
||||
// don't restart/reconnect -- there was an internal network error
|
||||
pendingMessagesQueue.put(message)
|
||||
connection.logger.trace("[{}] queueing message", connection)
|
||||
}
|
||||
else if (!connection.isClosedWithTimeout()) {
|
||||
// there was an issue - the connection should automatically reconnect
|
||||
pendingMessagesQueue.put(message)
|
||||
connection.logger.trace("[{}] queueing message", connection)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -14,4 +14,4 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dorkbox.network.connection.session;
|
||||
package dorkbox.network.connection.buffer;
|
|
@ -1,141 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 dorkbox, llc
|
||||
*
|
||||
* 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 dorkbox.network.connection.session
|
||||
|
||||
import dorkbox.network.rmi.RemoteObject
|
||||
import kotlinx.atomicfu.locks.ReentrantLock
|
||||
import kotlinx.atomicfu.locks.withLock
|
||||
import java.util.concurrent.*
|
||||
|
||||
open class Session<CONNECTION: SessionConnection>(@Volatile var connection: CONNECTION) {
|
||||
|
||||
|
||||
// the RMI objects are saved when the connection is removed, and restored BEFORE the connection is initialized, so there are no concerns
|
||||
// regarding the collision of RMI IDs and objects
|
||||
private val lock = ReentrantLock()
|
||||
private var oldProxyObjects: List<RemoteObject<*>>? = null
|
||||
private var oldProxyCallbacks: List<Pair<Int, Any.(Int) -> Unit>>? = null
|
||||
private var oldImplObjects: List<Pair<Int, Any>>? = null
|
||||
|
||||
/**
|
||||
* Only used when configured. Will re-send all missing messages to a connection when a connection re-connects.
|
||||
*/
|
||||
val pendingMessagesQueue: LinkedTransferQueue<Any> = LinkedTransferQueue()
|
||||
|
||||
|
||||
/**
|
||||
* the FIRST time this method is called, it will be true. EVERY SUBSEQUENT TIME, it will be false
|
||||
*/
|
||||
@Volatile
|
||||
internal var isNewSession = true
|
||||
private set
|
||||
get() {
|
||||
val orig = field
|
||||
if (orig) {
|
||||
field = false
|
||||
newSession = false
|
||||
}
|
||||
return orig
|
||||
}
|
||||
|
||||
|
||||
@Volatile
|
||||
private var newSession = true
|
||||
|
||||
|
||||
fun restore(connection: CONNECTION) {
|
||||
this.connection = connection
|
||||
|
||||
if (newSession) {
|
||||
connection.logger.debug("[{}] Session connection established", connection)
|
||||
return
|
||||
}
|
||||
|
||||
connection.logger.debug("[{}] Restoring session connection", connection)
|
||||
|
||||
lock.withLock {
|
||||
val oldProxyObjects = oldProxyObjects
|
||||
val oldProxyCallbacks = oldProxyCallbacks
|
||||
val oldImplObjects = oldImplObjects
|
||||
|
||||
// this is called, even on a brand-new session, so we must have extra checks in place.
|
||||
val rmi = connection.rmi
|
||||
if (oldProxyObjects != null) {
|
||||
rmi.recreateProxyObjects(oldProxyObjects)
|
||||
this.oldProxyObjects = null
|
||||
}
|
||||
if (oldProxyCallbacks != null) {
|
||||
rmi.restoreCallbacks(oldProxyCallbacks)
|
||||
this.oldProxyCallbacks = null
|
||||
}
|
||||
if (oldImplObjects != null) {
|
||||
rmi.restoreImplObjects(oldImplObjects)
|
||||
this.oldImplObjects = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun save(connection: CONNECTION) {
|
||||
connection.logger.debug("[{}] Saving session connection", connection)
|
||||
|
||||
val rmi = connection.rmi
|
||||
val allProxyObjects = rmi.getAllProxyObjects()
|
||||
val allProxyCallbacks = rmi.getAllCallbacks()
|
||||
val allImplObjects = rmi.getAllImplObjects()
|
||||
|
||||
// we want to save all the connection RMI objects, so they can be recreated on connect
|
||||
lock.withLock {
|
||||
oldProxyObjects = allProxyObjects
|
||||
oldProxyCallbacks = allProxyCallbacks
|
||||
oldImplObjects = allImplObjects
|
||||
}
|
||||
}
|
||||
|
||||
fun queueMessage(connection: SessionConnection, message: Any, abortEarly: Boolean): Boolean {
|
||||
if (this.connection != connection) {
|
||||
connection.logger.trace("[{}] message received on old connection, resending", connection)
|
||||
|
||||
// we received a message on an OLD connection (which is no longer connected ---- BUT we have a NEW connection that is connected)
|
||||
// this can happen on RMI object that are old
|
||||
val success = this.connection.send(message, abortEarly)
|
||||
if (success) {
|
||||
connection.logger.trace("[{}] successfully resent message", connection)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (!abortEarly) {
|
||||
// this was a "normal" send (instead of the disconnect message).
|
||||
pendingMessagesQueue.put(message)
|
||||
connection.logger.trace("[{}] queueing message", connection)
|
||||
}
|
||||
else if (connection.endPoint.aeronDriver.internal.mustRestartDriverOnError) {
|
||||
// the only way we get errors, is if the connection is bad OR if we are sending so fast that the connection cannot keep up.
|
||||
|
||||
// don't restart/reconnect -- there was an internal network error
|
||||
pendingMessagesQueue.put(message)
|
||||
connection.logger.trace("[{}] queueing message", connection)
|
||||
}
|
||||
else if (!connection.isClosedWithTimeout()) {
|
||||
// there was an issue - the connection should automatically reconnect
|
||||
pendingMessagesQueue.put(message)
|
||||
connection.logger.trace("[{}] queueing message", connection)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 dorkbox, llc
|
||||
*
|
||||
* 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 dorkbox.network.connection.session
|
||||
|
||||
import dorkbox.network.Client
|
||||
import dorkbox.network.ClientConfiguration
|
||||
import dorkbox.network.connection.ConnectionParams
|
||||
|
||||
open class SessionClient<CONNECTION: SessionConnection>(config: ClientConfiguration = ClientConfiguration(), loggerName: String = Client::class.java.simpleName):
|
||||
Client<CONNECTION>(config, loggerName), SessionEndpoint<CONNECTION> {
|
||||
|
||||
override fun newConnection(connectionParameters: ConnectionParams<CONNECTION>): CONNECTION {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return SessionConnection(connectionParameters) as CONNECTION
|
||||
}
|
||||
|
||||
override fun newSession(connection: CONNECTION): Session<CONNECTION> {
|
||||
return Session(connection)
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 dorkbox, llc
|
||||
*
|
||||
* 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 dorkbox.network.connection.session
|
||||
|
||||
import dorkbox.network.connection.Connection
|
||||
import dorkbox.network.connection.ConnectionParams
|
||||
|
||||
open class SessionConnection(connectionParameters: ConnectionParams<*>): Connection(connectionParameters) {
|
||||
@Volatile
|
||||
lateinit var session: Session<*>
|
||||
|
||||
override fun send(message: Any, abortEarly: Boolean): Boolean {
|
||||
val success = super.send(message, abortEarly)
|
||||
if (!success) {
|
||||
return session.queueMessage(this, message, abortEarly)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
internal fun sendPendingMessages() {
|
||||
// now send all pending messages
|
||||
if (logger.isDebugEnabled) {
|
||||
logger.debug("Sending pending messages: ${session.pendingMessagesQueue.size}")
|
||||
}
|
||||
session.pendingMessagesQueue.forEach {
|
||||
super.send(it, false)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 dorkbox, llc
|
||||
*
|
||||
* 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 dorkbox.network.connection.session
|
||||
|
||||
import dorkbox.network.connection.ConnectionParams
|
||||
|
||||
interface SessionEndpoint<CONNECTION: SessionConnection> {
|
||||
fun newConnection(connectionParameters: ConnectionParams<CONNECTION>): CONNECTION
|
||||
fun newSession(connection: CONNECTION): Session<CONNECTION>
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 dorkbox, llc
|
||||
*
|
||||
* 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 dorkbox.network.connection.session
|
||||
|
||||
import dorkbox.network.connection.Connection
|
||||
|
||||
interface SessionManager<CONNECTION : SessionConnection> {
|
||||
|
||||
fun enabled(): Boolean
|
||||
fun onNewConnection(connection: Connection)
|
||||
fun onInit(connection: Connection): Boolean
|
||||
fun onDisconnect(connection: CONNECTION)
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 dorkbox, llc
|
||||
*
|
||||
* 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 dorkbox.network.connection.session
|
||||
|
||||
import dorkbox.network.connection.Connection
|
||||
|
||||
class SessionManagerNoOp<CONNECTION : SessionConnection>: SessionManager<CONNECTION> {
|
||||
override fun enabled(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onNewConnection(connection: Connection) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
override fun onInit(connection: Connection): Boolean {
|
||||
// do nothing
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDisconnect(connection: CONNECTION) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 dorkbox, llc
|
||||
*
|
||||
* 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 dorkbox.network.connection.session
|
||||
|
||||
import dorkbox.network.Server
|
||||
import dorkbox.network.ServerConfiguration
|
||||
import dorkbox.network.connection.ConnectionParams
|
||||
|
||||
open class SessionServer<CONNECTION: SessionConnection>(config: ServerConfiguration = ServerConfiguration(), loggerName: String = Server::class.java.simpleName):
|
||||
Server<CONNECTION>(config, loggerName), SessionEndpoint<CONNECTION> {
|
||||
override fun newConnection(connectionParameters: ConnectionParams<CONNECTION>): CONNECTION {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return SessionConnection(connectionParameters) as CONNECTION
|
||||
}
|
||||
|
||||
override fun newSession(connection: CONNECTION): Session<CONNECTION> {
|
||||
return Session(connection)
|
||||
}
|
||||
}
|
|
@ -274,10 +274,8 @@ internal class RmiClient(val isGlobal: Boolean,
|
|||
|
||||
val success = connection.send(invokeMethod)
|
||||
if (!success) {
|
||||
if (!connection.endPoint.sessionManager.enabled()) {
|
||||
throw RmiException("Unable to send async message, an error occurred during the send process")
|
||||
}
|
||||
}
|
||||
|
||||
// if we are async then we return immediately (but must return the correct type!)
|
||||
// If you want the response value, disable async!
|
||||
|
@ -310,11 +308,9 @@ internal class RmiClient(val isGlobal: Boolean,
|
|||
|
||||
val success = connection.send(invokeMethod)
|
||||
if (!success) {
|
||||
if (!connection.endPoint.sessionManager.enabled()) {
|
||||
responseManager.abort(responseWaiter, logger)
|
||||
throw RmiException("Unable to send message, an error occurred during the send process")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// if a 'suspend' function is called, then our last argument is a 'Continuation' object
|
||||
|
|
|
@ -3,7 +3,7 @@ module dorkbox.network {
|
|||
exports dorkbox.network.aeron;
|
||||
exports dorkbox.network.connection;
|
||||
exports dorkbox.network.connection.streaming;
|
||||
exports dorkbox.network.connection.session;
|
||||
exports dorkbox.network.connection.buffer;
|
||||
exports dorkbox.network.connectionType;
|
||||
exports dorkbox.network.exceptions;
|
||||
exports dorkbox.network.handshake;
|
||||
|
|
|
@ -1,366 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 dorkbox, llc
|
||||
*
|
||||
* 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 dorkboxTest.network
|
||||
|
||||
import dorkbox.network.Client
|
||||
import dorkbox.network.Server
|
||||
import dorkbox.network.connection.Connection
|
||||
import dorkbox.network.connection.session.SessionClient
|
||||
import dorkbox.network.connection.session.SessionConnection
|
||||
import dorkbox.network.connection.session.SessionServer
|
||||
import dorkbox.network.rmi.RemoteObject
|
||||
import dorkboxTest.network.rmi.cows.TestCow
|
||||
import dorkboxTest.network.rmi.cows.TestCowImpl
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.*
|
||||
|
||||
|
||||
class MessageToContinue
|
||||
|
||||
class SessionReconnectTest: BaseTest() {
|
||||
@Test
|
||||
fun rmiReconnectSessions() {
|
||||
val server = run {
|
||||
val configuration = serverConfig()
|
||||
|
||||
configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java)
|
||||
configuration.serialization.register(MessageToContinue::class.java)
|
||||
configuration.serialization.register(UnsupportedOperationException::class.java)
|
||||
|
||||
// for Client -> Server RMI
|
||||
configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java)
|
||||
|
||||
val server = SessionServer<SessionConnection>(configuration)
|
||||
|
||||
addEndPoint(server)
|
||||
|
||||
server.onMessage<MessageToContinue> { m ->
|
||||
send(MessageToContinue())
|
||||
}
|
||||
|
||||
server
|
||||
}
|
||||
|
||||
val client = run {
|
||||
val configuration = clientConfig()
|
||||
|
||||
|
||||
val client = SessionClient<SessionConnection>(configuration)
|
||||
|
||||
addEndPoint(client)
|
||||
|
||||
var rmiId = 0
|
||||
|
||||
client.onConnect {
|
||||
logger.error("Connecting")
|
||||
|
||||
rmi.create<TestCow>(23) {
|
||||
rmiId = it
|
||||
moo("Client -> Server")
|
||||
|
||||
this@onConnect.send(MessageToContinue())
|
||||
}
|
||||
}
|
||||
|
||||
client.onMessage<MessageToContinue> { _ ->
|
||||
val get = rmi.get<TestCow>(rmiId)
|
||||
RemoteObject.cast(get).responseTimeout = 50_000
|
||||
|
||||
get.moo("NOT CRASHED!")
|
||||
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
GlobalScope.launch {
|
||||
latch.await()
|
||||
get.moo("DELAYED AND NOT CRASHED!")
|
||||
stopEndPoints()
|
||||
}
|
||||
|
||||
client.close(false)
|
||||
client.waitForClose()
|
||||
client.connectIpc() // reconnect
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
server.bindIpc()
|
||||
client.connectIpc()
|
||||
|
||||
waitForThreads()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rmiReconnectReplayMessages() {
|
||||
val server = run {
|
||||
val configuration = serverConfig()
|
||||
|
||||
configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java)
|
||||
configuration.serialization.register(MessageToContinue::class.java)
|
||||
configuration.serialization.register(UnsupportedOperationException::class.java)
|
||||
|
||||
// for Client -> Server RMI
|
||||
configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java)
|
||||
|
||||
val server = SessionServer<SessionConnection>(configuration)
|
||||
|
||||
addEndPoint(server)
|
||||
|
||||
server.onMessage<MessageToContinue> { m ->
|
||||
send(MessageToContinue())
|
||||
}
|
||||
|
||||
server
|
||||
}
|
||||
|
||||
val client = run {
|
||||
val configuration = clientConfig()
|
||||
|
||||
|
||||
val client = SessionClient<SessionConnection>(configuration)
|
||||
|
||||
addEndPoint(client)
|
||||
|
||||
var rmiId = 0
|
||||
|
||||
client.onConnect {
|
||||
logger.error("Connecting")
|
||||
|
||||
rmi.create<TestCow>(23) {
|
||||
rmiId = it
|
||||
moo("Client -> Server")
|
||||
|
||||
this@onConnect.send(MessageToContinue())
|
||||
}
|
||||
}
|
||||
|
||||
client.onMessage<MessageToContinue> { _ ->
|
||||
logger.error("Starting reconnect bits")
|
||||
val obj = rmi.get<TestCow>(rmiId)
|
||||
|
||||
// closing client
|
||||
client.close(false)
|
||||
client.waitForClose()
|
||||
|
||||
logger.error("Getting object again. it should be the cached version")
|
||||
val cast = RemoteObject.cast(obj)
|
||||
cast.responseTimeout = 50_000
|
||||
|
||||
GlobalScope.launch {
|
||||
delay(1000) // must be shorter than the RMI timeout for our tests
|
||||
|
||||
client.connectIpc()
|
||||
|
||||
delay(1000) // give some time for the messages to go! If we close instantly, the return RMI message will fail
|
||||
stopEndPoints()
|
||||
}
|
||||
|
||||
// this is SYNC, so it waits for a response!
|
||||
try {
|
||||
cast.async = false
|
||||
obj.moo("DELAYED AND NOT CRASHED!") // will wait for a response
|
||||
}
|
||||
catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
e.cause?.printStackTrace()
|
||||
Assert.fail(".moo() should not throw an exception, because it will succeed before the timeout")
|
||||
}
|
||||
}
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
server.bindIpc()
|
||||
client.connectIpc()
|
||||
|
||||
waitForThreads()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rmiReconnectSessionsFail() {
|
||||
var rmiId = 0
|
||||
|
||||
val server = run {
|
||||
val configuration = serverConfig()
|
||||
|
||||
configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java)
|
||||
configuration.serialization.register(MessageToContinue::class.java)
|
||||
configuration.serialization.register(UnsupportedOperationException::class.java)
|
||||
|
||||
// for Client -> Server RMI
|
||||
configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java)
|
||||
|
||||
val server = SessionServer<SessionConnection>(configuration)
|
||||
|
||||
addEndPoint(server)
|
||||
|
||||
server.onMessage<MessageToContinue> { m ->
|
||||
rmi.delete(rmiId)
|
||||
|
||||
// NOTE: if we send an RMI object, it will automatically be saved!
|
||||
send(MessageToContinue())
|
||||
}
|
||||
|
||||
server
|
||||
}
|
||||
|
||||
val client = run {
|
||||
val configuration = clientConfig()
|
||||
|
||||
val client = SessionClient<SessionConnection>(configuration)
|
||||
|
||||
addEndPoint(client)
|
||||
|
||||
var firstTime = true
|
||||
|
||||
client.onConnect {
|
||||
if (firstTime) {
|
||||
firstTime = false
|
||||
logger.error("Connecting")
|
||||
|
||||
rmi.create<TestCow>(23) {
|
||||
rmiId = it
|
||||
moo("Client -> Server")
|
||||
this@onConnect.send(MessageToContinue())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.onMessage<MessageToContinue> { _ ->
|
||||
val obj = rmi.get<TestCow>(rmiId)
|
||||
val o2 = RemoteObject.cast(obj)
|
||||
|
||||
GlobalScope.launch {
|
||||
delay(4000)
|
||||
|
||||
try {
|
||||
o2.sync {
|
||||
obj.moo("DELAYED AND NOT CRASHED!")
|
||||
Assert.fail(".moo() should throw an timeout exception, the backing RMI object doesn't exist!")
|
||||
}
|
||||
}
|
||||
catch (ignored: Exception) {
|
||||
}
|
||||
|
||||
stopEndPoints()
|
||||
}
|
||||
|
||||
client.close(false)
|
||||
client.connectIpc()
|
||||
}
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
server.bindIpc()
|
||||
client.connectIpc()
|
||||
|
||||
waitForThreads()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rmiReconnectSessionsFail2() {
|
||||
|
||||
var rmiId = 0
|
||||
|
||||
val server = run {
|
||||
val configuration = serverConfig()
|
||||
|
||||
configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java)
|
||||
configuration.serialization.register(MessageToContinue::class.java)
|
||||
configuration.serialization.register(UnsupportedOperationException::class.java)
|
||||
|
||||
// for Client -> Server RMI
|
||||
configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java)
|
||||
|
||||
val server = Server<Connection>(configuration)
|
||||
|
||||
addEndPoint(server)
|
||||
|
||||
server.onMessage<MessageToContinue> { m ->
|
||||
rmi.delete(rmiId)
|
||||
|
||||
// NOTE: if we send an RMI object, it will automatically be saved!
|
||||
send(MessageToContinue())
|
||||
}
|
||||
|
||||
server
|
||||
}
|
||||
|
||||
val client = run {
|
||||
val configuration = clientConfig()
|
||||
|
||||
|
||||
val client = Client<Connection>(configuration)
|
||||
|
||||
addEndPoint(client)
|
||||
|
||||
var firstTime = true
|
||||
|
||||
client.onConnect {
|
||||
if (firstTime) {
|
||||
firstTime = false
|
||||
logger.error("Connecting")
|
||||
|
||||
rmi.create<TestCow>(23) {
|
||||
rmiId = it
|
||||
moo("Client -> Server")
|
||||
|
||||
this@onConnect.send(MessageToContinue())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.onMessage<MessageToContinue> { _ ->
|
||||
val obj = rmi.get<TestCow>(rmiId)
|
||||
val o2 = RemoteObject.cast(obj)
|
||||
o2.responseTimeout = 50_000
|
||||
|
||||
GlobalScope.launch {
|
||||
delay(4000)
|
||||
|
||||
try {
|
||||
o2.sync {
|
||||
obj.moo("CRASHED!")
|
||||
Assert.fail(".moo() should throw an timeout exception, the backing RMI object doesn't exist!")
|
||||
}
|
||||
}
|
||||
catch (e: Exception) {
|
||||
logger.error("Successfully caught error!")
|
||||
}
|
||||
|
||||
stopEndPoints()
|
||||
}
|
||||
|
||||
client.close(false)
|
||||
client.connectIpc()
|
||||
}
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
server.bindIpc()
|
||||
client.connectIpc()
|
||||
|
||||
waitForThreads()
|
||||
}
|
||||
}
|
|
@ -1,274 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 dorkbox, llc
|
||||
*
|
||||
* 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 dorkboxTest.network.app
|
||||
|
||||
import ch.qos.logback.classic.Level
|
||||
import ch.qos.logback.classic.Logger
|
||||
import ch.qos.logback.classic.encoder.PatternLayoutEncoder
|
||||
import ch.qos.logback.classic.joran.JoranConfigurator
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent
|
||||
import ch.qos.logback.core.ConsoleAppender
|
||||
import dorkbox.netUtil.IPv4
|
||||
import dorkbox.network.ClientConfiguration
|
||||
import dorkbox.network.ServerConfiguration
|
||||
import dorkbox.network.connection.session.SessionClient
|
||||
import dorkbox.network.connection.session.SessionConnection
|
||||
import dorkbox.network.connection.session.SessionServer
|
||||
import dorkbox.network.ipFilter.IpSubnetFilterRule
|
||||
import dorkbox.storage.Storage
|
||||
import dorkbox.util.Sys
|
||||
import kotlinx.atomicfu.atomic
|
||||
import org.agrona.concurrent.SigInt
|
||||
import org.slf4j.LoggerFactory
|
||||
import sun.misc.Unsafe
|
||||
import java.lang.reflect.Field
|
||||
|
||||
/**
|
||||
* THIS WILL RUN FOREVER. It is primarily used for profiling.
|
||||
*/
|
||||
class AeronClientServerSessionForever {
|
||||
companion object {
|
||||
init {
|
||||
try {
|
||||
val theUnsafe = Unsafe::class.java.getDeclaredField("theUnsafe")
|
||||
theUnsafe.isAccessible = true
|
||||
val u = theUnsafe.get(null) as Unsafe
|
||||
val cls = Class.forName("jdk.internal.module.IllegalAccessLogger")
|
||||
val logger: Field = cls.getDeclaredField("logger")
|
||||
u.putObjectVolatile(cls, u.staticFieldOffset(logger), null)
|
||||
} catch (e: NoSuchFieldException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: IllegalAccessException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: ClassNotFoundException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
// assume SLF4J is bound to logback in the current environment
|
||||
val rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as Logger
|
||||
val context = rootLogger.loggerContext
|
||||
val jc = JoranConfigurator()
|
||||
jc.context = context
|
||||
context.reset() // override default configuration
|
||||
|
||||
// rootLogger.setLevel(Level.OFF);
|
||||
|
||||
// rootLogger.setLevel(Level.INFO);
|
||||
// rootLogger.level = Level.DEBUG
|
||||
rootLogger.level = Level.TRACE
|
||||
// rootLogger.setLevel(Level.ALL);
|
||||
|
||||
|
||||
// we only want error messages
|
||||
val nettyLogger = LoggerFactory.getLogger("io.netty") as Logger
|
||||
nettyLogger.level = Level.ERROR
|
||||
|
||||
// we only want error messages
|
||||
val kryoLogger = LoggerFactory.getLogger("com.esotericsoftware") as Logger
|
||||
kryoLogger.level = Level.ERROR
|
||||
|
||||
|
||||
val encoder = PatternLayoutEncoder()
|
||||
encoder.context = context
|
||||
encoder.pattern = "%date{HH:mm:ss.SSS} %-5level [%logger{35}] %msg%n"
|
||||
encoder.start()
|
||||
|
||||
val consoleAppender = ConsoleAppender<ILoggingEvent>()
|
||||
consoleAppender.context = context
|
||||
consoleAppender.encoder = encoder
|
||||
consoleAppender.start()
|
||||
|
||||
rootLogger.addAppender(consoleAppender)
|
||||
}
|
||||
|
||||
/**
|
||||
* Command-line entry point.
|
||||
*
|
||||
* @param args Command-line arguments
|
||||
*
|
||||
* @throws Exception On any error
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
val acs = AeronClientServerSessionForever()
|
||||
|
||||
if (args.contains("server")) {
|
||||
val server = acs.server()
|
||||
server.waitForClose()
|
||||
} else {
|
||||
val client = acs.client("172.31.70.86")
|
||||
client.waitForClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun client(remoteAddress: String): SessionClient<SessionConnection> {
|
||||
val count = atomic(0L)
|
||||
val time = Stopwatch.createUnstarted()
|
||||
|
||||
val configuration = ClientConfiguration()
|
||||
configuration.settingsStore = Storage.Memory() // don't want to persist anything on disk!
|
||||
configuration.appId = "aeron_test"
|
||||
|
||||
configuration.enableIpc = false
|
||||
// configuration.enableIPv4 = false
|
||||
configuration.enableIPv6 = false
|
||||
|
||||
// configuration.uniqueAeronDirectory = true
|
||||
|
||||
val client = SessionClient<SessionConnection>(configuration)
|
||||
|
||||
client.onInit {
|
||||
logger.error("initialized")
|
||||
}
|
||||
|
||||
client.onConnect {
|
||||
logger.error("connected")
|
||||
time.start()
|
||||
send(NoGarbageObj())
|
||||
}
|
||||
|
||||
client.onDisconnect {
|
||||
logger.error("disconnect")
|
||||
}
|
||||
|
||||
client.onError { throwable ->
|
||||
logger.error("has error")
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
|
||||
client.onMessage<NoGarbageObj> { message ->
|
||||
val andIncrement = count.getAndIncrement()
|
||||
if ((andIncrement % 10) == 0L) {
|
||||
logger.error("Sending messages: $andIncrement")
|
||||
}
|
||||
if (andIncrement > 0 && (andIncrement % 500000) == 0L) {
|
||||
// we are measuring roundtrip performance
|
||||
logger.error("For 1,000,000 messages: ${Sys.getTimePrettyFull(time.elapsedNanos())}")
|
||||
time.reset()
|
||||
time.start()
|
||||
}
|
||||
|
||||
val success = send(message)
|
||||
logger.error("SUCCESS:: $success")
|
||||
}
|
||||
|
||||
client.connect(remoteAddress, 2000) // UDP connection via loopback
|
||||
|
||||
return client
|
||||
|
||||
// different ones needed
|
||||
// send - reliable
|
||||
// send - unreliable
|
||||
// send - priority (0-255 -- 255 is MAX priority) when sending, max is always sent immediately, then lower priority is sent if there is no backpressure from the MediaDriver.
|
||||
// send - IPC/local
|
||||
// runBlocking {
|
||||
// while (!client.isShutdown()) {
|
||||
// client.send("ECHO " + java.lang.Long.toUnsignedString(client.crypto.secureRandom.nextLong(), 16))
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// connection needs to know
|
||||
// is UDP or IPC
|
||||
// host address
|
||||
|
||||
// RMI
|
||||
// client.get(5) -> gets from the server connection, if exists, then global.
|
||||
// on server, a connection local RMI object "uses" an id for global, so there will never be a conflict
|
||||
// using some tricks, we can make it so that it DOESN'T matter the order in which objects are created,
|
||||
// and can specify, if we want, the object created.
|
||||
// Once created though, as NEW ONE with the same ID cannot be created until the old one is removed!
|
||||
|
||||
// Thread.sleep(2000L)
|
||||
// client.close()
|
||||
}
|
||||
|
||||
|
||||
fun server(): SessionServer<SessionConnection> {
|
||||
val configuration = ServerConfiguration()
|
||||
configuration.settingsStore = Storage.Memory() // don't want to persist anything on disk!
|
||||
configuration.listenIpAddress = "*"
|
||||
configuration.maxClientCount = 50
|
||||
configuration.appId = "aeron_test"
|
||||
|
||||
configuration.enableIpc = false
|
||||
// configuration.enableIPv4 = false
|
||||
configuration.enableIPv6 = false
|
||||
|
||||
configuration.maxConnectionsPerIpAddress = 50
|
||||
configuration.serialization.register(NoGarbageObj::class.java, NGOSerializer())
|
||||
|
||||
val server = SessionServer<SessionConnection>(configuration)
|
||||
|
||||
// we must always make sure that aeron is shut-down before starting again.
|
||||
if (!server.ensureStopped()) {
|
||||
throw IllegalStateException("Aeron was unable to shut down in a timely manner.")
|
||||
}
|
||||
|
||||
server.filter(IpSubnetFilterRule(IPv4.LOCALHOST, 32))
|
||||
|
||||
server.filter {
|
||||
println("should the connection $this be allowed?")
|
||||
true
|
||||
}
|
||||
|
||||
server.onInit {
|
||||
logger.error("initialized")
|
||||
}
|
||||
|
||||
server.onConnect {
|
||||
logger.error("connected: $this")
|
||||
}
|
||||
|
||||
server.onDisconnect {
|
||||
logger.error("disconnect: $this")
|
||||
}
|
||||
|
||||
server.onErrorGlobal { throwable ->
|
||||
server.logger.error("from test: has error")
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
|
||||
server.onError { throwable ->
|
||||
logger.error("from test: has connection error: $this")
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
|
||||
server.onMessage<NoGarbageObj> { message ->
|
||||
// bounce back message
|
||||
val success = send(message)
|
||||
logger.error("SUCCESS:: $success")
|
||||
}
|
||||
|
||||
var closeCalled = false
|
||||
SigInt.register {
|
||||
// only close once
|
||||
if (!closeCalled) {
|
||||
closeCalled = true
|
||||
server.logger.info("Shutting down via sig-int command")
|
||||
server.close()
|
||||
}
|
||||
}
|
||||
|
||||
server.bind(2000)
|
||||
|
||||
|
||||
return server
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue