From 33f3ca8ebcc969144d3498a15a7a459ce35c4468 Mon Sep 17 00:00:00 2001 From: Robinson Date: Tue, 6 Jul 2021 15:38:53 +0200 Subject: [PATCH] Supports the newest version of Aeron. Updated error log usage and notifications. Refactored RMI API (now RMI is accessible via '.rmi' calls). Updated unit tests. Added 'delete' to rmi methods (opposite of 'create'). --- LICENSE | 120 +++--- build.gradle.kts | 98 ++--- src/dorkbox/network/Client.kt | 345 +++--------------- src/dorkbox/network/Configuration.kt | 23 +- src/dorkbox/network/Server.kt | 203 ++--------- src/dorkbox/network/aeron/AeronDriver.kt | 2 + .../network/aeron/MediaDriverConnection.kt | 1 - .../aeron/UdpMediaDriverClientConnection.kt | 1 - src/dorkbox/network/connection/Connection.kt | 237 ++---------- .../network/connection/ConnectionParams.kt | 8 +- src/dorkbox/network/connection/EndPoint.kt | 196 +++++----- .../network/connection/ListenerManager.kt | 47 +-- .../network/handshake/ClientHandshake.kt | 95 ++--- .../network/handshake/PortAllocator.kt | 1 - .../network/handshake/ServerHandshake.kt | 181 ++++----- src/dorkbox/network/ping/PingManager.kt | 3 +- .../network/rmi/RemoteObjectStorage.kt | 8 +- src/dorkbox/network/rmi/ResponseManager.kt | 2 +- src/dorkbox/network/rmi/ResponseWaiter.kt | 8 +- .../network/rmi/RmiManagerConnections.kt | 198 ++++++++-- src/dorkbox/network/rmi/RmiManagerGlobal.kt | 304 +++++---------- src/dorkbox/network/rmi/RmiObjectCache.kt | 18 +- .../network/rmi/RmiSupportConnection.kt | 201 ++++++++++ src/dorkbox/network/rmi/RmiSupportServer.kt | 134 +++++++ src/dorkbox/network/rmi/RmiUtils.kt | 8 +- .../messages/ConnectionObjectDeleteRequest.kt | 25 ++ .../ConnectionObjectDeleteResponse.kt | 25 ++ .../rmi/messages/MethodRequestSerializer.kt | 5 +- .../rmi/messages/RmiClientSerializer.kt | 16 +- .../rmi/messages/RmiServerSerializer.kt | 21 +- .../serialization/ClassRegistration.kt | 7 +- .../serialization/ClassRegistration0.kt | 3 +- .../serialization/ClassRegistration1.kt | 3 +- .../serialization/ClassRegistration2.kt | 3 +- .../serialization/ClassRegistration3.kt | 3 +- .../serialization/ClassRegistrationForRmi.kt | 9 +- .../network/serialization/KryoExtra.kt | 12 +- .../network/serialization/Serialization.kt | 139 +++---- test/dorkboxTest/network/BaseTest.kt | 20 +- .../network/DisconnectReconnectTest.kt | 3 +- .../network/kryo/InputOutputByteBufTest.kt | 18 +- test/dorkboxTest/network/rmi/RmiNestedTest.kt | 20 +- .../network/rmi/RmiSimpleActionsTest.kt | 244 ++++--------- test/dorkboxTest/network/rmi/RmiSimpleTest.kt | 10 +- .../network/rmi/RmiSpamSyncSuspendingTest.kt | 14 +- .../network/rmi/multiJVM/TestServer.kt | 2 +- 46 files changed, 1478 insertions(+), 1566 deletions(-) create mode 100644 src/dorkbox/network/rmi/RmiSupportConnection.kt create mode 100644 src/dorkbox/network/rmi/RmiSupportServer.kt create mode 100644 src/dorkbox/network/rmi/messages/ConnectionObjectDeleteRequest.kt create mode 100644 src/dorkbox/network/rmi/messages/ConnectionObjectDeleteResponse.kt 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") //