Added support for callbacks on a message, so there can be 'happens-before' logic.

master
Robinson 2023-11-02 22:36:50 +01:00
parent 58535a923b
commit 91deea8b1a
No known key found for this signature in database
GPG Key ID: 8E7DB78588BD6F5C
10 changed files with 338 additions and 28 deletions

View File

@ -1012,6 +1012,26 @@ open class Client<CONNECTION : Connection>(config: ClientConfiguration = ClientC
}
}
/**
* Safely sends objects to a destination, where the callback is notified once the remote endpoint has received the message.
* This is to guarantee happens-before, and using this will depend upon APP+NETWORK latency, and is (by design) not as performant as
* sending a regular message!
*
* @return true if the message was sent successfully, false if the connection has been closed
*/
fun send(message: Any, onSuccessCallback: CONNECTION.() -> Unit): Boolean {
val c = connection0
return if (c != null) {
@Suppress("UNCHECKED_CAST")
c.send(message, onSuccessCallback as Connection.() -> Unit)
} else {
val exception = TransmitException("Cannot send-sync a message when there is no connection!")
listenerManager.notifyError(exception)
false
}
}
/**
* Sends a "ping" packet to measure **ROUND TRIP** time to the remote connection.
*
@ -1023,7 +1043,7 @@ open class Client<CONNECTION : Connection>(config: ClientConfiguration = ClientC
val c = connection0
if (c != null) {
return super.ping(c, function)
return c.ping(function)
} else {
val exception = TransmitException("Cannot send a ping when there is no connection!")
listenerManager.notifyError(exception)

View File

@ -264,6 +264,19 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
return send(message, false)
}
/**
* Safely sends objects to a destination, where the callback is notified once the remote endpoint has received the message.
*
* This is to guarantee happens-before, and using this will depend upon APP+NETWORK latency, and is (by design) not as performant as
* sending a regular message!
*
* @return true if the message was successfully sent, false otherwise. Exceptions are caught and NOT rethrown!
*/
fun send(message: Any, onSuccessCallback: Connection.() -> Unit): Boolean {
return sendSync(message, onSuccessCallback)
}
/**
* Sends a "ping" packet to measure **ROUND TRIP** time to the remote connection.
*
@ -555,6 +568,55 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
return id == other1.id
}
internal fun receiveSendSync(sendSync: SendSync) {
if (sendSync.message != null) {
// this is on the "remote end".
sendSync.message = null
if (!send(sendSync)) {
logger.error("Error returning send-sync: $sendSync")
}
} else {
// this is on the "local end" when the response comes back
val responseId = sendSync.id
// process the ping message so that our ping callback does something
// this will be null if the ping took longer than XXX seconds and was cancelled
val result = EndPoint.responseManager.removeWaiterCallback<Connection.() -> Unit>(responseId, logger)
if (result != null) {
result(this)
} else {
logger.error("Unable to receive send-sync, there was no waiting response for $sendSync ($responseId)")
}
}
}
/**
* Safely sends objects to a destination, the callback is notified once the remote endpoint has received the message.
*
* This is to guarantee happens-before, and using this will depend upon APP+NETWORK latency, and is (by design) not as performant as
* sending a regular message!
*
* @return true if the message was successfully sent, false otherwise. Exceptions are caught and NOT rethrown!
*/
private fun sendSync(message: Any, onSuccessCallback: Connection.() -> Unit): Boolean {
val id = EndPoint.responseManager.prepWithCallback(logger, onSuccessCallback)
val sendSync = SendSync()
sendSync.message = message
sendSync.id = id
// if there is no ping response EVER, it means that the connection is in a critically BAD state!
// eventually, all the ping replies (or, in our case, the RMI replies that have timed out) will
// become recycled.
// Is it a memory-leak? No, because the memory will **EVENTUALLY** get freed.
return send(sendSync, false)
}
internal fun receivePing(ping: Ping) {
if (ping.pongTime == 0L) {
// this is on the "remote end".
@ -567,21 +629,21 @@ open class Connection(connectionParameters: ConnectionParams<*>) {
// this is on the "local end" when the response comes back
ping.finishedTime = System.currentTimeMillis()
val rmiId = ping.packedId
val responseId = ping.packedId
// process the ping message so that our ping callback does something
// this will be null if the ping took longer than XXX seconds and was cancelled
val result = EndPoint.responseManager.removeWaiterCallback<Ping.() -> Unit>(rmiId, logger)
val result = EndPoint.responseManager.removeWaiterCallback<Ping.() -> Unit>(responseId, logger)
if (result != null) {
result(ping)
} else {
logger.error("Unable to receive ping, there was no waiting response for $ping ($rmiId)")
logger.error("Unable to receive ping, there was no waiting response for $ping ($responseId)")
}
}
}
internal fun sendPing(function: Ping.() -> Unit): Boolean {
private fun sendPing(function: Ping.() -> Unit): Boolean {
val id = EndPoint.responseManager.prepWithCallback(logger, function)
val ping = Ping()

View File

@ -492,15 +492,6 @@ abstract class EndPoint<CONNECTION : Connection> private constructor(val type: C
listenerManager.onMessage(function)
}
/**
* Sends a "ping" packet to measure **ROUND TRIP** time to the remote connection.
*
* @return true if the message was successfully sent by aeron
*/
internal fun ping(connection: Connection, function: Ping.() -> Unit): Boolean {
return connection.sendPing(function)
}
/**
* This is designed to permit modifying/overriding how data is processed on the network.
*
@ -682,7 +673,7 @@ abstract class EndPoint<CONNECTION : Connection> private constructor(val type: C
try {
connection.receivePing(message)
} catch (e: Exception) {
listenerManager.notifyError(connection, PingException("Error while processing Ping message", e))
listenerManager.notifyError(connection, PingException("Error while processing Ping message: $message", e))
}
}
@ -699,6 +690,33 @@ abstract class EndPoint<CONNECTION : Connection> private constructor(val type: C
}
}
is SendSync -> {
// SendSync enables us to NOTIFY the remote endpoint that we have received the message. This is to guarantee happens-before!
// Using this will depend upon APP+NETWORK latency, and is (by design) not as performant as sending a regular message!
try {
val message2 = message.message
if (message2 != null) {
// this is on the "remote end". Make sure to dispatch/notify the message BEFORE we send a message back!
try {
var hasListeners = listenerManager.notifyOnMessage(connection, message2)
// each connection registers, and is polled INDEPENDENTLY for messages.
hasListeners = hasListeners or connection.notifyOnMessage(message2)
if (!hasListeners) {
listenerManager.notifyError(connection, MessageDispatchException("No send-sync message callbacks found for ${message2::class.java.name}"))
}
} catch (e: Exception) {
listenerManager.notifyError(connection, MessageDispatchException("Error processing send-sync message ${message2::class.java.name}", e))
}
}
connection.receiveSendSync(message)
} catch (e: Exception) {
listenerManager.notifyError(connection, SendSyncException("Error while processing send-sync message: $message", e))
}
}
else -> {
try {
var hasListeners = listenerManager.notifyOnMessage(connection, message)

View File

@ -0,0 +1,46 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.connection
import dorkbox.network.rmi.RmiUtils
class SendSync {
var message: Any? = null
// used to notify the remote endpoint that the message has been processed
var id: Int = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SendSync) return false
if (message != other.message) return false
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
var result = message?.hashCode() ?: 0
result = 31 * result + id
return result
}
override fun toString(): String {
return "SendSync ${RmiUtils.unpackUnsignedRight(id)} (message=$message)"
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.exceptions
/**
* The type of exceptions raised for send-sync errors
*/
open class SendSyncException : Exception {
/**
* Create an exception.
*
* @param message The message
*/
constructor(message: String) : super(message)
/**
* Create an exception.
*
* @param cause The cause
*/
constructor(cause: Throwable) : super(cause)
/**
* Create an exception.
*
* @param message The message
* @param cause The cause
*/
constructor(message: String, cause: Throwable?) : super(message, cause)
}

View File

@ -31,7 +31,7 @@ import kotlin.concurrent.write
* - these are just looped around in a ring buffer.
* - these are stored here as int, however these are REALLY shorts and are int-packed when transferring data on the wire
*
* (By default, for RMI...)
* (By default, for RMI/Ping/SendSync...)
* - 0 is reserved for INVALID
* - 1 is reserved for ASYNC (the response will never be sent back, and we don't wait for it)
*
@ -41,7 +41,7 @@ internal class ResponseManager(maxValuesInCache: Int = 65534, minimumValue: Int
val TIMEOUT_EXCEPTION = TimeoutException().apply { stackTrace = arrayOf<StackTraceElement>() }
}
private val rmiWaitersInUse = atomic(0)
private val responseWaitersInUse = atomic(0)
private val waiterCache: Pool<ResponseWaiter>
private val pendingLock = ReentrantReadWriteLock()
@ -49,7 +49,7 @@ internal class ResponseManager(maxValuesInCache: Int = 65534, minimumValue: Int
init {
require(maxValuesInCache <= 65535) { "The maximum size for the values in the response manager is 65535"}
require(maxValuesInCache > minimumValue) { "< $minimumValue (0 and 1 for RMI) are reserved"}
require(maxValuesInCache > minimumValue) { "< $minimumValue (0 and 1 for RMI/Ping/SendSync) are reserved"}
require(minimumValue > 1) { "The minimum value $minimumValue must be > 1"}
// create a shuffled list of ID's. This operation is ONLY performed ONE TIME per endpoint!
@ -70,6 +70,7 @@ internal class ResponseManager(maxValuesInCache: Int = 65534, minimumValue: Int
* Called when we receive the answer for our initial request. If no response data, then the pending rmi data entry is deleted
*
* resume any pending remote object method invocations (if they are not async, or not manually waiting)
*
* NOTE: async RMI will never call this (because async doesn't return a response)
*/
fun notifyWaiter(id: Int, result: Any?, logger: Logger) {
@ -117,7 +118,7 @@ internal class ResponseManager(maxValuesInCache: Int = 65534, minimumValue: Int
// always return this to the cache!
previous.result = null
waiterCache.put(previous)
rmiWaitersInUse.getAndDecrement()
responseWaitersInUse.getAndDecrement()
return result as T
}
@ -132,9 +133,9 @@ internal class ResponseManager(maxValuesInCache: Int = 65534, minimumValue: Int
*/
fun prep(logger: Logger): ResponseWaiter {
val waiter = waiterCache.take()
rmiWaitersInUse.getAndIncrement()
responseWaitersInUse.getAndIncrement()
if (logger.isTraceEnabled) {
logger.trace("[RM] prep in-use: [${waiter.id}] ${rmiWaitersInUse.value}")
logger.trace("[RM] prep in-use: [${waiter.id}] ${responseWaitersInUse.value}")
}
// this will initialize the result
@ -154,9 +155,9 @@ internal class ResponseManager(maxValuesInCache: Int = 65534, minimumValue: Int
*/
fun prepWithCallback(logger: Logger, function: Any): Int {
val waiter = waiterCache.take()
rmiWaitersInUse.getAndIncrement()
responseWaitersInUse.getAndIncrement()
if (logger.isTraceEnabled) {
logger.trace("[RM] prep in-use: [${waiter.id}] ${rmiWaitersInUse.value}")
logger.trace("[RM] prep in-use: [${waiter.id}] ${responseWaitersInUse.value}")
}
// this will initialize the result
@ -199,7 +200,7 @@ internal class ResponseManager(maxValuesInCache: Int = 65534, minimumValue: Int
// always return the waiter to the cache
responseWaiter.result = null
waiterCache.put(responseWaiter)
rmiWaitersInUse.getAndDecrement()
responseWaitersInUse.getAndDecrement()
if (resultOrWaiter is ResponseWaiter) {
if (logger.isTraceEnabled) {
@ -228,17 +229,18 @@ internal class ResponseManager(maxValuesInCache: Int = 65534, minimumValue: Int
// always return the waiter to the cache
responseWaiter.result = null
waiterCache.put(responseWaiter)
rmiWaitersInUse.getAndDecrement()
responseWaitersInUse.getAndDecrement()
}
// This is only closed when shutting down the client/server.
fun close(logger: Logger) {
// technically, this isn't closing it, so much as it's cleaning it out
if (logger.isDebugEnabled) {
logger.debug("Closing the response manager for RMI")
logger.debug("Closing the response manager")
}
// wait for responses, or wait for timeouts!
while (rmiWaitersInUse.value > 0) {
while (responseWaitersInUse.value > 0) {
Thread.sleep(50)
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.network.serialization
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.SendSync
internal class SendSyncSerializer: Serializer<SendSync>() {
override fun write(kryo: Kryo, output: Output, ssync: SendSync) {
output.writeInt(ssync.id)
kryo.writeClassAndObject(output, ssync.message)
}
override fun read(kryo: Kryo, input: Input, type: Class<out SendSync>): SendSync {
val ssync = SendSync()
ssync.id = input.readInt()
ssync.message = kryo.readClassAndObject(input)
return ssync
}
}

View File

@ -25,6 +25,7 @@ import com.esotericsoftware.minlog.Log
import dorkbox.network.Server
import dorkbox.network.connection.Connection
import dorkbox.network.connection.DisconnectMessage
import dorkbox.network.connection.SendSync
import dorkbox.network.connection.streaming.StreamingControl
import dorkbox.network.connection.streaming.StreamingControlSerializer
import dorkbox.network.connection.streaming.StreamingData
@ -182,6 +183,7 @@ open class Serialization<CONNECTION: Connection>(private val references: Boolean
private val streamingControlSerializer = StreamingControlSerializer()
private val streamingDataSerializer = StreamingDataSerializer()
private val pingSerializer = PingSerializer()
private val sendSyncSerializer = SendSyncSerializer()
private val disconnectSerializer = DisconnectSerializer()
internal val fileContentsSerializer = FileContentsSerializer<CONNECTION>()
@ -433,6 +435,7 @@ open class Serialization<CONNECTION: Connection>(private val references: Boolean
kryo.register(StreamingData::class.java, streamingDataSerializer)
kryo.register(Ping::class.java, pingSerializer)
kryo.register(SendSync::class.java, sendSyncSerializer)
kryo.register(HandshakeMessage::class.java)
kryo.register(DisconnectMessage::class.java, disconnectSerializer)

View File

@ -18,7 +18,6 @@ package dorkboxTest.network
import dorkbox.network.Client
import dorkbox.network.Server
import dorkbox.network.connection.Connection
import kotlinx.coroutines.delay
import org.junit.Test
class ErrorLoggerTest : BaseTest() {

View File

@ -0,0 +1,79 @@
/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkboxTest.network
import dorkbox.network.Client
import dorkbox.network.Server
import dorkbox.network.connection.Connection
import kotlinx.atomicfu.atomic
import org.junit.Assert
import org.junit.Test
class SendSyncTest : BaseTest() {
val counter = atomic(0)
@Test
fun sendSync() {
// session/stream count errors
val serverSuccess = atomic(false)
val clientSuccess = atomic(false)
val server = run {
val configuration = serverConfig()
val server: Server<Connection> = Server(configuration)
addEndPoint(server)
server.onMessage<String> {
serverSuccess.value = true
}
server
}
val client = run {
val config = clientConfig()
val client: Client<Connection> = Client(config)
addEndPoint(client)
client.onConnect {
repeat(100) {
send("Hi, I'm waiting!") {
// a send-sync object is returned, once the round-trip is complete, and we are notified
val count = counter.getAndIncrement()
if (count == 99) {
clientSuccess.value = true
stopEndPoints()
}
}
}
}
client
}
server.bind(2000)
client.connect(LOCALHOST, 2000)
waitForThreads()
Assert.assertTrue(clientSuccess.value)
Assert.assertTrue(serverSuccess.value)
}
}