Supports the newest version of Aeron. Updated error log usage and notifications. Refactored RMI API (now RMI is accessible via '.rmi' calls). Updated unit tests. Added 'delete' to rmi methods (opposite of 'create').

This commit is contained in:
Robinson 2021-07-06 15:38:53 +02:00
parent 8550a24c04
commit 33f3ca8ebc
46 changed files with 1478 additions and 1566 deletions

120
LICENSE
View File

@ -116,11 +116,11 @@
Copyright 2021
Jonathan Halterman and friends
- Caffeine - Caffeine is a high performance, near optimal caching library based on Java 8.
- Jodah Expiring Map - high performance thread-safe map that expires entries
[The Apache Software License, Version 2.0]
https://github.com/ben-manes/caffeine
https://github.com/jhalterman/expiringmap
Copyright 2021
Ben Manes
Jonathan Halterman
- kotlin-logging - Lightweight logging framework for Kotlin
[The Apache Software License, Version 2.0]
@ -150,6 +150,21 @@
Copyright 2021
QOS.ch
- Updates - Software Update Management
[The Apache Software License, Version 2.0]
https://git.dorkbox.com/dorkbox/Updates
Copyright 2021
Dorkbox LLC
Extra license information
- Kotlin -
[The Apache Software License, Version 2.0]
https://github.com/JetBrains/kotlin
Copyright 2020
JetBrains s.r.o. and Kotlin Programming Language contributors
Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply
See: https://github.com/JetBrains/kotlin/blob/master/license/README.md
- Utilities - Utilities for use within Java projects
[The Apache Software License, Version 2.0]
https://git.dorkbox.com/dorkbox/Utilities
@ -276,6 +291,18 @@
Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply
See: https://github.com/JetBrains/kotlin/blob/master/license/README.md
- JNA - Simplified native library access for Java.
[The Apache Software License, Version 2.0]
https://github.com/twall/jna
Copyright 2021
Timothy Wall
- JNA-Platform - Mappings for a number of commonly used platform functions
[The Apache Software License, Version 2.0]
https://github.com/twall/jna
Copyright 2021
Timothy Wall
- Java Uuid Generator - A set of Java classes for working with UUIDs
[The Apache Software License, Version 2.0]
https://github.com/cowtowncoder/java-uuid-generator
@ -327,21 +354,6 @@
Lasse Collin
Igor Pavlov
- Updates - Software Update Management
[The Apache Software License, Version 2.0]
https://git.dorkbox.com/dorkbox/Updates
Copyright 2021
Dorkbox LLC
Extra license information
- Kotlin -
[The Apache Software License, Version 2.0]
https://github.com/JetBrains/kotlin
Copyright 2020
JetBrains s.r.o. and Kotlin Programming Language contributors
Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply
See: https://github.com/JetBrains/kotlin/blob/master/license/README.md
- Executor - Shell, JVM, and SSH command execution on Linux, MacOS, or Windows for Java 8+
[The Apache Software License, Version 2.0]
https://git.dorkbox.com/dorkbox/Executor
@ -436,30 +448,20 @@
Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply
See: https://github.com/JetBrains/kotlin/blob/master/license/README.md
- SwtJavaFx - Swt and JavaFx Utilities
- Updates - Software Update Management
[The Apache Software License, Version 2.0]
https://git.dorkbox.com/dorkbox/SwtJavaFx
https://git.dorkbox.com/dorkbox/Updates
Copyright 2021
Dorkbox LLC
Extra license information
- Eclipse Platform - Frameworks and common services to support the use of Eclipse and it's tools (SWT)
[Eclipse Public License (EPL)]
https://projects.eclipse.org/projects/eclipse.platform
Copyright 2021
The Eclipse Foundation, Inc.
- OpenJFX - OpenJFX client application platform for desktop, mobile and embedded systems
[GNU General Public License, version 2, with the Classpath Exception]
https://github.com/openjdk/jfx
Copyright 2021
Oracle and/or its affiliates
- SLF4J - Simple facade or abstraction for various logging frameworks
[MIT License]
http://www.slf4j.org
Copyright 2021
QOS.ch
- Kotlin -
[The Apache Software License, Version 2.0]
https://github.com/JetBrains/kotlin
Copyright 2020
JetBrains s.r.o. and Kotlin Programming Language contributors
Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply
See: https://github.com/JetBrains/kotlin/blob/master/license/README.md
- Updates - Software Update Management
[The Apache Software License, Version 2.0]
@ -483,13 +485,43 @@
Dorkbox LLC
Extra license information
- Kryo Serializers - Extra kryo serializers
- Kryo Serializers -
[The Apache Software License, Version 2.0]
https://github.com/magro/kryo-serializers
Copyright 2021
Martin Grotzke
Rafael Winterhalter
- Kotlin -
[The Apache Software License, Version 2.0]
https://github.com/JetBrains/kotlin
Copyright 2020
JetBrains s.r.o. and Kotlin Programming Language contributors
Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply
See: https://github.com/JetBrains/kotlin/blob/master/license/README.md
- Kryo - Fast and efficient binary object graph serialization framework for Java
[BSD 3-Clause License]
https://github.com/EsotericSoftware/kryo
Copyright 2021
Nathan Sweet
Extra license information
- ReflectASM -
[BSD 3-Clause License]
https://github.com/EsotericSoftware/reflectasm
Nathan Sweet
- Objenesis -
[The Apache Software License, Version 2.0]
http://objenesis.org
Objenesis Team and all contributors
- MinLog-SLF4J -
[BSD 3-Clause License]
https://github.com/EsotericSoftware/minlog
Nathan Sweet
- Bouncy Castle Crypto - Lightweight cryptography API and JCE Extension
[The Apache Software License, Version 2.0]
http://www.bouncycastle.org
@ -673,12 +705,6 @@
Copyright 2021
JetBrains s.r.o.
- SLF4J - Simple facade or abstraction for various logging frameworks
[MIT License]
http://www.slf4j.org
Copyright 2021
QOS.ch
- Conversant Disruptor - Disruptor is the highest performing intra-thread transfer mechanism available in Java.
[The Apache Software License, Version 2.0]
https://github.com/conversant/disruptor
@ -699,9 +725,3 @@
JetBrains s.r.o. and Kotlin Programming Language contributors
Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply
See: https://github.com/JetBrains/kotlin/blob/master/license/README.md
- PropertyLoader - Property annotation and loader for fields
[The Apache Software License, Version 2.0]
https://git.dorkbox.com/dorkbox/PropertyLoader
Copyright 2021
Dorkbox LLC

View File

@ -25,15 +25,25 @@ import java.time.Instant
gradle.startParameter.showStacktrace = ShowStacktrace.ALWAYS // always show the stacktrace!
gradle.startParameter.warningMode = WarningMode.All
//buildscript {
// dependencies {
// classpath(project.files("D:\\Code\\dorkbox\\public_projects_build_system\\GradleUtils\\build\\libs\\GradleUtils-2.7.jar"))
// }
//}
plugins {
id("com.dorkbox.GradleUtils") version "2.6"
id("com.dorkbox.Licensing") version "2.6"
id("com.dorkbox.GradleUtils") version "2.8"
id("com.dorkbox.Licensing") version "2.8.1"
id("com.dorkbox.VersionUpdate") version "2.3"
id("com.dorkbox.GradlePublish") version "1.11"
kotlin("jvm") version "1.5.0"
kotlin("jvm") version "1.5.20"
}
//apply(plugin = "com.dorkbox.GradleUtils")
//val GradleUtils = (project as org.gradle.api.plugins.ExtensionAware).extensions.getByName("GradleUtils") as dorkbox.gradle.StaticMethodsAndTools
object Extras {
// set for the project
const val description = "Encrypted, high-performance, and event-driven/reactive network stack for Java 8+"
@ -56,11 +66,15 @@ object Extras {
GradleUtils.load("$projectDir/../../gradle.properties", Extras)
GradleUtils.defaults()
// because of the api changes for stacktrace stuff, it's best for us to ONLY support 11+
GradleUtils.compileConfiguration(JavaVersion.VERSION_11) {
GradleUtils.compileConfiguration(JavaVersion.VERSION_1_8) {
// see: https://kotlinlang.org/docs/reference/using-gradle.html
// enable the use of inline classes. see https://kotlinlang.org/docs/reference/inline-classes.html
freeCompilerArgs = listOf("-Xinline-classes")
}
//GradleUtils.jpms(JavaVersion.VERSION_1_9)
// TODO: ping! (still WIP)
// ratelimiter, "other" package
// ping, rest of unit tests
@ -93,32 +107,32 @@ licensing {
author(Extras.vendor)
extra("KryoNet RMI", License.BSD_3) {
it.copyright(2008)
it.author("Nathan Sweet")
it.url("https://github.com/EsotericSoftware/kryonet")
copyright(2008)
author("Nathan Sweet")
url("https://github.com/EsotericSoftware/kryonet")
}
extra("Kryo Serialization", License.BSD_3) {
it.copyright(2020)
it.author("Nathan Sweet")
it.url("https://github.com/EsotericSoftware/kryo")
copyright(2020)
author("Nathan Sweet")
url("https://github.com/EsotericSoftware/kryo")
}
extra("LAN HostDiscovery from Apache Commons JCS", License.APACHE_2) {
it.copyright(2014)
it.author("The Apache Software Foundation")
it.url("https://issues.apache.org/jira/browse/JCS-40")
copyright(2014)
author("The Apache Software Foundation")
url("https://issues.apache.org/jira/browse/JCS-40")
}
extra("MathUtils, IntArray, IntMap", License.APACHE_2) {
it.copyright(2013)
it.author("Mario Zechner <badlogicgames@gmail.com>")
it.author("Nathan Sweet <nathan.sweet@gmail.com>")
it.url("http://github.com/libgdx/libgdx")
copyright(2013)
author("Mario Zechner <badlogicgames@gmail.com>")
author("Nathan Sweet <nathan.sweet@gmail.com>")
url("http://github.com/libgdx/libgdx")
}
extra("Netty (Various network + platform utilities)", License.APACHE_2) {
it.copyright(2014)
it.description("An event-driven asynchronous network application framework")
it.author("The Netty Project")
it.author("Contributors. See source NOTICE")
it.url("https://netty.io")
copyright(2014)
description("An event-driven asynchronous network application framework")
author("The Netty Project")
author("Contributors. See source NOTICE")
url("https://netty.io")
}
}
}
@ -140,25 +154,25 @@ tasks.jar.get().apply {
dependencies {
implementation("org.jetbrains.kotlinx:atomicfu:0.16.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0")
// https://github.com/dorkbox
implementation("com.dorkbox:MinLog:2.1")
implementation("com.dorkbox:Utilities:1.10")
implementation("com.dorkbox:MinLog:2.4")
implementation("com.dorkbox:Utilities:1.12")
implementation("com.dorkbox:Updates:1.1")
implementation("com.dorkbox:Serializers:1.0")
implementation("com.dorkbox:NetworkUtils:2.7")
implementation("com.dorkbox:ObjectPool:3.3")
implementation("com.dorkbox:Serializers:1.2")
implementation("com.dorkbox:NetworkUtils:2.8")
implementation("com.dorkbox:ObjectPool:3.4")
// https://github.com/real-logic/aeron
val aeronVer = "1.32.0"
val aeronVer = "1.34.0"
// REMOVE UdpChannel when ISSUE https://github.com/real-logic/aeron/issues/1057 is resolved! (hopefully in 1.30.0)
implementation("io.aeron:aeron-client:$aeronVer")
implementation("io.aeron:aeron-driver:$aeronVer")
// https://github.com/EsotericSoftware/kryo
implementation("com.esotericsoftware:kryo:5.1.0") {
implementation("com.esotericsoftware:kryo:5.1.1") {
exclude("com.esotericsoftware", "minlog") // we use our own minlog, that logs to SLF4j instead
}
@ -181,27 +195,29 @@ dependencies {
// really fast storage
// https://github.com/lmdbjava/lmdbjava
compileOnly("org.lmdbjava:lmdbjava:0.8.1")
val lmdbJava = "org.lmdbjava:lmdbjava:0.8.1"
compileOnly(lmdbJava)
// https://github.com/OpenHFT/Chronicle-Map
compileOnly("net.openhft:chronicle-map:3.20.84")
val chronicleMap = "net.openhft:chronicle-map:3.20.84"
compileOnly(chronicleMap)
// Jodah Expiring Map (A high performance thread-safe map that expires entries)
// https://github.com/jhalterman/expiringmap
implementation("net.jodah:expiringmap:0.5.9")
// Caffeine High-throughput Timeout Cache
// https://github.com/ben-manes/caffeine
implementation("com.github.ben-manes.caffeine:caffeine:3.0.1") {
exclude("org.checkerframework", "checker-qual")
exclude("com.google.errorprone", "error_prone_annotations")
}
// https://github.com/MicroUtils/kotlin-logging
implementation("io.github.microutils:kotlin-logging:2.0.6")
implementation("io.github.microutils:kotlin-logging:2.0.8")
implementation("org.slf4j:slf4j-api:1.8.0-beta4")
testImplementation("org.lmdbjava:lmdbjava:0.8.1")
testImplementation("net.openhft:chronicle-map:3.20.3")
testImplementation(lmdbJava)
testImplementation(chronicleMap)
testImplementation("junit:junit:4.13.1")
testImplementation("ch.qos.logback:logback-classic:1.3.0-alpha4")

View File

@ -26,10 +26,6 @@ import dorkbox.network.exceptions.ClientRejectedException
import dorkbox.network.exceptions.ClientTimedOutException
import dorkbox.network.handshake.ClientHandshake
import dorkbox.network.ping.Ping
import dorkbox.network.rmi.RemoteObject
import dorkbox.network.rmi.RemoteObjectStorage
import dorkbox.network.rmi.RmiManagerConnections
import dorkbox.network.rmi.TimeoutException
import dorkbox.util.Sys
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.launch
@ -84,8 +80,6 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
private val previousClosedConnectionActivity: Long = 0
private val rmiConnectionSupport = RmiManagerConnections(logger, rmiGlobalSupport, serialization)
// This is set by the client so if there is a "connect()" call in the the disconnect callback, we can have proper
// lock-stop ordering for how disconnect and connect work with each-other
// GUARANTEE that the callbacks for 'onDisconnect' happens-before the 'onConnect'.
@ -95,13 +89,6 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
return ClientException(message, cause)
}
/**
* So the client class can get remote objects that are THE SAME OBJECT as if called from a connection
*/
final override fun getRmiConnectionSupport(): RmiManagerConnections<CONNECTION> {
return rmiConnectionSupport
}
/**
* Will attempt to connect to the server, with a default 30 second connection timeout and will block until completed.
*
@ -111,10 +98,11 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
* - a network name ("localhost", "loopback", "lo", "bob.example.org")
* - an IP address ("127.0.0.1", "123.123.123.123", "::1")
* - an InetAddress address
* - if no address is specified, and IPC is disabled in the config, then loopback will be selected
*
* ### For the IPC (Inter-Process-Communication) it must be:
* - `connect()`
* - `connect("")`
* - `connect()` (only if ipc is enabled in the configuration)
* - `connect("")` (only if ipc is enabled in the configuration)
* - `connectIpc()`
*
* ### Case does not matter, and "localhost" is the default.
@ -133,7 +121,7 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
reliable: Boolean = true) {
when {
// this is default IPC settings
remoteAddress.isEmpty() -> {
remoteAddress.isEmpty() && config.enableIpc == true -> {
connectIpc(connectionTimeoutMS = connectionTimeoutMS)
}
@ -254,6 +242,7 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
* @throws IllegalArgumentException if the remote address is invalid
* @throws ClientTimedOutException if the client is unable to connect in x amount of time
* @throws ClientRejectedException if the client connection is rejected
* @throws ClientException if there are misc errors
*/
@Suppress("DuplicatedCode")
private fun connect(remoteAddress: InetAddress? = null,
@ -274,7 +263,12 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
connection0 = null
// we are done with initial configuration, now initialize aeron and the general state of this endpoint
initEndpointState()
try {
initEndpointState()
} catch (e: Exception) {
logger.error("Unable to initialize the endpoint state", e)
return
}
// only try to connect via IPv4 if we have a network interface that supports it!
if (remoteAddress is Inet4Address && !IPv4.isAvailable) {
@ -298,7 +292,7 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
var isUsingIPC = false
val autoChangeToIpc = (config.enableIpc && (remoteAddress == null || remoteAddress.isLoopbackAddress)) || (!config.enableIpc && remoteAddress == null)
val handshake = ClientHandshake(crypto, this)
val handshake = ClientHandshake(crypto, this, logger)
val handshakeConnection = if (autoChangeToIpc) {
logger.info {"IPC for loopback enabled and aeron is already running. Auto-changing network connection from ${IP.toString(remoteAddress!!)} -> IPC" }
@ -359,8 +353,13 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
// this will block until the connection timeout, and throw an exception if we were unable to connect with the server
// @Throws(ConnectTimedOutException::class, ClientRejectedException::class)
val connectionInfo = handshake.handshakeHello(handshakeConnection, connectionTimeoutMS)
// throws(ConnectTimedOutException::class, ClientRejectedException::class, ClientException::class)
val connectionInfo = try {
handshake.handshakeHello(handshakeConnection, connectionTimeoutMS)
} catch (e: Exception) {
logger.error("Handshake error", e)
throw e
}
// VALIDATE:: check to see if the remote connection's public key has changed!
@ -373,7 +372,7 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
if (validateRemoteAddress == PublicKeyValidationState.INVALID) {
handshakeConnection.close()
val exception = ClientRejectedException("Connection to ${IP.toString(remoteAddress!!)} not allowed! Public key mismatch.")
listenerManager.notifyError(exception)
logger.error("Validation error", exception)
throw exception
}
@ -429,17 +428,17 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
ClientRejectedException("Connection to ${IP.toString(remoteAddress!!)} has incorrect class registration details!!")
}
listenerManager.notifyError(exception)
logger.error("Initialization error", exception)
throw exception
}
val newConnection: CONNECTION
if (isUsingIPC) {
newConnection = newConnection(ConnectionParams(this, clientConnection, PublicKeyValidationState.VALID))
newConnection = newConnection(ConnectionParams(this, clientConnection, PublicKeyValidationState.VALID, rmiConnectionSupport))
} else {
newConnection = newConnection(ConnectionParams(this, clientConnection, validateRemoteAddress))
newConnection = newConnection(ConnectionParams(this, clientConnection, validateRemoteAddress, rmiConnectionSupport))
remoteAddress!!
// VALIDATE are we allowed to connect to this server (now that we have the initial server information)
@ -448,7 +447,7 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
handshakeConnection.close()
val exception = ClientRejectedException("Connection to ${IP.toString(remoteAddress)} was not permitted!")
ListenerManager.cleanStackTrace(exception)
listenerManager.notifyError(exception)
logger.error("Permission error", exception)
throw exception
}
@ -465,7 +464,7 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
// on the client, we want to GUARANTEE that the disconnect happens-before the connect.
if (!lockStepForConnect.compareAndSet(null, SuspendWaiter())) {
listenerManager.notifyError(connection, IllegalStateException("lockStep for onConnect was in the wrong state!"))
logger.error("Connection ${newConnection.id}", "close lockStep for disconnect was in the wrong state!")
}
}
newConnection.postCloseAction = {
@ -486,7 +485,13 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
// tell the server our connection handshake is done, and the connection can now listen for data.
// also closes the handshake (will also throw connect timeout exception)
val canFinishConnecting = handshake.handshakeDone(handshakeConnection, connectionTimeoutMS)
val canFinishConnecting = try {
handshake.handshakeDone(handshakeConnection, connectionTimeoutMS)
} catch (e: ClientException) {
logger.error("Error during handshake", e)
false
}
if (canFinishConnecting) {
isConnected = true
@ -508,8 +513,8 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
// If the connection has either been closed, or has expired, it needs to be cleaned-up/deleted.
logger.debug {"[${newConnection.id}] connection expired"}
// eventloop is required, because we want to run this code AFTER the current coroutine has finished. This prevents
// odd race conditions when a client is restarted
// event-loop is required, because we want to run this code AFTER the current coroutine has finished. This prevents
// odd race conditions when a client is restarted. Can only be run from inside another co-routine!
actionDispatch.eventLoop {
// NOTE: We do not shutdown the client!! The client is only closed by explicitly calling `client.close()`
newConnection.close()
@ -537,9 +542,10 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
}
} else {
close()
val exception = ClientRejectedException("Unable to connect with server ${handshakeConnection.clientInfo()}")
ListenerManager.cleanStackTrace(exception)
listenerManager.notifyError(exception)
logger.error("Connection ${connection.id}", exception)
throw exception
}
}
@ -600,7 +606,7 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
true
} else {
val exception = ClientException("Cannot send a message when there is no connection!")
listenerManager.notifyError(exception)
logger.error("No connection!", exception)
false
}
}
@ -620,16 +626,19 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
* Sends a "ping" packet to measure **ROUND TRIP** time to the remote connection.
*
* @param function called when the ping returns (ie: update time/latency counters/metrics/etc)
*
* @return true if the ping was successfully sent to the client
*/
suspend fun ping(function: suspend Ping.() -> Unit) {
suspend fun ping(function: suspend Ping.() -> Unit): Boolean {
val c = connection0
if (c != null) {
pingManager.ping(c, function)
return pingManager.ping(c, function)
} else {
val exception = ClientException("Cannot send a ping when there is no connection!")
listenerManager.notifyError(exception)
logger.error("No connection!", ClientException("Cannot send a ping when there is no connection!"))
}
return false
}
/**
@ -637,8 +646,8 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
*
* @param function called when the ping returns (ie: update time/latency counters/metrics/etc)
*/
fun pingBlocking(function: suspend Ping.() -> Unit) {
runBlocking {
fun pingBlocking(function: suspend Ping.() -> Unit): Boolean {
return runBlocking {
ping(function)
}
}
@ -655,263 +664,7 @@ open class Client<CONNECTION : Connection>(config: Configuration = Configuration
}
// no impl
final override fun close0() {}
// RMI notes (in multiple places, copypasta, because this is confusing if not written down)
//
// only server can create a global object (in itself, via save)
// server
// -> saveGlobal (global)
//
// client
// -> save (connection)
// -> get (connection)
// -> create (connection)
// -> saveGlobal (global)
// -> getGlobal (global)
//
// connection
// -> save (connection)
// -> get (connection)
// -> getGlobal (global)
// -> create (connection)
//
//
// RMI - connection
//
//
/**
* Tells us to save an an already created object in the CONNECTION scope, so a remote connection can get it via [Connection.getObject]
*
* - This object is NOT THREAD SAFE, and is meant to ONLY be used from a single thread!
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the
* response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
*
* @return the newly registered RMI ID for this object. [RemoteObjectStorage.INVALID_RMI] means it was invalid (an error log will be emitted)
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun saveObject(`object`: Any): Int {
val rmiId = rmiConnectionSupport.saveImplObject(`object`)
if (rmiId == RemoteObjectStorage.INVALID_RMI) {
val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated")
ListenerManager.cleanStackTrace(exception)
listenerManager.notifyError(exception)
return rmiId
}
return rmiId
}
/**
* Tells us to save an an already created object in the CONNECTION scope using the specified ID, so a remote connection can get it via [Connection.getObject]
*
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the
* response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
* @return true if the object was successfully saved for the specified ID. If false, an error log will be emitted
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun saveObject(`object`: Any, objectId: Int): Boolean {
val success = rmiConnectionSupport.saveImplObject(`object`, objectId)
if (!success) {
val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated")
ListenerManager.cleanStackTrace(exception)
listenerManager.notifyError(exception)
}
return success
}
/**
* Get a CONNECTION scope REMOTE object via the ID.
*
* Global remote objects are accessible to ALL connections, where as a connection specific remote object is only accessible/visible
* to the connection.
*
* If you want to access a connection specific remote object, call [Connection.get(Int, RemoteObjectCallback<Iface>)] on a connection
* The callback will be notified when the remote object has been created.
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the remote object behavior, cast the object to a [RemoteObject] to access the different methods, for example:
* ie: `val remoteObject = test as RemoteObject`
*
* @see RemoteObject
*/
inline fun <reified Iface> getObject(objectId: Int): Iface {
// NOTE: It's not possible to have reified inside a virtual function
// https://stackoverflow.com/questions/60037849/kotlin-reified-generic-in-virtual-function
val kryoId = serialization.getKryoIdForRmiClient(Iface::class.java)
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")
return rmiConnectionSupport.getProxyObject(connection, kryoId, objectId, Iface::class.java)
}
/**
* Tells the remote connection to create a new proxy object that implements the specified interface in the CONNECTION scope.
*
* The methods on this object "map" to an object that is created remotely.
*
* The callback will be notified when the remote object has been created.
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the
* response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
* @see RemoteObject
*/
suspend inline fun <reified Iface> createObject(vararg objectParameters: Any?, noinline callback: suspend Iface.() -> Unit) {
// NOTE: It's not possible to have reified inside a virtual function
// https://stackoverflow.com/questions/60037849/kotlin-reified-generic-in-virtual-function
val kryoId = serialization.getKryoIdForRmiClient(Iface::class.java)
@Suppress("UNCHECKED_CAST")
objectParameters as Array<Any?>
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")
rmiConnectionSupport.createRemoteObject(connection, kryoId, objectParameters, callback)
}
/**
* Tells the remote connection to create a new proxy object that implements the specified interface in the CONNECTION scope.
*
* The methods on this object "map" to an object that is created remotely.
*
* The callback will be notified when the remote object has been created.
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the
* response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
* @see RemoteObject
*/
suspend inline fun <reified Iface> createObject(noinline callback: suspend Iface.() -> Unit) {
// NOTE: It's not possible to have reified inside a virtual function
// https://stackoverflow.com/questions/60037849/kotlin-reified-generic-in-virtual-function
val kryoId = serialization.getKryoIdForRmiClient(Iface::class.java)
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")
rmiConnectionSupport.createRemoteObject(connection, kryoId, null, callback)
}
//
//
// RMI - global
//
//
/**
* Tells us to save an an already created object in the GLOBAL scope, so a remote connection can get it via [Connection.getGlobalObject]
*
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the
* response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
*
* @return the newly registered RMI ID for this object. [RemoteObjectStorage.INVALID_RMI] means it was invalid (an error log will be emitted)
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun saveGlobalObject(`object`: Any): Int {
val rmiId = rmiGlobalSupport.saveImplObject(`object`)
if (rmiId == RemoteObjectStorage.INVALID_RMI) {
val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated")
ListenerManager.cleanStackTrace(exception)
listenerManager.notifyError(exception)
}
return rmiId
}
/**
* Tells us to save an an already created object in the GLOBAL scope using the specified ID, so a remote connection can get it via [Connection.getGlobalObject]
*
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the
* response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
* @return true if the object was successfully saved for the specified ID. If false, an error log will be emitted
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun saveGlobalObject(`object`: Any, objectId: Int): Boolean {
val success = rmiGlobalSupport.saveImplObject(`object`, objectId)
if (!success) {
val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated")
ListenerManager.cleanStackTrace(exception)
listenerManager.notifyError(exception)
}
return success
}
/**
* Get a GLOBAL scope remote object via the ID.
*
* Global remote objects are accessible to ALL connections, where as a connection specific remote object is only accessible/visible
* to the connection.
*
* If you want to access a connection specific remote object, call [Connection.get(Int, RemoteObjectCallback<Iface>)] on a connection
* The callback will be notified when the remote object has been created.
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the remote object behavior, cast the object to a [RemoteObject] to access the different methods, for example:
* ie: `val remoteObject = test as RemoteObject`
*
* @see RemoteObject
*/
inline fun <reified Iface> getGlobalObject(objectId: Int): Iface {
// NOTE: It's not possible to have reified inside a virtual function
// https://stackoverflow.com/questions/60037849/kotlin-reified-generic-in-virtual-function
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")
return rmiGlobalSupport.getGlobalRemoteObject(connection, objectId, Iface::class.java)
final override fun close0() {
// when we close(), don't permit reconnect. add "close(boolean)" (aka "shutdown"), to deny a connect request (and permanently stay closed)
}
}

View File

@ -21,6 +21,7 @@ import dorkbox.network.aeron.AeronDriver
import dorkbox.network.aeron.CoroutineBackoffIdleStrategy
import dorkbox.network.aeron.CoroutineIdleStrategy
import dorkbox.network.aeron.CoroutineSleepingMillisIdleStrategy
import dorkbox.network.connection.Connection
import dorkbox.network.serialization.Serialization
import dorkbox.network.storage.StorageType
import dorkbox.network.storage.types.PropertyStore
@ -28,6 +29,7 @@ import dorkbox.os.OS
import io.aeron.driver.Configuration
import io.aeron.driver.ThreadingMode
import mu.KLogger
import org.agrona.SystemUtil
import java.io.File
import java.util.concurrent.TimeUnit
@ -279,7 +281,7 @@ open class Configuration {
/**
* Specify the serialization manager to use.
*/
var serialization: Serialization = Serialization()
var serialization: Serialization<Connection> = Serialization()
set(value) {
require(!contextDefined) { errorMessage }
field = value
@ -401,6 +403,25 @@ open class Configuration {
field = value
}
/**
* Default initial window length for flow control sender to receiver purposes. This assumes a system free of pauses.
*
* Length of Initial Window:
*
* RTT (LAN) = 100 usec -- Throughput = 10 Gbps)
* RTT (LAN) = 100 usec -- Throughput = 1 Gbps
*
* Buffer = Throughput * RTT
*
* Buffer (10 Gps) = (10 * 1000 * 1000 * 1000 / 8) * 0.0001 = 125000 (Round to 128KB)
* Buffer (1 Gps) = (1 * 1000 * 1000 * 1000 / 8) * 0.0001 = 12500 (Round to 16KB)
*/
var initialWindowLength = SystemUtil.getSizeAsInt(Configuration.INITIAL_WINDOW_LENGTH_PROP_NAME, 16 * 1024)
set(value) {
require(!contextDefined) { errorMessage }
field = value
}
/**
* This option (ultimately SO_SNDBUF for the network socket) can impact loss rate. Loss can occur on the sender side due
* to this buffer being too small.

View File

@ -22,18 +22,13 @@ import dorkbox.network.aeron.IpcMediaDriverConnection
import dorkbox.network.aeron.UdpMediaDriverServerConnection
import dorkbox.network.connection.Connection
import dorkbox.network.connection.EndPoint
import dorkbox.network.connection.ListenerManager
import dorkbox.network.connection.eventLoop
import dorkbox.network.connectionType.ConnectionRule
import dorkbox.network.coroutines.SuspendWaiter
import dorkbox.network.exceptions.ClientRejectedException
import dorkbox.network.exceptions.ServerException
import dorkbox.network.handshake.HandshakeMessage
import dorkbox.network.handshake.ServerHandshake
import dorkbox.network.rmi.RemoteObject
import dorkbox.network.rmi.RemoteObjectStorage
import dorkbox.network.rmi.RmiManagerConnections
import dorkbox.network.rmi.TimeoutException
import dorkbox.network.rmi.RmiSupportServer
import io.aeron.FragmentAssembler
import io.aeron.Image
import io.aeron.logbuffer.Header
@ -74,6 +69,11 @@ open class Server<CONNECTION : Connection>(config: ServerConfiguration = ServerC
}
}
/**
* Methods supporting Remote Method Invocation and Objects for GLOBAL scope objects (different than CONNECTION scope objects)
*/
val rmiGlobal = RmiSupportServer(logger, rmiGlobalSupport)
/**
* @return true if this server has successfully bound to an IP address and is running
*/
@ -146,10 +146,6 @@ open class Server<CONNECTION : Connection>(config: ServerConfiguration = ServerC
return ServerException(message, cause)
}
final override fun getRmiConnectionSupport(): RmiManagerConnections<CONNECTION> {
return super.getRmiConnectionSupport()
}
private fun getIpcPoller(aeronDriver: AeronDriver, config: ServerConfiguration): AeronPoller {
val poller = if (config.enableIpc) {
val driver = IpcMediaDriverConnection(streamIdSubscription = config.ipcSubscriptionId,
@ -172,13 +168,18 @@ open class Server<CONNECTION : Connection>(config: ServerConfiguration = ServerC
// VALIDATE:: a Registration object is the only acceptable message during the connection phase
if (message !is HandshakeMessage) {
listenerManager.notifyError(ClientRejectedException("[$sessionId] Connection from IPC not allowed! Invalid connection request"))
logger.error("[$sessionId] Connection from IPC not allowed! Invalid connection request")
writeHandshakeMessage(publication, HandshakeMessage.error("Invalid connection request"))
try {
writeHandshakeMessage(publication, HandshakeMessage.error("Invalid connection request"))
} catch (e: Exception) {
logger.error("Handshake error!", e)
}
return@FragmentAssembler
}
handshake.processIpcHandshakeMessageServer(this@Server,
rmiConnectionSupport,
publication,
sessionId,
message,
@ -250,13 +251,18 @@ open class Server<CONNECTION : Connection>(config: ServerConfiguration = ServerC
// VALIDATE:: a Registration object is the only acceptable message during the connection phase
if (message !is HandshakeMessage) {
listenerManager.notifyError(ClientRejectedException("[$sessionId] Connection from $clientAddressString not allowed! Invalid connection request"))
logger.error("[$sessionId] Connection from $clientAddressString not allowed! Invalid connection request")
writeHandshakeMessage(publication, HandshakeMessage.error("Invalid connection request"))
try {
writeHandshakeMessage(publication, HandshakeMessage.error("Invalid connection request"))
} catch (e: Exception) {
logger.error("Handshake error!", e)
}
return@FragmentAssembler
}
handshake.processUdpHandshakeMessageServer(this@Server,
rmiConnectionSupport,
publication,
sessionId,
clientAddressString,
@ -331,13 +337,18 @@ open class Server<CONNECTION : Connection>(config: ServerConfiguration = ServerC
// VALIDATE:: a Registration object is the only acceptable message during the connection phase
if (message !is HandshakeMessage) {
listenerManager.notifyError(ClientRejectedException("[$sessionId] Connection from $clientAddressString not allowed! Invalid connection request"))
logger.error("[$sessionId] Connection from $clientAddressString not allowed! Invalid connection request")
writeHandshakeMessage(publication, HandshakeMessage.error("Invalid connection request"))
try {
writeHandshakeMessage(publication, HandshakeMessage.error("Invalid connection request"))
} catch (e: Exception) {
logger.error("Handshake error!", e)
}
return@FragmentAssembler
}
handshake.processUdpHandshakeMessageServer(this@Server,
rmiConnectionSupport,
publication,
sessionId,
clientAddressString,
@ -412,13 +423,18 @@ open class Server<CONNECTION : Connection>(config: ServerConfiguration = ServerC
// VALIDATE:: a Registration object is the only acceptable message during the connection phase
if (message !is HandshakeMessage) {
listenerManager.notifyError(ClientRejectedException("[$sessionId] Connection from $clientAddressString not allowed! Invalid connection request"))
logger.error("[$sessionId] Connection from $clientAddressString not allowed! Invalid connection request")
writeHandshakeMessage(publication, HandshakeMessage.error("Invalid connection request"))
try {
writeHandshakeMessage(publication, HandshakeMessage.error("Invalid connection request"))
} catch (e: Exception) {
logger.error("Handshake error!", e)
}
return@FragmentAssembler
}
handshake.processUdpHandshakeMessageServer(this@Server,
rmiConnectionSupport,
publication,
sessionId,
clientAddressString,
@ -447,7 +463,12 @@ open class Server<CONNECTION : Connection>(config: ServerConfiguration = ServerC
return
}
initEndpointState()
try {
initEndpointState()
} catch (e: Exception) {
logger.error("Unable to initialize the endpoint state", e)
return
}
config as ServerConfiguration
@ -575,7 +596,6 @@ open class Server<CONNECTION : Connection>(config: ServerConfiguration = ServerC
jobs.forEach { it.join() }
} catch (e: Exception) {
logger.error("Unexpected error during server message polling!", e)
listenerManager.notifyError(e)
} finally {
ipv4Poller.close()
ipv6Poller.close()
@ -612,15 +632,6 @@ open class Server<CONNECTION : Connection>(config: ServerConfiguration = ServerC
connectionRules.addAll(listOf(*rules))
}
/**
* Safely sends objects to a destination.
*/
suspend fun send(message: Any) {
connections.forEach {
it.send(message)
}
}
/**
* Runs an action for each connection
*/
@ -677,138 +688,4 @@ open class Server<CONNECTION : Connection>(config: ServerConfiguration = ServerC
// logger.debug("Validating {} Permitted type is: {}", remoteAddress, connectionType)
// return connectionType.type
// }
// RMI notes (in multiple places, copypasta, because this is confusing if not written down)
//
// only server can create a global object (in itself, via save)
// server
// -> saveGlobal (global)
//
// client
// -> save (connection)
// -> get (connection)
// -> create (connection)
// -> saveGlobal (global)
// -> getGlobal (global)
//
// connection
// -> save (connection)
// -> get (connection)
// -> getGlobal (global)
// -> create (connection)
//
//
// RMI
//
//
/**
* Tells us to save an an already created object, GLOBALLY, so a remote connection can get it via [Connection.getObject]
*
* FOR REMOTE CONNECTIONS:
* Methods that return a value will throw [TimeoutException] if the response is not received with the
* response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
*
* @return the newly registered RMI ID for this object. [RemoteObjectStorage.INVALID_RMI] means it was invalid (an error log will be emitted)
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun saveGlobalObject(`object`: Any): Int {
val rmiId = rmiGlobalSupport.saveImplObject(`object`)
if (rmiId == RemoteObjectStorage.INVALID_RMI) {
val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated")
ListenerManager.cleanStackTrace(exception)
listenerManager.notifyError(exception)
return rmiId
}
return rmiId
}
/**
* Tells us to save an already created object, GLOBALLY using the specified ID, so a remote connection can get it via [Connection.getObject]
*
* FOR REMOTE CONNECTIONS:
* Methods that return a value will throw [TimeoutException] if the response is not received with the
* response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
* @return true if the object was successfully saved for the specified ID. If false, an error log will be emitted
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun saveGlobalObject(`object`: Any, objectId: Int): Boolean {
val success = rmiGlobalSupport.saveImplObject(`object`, objectId)
if (!success) {
val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated")
ListenerManager.cleanStackTrace(exception)
listenerManager.notifyError(exception)
}
return success
}
/**
* Tells us to delete a previously saved object, GLOBALLY using the specified object.
*
* After this call, this object wil no longer be available to remote connections and the ID will be recycled (don't use it again)
*
* @return true if the object was successfully deleted. If false, an error log will be emitted
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun deleteGlobalObject(`object`: Any): Boolean {
val successRmiId = rmiGlobalSupport.getId(`object`)
val success = successRmiId != RemoteObjectStorage.INVALID_RMI
if (success) {
rmiGlobalSupport.removeImplObject<Any?>(successRmiId)
} else {
val exception = Exception("RMI implementation '${`object`::class.java}' could not be deleted! It does not exist")
ListenerManager.cleanStackTrace(exception)
listenerManager.notifyError(exception)
}
return success
}
/**
* Tells us to delete a previously saved object, GLOBALLY using the specified ID.
*
* After this call, this object wil no longer be available to remote connections and the ID will be recycled (don't use it again)
*
* @return true if the object was successfully deleted. If false, an error log will be emitted
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun deleteGlobalObject(objectId: Int): Boolean {
val previousObject = rmiGlobalSupport.removeImplObject<Any?>(objectId)
val success = previousObject != null
if (!success) {
val exception = Exception("RMI implementation UD '$objectId' could not be deleted! It does not exist")
ListenerManager.cleanStackTrace(exception)
listenerManager.notifyError(exception)
}
return success
}
}

View File

@ -160,6 +160,8 @@ class AeronDriver(val config: Configuration,
.publicationReservedSessionIdHigh(RESERVED_SESSION_ID_HIGH)
.threadingMode(config.threadingMode)
.mtuLength(config.networkMtuSize)
.initialWindowLength(config.initialWindowLength)
.socketSndbufLength(config.sendBufferSize)
.socketRcvbufLength(config.receiveBufferSize)

View File

@ -31,7 +31,6 @@ abstract class MediaDriverConnection(
lateinit var publication: Publication
@Throws(ClientTimedOutException::class)
abstract fun buildClient(aeronDriver: AeronDriver, logger: KLogger)
abstract fun buildServer(aeronDriver: AeronDriver, logger: KLogger, pairConnection: Boolean = false)

View File

@ -79,7 +79,6 @@ internal class UdpMediaDriverClientConnection(val address: InetAddress,
@Suppress("DuplicatedCode")
@Throws(ClientException::class)
override fun buildClient(aeronDriver: AeronDriver, logger: KLogger) {
val aeronAddressString = aeronConnectionString(address)

View File

@ -23,10 +23,7 @@ import dorkbox.network.handshake.ConnectionCounts
import dorkbox.network.handshake.RandomIdAllocator
import dorkbox.network.ping.Ping
import dorkbox.network.ping.PingMessage
import dorkbox.network.rmi.RemoteObject
import dorkbox.network.rmi.RemoteObjectStorage
import dorkbox.network.rmi.TimeoutException
import dorkbox.util.classes.ClassHelper
import dorkbox.network.rmi.RmiSupportConnection
import io.aeron.FragmentAssembler
import io.aeron.Publication
import io.aeron.Subscription
@ -36,7 +33,6 @@ import kotlinx.atomicfu.getAndUpdate
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.agrona.DirectBuffer
import java.io.IOException
import java.net.InetAddress
import java.util.concurrent.TimeUnit
@ -124,8 +120,10 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
// counter, which is also transmitted as an optimized int. (which is why it starts at 0, so the transmitted bytes are small)
// private val aes_gcm_iv = atomic(0)
// RMI support for this connection
internal val rmiConnectionSupport = endPoint.getRmiConnectionSupport()
/**
* Methods supporting Remote Method Invocation and Objects
*/
val rmi: RmiSupportConnection<out Connection>
// a record of how many messages are in progress of being sent. When closing the connection, this number must be 0
private val messagesInProgress = atomic(0)
@ -182,10 +180,11 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
endPoint.processMessage(buffer, offset, length, header, this@Connection)
}
@Suppress("LeakingThis")
rmi = connectionParameters.rmiConnectionSupport.getNewRmiSupport(this)
}
/**
* @return true if the remote public key changed. This can be useful if specific actions are necessary when the key has changed.
*/
@ -226,18 +225,24 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
/**
* Safely sends objects to a destination.
*
* @return true if the message was successfully sent by aeron
*/
suspend fun send(message: Any) {
suspend fun send(message: Any): Boolean {
messagesInProgress.getAndIncrement()
endPoint.send(message, publication, this)
val success = endPoint.send(message, publication, this)
messagesInProgress.getAndDecrement()
return success
}
/**
* Safely sends objects to a destination.
*
* @return true if the message was successfully sent by aeron
*/
fun sendBlocking(message: Any) {
runBlocking {
fun sendBlocking(message: Any): Boolean {
return runBlocking {
send(message)
}
}
@ -247,10 +252,10 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
*
* Only 1 in-flight ping can be performed at a time. Calling ping() again, before the previous ping returns will do nothing.
*
* @return Ping can have a listener attached, which will get called when the ping returns.
* @return true if the message was successfully sent by aeron
*/
suspend fun ping(function: suspend Ping.() -> Unit) {
endPoint.pingManager.ping(this, function)
suspend fun ping(function: suspend Ping.() -> Unit): Boolean {
return endPoint.pingManager.ping(this, function)
}
/**
@ -275,7 +280,7 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
suspend fun onDisconnect(function: suspend Connection.() -> Unit) {
// make sure we atomically create the listener manager, if necessary
listenerManager.getAndUpdate { origManager ->
origManager ?: ListenerManager()
origManager ?: ListenerManager(logger)
}
listenerManager.value!!.onDisconnect(function)
@ -287,7 +292,7 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
suspend fun <MESSAGE> onMessage(function: suspend Connection.(MESSAGE) -> Unit) {
// make sure we atomically create the listener manager, if necessary
listenerManager.getAndUpdate { origManager ->
origManager ?: ListenerManager()
origManager ?: ListenerManager(logger)
}
listenerManager.value!!.onMessage(function)
@ -380,11 +385,9 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
}
if (logFile.exists()) {
listenerManager.value?.notifyError(this, IOException("Unable to delete aeron publication log on close: $logFile"))
logger.error("Connection $id: Unable to delete aeron publication log on close: $logFile")
}
rmiConnectionSupport.clearProxyObjects()
endPoint.removeConnection(this)
@ -445,196 +448,4 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
streamIdAllocator.free(streamId)
}
}
// RMI notes (in multiple places, copypasta, because this is confusing if not written down)
//
// only server can create a global object (in itself, via save)
// server
// -> saveGlobal (global)
//
// client
// -> save (connection)
// -> get (connection)
// -> create (connection)
// -> saveGlobal (global)
// -> getGlobal (global)
//
// connection
// -> save (connection)
// -> get (connection)
// -> getGlobal (global)
// -> create (connection)
//
//
// RMI methods
//
//
/**
* Tells us to save an an already created object in the CONNECTION scope, so a remote connection can get it via [Connection.getObject]
*
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the
* response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
*
* @return the newly registered RMI ID for this object. [RemoteObjectStorage.INVALID_RMI] means it was invalid (an error log will be emitted)
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun saveObject(`object`: Any): Int {
val rmiId = rmiConnectionSupport.saveImplObject(`object`)
if (rmiId == RemoteObjectStorage.INVALID_RMI) {
val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated")
ListenerManager.cleanStackTrace(exception)
listenerManager.value?.notifyError(this, exception)
}
return rmiId
}
/**
* Tells us to save an an already created object in the CONNECTION scope using the specified ID, so a remote connection can get it via [Connection.getObject]
*
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the
* response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
* @return true if the object was successfully saved for the specified ID. If false, an error log will be emitted
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun saveObject(`object`: Any, objectId: Int): Boolean {
val success = rmiConnectionSupport.saveImplObject(`object`, objectId)
if (!success) {
val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated")
ListenerManager.cleanStackTrace(exception)
listenerManager.value?.notifyError(this, exception)
}
return success
}
/**
* Gets a CONNECTION scope remote object via the ID.
*
* Global remote objects are accessible to ALL connections, where as a connection specific remote object is only accessible/visible
* to the connection.
*
* If you want to access a connection specific remote object, call [Connection.get(Int, RemoteObjectCallback<Iface>)] on a connection
* The callback will be notified when the remote object has been created.
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the remote object behavior, cast the object to a [RemoteObject] to access the different methods, for example:
* ie: `val remoteObject = test as RemoteObject`
*
* @see RemoteObject
*/
inline fun <reified Iface> getObject(objectId: Int): Iface {
// NOTE: It's not possible to have reified inside a virtual function
// https://stackoverflow.com/questions/60037849/kotlin-reified-generic-in-virtual-function
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")
val kryoId = endPoint.serialization.getKryoIdForRmiClient(Iface::class.java)
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")
return rmiConnectionSupport.getProxyObject(this, kryoId, objectId, Iface::class.java)
}
/**
* Gets a global REMOTE object via the ID.
*
* Global remote objects are accessible to ALL connections, where as a connection specific remote object is only accessible/visible
* to the connection.
*
* If you want to access a connection specific remote object, call [Connection.get(Int, RemoteObjectCallback<Iface>)] on a connection
* The callback will be notified when the remote object has been created.
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the remote object behavior, cast the object to a [RemoteObject] to access the different methods, for example:
* ie: `val remoteObject = test as RemoteObject`
*
* @see RemoteObject
*/
inline fun <reified Iface> getGlobalObject(objectId: Int): Iface {
// NOTE: It's not possible to have reified inside a virtual function
// https://stackoverflow.com/questions/60037849/kotlin-reified-generic-in-virtual-function
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")
return rmiConnectionSupport.rmiGlobalSupport.getGlobalRemoteObject(this, objectId, Iface::class.java)
}
/**
* Tells the remote connection to create a new proxy object that implements the specified interface. The methods on this object "map"
* to an object that is created remotely.
*
* The callback will be notified when the remote object has been created.
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the
* response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
* @see RemoteObject
*/
suspend fun <Iface> createObject(vararg objectParameters: Any?, callback: suspend Iface.() -> Unit) {
val iFaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(Function1::class.java, callback.javaClass, 0)
val kryoId = endPoint.serialization.getKryoIdForRmiClient(iFaceClass)
@Suppress("UNCHECKED_CAST")
objectParameters as Array<Any?>
rmiConnectionSupport.createRemoteObject(this, kryoId, objectParameters, callback)
}
/**
* Tells the remote connection to create a new proxy object that implements the specified interface. The methods on this object "map"
* to an object that is created remotely.
*
* The callback will be notified when the remote object has been created.
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the
* response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
* @see RemoteObject
*/
suspend fun <Iface> createObject(callback: suspend Iface.() -> Unit) {
val iFaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(Function1::class.java, callback.javaClass, 0)
val kryoId = endPoint.serialization.getKryoIdForRmiClient(iFaceClass)
rmiConnectionSupport.createRemoteObject(this, kryoId, null, callback)
}
/**
* Removes
*/
fun removeObject(rmiObjectId: Int) {
TODO("Not yet implemented")
}
}

View File

@ -16,7 +16,11 @@
package dorkbox.network.connection
import dorkbox.network.aeron.MediaDriverConnection
import dorkbox.network.rmi.RmiManagerConnections
data class ConnectionParams<C : Connection>(val endPoint: EndPoint<C>,
data class ConnectionParams<CONNECTION : Connection>(
val endPoint: EndPoint<CONNECTION>,
val mediaDriverConnection: MediaDriverConnection,
val publicKeyValidation: PublicKeyValidationState)
val publicKeyValidation: PublicKeyValidationState,
val rmiConnectionSupport: RmiManagerConnections<CONNECTION>
)

View File

@ -22,7 +22,8 @@ import dorkbox.network.ServerConfiguration
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.aeron.CoroutineIdleStrategy
import dorkbox.network.coroutines.SuspendWaiter
import dorkbox.network.exceptions.MessageNotRegisteredException
import dorkbox.network.exceptions.ClientException
import dorkbox.network.exceptions.ServerException
import dorkbox.network.handshake.HandshakeMessage
import dorkbox.network.ipFilter.IpFilterRule
import dorkbox.network.ping.Ping
@ -30,6 +31,7 @@ import dorkbox.network.ping.PingManager
import dorkbox.network.ping.PingMessage
import dorkbox.network.rmi.RmiManagerConnections
import dorkbox.network.rmi.RmiManagerGlobal
import dorkbox.network.rmi.messages.MethodResponse
import dorkbox.network.rmi.messages.RmiMessage
import dorkbox.network.serialization.KryoExtra
import dorkbox.network.serialization.Serialization
@ -75,7 +77,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
internal val actionDispatch = CoroutineScope(Dispatchers.Default)
internal val listenerManager = ListenerManager<CONNECTION>()
internal val listenerManager = ListenerManager<CONNECTION>(logger)
internal val connections = ConnectionManager<CONNECTION>()
internal val pingManager = PingManager<CONNECTION>(logger, actionDispatch)
@ -85,15 +87,15 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
/**
* Returns the serialization wrapper if there is an object type that needs to be added outside of the basic types.
*/
val serialization: Serialization
val serialization: Serialization<CONNECTION>
private val handshakeKryo: KryoExtra
private val handshakeKryo: KryoExtra<CONNECTION>
private val sendIdleStrategy: CoroutineIdleStrategy
private val sendIdleStrategyHandShake: IdleStrategy
val pollIdleStrategy: CoroutineIdleStrategy
val pollIdleStrategyHandShake: IdleStrategy
private val pollIdleStrategy: CoroutineIdleStrategy
internal val pollIdleStrategyHandShake: IdleStrategy
/**
* Crypto and signature management
@ -112,25 +114,16 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
*/
val storage: SettingsStore
internal val rmiGlobalSupport = RmiManagerGlobal<CONNECTION>(logger, actionDispatch, config.serialization)
internal val rmiGlobalSupport = RmiManagerGlobal<CONNECTION>(logger, actionDispatch)
internal val rmiConnectionSupport: RmiManagerConnections<CONNECTION>
init {
require(!config.previouslyUsed) { "${type.simpleName} configuration cannot be reused!" }
config.validate()
runBlocking {
// our default onError handler. All error messages go though this
listenerManager.onError { throwable ->
logger.error("Error processing events", throwable)
}
listenerManager.onError { throwable ->
logger.error("Error processing events for connection $this", throwable)
}
}
// serialization stuff
serialization = config.serialization
@Suppress("UNCHECKED_CAST")
serialization = config.serialization as Serialization<CONNECTION>
sendIdleStrategy = config.sendIdleStrategy
pollIdleStrategy = config.pollIdleStrategy
@ -148,28 +141,43 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
try {
aeronDriver = AeronDriver(config, type, logger)
} catch (e: Exception) {
listenerManager.notifyError(e)
logger.error("Error initialize endpoint", e)
throw e
}
if (type.javaClass == Server::class.java) {
// server cannot "get" global RMI objects, only the client can
@Suppress("UNCHECKED_CAST")
rmiConnectionSupport = RmiManagerConnections(logger, rmiGlobalSupport, config.serialization as Serialization<CONNECTION>)
{ _, _, _ ->
throw IllegalAccessException("Global RMI access is only possible from a Client connection!")
}
} else {
@Suppress("UNCHECKED_CAST")
rmiConnectionSupport = RmiManagerConnections(logger, rmiGlobalSupport, config.serialization as Serialization<CONNECTION>)
{ connection, objectId, interfaceClass ->
return@RmiManagerConnections rmiGlobalSupport.getGlobalRemoteObject(connection, objectId, interfaceClass)
}
}
}
/**
* @throws Exception if there is a problem starting the media driver
*/
internal fun initEndpointState() {
shutdown.getAndSet(false)
shutdownWaiter = SuspendWaiter()
// Only starts the media driver if we are NOT already running!
try {
aeronDriver.start()
} catch (e: Exception) {
listenerManager.notifyError(e)
throw e
}
aeronDriver.start()
}
abstract fun newException(message: String, cause: Throwable? = null): Throwable
// used internally to remove a connection
// used internally to remove a connection. Will also remove all proxy objects
internal fun removeConnection(connection: Connection) {
rmiConnectionSupport.close()
@Suppress("UNCHECKED_CAST")
removeConnection(connection as CONNECTION)
}
@ -213,14 +221,6 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
return Connection(connectionParameters) as CONNECTION
}
/**
* Used for the client, because the client only has ONE ever support connection, and it allows us to create connection specific objects
* from a "global" context
*/
internal open fun getRmiConnectionSupport() : RmiManagerConnections<CONNECTION> {
return RmiManagerConnections(logger, rmiGlobalSupport, serialization)
}
/**
* Adds an IP+subnet rule that defines if that IP+subnet is allowed/denied connectivity to this server.
*
@ -320,9 +320,14 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
}
}
/**
* NOTE: this **MUST** stay on the same co-routine that calls "send". This cannot be re-dispatched onto a different coroutine!
* CANNOT be called in action dispatch. ALWAYS ON SAME THREAD
*
* @return true if the message was successfully sent by aeron
*/
@Suppress("DuplicatedCode")
// note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD
internal fun writeHandshakeMessage(publication: Publication, message: HandshakeMessage) {
internal fun writeHandshakeMessage(publication: Publication, message: HandshakeMessage): Boolean {
// The handshake sessionId IS NOT globally unique
logger.trace {
"[${publication.sessionId()}] send HS: $message"
@ -339,7 +344,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
result = publication.offer(internalBuffer, 0, objectSize)
if (result >= 0) {
// success!
return
return true
}
/**
@ -360,15 +365,21 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
}
// more critical error sending the message. we shouldn't retry or anything.
// this exception will be a ClientException or a ServerException
val exception = newException("[${publication.sessionId()}] Error sending handshake message. $message (${errorCodeName(result)})")
ListenerManager.cleanStackTraceInternal(exception)
listenerManager.notifyError(exception)
return
throw exception
}
} catch (e: Exception) {
val exception = newException("[${publication.sessionId()}] Error serializing handshake message $message", e)
ListenerManager.cleanStackTrace(exception, 2) // 2 because we do not want to see the stack for the abstract `newException`
listenerManager.notifyError(exception)
if (e is ClientException || e is ServerException) {
throw e
} else {
val exception = newException("[${publication.sessionId()}] Error serializing handshake message $message", e)
ListenerManager.cleanStackTrace(exception, 2) // 2 because we do not want to see the stack for the abstract `newException`
listenerManager.notifyError(exception)
throw exception
}
} finally {
sendIdleStrategyHandShake.reset()
}
@ -384,24 +395,19 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
*/
// note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD
internal fun readHandshakeMessage(buffer: DirectBuffer, offset: Int, length: Int, header: Header): Any? {
try {
return try {
val message = handshakeKryo.read(buffer, offset, length)
logger.trace {
"[${header.sessionId()}] received HS: $message"
}
return message
message
} catch (e: Exception) {
// The handshake sessionId IS NOT globally unique
val sessionId = header.sessionId()
val exception = newException("[${sessionId}] Error de-serializing message", e)
ListenerManager.cleanStackTrace(exception, 2) // 2 because we do not want to see the stack for the abstract `newException`
listenerManager.notifyError(exception)
logger.error("Error de-serializing message on connection ${header.sessionId()}!", e)
return null
listenerManager.notifyError(e)
null
}
}
@ -427,11 +433,8 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
}
} catch (e: Exception) {
// The handshake sessionId IS NOT globally unique
val sessionId = header.sessionId()
val exception = newException("[${sessionId}] Error de-serializing message", e)
ListenerManager.cleanStackTrace(exception, 2) // 2 because we do not want to see the stack for the abstract `newException`
listenerManager.notifyError(connection, exception)
logger.error("[${header.sessionId()}] Error de-serializing message", e)
listenerManager.notifyError(connection, e)
return // don't do anything!
}
@ -441,7 +444,12 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
is PingMessage -> {
// the ping listener
actionDispatch.launch {
pingManager.manage(this@EndPoint, connection, message, logger)
try {
pingManager.manage(this@EndPoint, connection, message, logger)
} catch (e: Exception) {
logger.error("Error processing PING message", e)
listenerManager.notifyError(connection, e)
}
}
}
@ -452,48 +460,59 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
actionDispatch.launch {
// if we are an RMI message/registration, we have very specific, defined behavior.
// We do not use the "normal" listener callback pattern because this require special functionality
rmiGlobalSupport.manage(this@EndPoint, connection, message, logger)
try {
rmiGlobalSupport.manage(this@EndPoint, connection, message, logger)
} catch (e: Exception) {
logger.error("Error processing RMI message", e)
listenerManager.notifyError(connection, e)
}
}
}
is Any -> {
actionDispatch.launch {
@Suppress("UNCHECKED_CAST")
var hasListeners = listenerManager.notifyOnMessage(connection, message)
try {
@Suppress("UNCHECKED_CAST")
var hasListeners = listenerManager.notifyOnMessage(connection, message)
// each connection registers, and is polled INDEPENDENTLY for messages.
hasListeners = hasListeners or connection.notifyOnMessage(message)
// each connection registers, and is polled INDEPENDENTLY for messages.
hasListeners = hasListeners or connection.notifyOnMessage(message)
if (!hasListeners) {
val exception = MessageNotRegisteredException("No message callbacks found for ${message::class.java.simpleName}")
ListenerManager.cleanStackTrace(exception)
listenerManager.notifyError(connection, exception)
if (!hasListeners) {
logger.error("No message callbacks found for ${message::class.java.simpleName}")
}
} catch (e: Exception) {
logger.error("Error processing message", e)
listenerManager.notifyError(connection, e)
}
}
}
else -> {
// do nothing, there were problems with the message
val exception = if (message != null) {
MessageNotRegisteredException("No message callbacks found for ${message::class.java.simpleName}")
if (message != null) {
logger.error("No message callbacks found for ${message::class.java.simpleName}")
} else {
MessageNotRegisteredException("Unknown message received!!")
logger.error("Unknown message received!!")
}
ListenerManager.cleanStackTrace(exception)
listenerManager.notifyError(exception)
}
}
}
// NOTE: this **MUST** stay on the same co-routine that calls "send". This cannot be re-dispatched onto a different coroutine!
@Suppress("DuplicatedCode")
internal suspend fun send(message: Any, publication: Publication, connection: Connection) {
/**
* NOTE: this **MUST** stay on the same co-routine that calls "send". This cannot be re-dispatched onto a different coroutine!
*
* @return true if the message was successfully sent by aeron
*/
@Suppress("DuplicatedCode", "UNCHECKED_CAST")
internal suspend fun send(message: Any, publication: Publication, connection: Connection): Boolean {
// The handshake sessionId IS NOT globally unique
logger.trace {
"[${publication.sessionId()}] send: $message"
}
connection as CONNECTION
// since ANY thread can call 'send', we have to take kryo instances in a safe way
val kryo: KryoExtra = serialization.takeKryo()
val kryo: KryoExtra<CONNECTION> = serialization.takeKryo()
try {
val buffer = kryo.write(connection, message)
val objectSize = buffer.position()
@ -504,7 +523,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
result = publication.offer(internalBuffer, 0, objectSize)
if (result >= 0) {
// success!
return
return true
}
/**
@ -530,7 +549,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
// done executing. If the connection is *closed* first (because an RMI method closed it), then we will not be able to
// send the message.
// NOTE: we already know the connection is closed. we closed it (so it doesn't make sense to emit an error about this)
return
return false
}
// more critical error sending the message. we shouldn't retry or anything.
@ -539,22 +558,29 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A
// either client or server. No other choices. We create an exception, because it's more useful!
val exception = newException(errorMessage)
// 2 because we do not want to see the stack for the abstract `newException`
// 2 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// +2 because we do not want to see the stack for the abstract `newException`
// +2 more because we do not need to see the "internals" for sending messages. The important part of the stack trace is
// where we see who is calling "send()"
ListenerManager.cleanStackTrace(exception, 4)
@Suppress("UNCHECKED_CAST")
listenerManager.notifyError(connection as CONNECTION, exception)
return
logger.error("Aeron error!", exception)
listenerManager.notifyError(connection, exception)
}
} catch (e: Exception) {
logger.error("[${publication.sessionId()}] Error serializing message $message", e)
if (message is MethodResponse && message.result is Exception) {
val result = message.result as Exception
logger.error("[${publication.sessionId()}] Error serializing message $message", result)
listenerManager.notifyError(connection, result)
} else {
logger.error("[${publication.sessionId()}] Error serializing message $message", e)
listenerManager.notifyError(connection, e)
}
} finally {
sendIdleStrategy.reset()
serialization.returnKryo(kryo)
}
return false
}

View File

@ -24,37 +24,34 @@ import dorkbox.util.collections.IdentityMap
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import mu.KLogger
import net.jodah.typetools.TypeResolver
import java.io.IOException
/**
* Manages all of the different connect/disconnect/etc listeners
*/
internal class ListenerManager<CONNECTION: Connection> {
internal class ListenerManager<CONNECTION: Connection>(private val logger: KLogger) {
companion object {
/**
* Specifies the load-factor for the IdentityMap used to manage keeping track of the number of connections + listeners
*/
val LOAD_FACTOR = OS.getFloat(ListenerManager::class.qualifiedName + "LOAD_FACTOR", 0.8f)
/**
* Remove from the stacktrace EVERYTHING except the message. This is for propagating internal errors
*
* Neither of these are useful in resolving exception handling from a users perspective, and only clutter the stacktrace.
*/
fun noStackTrace(throwable: Throwable) {
// keep just one, since it's a stack frame INSIDE our network library, and we need that!
throwable.stackTrace = throwable.stackTrace.copyOfRange(0, 1)
}
/**
* Remove from the stacktrace kotlin coroutine info + dorkbox network call stack. This is NOT used by RMI
*
* Neither of these are useful in resolving exception handling from a users perspective, and only clutter the stacktrace.
*/
fun cleanStackTrace(throwable: Throwable, adjustedStartOfStack: Int = 0) {
// we never care about coroutine stacks, so filter then to start with
val stackTrace = throwable.stackTrace.filterNot {
// we never care about coroutine stacks, so filter then to start with.
val origStackTrace = throwable.stackTrace
val size = origStackTrace.size
if (size == 0) {
return
}
val stackTrace = origStackTrace.filterNot {
val stackName = it.className
stackName.startsWith("kotlinx.coroutines.") ||
stackName.startsWith("kotlin.coroutines.")
@ -67,7 +64,9 @@ internal class ListenerManager<CONNECTION: Connection> {
var newStartIndex = adjustedStartOfStack
// sometimes we want to see the VERY first invocation, but not always
val savedFirstStack = if (stackTrace[newStartIndex].methodName == "invokeSuspend") {
val savedFirstStack =
if (newEndIndex > 1 && newStartIndex < newEndIndex && // this fixes some out-of-bounds errors that can potentially occur
stackTrace[newStartIndex].methodName == "invokeSuspend") {
newStartIndex++
stackTrace.copyOfRange(adjustedStartOfStack, newStartIndex)
} else {
@ -105,7 +104,13 @@ internal class ListenerManager<CONNECTION: Connection> {
fun cleanStackTraceInternal(throwable: Throwable) {
// NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace
val stackTrace = throwable.stackTrace
var newEndIndex = stackTrace.size -1 // offset by 1 because we have to adjust for the access index
val size = stackTrace.size
if (size == 0) {
return
}
var newEndIndex = size -1 // offset by 1 because we have to adjust for the access index
for (i in newEndIndex downTo 0) {
val stackName = stackTrace[i].className
@ -325,7 +330,7 @@ internal class ListenerManager<CONNECTION: Connection> {
// remote address will NOT be null at this stage, but best to verify.
val remoteAddress = connection.remoteAddress
if (remoteAddress == null) {
notifyError(connection, IOException("Unable to attempt connection stages when no remote address is present"))
logger.error("Connection ${connection.id}: Unable to attempt connection stages when no remote address is present")
return false
}
@ -356,7 +361,7 @@ internal class ListenerManager<CONNECTION: Connection> {
} catch (t: Throwable) {
// NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace
cleanStackTrace(t)
notifyError(connection, t)
logger.error("Connection ${connection.id} error", t)
}
}
}
@ -371,7 +376,7 @@ internal class ListenerManager<CONNECTION: Connection> {
} catch (t: Throwable) {
// NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace
cleanStackTrace(t)
notifyError(connection, t)
logger.error("Connection ${connection.id} error", t)
}
}
}
@ -435,7 +440,7 @@ internal class ListenerManager<CONNECTION: Connection> {
func(connection, message)
} catch (t: Throwable) {
cleanStackTrace(t)
notifyError(connection, t)
logger.error("Connection ${connection.id} error", t)
}
}
}
@ -454,7 +459,7 @@ internal class ListenerManager<CONNECTION: Connection> {
} catch (t: Throwable) {
// NOTE: when we remove stuff, we ONLY want to remove the "tail" of the stacktrace, not ALL parts of the stacktrace
cleanStackTrace(t)
notifyError(connection, t)
logger.error("Connection ${connection.id} error", t)
}
}
}

View File

@ -15,19 +15,23 @@
*/
package dorkbox.network.handshake
import dorkbox.network.Client
import dorkbox.network.aeron.MediaDriverConnection
import dorkbox.network.connection.Connection
import dorkbox.network.connection.CryptoManagement
import dorkbox.network.connection.EndPoint
import dorkbox.network.connection.ListenerManager
import dorkbox.network.exceptions.ClientException
import dorkbox.network.exceptions.ClientTimedOutException
import io.aeron.FragmentAssembler
import io.aeron.logbuffer.FragmentHandler
import io.aeron.logbuffer.Header
import mu.KLogger
import org.agrona.DirectBuffer
internal class ClientHandshake<CONNECTION: Connection>(private val crypto: CryptoManagement, private val endPoint: EndPoint<CONNECTION>) {
internal class ClientHandshake<CONNECTION: Connection>(
private val crypto: CryptoManagement,
private val endPoint: Client<CONNECTION>,
private val logger: KLogger
) {
// @Volatile is used BECAUSE suspension of coroutines can continue on a DIFFERENT thread. We want to make sure that thread visibility is
// correct when this happens. There are no race-conditions to be wary of.
@ -48,7 +52,10 @@ internal class ClientHandshake<CONNECTION: Connection>(private val crypto: Crypt
private var needToRetry = false
@Volatile
private var failed: Exception? = null
private var failedMessage: String = ""
@Volatile
private var failed: Boolean = true
init {
// now we have a bi-directional connection with the server on the handshake "socket".
@ -60,17 +67,15 @@ internal class ClientHandshake<CONNECTION: Connection>(private val crypto: Crypt
// it must be a registration message
if (message !is HandshakeMessage) {
val exception = ClientException("[$sessionId] cancelled handshake for unrecognized message: $message")
ListenerManager.noStackTrace(exception)
failed = exception
failedMessage = "[$sessionId] cancelled handshake for unrecognized message: $message"
failed = true
return@FragmentAssembler
}
// this is an error message
if (message.state == HandshakeMessage.INVALID) {
val exception = ClientException("[$sessionId] cancelled handshake for error: ${message.errorMessage}")
ListenerManager.noStackTrace(exception)
failed = exception
failedMessage = "[$sessionId] cancelled handshake for error: ${message.errorMessage}"
failed = true
return@FragmentAssembler
}
@ -82,9 +87,7 @@ internal class ClientHandshake<CONNECTION: Connection>(private val crypto: Crypt
}
if (oneTimeKey != message.oneTimeKey) {
val exception = ClientException("[$message.sessionId] ignored message (one-time key: ${message.oneTimeKey}) intended for another client (mine is: ${oneTimeKey})")
ListenerManager.noStackTrace(exception)
endPoint.listenerManager.notifyError(exception)
logger.error("[$message.sessionId] ignored message (one-time key: ${message.oneTimeKey}) intended for another client (mine is: ${oneTimeKey})")
return@FragmentAssembler
}
@ -100,9 +103,8 @@ internal class ClientHandshake<CONNECTION: Connection>(private val crypto: Crypt
if (registrationData != null && serverPublicKeyBytes != null) {
connectionHelloInfo = crypto.decrypt(registrationData, serverPublicKeyBytes)
} else {
val exception = ClientException("[$message.sessionId] canceled handshake for message without registration and/or public key info")
ListenerManager.noStackTrace(exception)
failed = exception
failedMessage = "[$message.sessionId] canceled handshake for message without registration and/or public key info"
failed = true
}
}
HandshakeMessage.HELLO_ACK_IPC -> {
@ -125,18 +127,16 @@ internal class ClientHandshake<CONNECTION: Connection>(private val crypto: Crypt
publicationPort = streamPubId,
kryoRegistrationDetails = regDetails)
} else {
val exception = ClientException("[$message.sessionId] canceled handshake for message without registration data")
ListenerManager.noStackTrace(exception)
failed = exception
failedMessage = "[$message.sessionId] canceled handshake for message without registration data"
failed = true
}
}
HandshakeMessage.DONE_ACK -> {
connectionDone = true
}
else -> {
val exception = ClientException("[$sessionId] cancelled handshake for message that is ${HandshakeMessage.toStateString(message.state)}")
ListenerManager.noStackTrace(exception)
failed = exception
failedMessage = "[$sessionId] cancelled handshake for message that is ${HandshakeMessage.toStateString(message.state)}"
failed = true
}
}
}
@ -144,7 +144,7 @@ internal class ClientHandshake<CONNECTION: Connection>(private val crypto: Crypt
// called from the connect thread
fun handshakeHello(handshakeConnection: MediaDriverConnection, connectionTimeoutMS: Long) : ClientConnectionInfo {
failed = null
failed = false
oneTimeKey = endPoint.crypto.secureRandom.nextInt()
val publicKey = endPoint.storage.getPublicKey()!!
@ -153,8 +153,12 @@ internal class ClientHandshake<CONNECTION: Connection>(private val crypto: Crypt
val subscription = handshakeConnection.subscription
val pollIdleStrategy = endPoint.pollIdleStrategyHandShake
endPoint.writeHandshakeMessage(publication, HandshakeMessage.helloFromClient(oneTimeKey, publicKey))
try {
endPoint.writeHandshakeMessage(publication, HandshakeMessage.helloFromClient(oneTimeKey, publicKey))
} catch (e: Exception) {
logger.error("Handshake error!", e)
throw e
}
// block until we receive the connection information from the server
var pollCount: Int
@ -165,14 +169,7 @@ internal class ClientHandshake<CONNECTION: Connection>(private val crypto: Crypt
// `.poll(handler, 4)` == `.poll(handler, 2)` + `.poll(handler, 2)`
pollCount = subscription.poll(handler, 1)
if (failed != null) {
// no longer necessary to hold this connection open
handshakeConnection.close()
throw failed as Exception
}
if (connectionHelloInfo != null) {
// we close the handshake connection after the DONE message
if (failed || connectionHelloInfo != null) {
break
}
@ -180,8 +177,13 @@ internal class ClientHandshake<CONNECTION: Connection>(private val crypto: Crypt
pollIdleStrategy.idle(pollCount)
}
if (failed) {
// no longer necessary to hold this connection open (if not a failure, we close the handshake after the DONE message)
handshakeConnection.close()
throw ClientException(failedMessage)
}
if (connectionHelloInfo == null) {
// no longer necessary to hold this connection open
// no longer necessary to hold this connection open (if not a failure, we close the handshake after the DONE message)
handshakeConnection.close()
throw ClientTimedOutException("Waiting for registration response from server")
}
@ -194,12 +196,16 @@ internal class ClientHandshake<CONNECTION: Connection>(private val crypto: Crypt
val registrationMessage = HandshakeMessage.doneFromClient(oneTimeKey)
// Send the done message to the server.
endPoint.writeHandshakeMessage(handshakeConnection.publication, registrationMessage)
try {
endPoint.writeHandshakeMessage(handshakeConnection.publication, registrationMessage)
} catch (e: Exception) {
logger.error("Handshake error!", e)
return false
}
// block until we receive the connection information from the server
failed = null
failed = false
var pollCount: Int
val subscription = handshakeConnection.subscription
val pollIdleStrategy = endPoint.pollIdleStrategyHandShake
@ -210,10 +216,8 @@ internal class ClientHandshake<CONNECTION: Connection>(private val crypto: Crypt
// `.poll(handler, 4)` == `.poll(handler, 2)` + `.poll(handler, 2)`
pollCount = subscription.poll(handler, 1)
if (failed != null) {
// no longer necessary to hold this connection open
handshakeConnection.close()
throw failed as Exception
if (failed || connectionDone) {
break
}
if (needToRetry) {
@ -223,17 +227,16 @@ internal class ClientHandshake<CONNECTION: Connection>(private val crypto: Crypt
startTime = System.currentTimeMillis()
}
if (connectionDone) {
break
}
// 0 means we idle. >0 means reset and don't idle (because there are likely more)
pollIdleStrategy.idle(pollCount)
}
// no longer necessary to hold this connection open
// finished with the handshake, so always close the connection
handshakeConnection.close()
if (failed) {
throw ClientException(failedMessage)
}
if (!connectionDone) {
throw ClientTimedOutException("Waiting for registration response from server")
}

View File

@ -70,7 +70,6 @@ class PortAllocator(basePort: Int, numberOfPortsToAllocate: Int) {
*
* @throws PortAllocationException If there are fewer than `count` ports available to allocate
*/
@Throws(IllegalArgumentException::class)
fun allocate(count: Int): IntArray {
if (freePorts.size < count) {
throw IllegalArgumentException("Too few ports available to allocate $count ports")

View File

@ -15,21 +15,20 @@
*/
package dorkbox.network.handshake
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.RemovalCause
import com.github.benmanes.caffeine.cache.RemovalListener
import dorkbox.network.Server
import dorkbox.network.ServerConfiguration
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.aeron.IpcMediaDriverConnection
import dorkbox.network.aeron.UdpMediaDriverPairedConnection
import dorkbox.network.connection.*
import dorkbox.network.exceptions.*
import dorkbox.network.exceptions.AllocationException
import dorkbox.network.rmi.RmiManagerConnections
import io.aeron.Publication
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking
import mu.KLogger
import net.jodah.expiringmap.ExpirationPolicy
import net.jodah.expiringmap.ExpiringMap
import java.net.Inet4Address
import java.net.InetAddress
import java.util.concurrent.TimeUnit
@ -44,21 +43,19 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
private val listenerManager: ListenerManager<CONNECTION>) {
// note: the expire time here is a LITTLE longer than the expire time in the client, this way we can adjust for network lag if it's close
private val pendingConnections: Cache<Int, CONNECTION> = Caffeine.newBuilder()
.expireAfterAccess(config.connectionCloseTimeoutInSeconds.toLong() * 2, TimeUnit.SECONDS)
.removalListener(RemovalListener<Int, CONNECTION> { sessionId, connection, cause ->
if (cause == RemovalCause.EXPIRED) {
connection!!
private val pendingConnections = ExpiringMap.builder()
.expiration(config.connectionCloseTimeoutInSeconds.toLong() * 2, TimeUnit.SECONDS)
.expirationPolicy(ExpirationPolicy.CREATED)
.expirationListener<Int, CONNECTION> { _, connection ->
// this blocks until it fully runs (which is ok. this is fast)
logger.error("[${connection.id}] Timed out waiting for registration response from client")
val exception = ClientTimedOutException("[${connection.id}] Waiting for registration response from client")
ListenerManager.noStackTrace(exception)
listenerManager.notifyError(exception)
runBlocking {
connection.close()
}
runBlocking {
connection.close()
}
}).build()
}
.build<Int, CONNECTION>()
private val connectionsPerIpCounts = ConnectionCounts()
@ -82,14 +79,16 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
// this can happen if there are multiple connections from the SAME ip address (ie: localhost)
if (message.state == HandshakeMessage.HELLO) {
// this should be null.
val hasExistingSessionId = pendingConnections.getIfPresent(sessionId) != null
val hasExistingSessionId = pendingConnections[sessionId] != null
if (hasExistingSessionId) {
// WHOOPS! tell the client that it needs to retry, since a DIFFERENT client has a handshake in progress with the same sessionId
val exception = ClientException("[$sessionId] Connection from $connectionString had an in-use session ID! Telling client to retry.")
ListenerManager.noStackTrace(exception)
listenerManager.notifyError(exception)
logger.error("[$sessionId] Connection from $connectionString had an in-use session ID! Telling client to retry.")
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.retry("Handshake already in progress for sessionID!"))
try {
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.retry("Handshake already in progress for sessionID!"))
} catch (e: Error) {
logger.error("Handshake error!", e)
}
return false
}
@ -98,13 +97,11 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
// check to see if this is a pending connection
if (message.state == HandshakeMessage.DONE) {
val pendingConnection = pendingConnections.getIfPresent(sessionId)
pendingConnections.invalidate(sessionId)
val pendingConnection = pendingConnections[sessionId]
pendingConnections.remove(sessionId)
if (pendingConnection == null) {
val exception = ServerException("[$sessionId] Error! Connection from client $connectionString was null, and cannot complete handshake!")
ListenerManager.noStackTrace(exception)
listenerManager.notifyError(exception)
logger.error("[$sessionId] Error! Connection from client $connectionString was null, and cannot complete handshake!")
} else {
logger.trace { "[${pendingConnection.id}] Connection from client $connectionString done with handshake." }
@ -112,11 +109,15 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
server.addConnection(pendingConnection)
// now tell the client we are done
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.doneToClient(message.oneTimeKey, sessionId))
try {
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.doneToClient(message.oneTimeKey, sessionId))
// this always has to be on event dispatch, otherwise we can have weird logic loops if we reconnect within a disconnect callback
actionDispatch.eventLoop {
listenerManager.notifyConnect(pendingConnection)
// this always has to be on event dispatch, otherwise we can have weird logic loops if we reconnect within a disconnect callback
actionDispatch.eventLoop {
listenerManager.notifyConnect(pendingConnection)
}
} catch (e: Exception) {
logger.error("Handshake error!", e)
}
}
@ -139,11 +140,13 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
try {
// VALIDATE:: Check to see if there are already too many clients connected.
if (server.connections.connectionCount() >= config.maxClientCount) {
val exception = ClientRejectedException("Connection from $clientAddressString not allowed! Server is full. Max allowed is ${config.maxClientCount}")
ListenerManager.noStackTrace(exception)
listenerManager.notifyError(exception)
logger.error("Connection from $clientAddressString not allowed! Server is full. Max allowed is ${config.maxClientCount}")
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Server is full"))
try {
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Server is full"))
} catch (e: Exception) {
logger.error("Handshake error!", e)
}
return false
}
@ -154,21 +157,24 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
// decrement it now, since we aren't going to permit this connection (take the extra decrement hit on failure, instead of always)
connectionsPerIpCounts.decrement(clientAddress, currentCountForIp)
val exception = ClientRejectedException("Too many connections for IP address $clientAddressString. Max allowed is ${config.maxConnectionsPerIpAddress}")
ListenerManager.noStackTrace(exception)
listenerManager.notifyError(exception)
logger.error("Too many connections for IP address $clientAddressString. Max allowed is ${config.maxConnectionsPerIpAddress}")
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Too many connections for IP address"))
try {
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Too many connections for IP address"))
} catch (e: Exception) {
logger.error("Handshake error!", e)
}
return false
}
connectionsPerIpCounts.increment(clientAddress, currentCountForIp)
} catch (e: Exception) {
val exception = ClientRejectedException("could not validate client message", e)
ListenerManager.noStackTrace(exception)
listenerManager.notifyError(exception)
logger.error("could not validate client message", e)
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Invalid connection"))
return false
try {
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Invalid connection"))
} catch (e: Exception) {
logger.error("Handshake error!", e)
}
}
return true
@ -177,6 +183,7 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
// note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD
fun processIpcHandshakeMessageServer(server: Server<CONNECTION>,
rmiConnectionSupport: RmiManagerConnections<CONNECTION>,
handshakePublication: Publication,
sessionId: Int,
message: HandshakeMessage,
@ -202,11 +209,13 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
try {
connectionSessionId = sessionIdAllocator.allocate()
} catch (e: AllocationException) {
val exception = ClientRejectedException("Connection from $connectionString not allowed! Unable to allocate a session ID for the client connection!")
ListenerManager.noStackTrace(exception)
listenerManager.notifyError(exception)
logger.error("Connection from $connectionString not allowed! Unable to allocate a session ID for the client connection!")
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Connection error!"))
try {
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Connection error!"))
} catch (e: Exception) {
logger.error("Handshake error!", e)
}
return
}
@ -218,11 +227,13 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
// have to unwind actions!
sessionIdAllocator.free(connectionSessionId)
val exception = ClientRejectedException("Connection from $connectionString not allowed! Unable to allocate a stream ID for the client connection!")
ListenerManager.noStackTrace(exception)
listenerManager.notifyError(exception)
logger.error("Connection from $connectionString not allowed! Unable to allocate a stream ID for the client connection!")
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Connection error!"))
try {
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Connection error!"))
} catch (e: Exception) {
logger.error("Handshake error!", e)
}
return
}
@ -234,11 +245,13 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
sessionIdAllocator.free(connectionSessionId)
sessionIdAllocator.free(connectionStreamPubId)
val exception = ClientRejectedException("Connection from $connectionString not allowed! Unable to allocate a stream ID for the client connection!")
ListenerManager.noStackTrace(exception)
listenerManager.notifyError(exception)
logger.error("Connection from $connectionString not allowed! Unable to allocate a stream ID for the client connection!")
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Connection error!"))
try {
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Connection error!"))
} catch (e: Exception) {
logger.error("Handshake error!", e)
}
return
}
@ -256,7 +269,7 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
"[${clientConnection.sessionId}] IPC connection established to [${clientConnection.streamIdSubscription}|${clientConnection.streamId}]"
}
val connection = server.newConnection(ConnectionParams(server, clientConnection, PublicKeyValidationState.VALID))
val connection = server.newConnection(ConnectionParams(server, clientConnection, PublicKeyValidationState.VALID, rmiConnectionSupport))
// VALIDATE:: are we allowed to connect to this server (now that we have the initial server information)
// NOTE: all IPC client connections are, by default, always allowed to connect, because they are running on the same machine
@ -293,20 +306,19 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
pendingConnections.put(sessionId, connection)
// this tells the client all of the info to connect.
server.writeHandshakeMessage(handshakePublication, successMessage)
server.writeHandshakeMessage(handshakePublication, successMessage) // exception is already caught!
} catch (e: Exception) {
// have to unwind actions!
sessionIdAllocator.free(connectionSessionId)
streamIdAllocator.free(connectionStreamPubId)
val exception = ServerException("Connection handshake from $connectionString crashed! Message $message", e)
ListenerManager.noStackTrace(exception)
listenerManager.notifyError(exception)
logger.error("Connection handshake from $connectionString crashed! Message $message", e)
}
}
// note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD
fun processUdpHandshakeMessageServer(server: Server<CONNECTION>,
rmiConnectionSupport: RmiManagerConnections<CONNECTION>,
handshakePublication: Publication,
sessionId: Int,
clientAddressString: String,
@ -326,9 +338,7 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
// VALIDATE:: check to see if the remote connection's public key has changed!
validateRemoteAddress = server.crypto.validateRemoteAddress(clientAddress, clientPublicKeyBytes)
if (validateRemoteAddress == PublicKeyValidationState.INVALID) {
val exception = ClientRejectedException("Connection from $clientAddressString not allowed! Public key mismatch.")
ListenerManager.noStackTrace(exception)
listenerManager.notifyError(exception)
logger.error("Connection from $clientAddressString not allowed! Public key mismatch.")
return
}
@ -354,11 +364,13 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
// have to unwind actions!
connectionsPerIpCounts.decrementSlow(clientAddress)
val exception = ClientRejectedException("Connection from $clientAddressString not allowed! Unable to allocate a session ID for the client connection!")
ListenerManager.noStackTrace(exception)
listenerManager.notifyError(exception)
logger.error("Connection from $clientAddressString not allowed! Unable to allocate a session ID for the client connection!")
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Connection error!"))
try {
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Connection error!"))
} catch (e: Exception) {
logger.error("Handshake error!", e)
}
return
}
@ -371,11 +383,13 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
connectionsPerIpCounts.decrementSlow(clientAddress)
sessionIdAllocator.free(connectionSessionId)
val exception = ClientRejectedException("Connection from $clientAddressString not allowed! Unable to allocate a stream ID for the client connection!")
ListenerManager.noStackTrace(exception)
listenerManager.notifyError(exception)
logger.error("Connection from $clientAddressString not allowed! Unable to allocate a stream ID for the client connection!")
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Connection error!"))
try {
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Connection error!"))
} catch (e: Exception) {
logger.error("Handshake error!", e)
}
return
}
@ -413,7 +427,7 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
"Creating new connection from $clientAddressString [$subscriptionPort|$publicationPort] [$connectionStreamId|$connectionSessionId] (reliable:${message.isReliable})"
}
val connection = server.newConnection(ConnectionParams(server, clientConnection, validateRemoteAddress))
val connection = server.newConnection(ConnectionParams(server, clientConnection, validateRemoteAddress, rmiConnectionSupport))
// VALIDATE:: are we allowed to connect to this server (now that we have the initial server information)
val permitConnection = listenerManager.notifyFilter(connection)
@ -423,11 +437,13 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
sessionIdAllocator.free(connectionSessionId)
streamIdAllocator.free(connectionStreamId)
val exception = ClientRejectedException("Connection $clientAddressString was not permitted!")
ListenerManager.cleanStackTrace(exception)
listenerManager.notifyError(connection, exception)
logger.error("Connection $clientAddressString was not permitted!")
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Connection was not permitted!"))
try {
server.writeHandshakeMessage(handshakePublication, HandshakeMessage.error("Connection was not permitted!"))
} catch (e: Exception) {
logger.error("Handshake error!", e)
}
return
}
@ -458,16 +474,14 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
pendingConnections.put(sessionId, connection)
// this tells the client all of the info to connect.
server.writeHandshakeMessage(handshakePublication, successMessage)
server.writeHandshakeMessage(handshakePublication, successMessage) // exception is already caught
} catch (e: Exception) {
// have to unwind actions!
connectionsPerIpCounts.decrementSlow(clientAddress)
sessionIdAllocator.free(connectionSessionId)
streamIdAllocator.free(connectionStreamId)
val exception = ServerException("Connection handshake from $clientAddressString crashed! Message $message", e)
ListenerManager.noStackTrace(exception)
listenerManager.notifyError(exception)
logger.error("Connection handshake from $clientAddressString crashed! Message $message", e)
}
}
@ -487,7 +501,6 @@ internal class ServerHandshake<CONNECTION : Connection>(private val logger: KLog
sessionIdAllocator.clear()
streamIdAllocator.clear()
pendingConnections.invalidateAll()
pendingConnections.cleanUp()
pendingConnections.clear()
}
}

View File

@ -59,7 +59,7 @@ class PingManager<CONNECTION : Connection>(logger: KLogger, actionDispatch: Coro
// }
}
suspend fun ping(function1: Connection, function: suspend Ping.() -> Unit) {
suspend fun ping(function1: Connection, function: suspend Ping.() -> Unit): Boolean {
// val ping = PingMessage()
// ping.id = pingIdAllocator.allocate()
//
@ -86,6 +86,7 @@ class PingManager<CONNECTION : Connection>(logger: KLogger, actionDispatch: Coro
//// ping0(ping)
//// return pingFuture!!
// TODO()
return false
}
}

View File

@ -59,7 +59,7 @@ import kotlin.concurrent.write
*
* @author Nathan Robinson
*/
class RemoteObjectStorage(val logger: KLogger) {
internal class RemoteObjectStorage(val logger: KLogger) {
companion object {
const val INVALID_RMI = 0
@ -206,7 +206,7 @@ class RemoteObjectStorage(val logger: KLogger) {
objectMap.put(nextObjectId, `object`)
logger.trace {
"Object <proxy #$nextObjectId> registered with .toString() = '${`object`}'"
"Remote object <proxy:$nextObjectId> registered with .toString() = '${`object`}'"
}
}
@ -226,7 +226,7 @@ class RemoteObjectStorage(val logger: KLogger) {
objectMap.put(objectId, `object`)
logger.trace {
"Object <proxy #${objectId}> registered with .toString() = '${`object`}'"
"Remote object <proxy:$objectId> registered with .toString() = '${`object`}'"
}
return true
@ -243,7 +243,7 @@ class RemoteObjectStorage(val logger: KLogger) {
returnId(objectId)
logger.trace {
"Object <proxy #${objectId}> removed with .toString() = '${rmiObject}'"
"Object <proxy #${objectId}> removed"
}
@Suppress("UNCHECKED_CAST")
return rmiObject

View File

@ -32,7 +32,7 @@ import kotlin.concurrent.write
*
* response ID's and the memory they hold will leak if the response never arrives!
*/
internal class ResponseManager(private val logger: KLogger, private val actionDispatch: CoroutineScope) {
class ResponseManager(private val logger: KLogger, private val actionDispatch: CoroutineScope) {
companion object {
val TIMEOUT_EXCEPTION = Exception()
}

View File

@ -17,7 +17,7 @@ package dorkbox.network.rmi
import kotlinx.coroutines.channels.Channel
internal data class ResponseWaiter(val id: Int) {
data class ResponseWaiter(val id: Int) {
// this is bi-directional waiting. The method names to not reflect this, however there is no possibility of race conditions w.r.t. waiting
// https://stackoverflow.com/questions/55421710/how-to-suspend-kotlin-coroutine-until-notified
// https://kotlinlang.org/docs/reference/coroutines/channels.html
@ -46,14 +46,14 @@ internal data class ResponseWaiter(val id: Int) {
suspend fun doNotify() {
try {
channel.send(Unit)
} catch (ignored: Exception) {
} catch (ignored: Throwable) {
}
}
suspend fun doWait() {
try {
channel.receive()
} catch (ignored: Exception) {
} catch (ignored: Throwable) {
}
}
@ -61,7 +61,7 @@ internal data class ResponseWaiter(val id: Int) {
try {
isCancelled = true
channel.cancel()
} catch (ignored: Exception) {
} catch (ignored: Throwable) {
}
}
}

View File

@ -16,49 +16,75 @@
package dorkbox.network.rmi
import dorkbox.network.connection.Connection
import dorkbox.network.connection.EndPoint
import dorkbox.network.connection.ListenerManager
import dorkbox.network.rmi.messages.ConnectionObjectCreateRequest
import dorkbox.network.rmi.messages.ConnectionObjectCreateResponse
import dorkbox.network.rmi.messages.ConnectionObjectDeleteRequest
import dorkbox.network.rmi.messages.ConnectionObjectDeleteResponse
import dorkbox.network.serialization.Serialization
import dorkbox.util.classes.ClassHelper
import dorkbox.util.collections.LockFreeIntMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mu.KLogger
internal class RmiManagerConnections<CONNECTION: Connection>(logger: KLogger,
val rmiGlobalSupport: RmiManagerGlobal<CONNECTION>,
private val serialization: Serialization) : RmiObjectCache(logger) {
class RmiManagerConnections<CONNECTION: Connection> internal constructor(
private val logger: KLogger,
private val responseManager: ResponseManager,
private val listenerManager: ListenerManager<CONNECTION>,
private val serialization: Serialization<CONNECTION>,
private val getGlobalAction: (connection: CONNECTION, objectId: Int, interfaceClass: Class<*>) -> Any
) : RmiObjectCache(logger) {
// It is critical that all of the RMI proxy objects are unique, and are saved/cached PER CONNECTION. These cannot be shared between connections!
private val proxyObjects = LockFreeIntMap<RemoteObject>()
// callbacks for when a REMOTE object has been created
private val remoteObjectCreationCallbacks = RemoteObjectStorage(logger)
/**
* Removes a proxy object from the system
*
* @return true if it successfully removed the object
*/
fun removeProxyObject(rmiId: Int) {
proxyObjects.remove(rmiId)
fun removeProxyObject(rmiId: Int): Boolean {
return proxyObjects.remove(rmiId) != null
}
fun getProxyObject(rmiId: Int): RemoteObject? {
private fun getProxyObject(rmiId: Int): RemoteObject? {
return proxyObjects[rmiId]
}
fun saveProxyObject(rmiId: Int, remoteObject: RemoteObject) {
private fun saveProxyObject(rmiId: Int, remoteObject: RemoteObject) {
proxyObjects.put(rmiId, remoteObject)
}
internal fun <Iface> registerCallback(callback: suspend Iface.() -> Unit): Int {
return remoteObjectCreationCallbacks.register(callback)
}
private fun removeCallback(callbackId: Int): suspend Any.() -> Unit {
// callback's area always correct, because we track them ourselves.
return remoteObjectCreationCallbacks.remove(callbackId)!!
}
/**
* on the connection+client to get a connection-specific remote object (that exists on the server/client)
*/
fun <Iface> getProxyObject(connection: Connection, kryoId: Int, rmiId: Int, interfaceClass: Class<Iface>): Iface {
fun <Iface> getProxyObject(isGlobal: Boolean, connection: CONNECTION, rmiId: Int, interfaceClass: Class<Iface>): Iface {
require(interfaceClass.isInterface) { "iface must be an interface." }
// so we can just instantly create the proxy object (or get the cached one)
var proxyObject = getProxyObject(rmiId)
if (proxyObject == null) {
proxyObject = RmiManagerGlobal.createProxyObject(false,
val kryoId = connection.endPoint.serialization.getKryoIdForRmiClient(interfaceClass)
proxyObject = RmiManagerGlobal.createProxyObject(isGlobal,
connection,
serialization,
rmiGlobalSupport.responseManager,
responseManager,
kryoId,
rmiId,
interfaceClass)
@ -71,17 +97,35 @@ internal class RmiManagerConnections<CONNECTION: Connection>(logger: KLogger,
}
/**
* on the "client" to remove a connection-specific remote object (that exists on the server)
* on the connection+client to get a connection-specific remote object (that exists on the server/client)
*/
fun <Iface> deleteRemoteObject(connection: Connection, rmiId: Int) {
removeProxyObject(rmiId)
fun <Iface> getProxyObject(isGlobal: Boolean, connection: CONNECTION, kryoId: Int, rmiId: Int, interfaceClass: Class<Iface>): Iface {
require(interfaceClass.isInterface) { "iface must be an interface." }
// so we can just instantly create the proxy object (or get the cached one)
var proxyObject = getProxyObject(rmiId)
if (proxyObject == null) {
proxyObject = RmiManagerGlobal.createProxyObject(isGlobal,
connection,
serialization,
responseManager,
kryoId,
rmiId,
interfaceClass)
saveProxyObject(rmiId, proxyObject)
}
// this immediately returns BECAUSE the object must have already been created on the server (this is why we specify the rmiId)!
@Suppress("UNCHECKED_CAST")
return proxyObject as Iface
}
/**
* on the "client" to create a connection-specific remote object (that exists on the server)
*/
suspend fun <Iface> createRemoteObject(connection: Connection, kryoId: Int, objectParameters: Array<Any?>?, callback: suspend Iface.() -> Unit) {
val callbackId = rmiGlobalSupport.registerCallback(callback)
suspend fun <Iface> createRemoteObject(connection: CONNECTION, kryoId: Int, objectParameters: Array<Any?>?, callback: suspend Iface.() -> Unit) {
val callbackId = registerCallback(callback)
// There is no rmiID yet, because we haven't created it!
val message = ConnectionObjectCreateRequest(RmiUtils.packShorts(callbackId, kryoId), objectParameters)
@ -96,12 +140,15 @@ internal class RmiManagerConnections<CONNECTION: Connection>(logger: KLogger,
/**
* called on "server"
*/
suspend fun onConnectionObjectCreateRequest(endPoint: EndPoint<CONNECTION>, connection: CONNECTION, message: ConnectionObjectCreateRequest) {
fun onConnectionObjectCreateRequest(
serialization: Serialization<CONNECTION>,
connection: CONNECTION,
message: ConnectionObjectCreateRequest,
actionDispatch: CoroutineScope
) {
val callbackId = RmiUtils.unpackLeft(message.packedIds)
val kryoId = RmiUtils.unpackRight(message.packedIds)
val objectParameters = message.objectParameters
val serialization = endPoint.serialization
// We have to lookup the iface, since the proxy object requires it
val implObject = serialization.createRmiObject(kryoId, objectParameters)
@ -109,25 +156,126 @@ internal class RmiManagerConnections<CONNECTION: Connection>(logger: KLogger,
val response = if (implObject is Exception) {
// whoops!
ListenerManager.cleanStackTrace(implObject)
endPoint.listenerManager.notifyError(connection, implObject)
logger.error("RMI error connection ${connection.id}", implObject)
listenerManager.notifyError(connection, implObject)
ConnectionObjectCreateResponse(RmiUtils.packShorts(callbackId, RemoteObjectStorage.INVALID_RMI))
} else {
val rmiId = saveImplObject(implObject)
if (rmiId == RemoteObjectStorage.INVALID_RMI) {
val exception = NullPointerException("Trying to create an RMI object with the INVALID_RMI id!!")
ListenerManager.cleanStackTrace(exception)
endPoint.listenerManager.notifyError(connection, exception)
logger.error("RMI error connection ${connection.id}", exception)
listenerManager.notifyError(connection, exception)
}
ConnectionObjectCreateResponse(RmiUtils.packShorts(callbackId, rmiId))
}
// we send the message ALWAYS, because the client needs to know it worked or not
connection.send(response)
actionDispatch.launch {
// we send the message ALWAYS, because the client needs to know it worked or not
connection.send(response)
}
}
fun clearProxyObjects() {
/**
* called on "client"
*/
fun onConnectionObjectCreateResponse(
connection: CONNECTION,
message: ConnectionObjectCreateResponse,
actionDispatch: CoroutineScope
) {
val callbackId = RmiUtils.unpackLeft(message.packedIds)
val rmiId = RmiUtils.unpackRight(message.packedIds)
// we only create the proxy + execute the callback if the RMI id is valid!
if (rmiId == RemoteObjectStorage.INVALID_RMI) {
val exception = Exception("RMI ID '${rmiId}' is invalid. Unable to create RMI object on server.")
ListenerManager.cleanStackTrace(exception)
logger.error("RMI error connection ${connection.id}", exception)
listenerManager.notifyError(connection, exception)
return
}
val callback = removeCallback(callbackId)
val interfaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(RemoteObjectCallback::class.java, callback.javaClass, 0)
// create the client-side proxy object, if possible. This MUST be an object that is saved for the connection
val proxyObject = getProxyObject(false, connection, rmiId, interfaceClass)
// this should be executed on a NEW coroutine!
actionDispatch.launch {
try {
callback(proxyObject)
} catch (e: Exception) {
ListenerManager.cleanStackTrace(e)
logger.error("RMI error connection ${connection.id}", e)
listenerManager.notifyError(connection, e)
}
}
}
/**
* called on "client" or "server"
*/
fun onConnectionObjectDeleteRequest(
connection: CONNECTION,
message: ConnectionObjectDeleteRequest,
actionDispatch: CoroutineScope
) {
val rmiId = message.rmiId
// we only delete the impl object if the RMI id is valid!
if (rmiId == RemoteObjectStorage.INVALID_RMI) {
val exception = Exception("RMI ID '${rmiId}' is invalid. Unable to delete RMI object!")
ListenerManager.cleanStackTrace(exception)
logger.error("RMI error connection ${connection.id}", exception)
listenerManager.notifyError(connection, exception)
return
}
// it DOESN'T matter which "side" we are, just delete both (RMI id's must always represent the same object on both sides)
removeProxyObject(rmiId)
removeImplObject<Any?>(rmiId)
actionDispatch.launch {
// tell the "other side" to delete the proxy/impl object
connection.send(ConnectionObjectDeleteResponse(rmiId))
}
}
/**
* called on "client" or "server"
*/
fun onConnectionObjectDeleteResponse(connection: CONNECTION, message: ConnectionObjectDeleteResponse) {
val rmiId = message.rmiId
// we only create the proxy + execute the callback if the RMI id is valid!
if (rmiId == RemoteObjectStorage.INVALID_RMI) {
val exception = Exception("RMI ID '${rmiId}' is invalid. Unable to create RMI object on server.")
ListenerManager.cleanStackTrace(exception)
logger.error("RMI error connection ${connection.id}", exception)
listenerManager.notifyError(connection, exception)
return
}
// it DOESN'T matter which "side" we are, just delete both (RMI id's must always represent the same object on both sides)
removeProxyObject(rmiId)
removeImplObject<Any?>(rmiId)
}
fun close() {
proxyObjects.clear()
remoteObjectCreationCallbacks.close()
}
/**
* Methods supporting Remote Method Invocation and Objects. A new one is created for each connection (because the connection is different for each one)
*/
fun getNewRmiSupport(connection: Connection): RmiSupportConnection<CONNECTION> {
@Suppress("LeakingThis", "UNCHECKED_CAST")
return RmiSupportConnection(logger, connection as CONNECTION, this, serialization, getGlobalAction)
}
}

View File

@ -20,17 +20,14 @@ import dorkbox.network.connection.EndPoint
import dorkbox.network.connection.ListenerManager
import dorkbox.network.rmi.messages.*
import dorkbox.network.serialization.Serialization
import dorkbox.util.classes.ClassHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mu.KLogger
import java.lang.Throwable
import java.lang.reflect.Proxy
import java.util.*
internal class RmiManagerGlobal<CONNECTION : Connection>(logger: KLogger,
actionDispatch: CoroutineScope,
private val serialization: Serialization) : RmiObjectCache(logger) {
internal class RmiManagerGlobal<CONNECTION: Connection>(private val logger: KLogger,
private val listenerManager: ListenerManager<CONNECTION>) : RmiObjectCache(logger) {
companion object {
/**
@ -48,19 +45,21 @@ internal class RmiManagerGlobal<CONNECTION : Connection>(logger: KLogger,
* @param rmiId this is the remote object ID (assigned by RMI). This is NOT the kryo registration ID
* @param interfaceClass this is the RMI interface class
*/
internal fun createProxyObject(isGlobalObject: Boolean,
connection: Connection,
serialization: Serialization,
responseManager: ResponseManager,
kryoId: Int,
rmiId: Int,
interfaceClass: Class<*>): RemoteObject {
internal fun <CONNECTION : Connection> createProxyObject(
isGlobalObject: Boolean,
connection: CONNECTION,
serialization: Serialization<CONNECTION>,
responseManager: ResponseManager,
kryoId: Int,
rmiId: Int,
interfaceClass: Class<*>
): RemoteObject {
// duplicates are fine, as they represent the same object (as specified by the ID) on the remote side.
val cachedMethods = serialization.getMethods(kryoId)
val name = "<${connection.endPoint.type.simpleName}-proxy #$rmiId>"
val name = "<${connection.endPoint.type.simpleName}-proxy:$rmiId>"
// the ACTUAL proxy is created in the connection impl. Our proxy handler MUST BE suspending because of:
// 1) how we send data on the wire
@ -74,143 +73,58 @@ internal class RmiManagerGlobal<CONNECTION : Connection>(logger: KLogger,
}
}
internal val responseManager = ResponseManager(logger, actionDispatch)
// this is used for all connection specific ones as well.
private val remoteObjectCreationCallbacks = RemoteObjectStorage(logger)
internal fun <Iface> registerCallback(callback: suspend Iface.() -> Unit): Int {
return remoteObjectCreationCallbacks.register(callback)
}
private fun removeCallback(callbackId: Int): suspend Any.() -> Unit {
// callback's area always correct, because we track them ourselves.
return remoteObjectCreationCallbacks.remove(callbackId)!!
}
/**
* @return the implementation object based on if it is global, or not global
*/
fun <T> getImplObject(isGlobal: Boolean, rmiId: Int, connection: Connection): T? {
return if (isGlobal) getImplObject(rmiId) else connection.rmiConnectionSupport.getImplObject(rmiId)
}
/**
* @return the removed object. If null, an error log will be emitted
*/
fun <T> removeImplObject(endPoint: EndPoint<CONNECTION>, objectId: Int): T? {
val success = removeImplObject<Any>(objectId)
if (success == null) {
val exception = Exception("Error trying to remove RMI impl object id $objectId.")
ListenerManager.cleanStackTrace(exception)
endPoint.listenerManager.notifyError(exception)
}
@Suppress("UNCHECKED_CAST")
return success as T?
}
suspend fun close() {
responseManager.close()
remoteObjectCreationCallbacks.close()
}
/**
* called on "client"
*/
private fun onGenericObjectResponse(endPoint: EndPoint<CONNECTION>,
connection: CONNECTION,
isGlobal: Boolean,
rmiId: Int,
callback: suspend Any.() -> Unit,
serialization: Serialization) {
// we only create the proxy + execute the callback if the RMI id is valid!
if (rmiId == RemoteObjectStorage.INVALID_RMI) {
endPoint.listenerManager.notifyError(connection, Exception("RMI ID '${rmiId}' is invalid. Unable to create RMI object on server."))
return
}
val interfaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(RemoteObjectCallback::class.java, callback.javaClass, 0)
// create the client-side proxy object, if possible. This MUST be an object that is saved for the connection
var proxyObject = connection.rmiConnectionSupport.getProxyObject(rmiId)
if (proxyObject == null) {
val kryoId = endPoint.serialization.getKryoIdForRmiClient(interfaceClass)
proxyObject = createProxyObject(isGlobal, connection, serialization, responseManager, kryoId, rmiId, interfaceClass)
connection.rmiConnectionSupport.saveProxyObject(rmiId, proxyObject)
}
// this should be executed on a NEW coroutine!
endPoint.actionDispatch.launch {
try {
callback(proxyObject)
} catch (e: Exception) {
ListenerManager.cleanStackTrace(e)
endPoint.listenerManager.notifyError(e)
}
}
}
/**
* on the connection+client to get a global remote object (that exists on the server)
*
* NOTE: This must be cast correctly by the caller!
*/
fun <Iface> getGlobalRemoteObject(connection: Connection, objectId: Int, interfaceClass: Class<Iface>): Iface {
fun getGlobalRemoteObject(connection: CONNECTION, objectId: Int, interfaceClass: Class<*>): Any {
// this immediately returns BECAUSE the object must have already been created on the server (this is why we specify the rmiId)!
require(interfaceClass.isInterface) { "iface must be an interface." }
val kryoId = serialization.getKryoIdForRmiClient(interfaceClass)
// so we can just instantly create the proxy object (or get the cached one). This MUST be an object that is saved for the connection
var proxyObject = connection.rmiConnectionSupport.getProxyObject(objectId)
if (proxyObject == null) {
proxyObject = createProxyObject(true, connection, serialization, responseManager, kryoId, objectId, interfaceClass)
connection.rmiConnectionSupport.saveProxyObject(objectId, proxyObject)
}
require(interfaceClass.isInterface) { "generic parameter must be an interface!" }
@Suppress("UNCHECKED_CAST")
return proxyObject as Iface
val rmiConnectionSupport = connection.endPoint.rmiConnectionSupport as RmiManagerConnections<CONNECTION>
// so we can just instantly create the proxy object (or get the cached one). This MUST be an object that is saved for the connection
return rmiConnectionSupport.getProxyObject(true, connection, objectId, interfaceClass)
}
/**
* Manages ALL OF THE RMI stuff!
* Manages ALL OF THE RMI SCOPES
*/
@Suppress("DuplicatedCode")
suspend fun manage(endPoint: EndPoint<CONNECTION>, connection: CONNECTION, message: Any, logger: KLogger) {
fun manage(
endPoint: EndPoint<CONNECTION>,
serialization: Serialization<CONNECTION>,
connection: CONNECTION,
message: Any,
rmiConnectionSupport: RmiManagerConnections<CONNECTION>,
actionDispatch: CoroutineScope
) {
when (message) {
is ConnectionObjectCreateRequest -> {
/**
* called on "server"
*/
@Suppress("UNCHECKED_CAST")
val rmiConnectionSupport: RmiManagerConnections<CONNECTION> = connection.rmiConnectionSupport as RmiManagerConnections<CONNECTION>
rmiConnectionSupport.onConnectionObjectCreateRequest(endPoint, connection, message)
rmiConnectionSupport.onConnectionObjectCreateRequest(serialization, connection, message, actionDispatch)
}
is ConnectionObjectCreateResponse -> {
/**
* called on "client"
*/
val callbackId = RmiUtils.unpackLeft(message.packedIds)
val rmiId = RmiUtils.unpackRight(message.packedIds)
val callback = removeCallback(callbackId)
onGenericObjectResponse(endPoint, connection, false, rmiId, callback, serialization)
rmiConnectionSupport.onConnectionObjectCreateResponse(connection, message, actionDispatch)
}
is GlobalObjectCreateRequest -> {
is ConnectionObjectDeleteRequest -> {
/**
* called on "server"
* called on "client" or "server"
*/
onGlobalObjectCreateRequest(endPoint, connection, message)
rmiConnectionSupport.onConnectionObjectDeleteRequest(connection, message, actionDispatch)
}
is GlobalObjectCreateResponse -> {
is ConnectionObjectDeleteResponse -> {
/**
* called on "client"
* called on "client" or "server"
*/
val callbackId = RmiUtils.unpackLeft(message.packedIds)
val rmiId = RmiUtils.unpackRight(message.packedIds)
val callback = removeCallback(callbackId)
onGenericObjectResponse(endPoint, connection, true, rmiId, callback, serialization)
rmiConnectionSupport.onConnectionObjectDeleteResponse(connection, message)
}
is MethodRequest -> {
/**
@ -230,16 +144,24 @@ internal class RmiManagerGlobal<CONNECTION : Connection>(logger: KLogger,
logger.trace { "RMI received: $rmiId" }
val implObject = getImplObject<Any>(isGlobal, rmiObjectId, connection)
val implObject: Any? = if (isGlobal) {
getImplObject(rmiObjectId)
} else {
rmiConnectionSupport.getImplObject(rmiObjectId)
}
if (implObject == null) {
endPoint.listenerManager.notifyError(connection,
NullPointerException("Unable to resolve implementation object for [global=$isGlobal, objectID=$rmiObjectId, connection=$connection"))
logger.error("Connection ${connection.id}: Unable to resolve implementation object for [global=$isGlobal, objectID=$rmiObjectId, connection=$connection")
if (sendResponse) {
returnRmiMessage(connection,
message,
NullPointerException("Remote object for proxy [global=$isGlobal, rmiObjectID=$rmiObjectId] does not exist."),
logger)
val rmiMessage = returnRmiMessage(
message,
NullPointerException("Remote object for proxy [global=$isGlobal, rmiObjectID=$rmiObjectId] does not exist."),
logger
)
actionDispatch.launch {
connection.send(rmiMessage)
}
}
return
}
@ -269,47 +191,49 @@ internal class RmiManagerGlobal<CONNECTION : Connection>(logger: KLogger,
if (isCoroutine) {
// https://stackoverflow.com/questions/47654537/how-to-run-suspend-method-via-reflection
// https://discuss.kotlinlang.org/t/calling-coroutines-suspend-functions-via-reflection/4672
actionDispatch.launch {
var suspendResult = kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn<Any?> { cont ->
// if we are a coroutine, we have to replace the LAST arg with the coroutine object
// we KNOW this is OK, because a continuation arg will always be there!
args!![args.size - 1] = cont
var suspendResult = kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn<Any?> { cont ->
// if we are a coroutine, we have to replace the LAST arg with the coroutine object
// we KNOW this is OK, because a continuation arg will always be there!
args!![args.size - 1] = cont
var insideResult: Any?
try {
// args!! is safe to do here (even though it doesn't make sense)
insideResult = cachedMethod.invoke(connection, implObject, args)
} catch (ex: Exception) {
insideResult = ex.cause
// added to prevent a stack overflow when references is false, (because 'cause' == "this").
// See:
// https://groups.google.com/forum/?fromgroups=#!topic/kryo-users/6PDs71M1e9Y
if (insideResult == null) {
insideResult = ex
var insideResult: Any?
try {
// args!! is safe to do here (even though it doesn't make sense)
insideResult = cachedMethod.invoke(connection, implObject, args)
} catch (ex: Exception) {
insideResult = ex.cause
// added to prevent a stack overflow when references is false, (because 'cause' == "this").
// See:
// https://groups.google.com/forum/?fromgroups=#!topic/kryo-users/6PDs71M1e9Y
if (insideResult == null) {
insideResult = ex
}
else {
insideResult.initCause(null)
}
}
else {
(insideResult as Throwable).initCause(null)
}
}
insideResult
}
if (suspendResult === kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED) {
// we were suspending, and the stack will resume when possible, then it will call the response below
}
else {
if (suspendResult === Unit) {
// kotlin suspend returns, that DO NOT have a return value, REALLY return kotlin.Unit. This means there is no
// return value!
suspendResult = null
} else if (suspendResult is Exception) {
RmiUtils.cleanStackTraceForImpl(suspendResult, true)
endPoint.listenerManager.notifyError(connection, suspendResult)
insideResult
}
if (sendResponse) {
returnRmiMessage(connection, message, suspendResult, logger)
if (suspendResult === kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED) {
// we were suspending, and the stack will resume when possible, then it will call the response below
}
else {
if (suspendResult === Unit) {
// kotlin suspend returns, that DO NOT have a return value, REALLY return kotlin.Unit. This means there is no
// return value!
suspendResult = null
} else if (suspendResult is Exception) {
RmiUtils.cleanStackTraceForImpl(suspendResult, true)
logger.error("Connection ${connection.id}", suspendResult)
}
if (sendResponse) {
val rmiMessage = returnRmiMessage(message, suspendResult, logger)
connection.send(rmiMessage)
}
}
}
}
@ -327,7 +251,7 @@ internal class RmiManagerGlobal<CONNECTION : Connection>(logger: KLogger,
result = ex
}
else {
(result as Throwable).initCause(null)
result.initCause(null)
}
RmiUtils.cleanStackTraceForImpl(result as Exception, false)
@ -336,55 +260,29 @@ internal class RmiManagerGlobal<CONNECTION : Connection>(logger: KLogger,
}
if (sendResponse) {
returnRmiMessage(connection, message, result, logger)
val rmiMessage = returnRmiMessage(message, result, logger)
actionDispatch.launch {
connection.send(rmiMessage)
}
}
}
}
is MethodResponse -> {
// notify the pending proxy requests that we have a response!
responseManager.onRmiMessage(message)
actionDispatch.launch {
endPoint.responseManager.onRmiMessage(message)
}
}
}
}
private suspend fun returnRmiMessage(connection: Connection, message: MethodRequest, result: Any?, logger: KLogger) {
private fun returnRmiMessage(message: MethodRequest, result: Any?, logger: KLogger): MethodResponse {
logger.trace { "RMI return. Send: ${RmiUtils.unpackUnsignedRight(message.packedId)}" }
val rmiMessage = MethodResponse()
rmiMessage.packedId = message.packedId
rmiMessage.result = result
connection.send(rmiMessage)
}
/**
* called on "server"
*/
private suspend fun onGlobalObjectCreateRequest(endPoint: EndPoint<CONNECTION>,
connection: CONNECTION,
message: GlobalObjectCreateRequest) {
val interfaceClassId = RmiUtils.unpackLeft(message.packedIds)
val callbackId = RmiUtils.unpackRight(message.packedIds)
val objectParameters = message.objectParameters
val serialization = endPoint.serialization
// We have to lookup the iface, since the proxy object requires it
val implObject = serialization.createRmiObject(interfaceClassId, objectParameters)
val response = if (implObject is Exception) {
// whoops!
endPoint.listenerManager.notifyError(connection, implObject)
// we send the message ANYWAYS, because the client needs to know it did NOT succeed!
GlobalObjectCreateResponse(RmiUtils.packShorts(callbackId, RemoteObjectStorage.INVALID_RMI))
} else {
val rmiId = saveImplObject(implObject)
// we send the message ANYWAYS, because the client needs to know it did NOT succeed!
GlobalObjectCreateResponse(RmiUtils.packShorts(callbackId, rmiId))
}
connection.send(response)
return rmiMessage
}
}

View File

@ -23,7 +23,7 @@ import mu.KLogger
* The impl/proxy objects CANNOT be stored in the same data structure, because their IDs are not tied to the same ID source (and there
* would be conflicts in the data structure)
*/
internal open class RmiObjectCache(logger: KLogger) {
open class RmiObjectCache(logger: KLogger) {
private val implObjects = RemoteObjectStorage(logger)
@ -41,19 +41,27 @@ internal open class RmiObjectCache(logger: KLogger) {
return implObjects.register(rmiObject, objectId)
}
/**
* @return the implementation object from the specified ID
*/
fun <T> getImplObject(rmiId: Int): T? {
@Suppress("UNCHECKED_CAST")
return implObjects[rmiId] as T?
}
/**
* Removes the object using the ID registered.
*
* @return the object or null if not found
*/
fun <T> removeImplObject(rmiId: Int): T? {
return implObjects.remove(rmiId) as T?
}
/**
* @return the ID registered for the specified object, or INVALID_RMI if not found.
*/
fun <T> getId(implObject: T): Int {
return implObjects.getId(implObject)
}
fun <T> removeImplObject(rmiId: Int): T? {
return implObjects.remove(rmiId) as T?
}
}

View File

@ -0,0 +1,201 @@
/*
* Copyright 2021 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.rmi
import dorkbox.network.connection.Connection
import dorkbox.network.connection.ListenerManager
import dorkbox.network.rmi.messages.ConnectionObjectDeleteRequest
import dorkbox.network.serialization.Serialization
import dorkbox.util.classes.ClassHelper
import mu.KLogger
/**
* Only the server can create or delete a global object
*
* Regarding "scopes"
* GLOBAL -> all connections/clients access the same object, and the state is shared
* CONNECTION -> each object exists only within that specific connection, and only the corresponding remote connection has access to it's state.
*
* Connection scope objects can be remotely created or deleted by either end of the connection. Only the server can create/delete a global scope object
*/
class RmiSupportConnection<CONNECTION: Connection> internal constructor(
private val logger: KLogger,
private val connection: CONNECTION,
private val rmiConnectionSupport: RmiManagerConnections<CONNECTION>,
private val serialization: Serialization<CONNECTION>,
private val getGlobalAction: (connection: CONNECTION, objectId: Int, interfaceClass: Class<*>) -> Any
) {
/**
* Tells us to save an existing object in the CONNECTION scope, so a remote connection can get it via [Connection.rmi.get()]
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
*
* @return the newly registered RMI ID for this object. [RemoteObjectStorage.INVALID_RMI] means it was invalid (an error log will be emitted)
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun save(`object`: Any): Int {
val rmiId = rmiConnectionSupport.saveImplObject(`object`)
if (rmiId == RemoteObjectStorage.INVALID_RMI) {
val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated")
ListenerManager.cleanStackTrace(exception)
logger.error("RMI error connection ${connection.id}", exception)
}
return rmiId
}
/**
* Tells us to save an existing object in the CONNECTION scope using the specified ID, so a remote connection can get it via [Connection.rmi.get()]
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
* @return true if the object was successfully saved for the specified ID. If false, an error log will be emitted
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun save(`object`: Any, objectId: Int): Boolean {
val success = rmiConnectionSupport.saveImplObject(`object`, objectId)
if (!success) {
val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated")
ListenerManager.cleanStackTrace(exception)
logger.error("RMI error connection ${connection.id}", exception)
}
return success
}
/**
* Creates create a new proxy object where the implementation exists in a remote connection.
*
* The callback will be notified when the remote object has been created.
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
* @see RemoteObject
*/
suspend fun <Iface> create(vararg objectParameters: Any?, callback: suspend Iface.() -> Unit) {
val iFaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(Function1::class.java, callback.javaClass, 0)
val kryoId = serialization.getKryoIdForRmiClient(iFaceClass)
@Suppress("UNCHECKED_CAST")
objectParameters as Array<Any?>
rmiConnectionSupport.createRemoteObject(connection, kryoId, objectParameters, callback)
}
/**
* Creates create a new proxy object where the implementation exists in a remote connection.
*
* The callback will be notified when the remote object has been created.
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
* @see RemoteObject
*/
suspend fun <Iface> create(callback: suspend Iface.() -> Unit) {
val iFaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(Function1::class.java, callback.javaClass, 0)
val kryoId = serialization.getKryoIdForRmiClient(iFaceClass)
rmiConnectionSupport.createRemoteObject(connection, kryoId, null, callback)
}
/**
* This will remove both the proxy AND implementation objects. It does not matter which "side" of a connection this is called on.
*
* Any future method invocations will result in a error.
*
* Future '.get' requests will succeed, as they do not check the existence of the implementation object (methods called on it will fail)
*/
suspend fun delete(rmiObjectId: Int) {
// we only create the proxy + execute the callback if the RMI id is valid!
if (rmiObjectId == RemoteObjectStorage.INVALID_RMI) {
val exception = Exception("RMI ID '${rmiObjectId}' is invalid. Unable to delete RMI object!")
ListenerManager.cleanStackTrace(exception)
logger.error("RMI error connection ${connection.id}", exception)
return
}
// ALWAYS send a message because we don't know if we are the "client" or the "server" - and we want ALL sides cleaned up
connection.send(ConnectionObjectDeleteRequest(rmiObjectId))
}
/**
* Gets a CONNECTION scope remote object via the ID.
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the remote object behavior, cast the object to a [RemoteObject] to access the different methods, for example:
* ie: `val remoteObject = test as RemoteObject`
*
* @see RemoteObject
*/
inline fun <reified Iface> get(objectId: Int): Iface {
// NOTE: It's not possible to have reified inside a virtual function
// https://stackoverflow.com/questions/60037849/kotlin-reified-generic-in-virtual-function
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")
return rmiConnectionSupport.getProxyObject(false, connection, objectId, Iface::class.java)
}
/**
* Gets a GLOBAL scope object via the ID. Global remote objects share their state among all connections.
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the remote object behavior, cast the object to a [RemoteObject] to access the different methods, for example:
* ie: `val remoteObject = test as RemoteObject`
*
* @see RemoteObject
*/
inline fun <reified Iface> getGlobal(objectId: Int): Iface {
// NOTE: It's not possible to have reified inside a virtual function
// https://stackoverflow.com/questions/60037849/kotlin-reified-generic-in-virtual-function
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")
return getGlobalAction(connection, objectId, Iface::class.java) as Iface
}
}

View File

@ -0,0 +1,134 @@
/*
* Copyright 2021 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.rmi
import dorkbox.network.connection.Connection
import dorkbox.network.connection.ListenerManager
import mu.KLogger
/**
* Only the server can create or delete a global object
*
* Regarding "scopes"
* GLOBAL -> all connections/clients access the same object, and the state is shared
* CONNECTION -> each object exists only within that specific connection, and only the corresponding remote connection has access to it's state.
*
* Connection scope objects can be remotely created or deleted by either end of the connection. Only the server can create/delete a global scope object
*/
class RmiSupportServer<CONNECTION : Connection> internal constructor(
private val logger: KLogger,
private val rmiGlobalSupport: RmiManagerGlobal<CONNECTION>
) {
/**
* Tells us to save an existing object, GLOBALLY, so a remote connection can get it via [Connection.rmi.getGlobal()]
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
* @return the newly registered RMI ID for this object. [RemoteObjectStorage.INVALID_RMI] means it was invalid (an error log will be emitted)
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun save(`object`: Any): Int {
val rmiId = rmiGlobalSupport.saveImplObject(`object`)
if (rmiId == RemoteObjectStorage.INVALID_RMI) {
val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated")
ListenerManager.cleanStackTrace(exception)
logger.error("RMI error", exception)
}
return rmiId
}
/**
* Tells us to save an existing object, GLOBALLY using the specified ID, so a remote connection can get it via [Connection.rmi.getGlobal()]
*
* Methods that return a value will throw [TimeoutException] if the response is not received with the response timeout [RemoteObject.responseTimeout].
*
* If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side
* will have the proxy object replaced with the registered (non-proxy) object.
*
* If one wishes to change the default behavior, cast the object to access the different methods.
* ie: `val remoteObject = test as RemoteObject`
*
* @return true if the object was successfully saved for the specified ID. If false, an error log will be emitted
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun save(`object`: Any, objectId: Int): Boolean {
val success = rmiGlobalSupport.saveImplObject(`object`, objectId)
if (!success) {
val exception = Exception("RMI implementation '${`object`::class.java}' could not be saved! No more RMI id's could be generated")
ListenerManager.cleanStackTrace(exception)
logger.error("RMI error", exception)
}
return success
}
/**
* Tells us to delete a previously saved, GLOBAL scope, RMI object.
*
* After this call, this object will no longer be available to remote connections and the ID will be recycled (don't use it again)
*
* @return true if the object was successfully deleted. If false, an error log will be emitted
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun delete(`object`: Any): Boolean {
val successRmiId = rmiGlobalSupport.getId(`object`)
val success = successRmiId != RemoteObjectStorage.INVALID_RMI
if (success) {
rmiGlobalSupport.removeImplObject<Any?>(successRmiId)
} else {
val exception = Exception("RMI implementation '${`object`::class.java}' could not be deleted! It does not exist")
ListenerManager.cleanStackTrace(exception)
logger.error("RMI error", exception)
}
return success
}
/**
* Tells us to delete a previously saved, GLOBAL scope, RMI object.
*
* After this call, this object will no longer be available to remote connections and the ID will be recycled (don't use it again)
*
* @return true if the object was successfully deleted. If false, an error log will be emitted
*
* @see RemoteObject
*/
@Suppress("DuplicatedCode")
fun delete(objectId: Int): Boolean {
val previousObject = rmiGlobalSupport.removeImplObject<Any?>(objectId)
val success = previousObject != null
if (!success) {
val exception = Exception("RMI implementation UD '$objectId' could not be deleted! It does not exist")
ListenerManager.cleanStackTrace(exception)
logger.error("RMI error", exception)
}
return success
}
}

View File

@ -549,6 +549,12 @@ object RmiUtils {
val packageName = RmiUtils::class.java.packageName
val stackTrace = exception.stackTrace
val size = stackTrace.size
if (size == 0) {
return
}
var newEndIndex = -1 // because we index by size, but access from 0
// step 1: starting at 0, find the start of our RMI method invocation
@ -582,7 +588,7 @@ object RmiUtils {
}
// if we are a KOTLIN suspend function, there is ONE stack frame extra we have to remove
if (isSuspendFunction) {
if (isSuspendFunction && newEndIndex > 0) {
newEndIndex--
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2020 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.rmi.messages
/**
* @param rmiId which rmi object to delete
*/
data class ConnectionObjectDeleteRequest(val rmiId: Int) : RmiMessage {
override fun toString(): String {
return "ConnectionObjectDeleteRequest(id: $rmiId)"
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2020 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.rmi.messages
/**
* @param rmiId which rmi object was deleted
*/
data class ConnectionObjectDeleteResponse(val rmiId: Int) : RmiMessage {
override fun toString(): String {
return "ConnectionObjectDeleteResponse(id: $rmiId)"
}
}

View File

@ -39,6 +39,7 @@ import com.esotericsoftware.kryo.KryoException
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import dorkbox.network.connection.Connection
import dorkbox.network.rmi.CachedMethod
import dorkbox.network.rmi.RmiUtils
import dorkbox.network.serialization.KryoExtra
@ -49,7 +50,7 @@ import java.lang.reflect.Method
* Internal message to invoke methods remotely.
*/
@Suppress("ConstantConditionIf")
class MethodRequestSerializer(private val methodCache: Int2ObjectHashMap<Array<CachedMethod>>) : Serializer<MethodRequest>() {
class MethodRequestSerializer<CONNECTION: Connection>(private val methodCache: Int2ObjectHashMap<Array<CachedMethod>>) : Serializer<MethodRequest>() {
override fun write(kryo: Kryo, output: Output, methodRequest: MethodRequest) {
val method = methodRequest.cachedMethod
@ -82,7 +83,7 @@ class MethodRequestSerializer(private val methodCache: Int2ObjectHashMap<Array<C
val methodIndex = RmiUtils.unpackRight(methodInfo)
val isGlobal = input.readBoolean()
(kryo as KryoExtra)
kryo as KryoExtra<CONNECTION>
val cachedMethod = try {
methodCache[methodClassId][methodIndex]

View File

@ -19,6 +19,8 @@ import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import dorkbox.network.connection.Connection
import dorkbox.network.connection.EndPoint
import dorkbox.network.rmi.RmiClient
import dorkbox.network.serialization.KryoExtra
import java.lang.reflect.Proxy
@ -52,7 +54,8 @@ import java.lang.reflect.Proxy
* During the handshake, if the impl object 'lives' on the CLIENT, then the client must tell the server that the iface ID must use this serializer.
* If the impl object 'lives' on the SERVER, then the server must tell the client about the iface ID
*/
class RmiClientSerializer : Serializer<Any>() {
@Suppress("UNCHECKED_CAST")
class RmiClientSerializer<CONNECTION: Connection>: Serializer<Any>() {
override fun write(kryo: Kryo, output: Output, proxyObject: Any) {
val handler = Proxy.getInvocationHandler(proxyObject) as RmiClient
output.writeBoolean(handler.isGlobal)
@ -63,8 +66,13 @@ class RmiClientSerializer : Serializer<Any>() {
val isGlobal = input.readBoolean()
val objectId = input.readInt(true)
kryo as KryoExtra
val connection = kryo.connection
return connection.endPoint.rmiGlobalSupport.getImplObject(isGlobal, objectId, connection)
kryo as KryoExtra<CONNECTION>
val endPoint: EndPoint<CONNECTION> = kryo.connection.endPoint as EndPoint<CONNECTION>
return if (isGlobal) {
endPoint.rmiGlobalSupport.getImplObject(objectId)
} else {
endPoint.rmiConnectionSupport.getImplObject(objectId)
}
}
}

View File

@ -38,7 +38,9 @@ import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import dorkbox.network.connection.Connection
import dorkbox.network.rmi.RemoteObjectStorage
import dorkbox.network.rmi.RmiManagerConnections
import dorkbox.network.serialization.KryoExtra
/**
@ -70,12 +72,13 @@ import dorkbox.network.serialization.KryoExtra
* During the handshake, if the impl object 'lives' on the CLIENT, then the client must tell the server that the iface ID must use this serializer.
* If the impl object 'lives' on the SERVER, then the server must tell the client about the iface ID
*/
class RmiServerSerializer : Serializer<Any>(false) {
@Suppress("UNCHECKED_CAST")
class RmiServerSerializer<CONNECTION: Connection> : Serializer<Any>(false) {
override fun write(kryo: Kryo, output: Output, `object`: Any) {
val kryoExtra = kryo as KryoExtra
val kryoExtra = kryo as KryoExtra<CONNECTION>
val connection = kryoExtra.connection
val rmiConnectionSupport = connection.rmiConnectionSupport
val rmiConnectionSupport = connection.endPoint.rmiConnectionSupport
// have to write what the rmi ID is ONLY. A remote object sent via a connection IS ONLY a connection-scope object!
@ -94,11 +97,12 @@ class RmiServerSerializer : Serializer<Any>(false) {
}
override fun read(kryo: Kryo, input: Input, interfaceClass: Class<*>): Any? {
val kryoExtra = kryo as KryoExtra
val kryoExtra = kryo as KryoExtra<CONNECTION>
val rmiId = input.readInt(true)
val connection = kryoExtra.connection
val serialization = connection.endPoint.serialization
val endPoint = connection.endPoint
val serialization = endPoint.serialization
if (rmiId == RemoteObjectStorage.INVALID_RMI) {
throw NullPointerException("RMI ID is invalid. Unable to use proxy object!")
@ -107,18 +111,21 @@ class RmiServerSerializer : Serializer<Any>(false) {
// the rmi-server will have iface+impl id's
// the rmi-client will have iface id's
val rmiConnectionSupport = endPoint.rmiConnectionSupport as RmiManagerConnections<CONNECTION>
return if (interfaceClass.isInterface) {
// normal case. RMI only on 1 side
val kryoId = serialization.rmiHolder.ifaceToId[interfaceClass]
require(kryoId != null) { "Registration for $interfaceClass is invalid!!" }
connection.rmiConnectionSupport.getProxyObject(connection, kryoId, rmiId, interfaceClass)
rmiConnectionSupport.getProxyObject(false, connection, kryoId, rmiId, interfaceClass)
} else {
// BI-DIRECTIONAL RMI -- THIS IS NOT NORMAL!
// this won't be an interface. It will be an impl (because of how RMI is setup)
val kryoId = serialization.rmiHolder.implToId[interfaceClass]
require(kryoId != null) { "Registration for $interfaceClass is invalid!!" }
val iface = serialization.rmiHolder.idToIface[kryoId]
connection.rmiConnectionSupport.getProxyObject(connection, kryoId, rmiId, iface)
rmiConnectionSupport.getProxyObject(false, connection, kryoId, rmiId, iface)
}
}
}

View File

@ -17,9 +17,10 @@ package dorkbox.network.serialization
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Serializer
import dorkbox.network.connection.Connection
import dorkbox.network.rmi.messages.RmiServerSerializer
internal abstract class ClassRegistration(val clazz: Class<*>, val serializer: Serializer<*>? = null, var id: Int = 0) {
internal abstract class ClassRegistration<CONNECTION: Connection>(val clazz: Class<*>, val serializer: Serializer<*>? = null, var id: Int = 0) {
companion object {
const val IGNORE_REGISTRATION = -1
}
@ -32,7 +33,7 @@ internal abstract class ClassRegistration(val clazz: Class<*>, val serializer: S
* If so, we ignore it - any IFACE or IMPL that already has been assigned to an RMI serializer, *MUST* remain an RMI serializer
* If this class registration will EVENTUALLY be for RMI, then [ClassRegistrationForRmi] will reassign the serializer
*/
open fun register(kryo: KryoExtra, rmi: RmiHolder) {
open fun register(kryo: KryoExtra<CONNECTION>, rmi: RmiHolder) {
// ClassRegistrationForRmi overrides this method
if (id == IGNORE_REGISTRATION) {
// we have previously specified that this registration should be ignored!
@ -61,7 +62,7 @@ internal abstract class ClassRegistration(val clazz: Class<*>, val serializer: S
if (savedKryoId != null) {
overriddenSerializer = kryo.classResolver.getRegistration(savedKryoId)?.serializer
when (overriddenSerializer) {
is RmiServerSerializer -> {
is RmiServerSerializer<*> -> {
// do nothing, because this is ALREADY registered for RMI
info = if (serializer == null) {
"CONFLICTED $savedKryoId -> (RMI) Ignored duplicate registration for ${clazz.name}"

View File

@ -17,8 +17,9 @@ package dorkbox.network.serialization
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Serializer
import dorkbox.network.connection.Connection
internal class ClassRegistration0(clazz: Class<*>, serializer: Serializer<*>) : ClassRegistration(clazz, serializer) {
internal class ClassRegistration0<CONNECTION: Connection>(clazz: Class<*>, serializer: Serializer<*>) : ClassRegistration<CONNECTION>(clazz, serializer) {
override fun register(kryo: Kryo) {
id = kryo.register(clazz, serializer).id
info = "Registered $id -> ${clazz.name} using ${serializer!!.javaClass.name}"

View File

@ -16,8 +16,9 @@
package dorkbox.network.serialization
import com.esotericsoftware.kryo.Kryo
import dorkbox.network.connection.Connection
internal class ClassRegistration1(clazz: Class<*>, id: Int) : ClassRegistration(clazz, null, id) {
internal class ClassRegistration1<CONNECTION: Connection>(clazz: Class<*>, id: Int) : ClassRegistration<CONNECTION>(clazz, null, id) {
override fun register(kryo: Kryo) {
kryo.register(clazz, id)
info = "Registered $id -> (specified) ${clazz.name}"

View File

@ -17,8 +17,9 @@ package dorkbox.network.serialization
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Serializer
import dorkbox.network.connection.Connection
internal class ClassRegistration2(clazz: Class<*>, serializer: Serializer<*>, id: Int) : ClassRegistration(clazz, serializer, id) {
internal class ClassRegistration2<CONNECTION: Connection>(clazz: Class<*>, serializer: Serializer<*>, id: Int) : ClassRegistration<CONNECTION>(clazz, serializer, id) {
override fun register(kryo: Kryo) {
kryo.register(clazz, serializer, id)

View File

@ -16,8 +16,9 @@
package dorkbox.network.serialization
import com.esotericsoftware.kryo.Kryo
import dorkbox.network.connection.Connection
internal open class ClassRegistration3(clazz: Class<*>) : ClassRegistration(clazz) {
internal open class ClassRegistration3<CONNECTION: Connection>(clazz: Class<*>) : ClassRegistration<CONNECTION>(clazz) {
override fun register(kryo: Kryo) {
id = kryo.register(clazz).id

View File

@ -15,6 +15,7 @@
*/
package dorkbox.network.serialization
import dorkbox.network.connection.Connection
import dorkbox.network.rmi.messages.RmiServerSerializer
/**
@ -44,9 +45,9 @@ import dorkbox.network.rmi.messages.RmiServerSerializer
* During the handshake, if the impl object 'lives' on the CLIENT, then the client must tell the server that the iface ID must use this serializer.
* If the impl object 'lives' on the SERVER, then the server must tell the client about the iface ID
*/
internal class ClassRegistrationForRmi(ifaceClass: Class<*>,
var implClass: Class<*>?,
serializer: RmiServerSerializer) : ClassRegistration(ifaceClass, serializer) {
internal class ClassRegistrationForRmi<CONNECTION: Connection>(ifaceClass: Class<*>,
var implClass: Class<*>?,
serializer: RmiServerSerializer<CONNECTION>) : ClassRegistration<CONNECTION>(ifaceClass, serializer) {
/**
* In general:
*
@ -103,7 +104,7 @@ internal class ClassRegistrationForRmi(ifaceClass: Class<*>,
* send: register IMPL object class with RmiServerSerializer
* lookup IMPL object -> rmiID
*/
override fun register(kryo: KryoExtra, rmi: RmiHolder) {
override fun register(kryo: KryoExtra<CONNECTION>, rmi: RmiHolder) {
// we override this, because we ALWAYS will call our RMI registration!
if (id == IGNORE_REGISTRATION) {
// we have previously specified that this registration should be ignored!

View File

@ -24,7 +24,7 @@ import org.agrona.DirectBuffer
/**
* READ and WRITE are exclusive to each other and can be performed in different threads.
*/
class KryoExtra() : Kryo() {
class KryoExtra<CONNECTION: Connection>() : Kryo() {
// for kryo serialization
private val readerBuffer = AeronInput()
private val writerBuffer = AeronOutput()
@ -35,7 +35,7 @@ class KryoExtra() : Kryo() {
// private val temp = ByteArray(ABSOLUTE_MAX_SIZE_OBJECT)
// This is unique per connection. volatile/etc is not necessary because it is set/read in the same thread
lateinit var connection: Connection
lateinit var connection: CONNECTION
// private val secureRandom = SecureRandom()
// private var cipher: Cipher? = null
@ -87,7 +87,7 @@ class KryoExtra() : Kryo() {
* ++++++++++++++++++++++++++
*/
@Throws(Exception::class)
fun write(connection: Connection, message: Any): AeronOutput {
fun write(connection: CONNECTION, message: Any): AeronOutput {
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
this.connection = connection
@ -133,7 +133,7 @@ class KryoExtra() : Kryo() {
* ++++++++++++++++++++++++++
*/
@Throws(Exception::class)
fun read(buffer: DirectBuffer, offset: Int, length: Int, connection: Connection): Any {
fun read(buffer: DirectBuffer, offset: Int, length: Int, connection: CONNECTION): Any {
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
this.connection = connection
@ -173,7 +173,7 @@ class KryoExtra() : Kryo() {
* + class and object bytes +
* ++++++++++++++++++++++++++
*/
private fun write(connection: Connection, writer: Output, message: Any) {
private fun write(connection: CONNECTION, writer: Output, message: Any) {
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
this.connection = connection
@ -200,7 +200,7 @@ class KryoExtra() : Kryo() {
* + class and object bytes +
* ++++++++++++++++++++++++++
*/
private fun read(connection: Connection, reader: Input): Any {
private fun read(connection: CONNECTION, reader: Input): Any {
// required by RMI and some serializers to determine which connection wrote (or has info about) this object
this.connection = connection

View File

@ -31,6 +31,7 @@ import dorkbox.objectPool.Pool
import dorkbox.objectPool.PoolObject
import dorkbox.os.OS
import dorkbox.serializers.SerializationDefaults
import kotlinx.atomicfu.AtomicBoolean
import kotlinx.atomicfu.atomic
import mu.KLogger
import mu.KotlinLogging
@ -63,7 +64,7 @@ import kotlin.coroutines.Continuation
* an object's type. Default is [ReflectionSerializerFactory] with [FieldSerializer]. @see
* Kryo#newDefaultSerializer(Class)
*/
open class Serialization(private val references: Boolean = true, private val factory: SerializerFactory<*>? = null) {
open class Serialization<CONNECTION: Connection>(private val references: Boolean = true, private val factory: SerializerFactory<*>? = null) {
companion object {
// -2 is the same value that kryo uses for invalid id's
@ -74,6 +75,40 @@ open class Serialization(private val references: Boolean = true, private val fac
}
}
open class RmiSupport<CONNECTION: Connection> internal constructor(
private val initialized: AtomicBoolean,
private val classesToRegister: MutableList<ClassRegistration<CONNECTION>>,
private val rmiServerSerializer: RmiServerSerializer<CONNECTION>
) {
/**
* There is additional overhead to using RMI.
*
* This enables a "remote endpoint" to access methods and create objects (RMI) for this endpoint.
*
* This is NOT bi-directional, and this endpoint cannot access or create remote objects on the "remote client".
*
* @param ifaceClass this must be the interface class used for RMI
* @param implClass this must be the implementation class used for RMI
* If *null* it means that this endpoint is the rmi-client
* If *not-null* it means that this endpoint is the rmi-server
*
* @throws IllegalArgumentException if the iface/impl have previously been overridden
*/
@Synchronized
open fun <Iface, Impl : Iface> register(ifaceClass: Class<Iface>, implClass: Class<Impl>? = null): RmiSupport<CONNECTION> {
require(!initialized.value) { "Serialization 'registerRmi(Class, Class)' cannot happen after client/server initialization!" }
require(ifaceClass.isInterface) { "Cannot register an implementation for RMI access. It must be an interface." }
if (implClass != null) {
require(!implClass.isInterface) { "Cannot register an interface for RMI implementations. It must be an implementation." }
}
classesToRegister.add(ClassRegistrationForRmi(ifaceClass, implClass, rmiServerSerializer))
return this
}
}
private lateinit var logger: KLogger
private var initialized = atomic(false)
@ -81,7 +116,7 @@ open class Serialization(private val references: Boolean = true, private val fac
// used by operations performed during kryo initialization, which are by default package access (since it's an anon-inner class)
// All registration MUST happen in-order of when the register(*) method was called, otherwise there are problems.
// Object checking is performed during actual registration.
private val classesToRegister = mutableListOf<ClassRegistration>()
private val classesToRegister = mutableListOf<ClassRegistration<CONNECTION>>()
private lateinit var savedRegistrationDetails: ByteArray
// the purpose of the method cache, is to accelerate looking up methods for specific class
@ -92,12 +127,21 @@ open class Serialization(private val references: Boolean = true, private val fac
// StdInstantiatorStrategy will create classes bypasses the constructor (which can be useful in some cases) THIS IS A FALLBACK!
private val instantiatorStrategy = DefaultInstantiatorStrategy(StdInstantiatorStrategy())
private val methodRequestSerializer = MethodRequestSerializer(methodCache) // note: the methodCache is configured BEFORE anything reads from it!
private val methodRequestSerializer = MethodRequestSerializer<CONNECTION>(methodCache) // note: the methodCache is configured BEFORE anything reads from it!
private val methodResponseSerializer = MethodResponseSerializer()
private val continuationSerializer = ContinuationSerializer()
private val rmiClientSerializer = RmiClientSerializer()
private val rmiServerSerializer = RmiServerSerializer()
private val rmiClientSerializer = RmiClientSerializer<CONNECTION>()
private val rmiServerSerializer = RmiServerSerializer<CONNECTION>()
/**
* There is additional overhead to using RMI.
*
* This enables access to methods from a "remote endpoint", in such a way as if it were local.
*
* This is NOT bi-directional.
*/
val rmi: RmiSupport<CONNECTION>
val rmiHolder = RmiHolder()
@ -109,16 +153,17 @@ open class Serialization(private val references: Boolean = true, private val fac
// NOTE: These following can ONLY be called on a single thread!
private var readKryo = initGlobalKryo()
private val kryoPool: Pool<KryoExtra>
private val kryoPool: Pool<KryoExtra<CONNECTION>>
init {
val poolObject = object : PoolObject<KryoExtra>() {
override fun newInstance(): KryoExtra {
val poolObject = object : PoolObject<KryoExtra<CONNECTION>>() {
override fun newInstance(): KryoExtra<CONNECTION> {
return initKryo()
}
}
kryoPool = ObjectPool.nonBlocking(poolObject)
rmi = RmiSupport(initialized, classesToRegister, rmiServerSerializer)
}
@ -136,7 +181,7 @@ open class Serialization(private val references: Boolean = true, private val fac
*
* This must happen before the creation of the client/server
*/
open fun <T> register(clazz: Class<T>): Serialization {
open fun <T> register(clazz: Class<T>): Serialization<CONNECTION> {
require(!initialized.value) { "Serialization 'register(class)' cannot happen after client/server initialization!" }
// The reason it must be an implementation, is because the reflection serializer DOES NOT WORK with field types, but rather
@ -161,7 +206,7 @@ open class Serialization(private val references: Boolean = true, private val fac
* @param id Must be >= 0. Smaller IDs are serialized more efficiently. IDs 0-8 are used by default for primitive types and String, but
* these IDs can be repurposed.
*/
open fun <T> register(clazz: Class<T>, id: Int): Serialization {
open fun <T> register(clazz: Class<T>, id: Int): Serialization<CONNECTION> {
require(!initialized.value) { "Serialization 'register(Class, int)' cannot happen after client/server initialization!" }
// The reason it must be an implementation, is because the reflection serializer DOES NOT WORK with field types, but rather
@ -184,7 +229,7 @@ open class Serialization(private val references: Boolean = true, private val fac
* method. The order must be the same at deserialization as it was for serialization.
*/
@Synchronized
open fun <T> register(clazz: Class<T>, serializer: Serializer<T>): Serialization {
open fun <T> register(clazz: Class<T>, serializer: Serializer<T>): Serialization<CONNECTION> {
require(!initialized.value) { "Serialization 'register(Class, Serializer)' cannot happen after client/server initialization!" }
// The reason it must be an implementation, is because the reflection serializer DOES NOT WORK with field types, but rather
@ -209,7 +254,7 @@ open class Serialization(private val references: Boolean = true, private val fac
* these IDs can be repurposed.
*/
@Synchronized
open fun <T> register(clazz: Class<T>, serializer: Serializer<T>, id: Int): Serialization {
open fun <T> register(clazz: Class<T>, serializer: Serializer<T>, id: Int): Serialization<CONNECTION> {
require(!initialized.value) { "Serialization 'register(Class, Serializer, int)' cannot happen after client/server initialization!" }
// The reason it must be an implementation, is because the reflection serializer DOES NOT WORK with field types, but rather
@ -220,34 +265,6 @@ open class Serialization(private val references: Boolean = true, private val fac
return this
}
/**
* There is additional overhead to using RMI.
*
* This enables a "remote endpoint" to access methods and create objects (RMI) for this endpoint.
*
* This is NOT bi-directional, and this endpoint cannot access or create remote objects on the "remote client".
*
* @param ifaceClass this must be the interface class used for RMI
* @param implClass this must be the implementation class used for RMI
* If *null* it means that this endpoint is the rmi-client
* If *not-null* it means that this endpoint is the rmi-server
*
* @throws IllegalArgumentException if the iface/impl have previously been overridden
*/
@Synchronized
open fun <Iface, Impl : Iface> registerRmi(ifaceClass: Class<Iface>, implClass: Class<Impl>? = null): Serialization {
require(!initialized.value) { "Serialization 'registerRmi(Class, Class)' cannot happen after client/server initialization!" }
require(ifaceClass.isInterface) { "Cannot register an implementation for RMI access. It must be an interface." }
if (implClass != null) {
require(!implClass.isInterface) { "Cannot register an interface for RMI implementations. It must be an implementation." }
}
classesToRegister.add(ClassRegistrationForRmi(ifaceClass, implClass, rmiServerSerializer))
return this
}
/**
* NOTE: When this fails, the CLIENT will just time out. We DO NOT want to send an error message to the client
* (it should check for updates or something else). We do not want to give "rogue" clients knowledge of the
@ -262,8 +279,8 @@ open class Serialization(private val references: Boolean = true, private val fac
/**
* Kryo specifically for handshakes
*/
internal fun initHandshakeKryo(): KryoExtra {
val kryo = KryoExtra()
internal fun initHandshakeKryo(): KryoExtra<CONNECTION> {
val kryo = KryoExtra<CONNECTION>()
kryo.instantiatorStrategy = instantiatorStrategy
kryo.references = references
@ -283,10 +300,10 @@ open class Serialization(private val references: Boolean = true, private val fac
/**
* called as the first thing inside when initializing the classesToRegister
*/
private fun initGlobalKryo(): KryoExtra {
private fun initGlobalKryo(): KryoExtra<CONNECTION> {
// NOTE: classesToRegister.forEach will be called after serialization init!
val kryo = KryoExtra()
val kryo = KryoExtra<CONNECTION>()
kryo.instantiatorStrategy = instantiatorStrategy
kryo.references = references
@ -309,11 +326,10 @@ open class Serialization(private val references: Boolean = true, private val fac
// serialization.register(Message::class.java) // must use full package name!
// RMI stuff!
kryo.register(GlobalObjectCreateRequest::class.java)
kryo.register(GlobalObjectCreateResponse::class.java)
kryo.register(ConnectionObjectCreateRequest::class.java)
kryo.register(ConnectionObjectCreateResponse::class.java)
kryo.register(ConnectionObjectDeleteRequest::class.java)
kryo.register(ConnectionObjectDeleteResponse::class.java)
kryo.register(MethodRequest::class.java, methodRequestSerializer)
kryo.register(MethodResponse::class.java, methodResponseSerializer)
@ -329,8 +345,8 @@ open class Serialization(private val references: Boolean = true, private val fac
/**
* called as the first thing inside when initializing the classesToRegister
*/
private fun initKryo(): KryoExtra {
val kryo = KryoExtra()
private fun initKryo(): KryoExtra<CONNECTION> {
val kryo = KryoExtra<CONNECTION>()
kryo.instantiatorStrategy = instantiatorStrategy
kryo.references = references
@ -353,11 +369,10 @@ open class Serialization(private val references: Boolean = true, private val fac
// serialization.register(Message::class.java) // must use full package name!
// RMI stuff!
kryo.register(GlobalObjectCreateRequest::class.java)
kryo.register(GlobalObjectCreateResponse::class.java)
kryo.register(ConnectionObjectCreateRequest::class.java)
kryo.register(ConnectionObjectCreateResponse::class.java)
kryo.register(ConnectionObjectDeleteRequest::class.java)
kryo.register(ConnectionObjectDeleteResponse::class.java)
kryo.register(MethodRequest::class.java, methodRequestSerializer)
kryo.register(MethodResponse::class.java, methodResponseSerializer)
@ -409,7 +424,7 @@ open class Serialization(private val references: Boolean = true, private val fac
}
@Suppress("UNCHECKED_CAST")
val classesToRegisterForRmi = listOf(*classesToRegister.toTypedArray()) as List<ClassRegistrationForRmi>
val classesToRegisterForRmi = listOf(*classesToRegister.toTypedArray()) as List<ClassRegistrationForRmi<CONNECTION>>
classesToRegister.clear()
// NOTE: to be clear, the "client" can ONLY registerRmi(IFACE, IMPL), to have extra info as the RMI-SERVER!!
@ -419,10 +434,10 @@ open class Serialization(private val references: Boolean = true, private val fac
}
}
private fun initializeClassRegistrations(kryo: KryoExtra): Boolean {
private fun initializeClassRegistrations(kryo: KryoExtra<CONNECTION>): Boolean {
// now MERGE all of the registrations (since we can have registrations overwrite newer/specific registrations based on ID
// in order to get the ID's, these have to be registered with a kryo instance!
val mergedRegistrations = mutableListOf<ClassRegistration>()
val mergedRegistrations = mutableListOf<ClassRegistration<CONNECTION>>()
classesToRegister.forEach { registration ->
val id = registration.id
@ -540,8 +555,8 @@ open class Serialization(private val references: Boolean = true, private val fac
@Suppress("UNCHECKED_CAST")
private fun initializeClient(kryoRegistrationDetailsFromServer: ByteArray,
classesToRegisterForRmi: List<ClassRegistrationForRmi>,
kryo: KryoExtra): Boolean {
classesToRegisterForRmi: List<ClassRegistrationForRmi<CONNECTION>>,
kryo: KryoExtra<CONNECTION>): Boolean {
val input = AeronInput(kryoRegistrationDetailsFromServer)
val clientClassRegistrations = kryo.read(input) as Array<Array<Any>>
@ -579,7 +594,9 @@ open class Serialization(private val references: Boolean = true, private val fac
}
}
logger.trace("CLIENT RMI REG $clazz $implClass")
implClass!!
logger.trace("REGISTRATION (RMI-CLIENT) ${clazz.name} -> ${implClass.name}")
// implClass MIGHT BE NULL!
classesToRegister.add(ClassRegistrationForRmi(clazz, implClass, rmiServerSerializer))
@ -608,14 +625,14 @@ open class Serialization(private val references: Boolean = true, private val fac
/**
* @return takes a kryo instance from the pool, or creates one if the pool was empty
*/
fun takeKryo(): KryoExtra {
fun takeKryo(): KryoExtra<CONNECTION> {
return kryoPool.take()
}
/**
* Returns a kryo instance to the pool for re-use later on
*/
fun returnKryo(kryo: KryoExtra) {
fun returnKryo(kryo: KryoExtra<CONNECTION>) {
kryoPool.put(kryo)
}
@ -695,7 +712,7 @@ open class Serialization(private val references: Boolean = true, private val fac
}
// NOTE: These following functions are ONLY called on a single thread!
fun readMessage(buffer: DirectBuffer, offset: Int, length: Int, connection: Connection): Any? {
fun readMessage(buffer: DirectBuffer, offset: Int, length: Int, connection: CONNECTION): Any? {
return readKryo.read(buffer, offset, length, connection)
}

View File

@ -70,12 +70,6 @@ abstract class BaseTest {
private var autoFailThread: Thread? = null
companion object {
fun setLog() {
setLogLevel(Level.TRACE)
// setLogLevel(Level.ERROR)
// setLogLevel(Level.DEBUG)
}
const val LOOPBACK = "loopback"
fun clientConfig(block: Configuration.() -> Unit = {}): Configuration {
@ -87,8 +81,6 @@ abstract class BaseTest {
configuration.enableIpc = false
block(configuration)
setLog()
return configuration
}
@ -104,8 +96,6 @@ abstract class BaseTest {
configuration.maxConnectionsPerIpAddress = 5
block(configuration)
setLog()
return configuration
}
@ -179,6 +169,16 @@ abstract class BaseTest {
init {
println("---- " + this.javaClass.simpleName)
setLogLevel(Level.TRACE)
// setLogLevel(Level.ERROR)
// setLogLevel(Level.DEBUG)
// we must always make sure that aeron is shut-down before starting again.
while (Server.isRunning(serverConfig())) {
println("Aeron was still running. Waiting for it to stop...")
Thread.sleep(2000)
}
}
fun addEndPoint(endPointConnection: EndPoint<*>) {

View File

@ -155,7 +155,8 @@ class DisconnectReconnectTest : BaseTest() {
delay(2000)
logger.error("Disconnecting via RMI ....")
val closerObject = rmi.getGlobal<CloseIface>(CLOSE_ID)
val closerObject = rmi.get<CloseIface>(CLOSE_ID)
closerObject.close()
}
}

View File

@ -871,15 +871,15 @@ class InputOutputByteBufTest : KryoTestCase() {
write.writeChar(32767.toChar())
write.writeChar(65535.toChar())
val read = AeronInput(write.toBytes())
Assert.assertEquals(0, read.readChar().toLong())
Assert.assertEquals(63, read.readChar().toLong())
Assert.assertEquals(64, read.readChar().toLong())
Assert.assertEquals(127, read.readChar().toLong())
Assert.assertEquals(128, read.readChar().toLong())
Assert.assertEquals(8192, read.readChar().toLong())
Assert.assertEquals(16384, read.readChar().toLong())
Assert.assertEquals(32767, read.readChar().toLong())
Assert.assertEquals(65535, read.readChar().toLong())
Assert.assertEquals(0, read.readChar().code)
Assert.assertEquals(63, read.readChar().code)
Assert.assertEquals(64, read.readChar().code)
Assert.assertEquals(127, read.readChar().code)
Assert.assertEquals(128, read.readChar().code)
Assert.assertEquals(8192, read.readChar().code)
Assert.assertEquals(16384, read.readChar().code)
Assert.assertEquals(32767, read.readChar().code)
Assert.assertEquals(65535, read.readChar().code)
}
@Test

View File

@ -65,8 +65,6 @@ class RmiNestedTest : BaseTest() {
val server = Server<Connection>(configuration)
addEndPoint(server)
server.rmiGlobal.save(TestObjectImpl(), 1)
server.onMessage<OtherObject> { message ->
// The test is complete when the client sends the OtherObject instance.
// this 'object' is the REAL object, not a proxy, because this object is created within this connection.
@ -89,7 +87,8 @@ class RmiNestedTest : BaseTest() {
client.onConnect {
logger.error("Connected")
rmi.getGlobal<TestObject>(1).apply {
rmi.create<TestObject> {
logger.error("Starting test")
setValue(43.21f)
@ -132,8 +131,6 @@ class RmiNestedTest : BaseTest() {
val server = Server<Connection>(configuration)
addEndPoint(server)
server.rmiGlobal.save(TestObjectImpl(), 1)
server.onMessage<OtherObject> { message ->
// The test is complete when the client sends the OtherObject instance.
// this 'object' is the REAL object, not a proxy, because this object is created within this connection.
@ -156,7 +153,7 @@ class RmiNestedTest : BaseTest() {
client.onConnect {
logger.error("Connected")
rmi.getGlobal<TestObject>(1).apply {
rmi.create<TestObject> {
logger.error("Starting test")
setValue(43.21f)
@ -199,8 +196,6 @@ class RmiNestedTest : BaseTest() {
val server = Server<Connection>(configuration)
addEndPoint(server)
server.rmiGlobal.save(TestObjectImpl(), 1)
server.onMessage<OtherObject> { message ->
// The test is complete when the client sends the OtherObject instance.
// this 'object' is the REAL object
@ -224,7 +219,7 @@ class RmiNestedTest : BaseTest() {
client.onConnect {
logger.error("Connected")
rmi.getGlobal<TestObject>(1).apply {
rmi.create<TestObject> {
logger.error("Starting test")
setOtherValue(43.21f)
@ -263,7 +258,7 @@ class RmiNestedTest : BaseTest() {
server.onConnect {
logger.error("Connected")
rmi.get<TestObject>(1).apply {
rmi.create<TestObject> {
logger.error("Starting test")
setOtherValue(43.21f)
@ -295,11 +290,6 @@ class RmiNestedTest : BaseTest() {
val client = Client<Connection>(configuration)
addEndPoint(client)
client.onConnect {
rmi.save(TestObjectImpl(), 1)
}
client.onMessage<OtherObject> { message ->
// The test is complete when the client sends the OtherObject instance.
// this 'object' is the REAL object

View File

@ -40,166 +40,66 @@ import dorkbox.network.Client
import dorkbox.network.Configuration
import dorkbox.network.Server
import dorkbox.network.connection.Connection
import dorkbox.network.rmi.RemoteObject
import dorkboxTest.network.BaseTest
import dorkboxTest.network.rmi.cows.MessageWithTestCow
import dorkboxTest.network.rmi.cows.TestCow
import dorkboxTest.network.rmi.cows.TestCowImpl
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Test
class RmiSimpleActionsTest : BaseTest() {
@Test
fun testGlobalDelete() {
val configuration = serverConfig()
configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java)
configuration.serialization.register(MessageWithTestCow::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)
val OBJ_ID = 3423
val testCowImpl = TestCowImpl(OBJ_ID)
server.rmiGlobal.save(testCowImpl, OBJ_ID)
Assert.assertTrue(server.rmiGlobal.delete(testCowImpl))
Assert.assertFalse(server.rmiGlobal.delete(testCowImpl))
Assert.assertFalse(server.rmiGlobal.delete(OBJ_ID))
val newId = server.rmiGlobal.save(testCowImpl)
Assert.assertTrue(server.rmiGlobal.delete(newId))
Assert.assertFalse(server.rmiGlobal.delete(newId))
Assert.assertFalse(server.rmiGlobal.delete(testCowImpl))
val newId2 = server.rmiGlobal.save(testCowImpl)
Assert.assertTrue(server.rmiGlobal.delete(testCowImpl))
Assert.assertFalse(server.rmiGlobal.delete(testCowImpl))
Assert.assertFalse(server.rmiGlobal.delete(newId2))
}
@Test
fun rmiIPv4NetworkGlobalDelete() {
rmiConnectionDelete(isIpv4 = true, isIpv6 = false)
}
@Test
fun testGlobalDeleteServer() {
val OBJ_ID = 3423
val configuration = serverConfig()
configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java)
configuration.serialization.register(MessageWithTestCow::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)
val testCowImpl = TestCowImpl(OBJ_ID)
server.rmiGlobal.save(testCowImpl, OBJ_ID)
// Assert.assertNotNull(server.rmi.get(OBJ_ID))
Assert.assertTrue(server.rmiGlobal.delete(testCowImpl))
Assert.assertFalse(server.rmiGlobal.delete(testCowImpl))
Assert.assertFalse(server.rmiGlobal.delete(OBJ_ID))
// Assert.assertNull(server.rmi.get(OBJ_ID))
val newId = server.rmiGlobal.save(testCowImpl)
// Assert.assertNotNull(server.rmi.get(newId))
Assert.assertTrue(server.rmiGlobal.delete(newId))
Assert.assertFalse(server.rmiGlobal.delete(newId))
Assert.assertFalse(server.rmiGlobal.delete(testCowImpl))
// Assert.assertNull(server.rmi.get(newId))
}
@Test
fun testGlobalDelete() {
val SERVER_ID = 1123
val CLIENT_ID = 3423
val server = run {
val configuration = serverConfig()
configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java)
val server = Server<Connection>(configuration)
addEndPoint(server)
server.bind()
server
}
val client = run {
val configuration = clientConfig()
configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java)
val client = Client<Connection>(configuration)
addEndPoint(client)
client.connect(LOOPBACK)
client
}
val serverCow = TestCowImpl(SERVER_ID)
server.rmiGlobal.save(serverCow, SERVER_ID)
// val clientCow = TestCowImpl(CLIENT_ID)
// client.rmi.save(serverCow, CLIENT_ID)
val get1 = client.connection.rmi.get<TestCow>(CLIENT_ID)
get1 as RemoteObject
get1.enableEquals(true)
Assert.assertNotNull(get1)
Assert.assertTrue(get1 == serverCow)
// Assert.assertTrue(client.rmi.delete(clientCow))
// Assert.assertFalse(client.rmi.delete(clientCow))
// Assert.assertFalse(client.rmi.delete(CLIENT_ID))
// Assert.assertNull(client.rmi.get(CLIENT_ID))
//
// val newId = client.rmi.save(clientCow)
// Assert.assertNotNull(client.rmi.get(CLIENT_ID))
//
// Assert.assertTrue(client.rmi.delete(CLIENT_ID))
// Assert.assertFalse(client.rmi.delete(CLIENT_ID))
// Assert.assertFalse(client.rmi.delete(clientCow))
// Assert.assertNull(client.rmi.get(CLIENT_ID))
runBlocking {
stopEndPoints()
waitForThreads()
private fun doConnect(isIpv4: Boolean, isIpv6: Boolean, runIpv4Connect: Boolean, client: Client<Connection>) {
when {
isIpv4 && isIpv6 && runIpv4Connect -> client.connect(IPv4.LOCALHOST)
isIpv4 && isIpv6 && !runIpv4Connect -> client.connect(IPv6.LOCALHOST)
isIpv4 -> client.connect(IPv4.LOCALHOST)
isIpv6 -> client.connect(IPv6.LOCALHOST)
else -> client.connect()
}
}
@Test
fun testGlobalDelete3() {
val OBJ_ID = 3423
val configuration = serverConfig()
configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java)
configuration.serialization.register(MessageWithTestCow::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)
val testCowImpl = TestCowImpl(OBJ_ID)
server.rmiGlobal.save(testCowImpl, OBJ_ID)
Assert.assertTrue(server.rmiGlobal.delete(OBJ_ID))
Assert.assertFalse(server.rmiGlobal.delete(testCowImpl))
}
@Test
fun testGlobalDelete4() {
val OBJ_ID = 3423
val configuration = serverConfig()
configuration.serialization.rmi.register(TestCow::class.java, TestCowImpl::class.java)
configuration.serialization.register(MessageWithTestCow::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)
val testCowImpl = TestCowImpl(OBJ_ID)
server.rmiGlobal.save(testCowImpl, OBJ_ID)
Assert.assertTrue(server.rmiGlobal.delete(OBJ_ID))
Assert.assertFalse(server.rmiGlobal.delete(testCowImpl))
}
fun rmiConnectionDelete(isIpv4: Boolean = false, isIpv6: Boolean = false, runIpv4Connect: Boolean = true, config: Configuration.() -> Unit = {}) {
val OBJ_ID = 3423
run {
val configuration = serverConfig()
configuration.enableIPv4 = isIpv4
@ -217,70 +117,54 @@ class RmiSimpleActionsTest : BaseTest() {
addEndPoint(server)
server.bind()
server.rmiGlobal.save(TestCowImpl(44), 44)
server.onMessage<MessageWithTestCow> { m ->
server.logger.error("Received finish signal for test for: Client -> Server")
val `object` = m.testCow
val id = `object`.id()
Assert.assertEquals(44, id.toLong())
Assert.assertEquals(23, id)
server.logger.error("Finished test for: Client -> Server")
// normally this is in the 'connected', but we do it here, so that it's more linear and easier to debug
server.logger.error("Running test for: Server -> Client")
RmiCommonTest.runTests(this@onMessage, rmi.getGlobal(4), 4)
server.logger.error("Done with test for: Server -> Client")
rmi.delete(23)
// `object` is still a reference to the object!
// so we don't want to pass that back -- so pass back a new one
send(MessageWithTestCow(TestCowImpl(1)))
}
}
run {
val configuration = clientConfig()
config(configuration)
// configuration.serialization.registerRmi(TestCow::class.java, TestCowImpl::class.java)
// configuration.serialization.registerRmi(TestCow::class.java, TestCowImpl::class.java)
val client = Client<Connection>(configuration)
addEndPoint(client)
client.onConnect {
rmi.save(TestCowImpl(4), 4)
rmi.create<TestCow>(23) {
client.logger.error("Running test for: Client -> Server")
RmiCommonTest.runTests(this@onConnect, this, 23)
client.logger.error("Done with test for: Client -> Server")
}
}
client.onMessage<MessageWithTestCow> { _ ->
// check if 23 still exists (it should not)
val obj = rmi.get<TestCow>(23)
try {
obj.id()
Assert.fail(".id() should throw an exception, the backing RMI object doesn't exist!")
} catch (e: Exception) {
// this is expected
}
client.onMessage<MessageWithTestCow> { m ->
client.logger.error("Received finish signal for test for: Client -> Server")
val `object` = m.testCow
val id = `object`.id()
Assert.assertEquals(4, id.toLong())
client.logger.error("Finished test for: Client -> Server")
stopEndPoints(2000)
}
when {
isIpv4 && isIpv6 && runIpv4Connect -> client.connect(IPv4.LOCALHOST)
isIpv4 && isIpv6 && !runIpv4Connect -> client.connect(IPv6.LOCALHOST)
isIpv4 -> client.connect(IPv4.LOCALHOST)
isIpv6 -> client.connect(IPv6.LOCALHOST)
else -> client.connect()
}
client.logger.error("Starting test for: Client -> Server")
// this creates a GLOBAL object on the server (instead of a connection specific object)
runBlocking {
// client.rmi.get()
// client.createObject<TestCow>(44) {
// client.deleteObject(this)
// client.logger.error("Running test for: Client -> Server")
// RmiCommonTest.runTests(client.connection, this, 44)
// client.logger.error("Done with test for: Client -> Server")
// }
}
doConnect(isIpv4, isIpv6, runIpv4Connect, client)
}
waitForThreads()

View File

@ -170,6 +170,10 @@ class RmiSimpleTest : BaseTest() {
client.onConnect {
rmi.save(TestCowImpl(4), 4)
client.logger.error("Running test for: Client -> Server")
RmiCommonTest.runTests(this, rmi.getGlobal(44), 44)
client.logger.error("Done with test for: Client -> Server")
}
client.onMessage<MessageWithTestCow> { m ->
@ -187,9 +191,7 @@ class RmiSimpleTest : BaseTest() {
// this creates a GLOBAL object on the server (instead of a connection specific object)
runBlocking {
client.logger.error("Running test for: Client -> Server")
RmiCommonTest.runTests(client.connection, client.connection.rmi.getGlobal(44), 44)
client.logger.error("Done with test for: Client -> Server")
}
}
@ -252,7 +254,7 @@ class RmiSimpleTest : BaseTest() {
client.logger.error("Received finish signal for test for: Client -> Server")
val `object` = m.testCow
val id = `object`.id()
Assert.assertEquals(123, id.toLong())
Assert.assertEquals(123, id)
client.logger.error("Finished test for: Client -> Server")
stopEndPoints(2000)
}

View File

@ -31,20 +31,22 @@ class RmiSpamSyncSuspendingTest : BaseTest() {
private val RMI_ID = 12251
init {
// the logger cannot keep-up if it's on trace
setLogLevel(ch.qos.logback.classic.Level.DEBUG)
}
@Test
fun rmiNetwork() {
rmi {
enableIpc = false
}
// the logger cannot keep-up if it's on trace
setLogLevel(ch.qos.logback.classic.Level.DEBUG)
rmi()
}
@Test
fun rmiIpc() {
rmi()
// the logger cannot keep-up if it's on trace
setLogLevel(ch.qos.logback.classic.Level.DEBUG)
rmi {
enableIpc = true
}
}

View File

@ -54,7 +54,7 @@ object TestServer {
logger.error("Received finish signal for test for: Client -> Server")
val `object` = m.testCow
val id = `object`.id()
Assert.assertEquals(124123, id.toLong())
Assert.assertEquals(124123, id)
logger.error("Finished test for: Client -> Server")
//