diff --git a/LICENSE b/LICENSE index 9aec8d82..a218e2d6 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/build.gradle.kts b/build.gradle.kts index 728ddbc7..0419d63f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 ") - it.author("Nathan Sweet ") - it.url("http://github.com/libgdx/libgdx") + copyright(2013) + author("Mario Zechner ") + author("Nathan Sweet ") + 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") diff --git a/src/dorkbox/network/Client.kt b/src/dorkbox/network/Client.kt index a44d9310..e03ffdee 100644 --- a/src/dorkbox/network/Client.kt +++ b/src/dorkbox/network/Client.kt @@ -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(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(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 { - 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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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)] 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 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 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 - - @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 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)] 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 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) } } diff --git a/src/dorkbox/network/Configuration.kt b/src/dorkbox/network/Configuration.kt index 06205671..9bb89cd6 100644 --- a/src/dorkbox/network/Configuration.kt +++ b/src/dorkbox/network/Configuration.kt @@ -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 = 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. diff --git a/src/dorkbox/network/Server.kt b/src/dorkbox/network/Server.kt index 348084bf..9abb6c57 100644 --- a/src/dorkbox/network/Server.kt +++ b/src/dorkbox/network/Server.kt @@ -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(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(config: ServerConfiguration = ServerC return ServerException(message, cause) } - final override fun getRmiConnectionSupport(): RmiManagerConnections { - 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(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(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(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(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(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(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(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(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(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(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 - } } diff --git a/src/dorkbox/network/aeron/AeronDriver.kt b/src/dorkbox/network/aeron/AeronDriver.kt index 60c44261..f0ba747d 100644 --- a/src/dorkbox/network/aeron/AeronDriver.kt +++ b/src/dorkbox/network/aeron/AeronDriver.kt @@ -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) diff --git a/src/dorkbox/network/aeron/MediaDriverConnection.kt b/src/dorkbox/network/aeron/MediaDriverConnection.kt index e44c0df4..8cfd5751 100644 --- a/src/dorkbox/network/aeron/MediaDriverConnection.kt +++ b/src/dorkbox/network/aeron/MediaDriverConnection.kt @@ -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) diff --git a/src/dorkbox/network/aeron/UdpMediaDriverClientConnection.kt b/src/dorkbox/network/aeron/UdpMediaDriverClientConnection.kt index 0dce4303..c1b1651f 100644 --- a/src/dorkbox/network/aeron/UdpMediaDriverClientConnection.kt +++ b/src/dorkbox/network/aeron/UdpMediaDriverClientConnection.kt @@ -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) diff --git a/src/dorkbox/network/connection/Connection.kt b/src/dorkbox/network/connection/Connection.kt index a2fdbc25..4da8af21 100644 --- a/src/dorkbox/network/connection/Connection.kt +++ b/src/dorkbox/network/connection/Connection.kt @@ -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 // 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 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)] 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 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)] 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 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 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 - - 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 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") - } } diff --git a/src/dorkbox/network/connection/ConnectionParams.kt b/src/dorkbox/network/connection/ConnectionParams.kt index c92ed338..56c75266 100644 --- a/src/dorkbox/network/connection/ConnectionParams.kt +++ b/src/dorkbox/network/connection/ConnectionParams.kt @@ -16,7 +16,11 @@ package dorkbox.network.connection import dorkbox.network.aeron.MediaDriverConnection +import dorkbox.network.rmi.RmiManagerConnections -data class ConnectionParams(val endPoint: EndPoint, +data class ConnectionParams( + val endPoint: EndPoint, val mediaDriverConnection: MediaDriverConnection, - val publicKeyValidation: PublicKeyValidationState) + val publicKeyValidation: PublicKeyValidationState, + val rmiConnectionSupport: RmiManagerConnections +) diff --git a/src/dorkbox/network/connection/EndPoint.kt b/src/dorkbox/network/connection/EndPoint.kt index 0e05794e..9dd5fc2e 100644 --- a/src/dorkbox/network/connection/EndPoint.kt +++ b/src/dorkbox/network/connection/EndPoint.kt @@ -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() + internal val listenerManager = ListenerManager(logger) internal val connections = ConnectionManager() internal val pingManager = PingManager(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 - private val handshakeKryo: KryoExtra + private val handshakeKryo: KryoExtra 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(logger, actionDispatch, config.serialization) + internal val rmiGlobalSupport = RmiManagerGlobal(logger, actionDispatch) + internal val rmiConnectionSupport: RmiManagerConnections 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 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) + { _, _, _ -> + 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, 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 { - 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 = 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 } diff --git a/src/dorkbox/network/connection/ListenerManager.kt b/src/dorkbox/network/connection/ListenerManager.kt index 5b4a338f..313d13e3 100644 --- a/src/dorkbox/network/connection/ListenerManager.kt +++ b/src/dorkbox/network/connection/ListenerManager.kt @@ -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 { +internal class ListenerManager(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 { 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 { 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 { // 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 { } 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 { } 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 { 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 { } 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) } } } diff --git a/src/dorkbox/network/handshake/ClientHandshake.kt b/src/dorkbox/network/handshake/ClientHandshake.kt index 7050025a..9904e4c8 100644 --- a/src/dorkbox/network/handshake/ClientHandshake.kt +++ b/src/dorkbox/network/handshake/ClientHandshake.kt @@ -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(private val crypto: CryptoManagement, private val endPoint: EndPoint) { +internal class ClientHandshake( + private val crypto: CryptoManagement, + private val endPoint: Client, + 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(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(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(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(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(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(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(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(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(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(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(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(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") } diff --git a/src/dorkbox/network/handshake/PortAllocator.kt b/src/dorkbox/network/handshake/PortAllocator.kt index 8c7c3354..6cd27d87 100644 --- a/src/dorkbox/network/handshake/PortAllocator.kt +++ b/src/dorkbox/network/handshake/PortAllocator.kt @@ -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") diff --git a/src/dorkbox/network/handshake/ServerHandshake.kt b/src/dorkbox/network/handshake/ServerHandshake.kt index 1e555398..2a1758a2 100644 --- a/src/dorkbox/network/handshake/ServerHandshake.kt +++ b/src/dorkbox/network/handshake/ServerHandshake.kt @@ -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(private val logger: KLog private val listenerManager: ListenerManager) { // 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 = Caffeine.newBuilder() - .expireAfterAccess(config.connectionCloseTimeoutInSeconds.toLong() * 2, TimeUnit.SECONDS) - .removalListener(RemovalListener { sessionId, connection, cause -> - if (cause == RemovalCause.EXPIRED) { - connection!! + private val pendingConnections = ExpiringMap.builder() + .expiration(config.connectionCloseTimeoutInSeconds.toLong() * 2, TimeUnit.SECONDS) + .expirationPolicy(ExpirationPolicy.CREATED) + .expirationListener { _, 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() + private val connectionsPerIpCounts = ConnectionCounts() @@ -82,14 +79,16 @@ internal class ServerHandshake(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(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(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(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(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(private val logger: KLog // note: CANNOT be called in action dispatch. ALWAYS ON SAME THREAD fun processIpcHandshakeMessageServer(server: Server, + rmiConnectionSupport: RmiManagerConnections, handshakePublication: Publication, sessionId: Int, message: HandshakeMessage, @@ -202,11 +209,13 @@ internal class ServerHandshake(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(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(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(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(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, + rmiConnectionSupport: RmiManagerConnections, handshakePublication: Publication, sessionId: Int, clientAddressString: String, @@ -326,9 +338,7 @@ internal class ServerHandshake(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(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(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(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(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(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(private val logger: KLog sessionIdAllocator.clear() streamIdAllocator.clear() - pendingConnections.invalidateAll() - pendingConnections.cleanUp() + pendingConnections.clear() } } diff --git a/src/dorkbox/network/ping/PingManager.kt b/src/dorkbox/network/ping/PingManager.kt index 41e239e4..9a7d4b66 100644 --- a/src/dorkbox/network/ping/PingManager.kt +++ b/src/dorkbox/network/ping/PingManager.kt @@ -59,7 +59,7 @@ class PingManager(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(logger: KLogger, actionDispatch: Coro //// ping0(ping) //// return pingFuture!! // TODO() + return false } } diff --git a/src/dorkbox/network/rmi/RemoteObjectStorage.kt b/src/dorkbox/network/rmi/RemoteObjectStorage.kt index 3ae39ddb..d205d74f 100644 --- a/src/dorkbox/network/rmi/RemoteObjectStorage.kt +++ b/src/dorkbox/network/rmi/RemoteObjectStorage.kt @@ -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 registered with .toString() = '${`object`}'" + "Remote object registered with .toString() = '${`object`}'" } } @@ -226,7 +226,7 @@ class RemoteObjectStorage(val logger: KLogger) { objectMap.put(objectId, `object`) logger.trace { - "Object registered with .toString() = '${`object`}'" + "Remote object registered with .toString() = '${`object`}'" } return true @@ -243,7 +243,7 @@ class RemoteObjectStorage(val logger: KLogger) { returnId(objectId) logger.trace { - "Object removed with .toString() = '${rmiObject}'" + "Object removed" } @Suppress("UNCHECKED_CAST") return rmiObject diff --git a/src/dorkbox/network/rmi/ResponseManager.kt b/src/dorkbox/network/rmi/ResponseManager.kt index 088b1975..9d40f692 100644 --- a/src/dorkbox/network/rmi/ResponseManager.kt +++ b/src/dorkbox/network/rmi/ResponseManager.kt @@ -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() } diff --git a/src/dorkbox/network/rmi/ResponseWaiter.kt b/src/dorkbox/network/rmi/ResponseWaiter.kt index 2476b0a7..8ab8d4af 100644 --- a/src/dorkbox/network/rmi/ResponseWaiter.kt +++ b/src/dorkbox/network/rmi/ResponseWaiter.kt @@ -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) { } } } diff --git a/src/dorkbox/network/rmi/RmiManagerConnections.kt b/src/dorkbox/network/rmi/RmiManagerConnections.kt index 2c4b2297..496d9f29 100644 --- a/src/dorkbox/network/rmi/RmiManagerConnections.kt +++ b/src/dorkbox/network/rmi/RmiManagerConnections.kt @@ -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(logger: KLogger, - val rmiGlobalSupport: RmiManagerGlobal, - private val serialization: Serialization) : RmiObjectCache(logger) { +class RmiManagerConnections internal constructor( + private val logger: KLogger, + private val responseManager: ResponseManager, + private val listenerManager: ListenerManager, + private val serialization: Serialization, + 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() + // 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 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 getProxyObject(connection: Connection, kryoId: Int, rmiId: Int, interfaceClass: Class): Iface { + fun getProxyObject(isGlobal: Boolean, connection: CONNECTION, rmiId: Int, interfaceClass: Class): 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(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 deleteRemoteObject(connection: Connection, rmiId: Int) { - removeProxyObject(rmiId) + fun getProxyObject(isGlobal: Boolean, connection: CONNECTION, kryoId: Int, rmiId: Int, interfaceClass: Class): 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 createRemoteObject(connection: Connection, kryoId: Int, objectParameters: Array?, callback: suspend Iface.() -> Unit) { - val callbackId = rmiGlobalSupport.registerCallback(callback) + suspend fun createRemoteObject(connection: CONNECTION, kryoId: Int, objectParameters: Array?, 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(logger: KLogger, /** * called on "server" */ - suspend fun onConnectionObjectCreateRequest(endPoint: EndPoint, connection: CONNECTION, message: ConnectionObjectCreateRequest) { - + fun onConnectionObjectCreateRequest( + serialization: Serialization, + 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(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(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(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 { + @Suppress("LeakingThis", "UNCHECKED_CAST") + return RmiSupportConnection(logger, connection as CONNECTION, this, serialization, getGlobalAction) } } diff --git a/src/dorkbox/network/rmi/RmiManagerGlobal.kt b/src/dorkbox/network/rmi/RmiManagerGlobal.kt index c0310e4c..5deab637 100644 --- a/src/dorkbox/network/rmi/RmiManagerGlobal.kt +++ b/src/dorkbox/network/rmi/RmiManagerGlobal.kt @@ -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(logger: KLogger, - actionDispatch: CoroutineScope, - private val serialization: Serialization) : RmiObjectCache(logger) { +internal class RmiManagerGlobal(private val logger: KLogger, + private val listenerManager: ListenerManager) : RmiObjectCache(logger) { companion object { /** @@ -48,19 +45,21 @@ internal class RmiManagerGlobal(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 createProxyObject( + isGlobalObject: Boolean, + connection: CONNECTION, + serialization: Serialization, + 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(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 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 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 removeImplObject(endPoint: EndPoint, objectId: Int): T? { - val success = removeImplObject(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, - 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 getGlobalRemoteObject(connection: Connection, objectId: Int, interfaceClass: Class): 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 + + // 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, message: Any, logger: KLogger) { + fun manage( + endPoint: EndPoint, + serialization: Serialization, + connection: CONNECTION, + message: Any, + rmiConnectionSupport: RmiManagerConnections, + actionDispatch: CoroutineScope + ) { when (message) { is ConnectionObjectCreateRequest -> { /** * called on "server" */ - @Suppress("UNCHECKED_CAST") - val rmiConnectionSupport: RmiManagerConnections = connection.rmiConnectionSupport as RmiManagerConnections - 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(logger: KLogger, logger.trace { "RMI received: $rmiId" } - val implObject = getImplObject(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(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 { 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 { 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(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(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, - 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 } } diff --git a/src/dorkbox/network/rmi/RmiObjectCache.kt b/src/dorkbox/network/rmi/RmiObjectCache.kt index a1fb273b..da169670 100644 --- a/src/dorkbox/network/rmi/RmiObjectCache.kt +++ b/src/dorkbox/network/rmi/RmiObjectCache.kt @@ -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 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 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 getId(implObject: T): Int { return implObjects.getId(implObject) } - - fun removeImplObject(rmiId: Int): T? { - return implObjects.remove(rmiId) as T? - } } diff --git a/src/dorkbox/network/rmi/RmiSupportConnection.kt b/src/dorkbox/network/rmi/RmiSupportConnection.kt new file mode 100644 index 00000000..718e498b --- /dev/null +++ b/src/dorkbox/network/rmi/RmiSupportConnection.kt @@ -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 internal constructor( + private val logger: KLogger, + private val connection: CONNECTION, + private val rmiConnectionSupport: RmiManagerConnections, + private val serialization: Serialization, + 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 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 + + 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 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 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 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 + } +} diff --git a/src/dorkbox/network/rmi/RmiSupportServer.kt b/src/dorkbox/network/rmi/RmiSupportServer.kt new file mode 100644 index 00000000..e49cb019 --- /dev/null +++ b/src/dorkbox/network/rmi/RmiSupportServer.kt @@ -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 internal constructor( + private val logger: KLogger, + private val rmiGlobalSupport: RmiManagerGlobal +) { + /** + * 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(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(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 + } +} diff --git a/src/dorkbox/network/rmi/RmiUtils.kt b/src/dorkbox/network/rmi/RmiUtils.kt index c9dbfd72..d50ec6e8 100644 --- a/src/dorkbox/network/rmi/RmiUtils.kt +++ b/src/dorkbox/network/rmi/RmiUtils.kt @@ -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-- } diff --git a/src/dorkbox/network/rmi/messages/ConnectionObjectDeleteRequest.kt b/src/dorkbox/network/rmi/messages/ConnectionObjectDeleteRequest.kt new file mode 100644 index 00000000..a0dedd4b --- /dev/null +++ b/src/dorkbox/network/rmi/messages/ConnectionObjectDeleteRequest.kt @@ -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)" + } +} diff --git a/src/dorkbox/network/rmi/messages/ConnectionObjectDeleteResponse.kt b/src/dorkbox/network/rmi/messages/ConnectionObjectDeleteResponse.kt new file mode 100644 index 00000000..cac176c2 --- /dev/null +++ b/src/dorkbox/network/rmi/messages/ConnectionObjectDeleteResponse.kt @@ -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)" + } +} diff --git a/src/dorkbox/network/rmi/messages/MethodRequestSerializer.kt b/src/dorkbox/network/rmi/messages/MethodRequestSerializer.kt index a7ba2a9e..f3f34c49 100644 --- a/src/dorkbox/network/rmi/messages/MethodRequestSerializer.kt +++ b/src/dorkbox/network/rmi/messages/MethodRequestSerializer.kt @@ -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>) : Serializer() { +class MethodRequestSerializer(private val methodCache: Int2ObjectHashMap>) : Serializer() { override fun write(kryo: Kryo, output: Output, methodRequest: MethodRequest) { val method = methodRequest.cachedMethod @@ -82,7 +83,7 @@ class MethodRequestSerializer(private val methodCache: Int2ObjectHashMap val cachedMethod = try { methodCache[methodClassId][methodIndex] diff --git a/src/dorkbox/network/rmi/messages/RmiClientSerializer.kt b/src/dorkbox/network/rmi/messages/RmiClientSerializer.kt index eeca2f6e..a86e0571 100644 --- a/src/dorkbox/network/rmi/messages/RmiClientSerializer.kt +++ b/src/dorkbox/network/rmi/messages/RmiClientSerializer.kt @@ -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() { +@Suppress("UNCHECKED_CAST") +class RmiClientSerializer: Serializer() { 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() { 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 + val endPoint: EndPoint = kryo.connection.endPoint as EndPoint + + return if (isGlobal) { + endPoint.rmiGlobalSupport.getImplObject(objectId) + } else { + endPoint.rmiConnectionSupport.getImplObject(objectId) + } } } diff --git a/src/dorkbox/network/rmi/messages/RmiServerSerializer.kt b/src/dorkbox/network/rmi/messages/RmiServerSerializer.kt index 61e73620..2a732d85 100644 --- a/src/dorkbox/network/rmi/messages/RmiServerSerializer.kt +++ b/src/dorkbox/network/rmi/messages/RmiServerSerializer.kt @@ -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(false) { +@Suppress("UNCHECKED_CAST") +class RmiServerSerializer : Serializer(false) { override fun write(kryo: Kryo, output: Output, `object`: Any) { - val kryoExtra = kryo as KryoExtra + val kryoExtra = kryo as KryoExtra 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(false) { } override fun read(kryo: Kryo, input: Input, interfaceClass: Class<*>): Any? { - val kryoExtra = kryo as KryoExtra + val kryoExtra = kryo as KryoExtra 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(false) { // the rmi-server will have iface+impl id's // the rmi-client will have iface id's + val rmiConnectionSupport = endPoint.rmiConnectionSupport as RmiManagerConnections 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) } } } diff --git a/src/dorkbox/network/serialization/ClassRegistration.kt b/src/dorkbox/network/serialization/ClassRegistration.kt index 093c40a0..a9d4fabc 100644 --- a/src/dorkbox/network/serialization/ClassRegistration.kt +++ b/src/dorkbox/network/serialization/ClassRegistration.kt @@ -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(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, 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}" diff --git a/src/dorkbox/network/serialization/ClassRegistration0.kt b/src/dorkbox/network/serialization/ClassRegistration0.kt index a56f0686..6224d850 100644 --- a/src/dorkbox/network/serialization/ClassRegistration0.kt +++ b/src/dorkbox/network/serialization/ClassRegistration0.kt @@ -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(clazz: Class<*>, serializer: Serializer<*>) : ClassRegistration(clazz, serializer) { override fun register(kryo: Kryo) { id = kryo.register(clazz, serializer).id info = "Registered $id -> ${clazz.name} using ${serializer!!.javaClass.name}" diff --git a/src/dorkbox/network/serialization/ClassRegistration1.kt b/src/dorkbox/network/serialization/ClassRegistration1.kt index 9b2a15e9..8b9113a7 100644 --- a/src/dorkbox/network/serialization/ClassRegistration1.kt +++ b/src/dorkbox/network/serialization/ClassRegistration1.kt @@ -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(clazz: Class<*>, id: Int) : ClassRegistration(clazz, null, id) { override fun register(kryo: Kryo) { kryo.register(clazz, id) info = "Registered $id -> (specified) ${clazz.name}" diff --git a/src/dorkbox/network/serialization/ClassRegistration2.kt b/src/dorkbox/network/serialization/ClassRegistration2.kt index 94e788f8..a7500613 100644 --- a/src/dorkbox/network/serialization/ClassRegistration2.kt +++ b/src/dorkbox/network/serialization/ClassRegistration2.kt @@ -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(clazz: Class<*>, serializer: Serializer<*>, id: Int) : ClassRegistration(clazz, serializer, id) { override fun register(kryo: Kryo) { kryo.register(clazz, serializer, id) diff --git a/src/dorkbox/network/serialization/ClassRegistration3.kt b/src/dorkbox/network/serialization/ClassRegistration3.kt index 4248db64..02f650cf 100644 --- a/src/dorkbox/network/serialization/ClassRegistration3.kt +++ b/src/dorkbox/network/serialization/ClassRegistration3.kt @@ -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(clazz: Class<*>) : ClassRegistration(clazz) { override fun register(kryo: Kryo) { id = kryo.register(clazz).id diff --git a/src/dorkbox/network/serialization/ClassRegistrationForRmi.kt b/src/dorkbox/network/serialization/ClassRegistrationForRmi.kt index 70d9b1eb..1125ad1a 100644 --- a/src/dorkbox/network/serialization/ClassRegistrationForRmi.kt +++ b/src/dorkbox/network/serialization/ClassRegistrationForRmi.kt @@ -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(ifaceClass: Class<*>, + var implClass: Class<*>?, + serializer: RmiServerSerializer) : ClassRegistration(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, 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! diff --git a/src/dorkbox/network/serialization/KryoExtra.kt b/src/dorkbox/network/serialization/KryoExtra.kt index dfd7b3cd..2c010312 100644 --- a/src/dorkbox/network/serialization/KryoExtra.kt +++ b/src/dorkbox/network/serialization/KryoExtra.kt @@ -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() : 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 diff --git a/src/dorkbox/network/serialization/Serialization.kt b/src/dorkbox/network/serialization/Serialization.kt index e930b241..99d75786 100644 --- a/src/dorkbox/network/serialization/Serialization.kt +++ b/src/dorkbox/network/serialization/Serialization.kt @@ -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(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 internal constructor( + private val initialized: AtomicBoolean, + private val classesToRegister: MutableList>, + private val rmiServerSerializer: RmiServerSerializer + ) { + /** + * 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 register(ifaceClass: Class, implClass: Class? = null): RmiSupport { + 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() + private val classesToRegister = mutableListOf>() 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(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() + private val rmiServerSerializer = RmiServerSerializer() + + /** + * 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 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 + private val kryoPool: Pool> init { - val poolObject = object : PoolObject() { - override fun newInstance(): KryoExtra { + val poolObject = object : PoolObject>() { + override fun newInstance(): KryoExtra { 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 register(clazz: Class): Serialization { + open fun register(clazz: Class): Serialization { 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 register(clazz: Class, id: Int): Serialization { + open fun register(clazz: Class, id: Int): Serialization { 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 register(clazz: Class, serializer: Serializer): Serialization { + open fun register(clazz: Class, serializer: Serializer): Serialization { 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 register(clazz: Class, serializer: Serializer, id: Int): Serialization { + open fun register(clazz: Class, serializer: Serializer, id: Int): Serialization { 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 registerRmi(ifaceClass: Class, implClass: Class? = 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 { + val kryo = KryoExtra() 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 { // NOTE: classesToRegister.forEach will be called after serialization init! - val kryo = KryoExtra() + val kryo = KryoExtra() 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 { + val kryo = KryoExtra() 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 + val classesToRegisterForRmi = listOf(*classesToRegister.toTypedArray()) as List> 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): 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() + val mergedRegistrations = mutableListOf>() 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, - kryo: KryoExtra): Boolean { + classesToRegisterForRmi: List>, + kryo: KryoExtra): Boolean { val input = AeronInput(kryoRegistrationDetailsFromServer) val clientClassRegistrations = kryo.read(input) as Array> @@ -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 { return kryoPool.take() } /** * Returns a kryo instance to the pool for re-use later on */ - fun returnKryo(kryo: KryoExtra) { + fun returnKryo(kryo: KryoExtra) { 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) } diff --git a/test/dorkboxTest/network/BaseTest.kt b/test/dorkboxTest/network/BaseTest.kt index 10db006a..9ffa7e5e 100644 --- a/test/dorkboxTest/network/BaseTest.kt +++ b/test/dorkboxTest/network/BaseTest.kt @@ -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<*>) { diff --git a/test/dorkboxTest/network/DisconnectReconnectTest.kt b/test/dorkboxTest/network/DisconnectReconnectTest.kt index 8ed4047f..d96210b8 100644 --- a/test/dorkboxTest/network/DisconnectReconnectTest.kt +++ b/test/dorkboxTest/network/DisconnectReconnectTest.kt @@ -155,7 +155,8 @@ class DisconnectReconnectTest : BaseTest() { delay(2000) logger.error("Disconnecting via RMI ....") - val closerObject = rmi.getGlobal(CLOSE_ID) + + val closerObject = rmi.get(CLOSE_ID) closerObject.close() } } diff --git a/test/dorkboxTest/network/kryo/InputOutputByteBufTest.kt b/test/dorkboxTest/network/kryo/InputOutputByteBufTest.kt index 57c14710..7dbd83f6 100644 --- a/test/dorkboxTest/network/kryo/InputOutputByteBufTest.kt +++ b/test/dorkboxTest/network/kryo/InputOutputByteBufTest.kt @@ -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 diff --git a/test/dorkboxTest/network/rmi/RmiNestedTest.kt b/test/dorkboxTest/network/rmi/RmiNestedTest.kt index bce41996..27523476 100644 --- a/test/dorkboxTest/network/rmi/RmiNestedTest.kt +++ b/test/dorkboxTest/network/rmi/RmiNestedTest.kt @@ -65,8 +65,6 @@ class RmiNestedTest : BaseTest() { val server = Server(configuration) addEndPoint(server) - server.rmiGlobal.save(TestObjectImpl(), 1) - server.onMessage { 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(1).apply { + + rmi.create { logger.error("Starting test") setValue(43.21f) @@ -132,8 +131,6 @@ class RmiNestedTest : BaseTest() { val server = Server(configuration) addEndPoint(server) - server.rmiGlobal.save(TestObjectImpl(), 1) - server.onMessage { 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(1).apply { + rmi.create { logger.error("Starting test") setValue(43.21f) @@ -199,8 +196,6 @@ class RmiNestedTest : BaseTest() { val server = Server(configuration) addEndPoint(server) - server.rmiGlobal.save(TestObjectImpl(), 1) - server.onMessage { 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(1).apply { + rmi.create { logger.error("Starting test") setOtherValue(43.21f) @@ -263,7 +258,7 @@ class RmiNestedTest : BaseTest() { server.onConnect { logger.error("Connected") - rmi.get(1).apply { + rmi.create { logger.error("Starting test") setOtherValue(43.21f) @@ -295,11 +290,6 @@ class RmiNestedTest : BaseTest() { val client = Client(configuration) addEndPoint(client) - client.onConnect { - rmi.save(TestObjectImpl(), 1) - } - - client.onMessage { message -> // The test is complete when the client sends the OtherObject instance. // this 'object' is the REAL object diff --git a/test/dorkboxTest/network/rmi/RmiSimpleActionsTest.kt b/test/dorkboxTest/network/rmi/RmiSimpleActionsTest.kt index 3a3339e7..f9aef22b 100644 --- a/test/dorkboxTest/network/rmi/RmiSimpleActionsTest.kt +++ b/test/dorkboxTest/network/rmi/RmiSimpleActionsTest.kt @@ -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(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(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(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(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(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) { + 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(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(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 { 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(configuration) addEndPoint(client) client.onConnect { - rmi.save(TestCowImpl(4), 4) + rmi.create(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 { _ -> + // check if 23 still exists (it should not) + val obj = rmi.get(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 { 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(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() diff --git a/test/dorkboxTest/network/rmi/RmiSimpleTest.kt b/test/dorkboxTest/network/rmi/RmiSimpleTest.kt index e5b890bb..c86182f6 100644 --- a/test/dorkboxTest/network/rmi/RmiSimpleTest.kt +++ b/test/dorkboxTest/network/rmi/RmiSimpleTest.kt @@ -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 { 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) } diff --git a/test/dorkboxTest/network/rmi/RmiSpamSyncSuspendingTest.kt b/test/dorkboxTest/network/rmi/RmiSpamSyncSuspendingTest.kt index 64590392..4a1a45fe 100644 --- a/test/dorkboxTest/network/rmi/RmiSpamSyncSuspendingTest.kt +++ b/test/dorkboxTest/network/rmi/RmiSpamSyncSuspendingTest.kt @@ -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 + } } diff --git a/test/dorkboxTest/network/rmi/multiJVM/TestServer.kt b/test/dorkboxTest/network/rmi/multiJVM/TestServer.kt index bceb65a1..21acb3d1 100644 --- a/test/dorkboxTest/network/rmi/multiJVM/TestServer.kt +++ b/test/dorkboxTest/network/rmi/multiJVM/TestServer.kt @@ -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") //