added extendable serialized exeptions handling registry

This commit is contained in:
Sergey Chernov 2022-09-07 12:24:49 +03:00
parent db7a85fb7b
commit 32280ffc61
6 changed files with 86 additions and 40 deletions

View File

@ -7,15 +7,15 @@ import kotlinx.serialization.json.Json
import net.sergeych.boss_serialization.BossDecoder import net.sergeych.boss_serialization.BossDecoder
import net.sergeych.boss_serialization_mp.BossEncoder import net.sergeych.boss_serialization_mp.BossEncoder
import net.sergeych.boss_serialization_mp.decodeBoss import net.sergeych.boss_serialization_mp.decodeBoss
import net.sergeych.cloudoc.api.ApiError.BAD_RESPONSE_PACKAGE
import net.sergeych.cloudoc.api.ApiError.UNKNOWN_ERROR
import net.sergeych.cloudoc.api.ApiException
import net.sergeych.cloudoc.api.Package import net.sergeych.cloudoc.api.Package
import net.sergeych.mp_logger.LogTag import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.debug import net.sergeych.mp_logger.debug
import net.sergeych.mp_logger.exception import net.sergeych.mp_logger.exception
import net.sergeych.mp_logger.warning import net.sergeych.mp_logger.warning
import net.sergeych.mptools.toDump import net.sergeych.mptools.toDump
import net.sergeych.parsec3.ExceptionsRegistry
import net.sergeych.parsec3.InvalidFrameException
import net.sergeych.parsec3.ParsecException
/** /**
* Create adapter, an interface to provide local API commands and invoke remote API commands * Create adapter, an interface to provide local API commands and invoke remote API commands
@ -66,11 +66,15 @@ import net.sergeych.mptools.toDump
* @param instance any instance that represent the state of the interface. Could be `Unit` for stateless. * @param instance any instance that represent the state of the interface. Could be `Unit` for stateless.
* @param commandHost the Api __this adapter provides to a remote__. It differs from the interface expected on the * @param commandHost the Api __this adapter provides to a remote__. It differs from the interface expected on the
* remote side. * remote side.
* @param exceptionRegistry allows to transform serialized parsec exception (see [ParsecException]) to
* application-specific exception classes. Default implementation only decodes
* parsec3 built-in exceptions.
* @param sendEncoded a method that performs actual sending of the packed binary frame to the remote side * @param sendEncoded a method that performs actual sending of the packed binary frame to the remote side
*/ */
open class Adapter<T>( open class Adapter<T>(
private val instance: T, private val instance: T,
private val commandHost: CommandHost<T>, private val commandHost: CommandHost<T>,
private val exceptionRegistry: ExceptionsRegistry = ExceptionsRegistry(),
private val sendEncoded: suspend (data: ByteArray) -> Unit, private val sendEncoded: suspend (data: ByteArray) -> Unit,
) : LogTag("ADPTR") { ) : LogTag("ADPTR") {
@ -113,15 +117,13 @@ open class Adapter<T>(
val handler = commandHost.handler(pe.name) val handler = commandHost.handler(pe.name)
val result = handler.invoke(instance, pe.args) val result = handler.invoke(instance, pe.args)
sendPackage( sendPackage(
Package.Response( Package.Response(pe.id, result)
pe.id, result
) )
) } catch (ae: ParsecException) {
} catch (ae: ApiException) {
sendPackage(Package.Response(pe.id, null, ae.code, ae.text)) sendPackage(Package.Response(pe.id, null, ae.code, ae.text))
} catch (ex: Throwable) { } catch (ex: Throwable) {
ex.printStackTrace() ex.printStackTrace()
sendPackage(Package.Response(pe.id, null, UNKNOWN_ERROR, ex.toString())) sendPackage(Package.Response(pe.id, null, "UNKNOWN_ERROR", ex.toString()))
} }
} }
@ -134,8 +136,8 @@ open class Adapter<T>(
dr.complete(pe.result) dr.complete(pe.result)
else else
dr.completeExceptionally( dr.completeExceptionally(
pe.errorCode?.let { ApiException(it, pe.errorText) } pe.errorCode?.let { exceptionRegistry.getException(it, pe.errorText) }
?: ApiException(BAD_RESPONSE_PACKAGE) ?: InvalidFrameException("invalid package: no result, no error code")
) )
} }
} }

View File

@ -2,8 +2,8 @@ package channel
import net.sergeych.boss_serialization.BossDecoder import net.sergeych.boss_serialization.BossDecoder
import net.sergeych.boss_serialization_mp.BossEncoder import net.sergeych.boss_serialization_mp.BossEncoder
import net.sergeych.cloudoc.api.ApiError
import net.sergeych.parsec3.AdapterDelegate import net.sergeych.parsec3.AdapterDelegate
import net.sergeych.parsec3.CommandNotFoundException
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
/** /**
@ -46,7 +46,7 @@ open class CommandHost<T> {
} }
} }
fun handler(name: String) = handlers.get(name) ?: ApiError.NOT_FOUND.raise("command not found") fun handler(name: String) = handlers.get(name) ?: throw CommandNotFoundException(name)
/** /**
* Provide a command delegate that creates type-safe command descriptor containint command name and * Provide a command delegate that creates type-safe command descriptor containint command name and

View File

@ -0,0 +1,52 @@
package net.sergeych.parsec3
/**
* Registry to restore exceptions from parsec block data. Serializing exceptions is dangerous: being a OS-bound
* objects, exceptions can carry too much sensitive or useless information (e.g. call stack), and serializng
* actual exceptions could be a pain, so parsec3 serializes exception information as 2 parameters: a code string
* which is very much like old good (and awful in a way) `C ERRNO`, and an optional message.
*
* This class reconstructs exceptions from these parameters using a registry, that is pre-filled with application
* codes and actual exception classes. Then [Adapter<T>] uses it to restore and throw actual exception on the calling
* party side.
*
* The good idea is to share one object inheriting from refistry to hold all exceptions info in one place
* and share it betweem client and server code.
*/
open class ExceptionsRegistry {
private val handlers = mutableMapOf<String,(String?)->Throwable>().also {
// predefined exceptions:
it[commandNotFoundCode] = { CommandNotFoundException(it ?: "???") }
it[unknownErrorCode] = { UnknownException(it ?: "???") }
}
/**
* Register an exception with a code with a handler that creates its instance. Note that the
* handler _should not throw anything_ but rather create an instance of the exception.
*/
fun <T: Throwable>register(code: String, block: (message: String?) -> T) {
handlers[code] = block
}
/**
* raise the exception using the proper handler. Throws [UnknownCodeException] of there is no handler
* for a given code.
*/
internal fun raise(code: String,message: String?): Nothing {
throw getException(code, message)
}
/**
* create the exception instanceusing the proper handler, or an [UnknownCodeException] if handler not found
*/
internal fun getException(code: String,message: String?): Throwable =
handlers[code]?.let { it(message) } ?: UnknownCodeException(code, message)
companion object {
val commandNotFoundCode = "_COMMAND_NOT_FOUND"
val unknownErrorCode = "_UNKNOWN_ERROR"
val invalidFrameCode = "_FRAME_INVALID"
}
}

View File

@ -12,7 +12,7 @@ sealed class Package {
data class Response( data class Response(
val toId: Int, val toId: Int,
val result: ByteArray? = null, val result: ByteArray? = null,
val errorCode: ApiError? = null, val errorCode: String? = null,
val errorText: String? = null, val errorText: String? = null,
) : Package() { ) : Package() {
init { init {

View File

@ -0,0 +1,19 @@
package net.sergeych.parsec3
/**
* The base class for exceptions that pass through parsec3 channel. Application exceptinos
* intended to be transmitted through this channel should inherit from it and be registered
* with proper [ExceptionsRegistry] instance provided to the exceptions-receiving adaper (better
* to both sides the same).
*/
open class ParsecException(val code: String, val text: String?=null, reason: Throwable?=null) : Exception(
text?.let { "($code): $text" } ?: code, reason
)
class CommandNotFoundException(name: String): ParsecException(ExceptionsRegistry.commandNotFoundCode,name)
class InvalidFrameException(text: String): ParsecException(ExceptionsRegistry.invalidFrameCode, text)
class UnknownCodeException(code: String,message: String?): ParsecException(code, message)
class UnknownException(message: String?): ParsecException(ExceptionsRegistry.unknownErrorCode, message)

View File

@ -1,27 +0,0 @@
package net.sergeych.cloudoc.api
enum class ApiError {
UNKNOWN_ERROR,
INTERNAL_ERROR,
BAD_RESPONSE_PACKAGE,
NOT_FOUND,
BAD_LOGIN,
OBJECT_ALREADY_EXISTS,
ACCESS_FORBIDDEN,
ILLEGAL_STATE,
NOT_LOGGED_IN,
BAD_PARAMETER,
EMAIL_IN_USE,
NAME_IN_USE;
fun raise(text: String? = null): Nothing {
throw ApiException(this, text ?: defaultText())
}
fun defaultText(): String = name.lowercase().replace('_', ' ')
}
class ApiException(val code: ApiError, _text: String? = null, cause: Throwable? = null):
Exception(_text ?: code.defaultText(), cause) {
val text by lazy { _text ?: code.defaultText() }
}