CORDA-3232: Support of multiple interfaces for RPC calls (#5495)

* CORDA-3232: Make backward compatible RPC client changes

Such that it will be able to talk to new and old server versions.

* CORDA-3232: Make backward compatible RPC server changes

Such that it will be able to talk to new and old client versions.

* CORDA-3232: Trick Detekt

* CORDA-3232: Integration test for multi-interface communication.

* CORDA-3232: Add legacy mode test.

* CORDA-3232: Making Detekt happier

* CORDA-3232: Fix Detekt baseline after merge with `4.3` branch

* CORDA-3232: Incrementing Platform version

As discussed with @lockathan

* CORDA-3232: Fix legacy test post platform version increment

* CORDA-3232: Use recursive logic to establish complete population of method names

* Revert "CORDA-3232: Incrementing Platform version"

This reverts commit d75f48aa

* CORDA-3232: Remove logic that conditions on PLATFORM_VERSION

* CORDA-3232: Making Detekt happier

* CORDA-3232: Few more changes after conversation with @mnesbit

* CORDA-3232: Make a strict match to `CordaRPCOps` on client side

Or else will fail:
net.corda.tools.shell.InteractiveShellIntegrationTest.dumpCheckpoints creates zip with json file for suspended flow

Flagging that `InternalCordaRPCOps.dumpCheckpoints` cannot be called.

* CORDA-3232: Address PR comments by @rick-r3

* CORDA-3232: Address further review input from @rick-r3

* Change the way how methods stored in the map;
* Extend test to make sure that `CordaRPCOps` can indeed be mixed with other RPC interfaces.
This commit is contained in:
Viktor Kolomeyko
2019-09-26 16:01:14 +01:00
committed by Matthew Nesbit
parent 298d8ba69c
commit 51330c2e44
6 changed files with 215 additions and 37 deletions

View File

@ -14,6 +14,7 @@ import net.corda.core.context.Trace.InvocationId
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.LifeCycle
import net.corda.core.internal.NamedCacheFactory
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.RPCOps
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationDefaults
@ -25,6 +26,7 @@ import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.serialization.amqp.RpcServerObservableSerializer
import net.corda.node.services.logging.pushToLoggingContext
import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.RPCApi.CLASS_METHOD_DIVIDER
import net.corda.nodeapi.externalTrace
import net.corda.nodeapi.impersonatedActor
import net.corda.nodeapi.internal.DeduplicationChecker
@ -67,15 +69,18 @@ data class RPCServerConfiguration(
}
/**
* The [RPCServer] implements the complement of [RPCClient]. When an RPC request arrives it dispatches to the
* corresponding function in [ops]. During serialisation of the reply (and later observations) the server subscribes to
* The [RPCServer] implements the complement of [net.corda.client.rpc.internal.RPCClient]. When an RPC request arrives it dispatches to the
* corresponding function in [opsList]. During serialisation of the reply (and later observations) the server subscribes to
* each Observable it encounters and captures the client address to associate with these Observables. Later it uses this
* address to forward observations arriving on the Observables.
*
* The way this is done is similar to that in [RPCClient], we use Kryo and add a context to stores the subscription map.
* The way this is done is similar to that in [net.corda.client.rpc.internal.RPCClient], we use AMQP and add a context to stores the subscription map.
*
* NB: The order of elements in [opsList] matters in case of legacy RPC clients who do not specify class name of the RPC Ops they are after.
* For Legacy RPC clients who supply method name alone, the calls are being targeted at first element in [opsList].
*/
class RPCServer(
private val ops: RPCOps,
private val opsList: List<RPCOps>,
private val rpcServerUsername: String,
private val rpcServerPassword: String,
private val serverLocator: ServerLocator,
@ -86,6 +91,8 @@ class RPCServer(
) {
private companion object {
private val log = contextLogger()
private data class InvocationTarget(val method: Method, val instance: RPCOps)
}
private enum class State {
@ -102,8 +109,13 @@ class RPCServer(
private data class MessageAndContext(val message: RPCApi.ServerToClient.RpcReply, val context: ObservableContext)
private val lifeCycle = LifeCycle(State.UNSTARTED)
/** The methodname->Method map to use for dispatching. */
private val methodTable: Map<String, Method>
/**
* The method name -> InvocationTarget used for servicing the actual call.
* NB: The key in this map can either be:
* - FQN of the method including interface name for all the interfaces except `CordaRPCOps`;
* - For `CordaRPCOps` interface this will be just plain method name. This is done to maintain wire compatibility with previous versions.
*/
private val methodTable: Map<String, InvocationTarget>
/** The observable subscription mapping. */
private val observableMap = createObservableSubscriptionMap()
/** A mapping from client addresses to IDs of associated Observables */
@ -130,16 +142,47 @@ class RPCServer(
private val deduplicationChecker = DeduplicationChecker(rpcConfiguration.deduplicationCacheExpiry, cacheFactory = cacheFactory)
private var deduplicationIdentity: String? = null
constructor (
ops: RPCOps,
rpcServerUsername: String,
rpcServerPassword: String,
serverLocator: ServerLocator,
securityManager: RPCSecurityManager,
nodeLegalName: CordaX500Name,
rpcConfiguration: RPCServerConfiguration,
cacheFactory: NamedCacheFactory
) : this(listOf(ops), rpcServerUsername, rpcServerPassword, serverLocator, securityManager, nodeLegalName, rpcConfiguration, cacheFactory)
init {
val groupedMethods = ops.javaClass.declaredMethods.groupBy { it.name }
groupedMethods.forEach { name, methods ->
if (methods.size > 1) {
throw IllegalArgumentException("Encountered more than one method called $name on ${ops.javaClass.name}")
val mutableMethodTable = mutableMapOf<String, InvocationTarget>()
opsList.forEach { ops ->
listOfApplicableInterfacesRec(ops.javaClass).toSet().forEach { interfaceClass ->
val groupedMethods = with(interfaceClass) {
if(interfaceClass == CordaRPCOps::class.java) {
methods.groupBy { it.name }
} else {
methods.groupBy { interfaceClass.name + CLASS_METHOD_DIVIDER + it.name }
}
}
groupedMethods.forEach { name, methods ->
if (methods.size > 1) {
throw IllegalArgumentException("Encountered more than one method called $name on ${interfaceClass.name}")
}
}
val interimMap = groupedMethods.mapValues { InvocationTarget(it.value.single(), ops) }
mutableMethodTable.putAll(interimMap)
}
}
methodTable = groupedMethods.mapValues { it.value.single() }
// Going forward it is should be treated as immutable construct.
methodTable = mutableMethodTable
}
private fun listOfApplicableInterfacesRec(clazz: Class<*>): List<Class<*>> =
clazz.interfaces.filter { RPCOps::class.java.isAssignableFrom(it) }.flatMap {
listOf(it) + listOfApplicableInterfacesRec(it)
}
private fun createObservableSubscriptionMap(): ObservableSubscriptionMap {
val onObservableRemove = RemovalListener<InvocationId, ObservableSubscription> { key, value, cause ->
log.debug { "Unsubscribing from Observable with id $key because of $cause" }
@ -349,18 +392,18 @@ class RPCServer(
}
}
private fun invokeRpc(context: RpcAuthContext, methodName: String, arguments: List<Any?>): Try<Any> {
private fun invokeRpc(context: RpcAuthContext, inMethodName: String, arguments: List<Any?>): Try<Any> {
return Try.on {
try {
CURRENT_RPC_CONTEXT.set(context)
log.trace { "Calling $methodName" }
val method = methodTable[methodName] ?:
throw RPCException("Received RPC for unknown method $methodName - possible client/server version skew?")
method.invoke(ops, *arguments.toTypedArray())
log.trace { "Calling $inMethodName" }
val invocationTarget = methodTable[inMethodName] ?:
throw RPCException("Received RPC for unknown method $inMethodName - possible client/server version skew?")
invocationTarget.method.invoke(invocationTarget.instance, *arguments.toTypedArray())
} catch (e: InvocationTargetException) {
throw e.cause ?: RPCException("Caught InvocationTargetException without cause")
} catch (e: Exception) {
log.warn("Caught exception attempting to invoke RPC $methodName", e)
log.warn("Caught exception attempting to invoke RPC $inMethodName", e)
throw e
} finally {
CURRENT_RPC_CONTEXT.remove()
@ -393,7 +436,7 @@ class RPCServer(
* we receive a notification that the client queue bindings were added.
*/
private fun bufferIfQueueNotBound(clientAddress: SimpleString, message: RPCApi.ServerToClient.RpcReply, context: ObservableContext): Boolean {
val clientBuffer = responseMessageBuffer.compute(clientAddress, { _, value ->
val clientBuffer = responseMessageBuffer.compute(clientAddress) { _, value ->
when (value) {
null -> BufferOrNone.Buffer(ArrayList()).apply {
container.add(MessageAndContext(message, context))
@ -403,7 +446,7 @@ class RPCServer(
}
is BufferOrNone.None -> value
}
})
}
return clientBuffer is BufferOrNone.Buffer
}