separate BreakConnectionException and more correct processing for ClosedException.

This commit is contained in:
Sergey Chernov 2024-06-17 13:01:09 +07:00
parent 4f6bc3c77e
commit 0d3a8ae95c
7 changed files with 84 additions and 16 deletions

View File

@ -14,8 +14,9 @@ class KiloInterface<S> : LocalInterface<KiloScope<S>>() {
init {
registerError { RemoteInterface.UnknownCommand() }
registerError { RemoteInterface.InternalError(it) }
registerError { RemoteInterface.ClosedException(it) }
registerError { RemoteInterface.SecurityException(it) }
// registerError { RemoteInterface.SecurityException(it) }
registerError { RemoteInterface.InvalidDataException(it) }
registerError { RemoteInterface.RemoteException(it) }
registerError { IllegalStateException() }

View File

@ -33,7 +33,7 @@ internal class KiloL0Interface<T>(
0u,
clientInterface.execute(params.scope, call.name, call.serializedArgs)
)
} catch(t: RemoteInterface.ClosedException) {
} catch(t: BreakConnectionException) {
throw t
}
catch (t: Throwable) {

View File

@ -32,8 +32,8 @@ class KiloServer<S>(
}
catch(_: CancellationException) {
}
catch(_: RemoteInterface.ClosedException) {
info { "Closed exception caught, closing" }
catch(cce: LocalInterface.BreakConnectionException) {
info { "Closed exception caught, closing (${cce.flushSendQueue}" }
}
catch (t: Throwable) {
exception { "unexpected while creating kiloclient" to t }

View File

@ -2,6 +2,7 @@ package net.sergeych.kiloparsec
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.Loggable
import net.sergeych.mp_logger.exception
import net.sergeych.mp_logger.info
import net.sergeych.tools.AtomicCounter
import net.sergeych.utools.firstNonNull
@ -15,6 +16,26 @@ open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.inc
private val commands = mutableMapOf<String, RawCommandHandler<S>>()
/**
* Instruct the transport to immediately break the connection.
* This exception is not passed to the remote end, instead, transport device breaks
* connection to remote when receiving it.
*
* Remote interface will throw [RemoteInterface.ClosedException] as the break will be detected. As reaction time
* it depends on the transport in use, we recommend sending some registered exception first if you need
* to pass important data, or implement special commands on both sides.
*
* __Important note:__ _it is not allowed to throw [RemoteInterface.ClosedException] directly!_
* This exception is processed internally and can't be sent over the network.
*/
open class BreakConnectionException(
text: String = "break connection request",
val flushSendQueue: Boolean = true,
) : RuntimeException(text) {
override val message: String?
get() = super.message + " (flush=$flushSendQueue)"
}
/**
* New session creator. Rarely needed directlym it can be used for delegation
* of local interfaces.
@ -77,8 +98,13 @@ open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.inc
errorByClass[t::class] ?: errorProviders.firstNonNull { it.getErrorCode(t) }
fun encodeError(forId: UInt, t: Throwable): Transport.Block.Error =
getErrorCode(t)?.let { Transport.Block.Error(forId, it, t.message) }
?: Transport.Block.Error(forId, "UnknownError", "${t::class.simpleName}: ${t.message}")
if (t is RemoteInterface.ClosedException) {
exception { "Illegal attempt to send ClosedException" to t }
encodeError(forId, RemoteInterface.InternalError("TCE"))
}
else
getErrorCode(t)?.let { Transport.Block.Error(forId, it, t.message) }
?: Transport.Block.Error(forId, "UnknownError", "${t::class.simpleName}: ${t.message}")
open fun getErrorBuilder(code: String): ((String, UByteArray?) -> Throwable)? =
errorBuilder[code] ?: errorProviders.firstNonNull { it.getErrorBuilder(code) }

View File

@ -15,11 +15,12 @@ interface RemoteInterface {
/**
* Is thrown when the channel is closed, in an attempt to execute a command, also to all pending
* calls (see [call]).
* calls (see [call]). Client code should never throw it. If command handler needs to break connection
* it should throw [LocalInterface.BreakConnectionException]
*/
open class ClosedException(t: String = "connection is closed") : Exception(t)
open class SecurityException(t: String = "invalid remote id and signature") : ClosedException(t)
open class SecurityException(t: String = "invalid remote id and signature") : LocalInterface.BreakConnectionException(t)
open class InvalidDataException(msg: String="invalid data, can't unpack") : Exception(msg)
@ -41,6 +42,8 @@ interface RemoteInterface {
*/
class UnknownCommand : RemoteException("UnknownCommand")
open class InternalError(code: String="0"): RemoteException("Internal error: $code")
suspend fun <R> call(cmd: Command<Unit, R>): R = call(cmd, Unit)
/**

View File

@ -180,11 +180,11 @@ class Transport<S>(
localInterface.execute(commandContext, b.name, b.packedArgs)
)
)
} catch (x: RemoteInterface.ClosedException) {
} catch (x: LocalInterface.BreakConnectionException) {
// handler forced close
warning { "handler requested closing of the connection"}
warning { "handler requested closing of the connection (${x.flushSendQueue}"}
isClosed = true
throw x
device.close()
} catch (x: RemoteInterface.RemoteException) {
send(Block.Error(b.id, x.code, x.text, x.extra))
} catch (t: Throwable) {
@ -203,6 +203,10 @@ class Transport<S>(
info { "closed receive channel"}
isClosed = true
}
catch(cce: LocalInterface.BreakConnectionException) {
info { "closing connection by local request ($cce)"}
device.close()
}
catch (_: CancellationException) {
info { "loop is cancelled with CancellationException" }
isClosed = true

View File

@ -7,7 +7,10 @@ import net.sergeych.crypto2.SigningKey
import net.sergeych.crypto2.initCrypto
import net.sergeych.kiloparsec.*
import net.sergeych.mp_logger.Log
import kotlin.test.*
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.fail
private var dcnt = 0
fun createTestDevice(): Pair<Transport.Device, Transport.Device> {
@ -154,6 +157,8 @@ class TransportTest {
d2.close()
}
class TestException(text: String) : Exception(text)
@Test
fun testClient() = runTest {
initCrypto()
@ -173,6 +178,10 @@ class TransportTest {
val serverId = SigningKey.pair()
val clientId = SigningKey.pair()
val cmdException by command<Unit, Unit>()
val cmdRemoteExceptionTest by command<Unit, String>()
val cmdBreak by command<Unit, Unit>()
val serverInterface = KiloInterface<String>().apply {
on(cmdPing) {
"pong! [$it]"
@ -189,15 +198,26 @@ class TransportTest {
on(cmdChainCallServer2) {
remote.call(cmdChainCallClient2, "$it-s2")
}
registerError { IllegalStateException() }
registerError { IllegalArgumentException(it) }
on(cmdException) { throw TestException("te1") }
on(cmdRemoteExceptionTest) {
try {
remote.call(cmdException)
"error!"
} catch (e: TestException) {
"ok: ${e.message}"
}
}
on(cmdBreak) { throw LocalInterface.BreakConnectionException() }
registerError { TestException(it) }
}
val kiloServerConnection = KiloServerConnection(serverInterface, d1, "server session", serverId.secretKey
val kiloServerConnection = KiloServerConnection(
serverInterface, d1, "server session", serverId.secretKey
)
launch { kiloServerConnection.run() }
var cnt = 0
val client = KiloClient {
addErrors(serverInterface)
session { "client session!" }
secretIdKey = clientId.secretKey
local {
@ -208,11 +228,13 @@ class TransportTest {
"client pong: $it"
}
on(cmdChainCallClient1) {
remote.call(cmdChainCallServer2,"$it-c1")
remote.call(cmdChainCallServer2, "$it-c1")
}
on(cmdChainCallClient2) { "$it-c2" }
on(cmdException) { throw TestException("te-local") }
}
connect {
println("Called connect: $cnt")
if (cnt++ > 0) {
cancel()
fail("connect called once again")
@ -226,6 +248,18 @@ class TransportTest {
assertEquals("server push: bar", kiloServerConnection.call(cmdPush, "bar"))
assertEquals("**-s1-c1-s2-c2", client.call(cmdChainCallServer1, "**"))
assertThrows<TestException> { client.call(cmdException) }
assertEquals("ok: te-local", client.call(cmdRemoteExceptionTest))
assertThrows<RemoteInterface.ClosedException> {
client.call(cmdBreak)
}
// Note that current transport test is too simple,
// therefore, we can't test reconnecting, also we need server and client instances
// not connections, so that's all
d1.close()
d2.close()
client.close()