mirror of
https://github.com/corda/corda.git
synced 2025-06-14 05:08:18 +00:00
RPC muxing, multithreading, RPC driver, performance tests
This commit is contained in:
@ -30,10 +30,7 @@ abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() {
|
||||
const val INTERNAL_PREFIX = "internal."
|
||||
const val PEERS_PREFIX = "${INTERNAL_PREFIX}peers."
|
||||
const val SERVICES_PREFIX = "${INTERNAL_PREFIX}services."
|
||||
const val CLIENTS_PREFIX = "clients."
|
||||
const val P2P_QUEUE = "p2p.inbound"
|
||||
const val RPC_REQUESTS_QUEUE = "rpc.requests"
|
||||
const val RPC_QUEUE_REMOVALS_QUEUE = "rpc.qremovals"
|
||||
const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications"
|
||||
const val NETWORK_MAP_QUEUE = "${INTERNAL_PREFIX}networkmap"
|
||||
|
||||
|
206
node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt
Normal file
206
node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt
Normal file
@ -0,0 +1,206 @@
|
||||
package net.corda.nodeapi
|
||||
|
||||
import com.esotericsoftware.kryo.pool.KryoPool
|
||||
import net.corda.core.ErrorOr
|
||||
import net.corda.core.serialization.KryoPoolWithContext
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.RPCApi.ClientToServer
|
||||
import net.corda.nodeapi.RPCApi.ObservableId
|
||||
import net.corda.nodeapi.RPCApi.RPC_CLIENT_BINDING_REMOVALS
|
||||
import net.corda.nodeapi.RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX
|
||||
import net.corda.nodeapi.RPCApi.RPC_SERVER_QUEUE_NAME
|
||||
import net.corda.nodeapi.RPCApi.RpcRequestId
|
||||
import net.corda.nodeapi.RPCApi.ServerToClient
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.client.*
|
||||
import org.apache.activemq.artemis.api.core.management.CoreNotificationType
|
||||
import org.apache.activemq.artemis.api.core.management.ManagementHelper
|
||||
import org.apache.activemq.artemis.reader.MessageUtil
|
||||
import rx.Notification
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* The RPC protocol:
|
||||
*
|
||||
* The server consumes the queue "[RPC_SERVER_QUEUE_NAME]" and receives RPC requests ([ClientToServer.RpcRequest]) on it.
|
||||
* When a client starts up it should create a queue for its inbound messages, this should be of the form
|
||||
* "[RPC_CLIENT_QUEUE_NAME_PREFIX].$username.$nonce". Each RPC request contains this address (in
|
||||
* [ClientToServer.RpcRequest.clientAddress]), this is where the server will send the reply to the request as well as
|
||||
* subsequent Observations rooted in the RPC. The requests/replies are muxed using a unique [RpcRequestId] generated by
|
||||
* the client for each request.
|
||||
*
|
||||
* If an RPC reply's payload ([ServerToClient.RpcReply.result]) contains [Observable]s then the server will generate a
|
||||
* unique [ObservableId] for each and serialise them in place of the [Observable]s themselves. Subsequently the client
|
||||
* should be prepared to receive observations ([ServerToClient.Observation]), muxed by the relevant [ObservableId].
|
||||
* In addition each observation itself may contain further [Observable]s, this case should behave the same as before.
|
||||
*
|
||||
* Additionally the client may send [ClientToServer.ObservablesClosed] messages indicating that certain observables
|
||||
* aren't consumed anymore, which should subsequently stop the stream from the server. Note that some observations may
|
||||
* already be in flight when this is sent, the client should handle this gracefully.
|
||||
*
|
||||
* An example session:
|
||||
* Client Server
|
||||
* ----------RpcRequest(RID0)-----------> // Client makes RPC request with ID "RID0"
|
||||
* <----RpcReply(RID0, Payload(OID0))---- // Server sends reply containing an observable with ID "OID0"
|
||||
* <---------Observation(OID0)----------- // Server sends observation onto "OID0"
|
||||
* <---Observation(OID0, Payload(OID1))-- // Server sends another observation, this time containing another observable
|
||||
* <---------Observation(OID1)----------- // Observation onto new "OID1"
|
||||
* <---------Observation(OID0)-----------
|
||||
* -----ObservablesClosed(OID0, OID1)---> // Client indicates it stopped consuming the Observables.
|
||||
* <---------Observation(OID1)----------- // Observation was already in-flight before the previous message was processed
|
||||
* (FIN)
|
||||
*
|
||||
* Note that multiple sessions like the above may interleave in an arbitrary fashion.
|
||||
*
|
||||
* Additionally the server may listen on client binding removals for cleanup using [RPC_CLIENT_BINDING_REMOVALS]. This
|
||||
* requires the server to create a filter on the artemis notification address using
|
||||
*/
|
||||
object RPCApi {
|
||||
private val TAG_FIELD_NAME = "tag"
|
||||
private val RPC_ID_FIELD_NAME = "rpc-id"
|
||||
private val OBSERVABLE_ID_FIELD_NAME = "observable-id"
|
||||
private val METHOD_NAME_FIELD_NAME = "method-name"
|
||||
|
||||
val RPC_SERVER_QUEUE_NAME = "rpc.server"
|
||||
val RPC_CLIENT_QUEUE_NAME_PREFIX = "rpc.client"
|
||||
val RPC_CLIENT_BINDING_REMOVALS = "rpc.clientqueueremovals"
|
||||
|
||||
val RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION =
|
||||
"${ManagementHelper.HDR_NOTIFICATION_TYPE} = '${CoreNotificationType.BINDING_REMOVED.name}' AND " +
|
||||
"${ManagementHelper.HDR_ROUTING_NAME} LIKE '$RPC_CLIENT_QUEUE_NAME_PREFIX.%'"
|
||||
|
||||
data class RpcRequestId(val toLong: Long)
|
||||
data class ObservableId(val toLong: Long)
|
||||
|
||||
object RpcRequestOrObservableIdKey
|
||||
|
||||
private fun ClientMessage.getBodyAsByteArray(): ByteArray {
|
||||
return ByteArray(bodySize).apply { bodyBuffer.readBytes(this) }
|
||||
}
|
||||
|
||||
sealed class ClientToServer {
|
||||
private enum class Tag {
|
||||
RPC_REQUEST,
|
||||
OBSERVABLES_CLOSED
|
||||
}
|
||||
|
||||
data class RpcRequest(
|
||||
val clientAddress: SimpleString,
|
||||
val id: RpcRequestId,
|
||||
val methodName: String,
|
||||
val arguments: List<Any?>
|
||||
) : ClientToServer() {
|
||||
fun writeToClientMessage(kryoPool: KryoPool, message: ClientMessage) {
|
||||
MessageUtil.setJMSReplyTo(message, clientAddress)
|
||||
message.putIntProperty(TAG_FIELD_NAME, Tag.RPC_REQUEST.ordinal)
|
||||
message.putLongProperty(RPC_ID_FIELD_NAME, id.toLong)
|
||||
message.putStringProperty(METHOD_NAME_FIELD_NAME, methodName)
|
||||
message.bodyBuffer.writeBytes(arguments.serialize(kryoPool).bytes)
|
||||
}
|
||||
}
|
||||
|
||||
data class ObservablesClosed(
|
||||
val ids: List<ObservableId>
|
||||
) : ClientToServer() {
|
||||
fun writeToClientMessage(message: ClientMessage) {
|
||||
message.putIntProperty(TAG_FIELD_NAME, Tag.OBSERVABLES_CLOSED.ordinal)
|
||||
val buffer = message.bodyBuffer
|
||||
buffer.writeInt(ids.size)
|
||||
ids.forEach {
|
||||
buffer.writeLong(it.toLong)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromClientMessage(kryoPool: KryoPool, message: ClientMessage): ClientToServer {
|
||||
val tag = Tag.values()[message.getIntProperty(TAG_FIELD_NAME)]
|
||||
return when (tag) {
|
||||
RPCApi.ClientToServer.Tag.RPC_REQUEST -> RpcRequest(
|
||||
clientAddress = MessageUtil.getJMSReplyTo(message),
|
||||
id = RpcRequestId(message.getLongProperty(RPC_ID_FIELD_NAME)),
|
||||
methodName = message.getStringProperty(METHOD_NAME_FIELD_NAME),
|
||||
arguments = message.getBodyAsByteArray().deserialize(kryoPool)
|
||||
)
|
||||
RPCApi.ClientToServer.Tag.OBSERVABLES_CLOSED -> {
|
||||
val ids = ArrayList<ObservableId>()
|
||||
val buffer = message.bodyBuffer
|
||||
val numberOfIds = buffer.readInt()
|
||||
for (i in 1 .. numberOfIds) {
|
||||
ids.add(ObservableId(buffer.readLong()))
|
||||
}
|
||||
ObservablesClosed(ids)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ServerToClient {
|
||||
private enum class Tag {
|
||||
RPC_REPLY,
|
||||
OBSERVATION
|
||||
}
|
||||
|
||||
abstract fun writeToClientMessage(kryoPool: KryoPool, message: ClientMessage)
|
||||
|
||||
data class RpcReply(
|
||||
val id: RpcRequestId,
|
||||
val result: ErrorOr<Any?>
|
||||
) : ServerToClient() {
|
||||
override fun writeToClientMessage(kryoPool: KryoPool, message: ClientMessage) {
|
||||
message.putIntProperty(TAG_FIELD_NAME, Tag.RPC_REPLY.ordinal)
|
||||
message.putLongProperty(RPC_ID_FIELD_NAME, id.toLong)
|
||||
message.bodyBuffer.writeBytes(result.serialize(kryoPool).bytes)
|
||||
}
|
||||
}
|
||||
|
||||
data class Observation(
|
||||
val id: ObservableId,
|
||||
val content: Notification<Any>
|
||||
) : ServerToClient() {
|
||||
override fun writeToClientMessage(kryoPool: KryoPool, message: ClientMessage) {
|
||||
message.putIntProperty(TAG_FIELD_NAME, Tag.OBSERVATION.ordinal)
|
||||
message.putLongProperty(OBSERVABLE_ID_FIELD_NAME, id.toLong)
|
||||
message.bodyBuffer.writeBytes(content.serialize(kryoPool).bytes)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromClientMessage(kryoPool: KryoPool, message: ClientMessage): ServerToClient {
|
||||
val tag = Tag.values()[message.getIntProperty(TAG_FIELD_NAME)]
|
||||
return when (tag) {
|
||||
RPCApi.ServerToClient.Tag.RPC_REPLY -> {
|
||||
val id = RpcRequestId(message.getLongProperty(RPC_ID_FIELD_NAME))
|
||||
val poolWithIdContext = KryoPoolWithContext(kryoPool, RpcRequestOrObservableIdKey, id.toLong)
|
||||
RpcReply(
|
||||
id = id,
|
||||
result = message.getBodyAsByteArray().deserialize(poolWithIdContext)
|
||||
)
|
||||
}
|
||||
RPCApi.ServerToClient.Tag.OBSERVATION -> {
|
||||
val id = ObservableId(message.getLongProperty(OBSERVABLE_ID_FIELD_NAME))
|
||||
val poolWithIdContext = KryoPoolWithContext(kryoPool, RpcRequestOrObservableIdKey, id.toLong)
|
||||
Observation(
|
||||
id = id,
|
||||
content = message.getBodyAsByteArray().deserialize(poolWithIdContext)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ArtemisProducer(
|
||||
val sessionFactory: ClientSessionFactory,
|
||||
val session: ClientSession,
|
||||
val producer: ClientProducer
|
||||
)
|
||||
|
||||
data class ArtemisConsumer(
|
||||
val sessionFactory: ClientSessionFactory,
|
||||
val session: ClientSession,
|
||||
val consumer: ClientConsumer
|
||||
)
|
@ -10,17 +10,10 @@ import net.corda.core.serialization.*
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.toObservable
|
||||
import net.corda.nodeapi.config.OldConfig
|
||||
import org.apache.commons.fileupload.MultipartStream
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import rx.Notification
|
||||
import rx.Observable
|
||||
|
||||
/** Global RPC logger */
|
||||
val rpcLog: Logger by lazy { LoggerFactory.getLogger("net.corda.rpc") }
|
||||
|
||||
/** Used in the RPC wire protocol to wrap an observation with the handle of the observable it's intended for. */
|
||||
data class MarshalledObservation(val forHandle: Int, val what: Notification<*>)
|
||||
import java.io.InputStream
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
data class User(
|
||||
@OldConfig("user")
|
||||
@ -35,28 +28,6 @@ data class User(
|
||||
@MustBeDocumented
|
||||
annotation class RPCSinceVersion(val version: Int)
|
||||
|
||||
/** The contents of an RPC request message, separated from the MQ layer. */
|
||||
data class ClientRPCRequestMessage(
|
||||
val args: SerializedBytes<Array<Any>>,
|
||||
val replyToAddress: String,
|
||||
val observationsToAddress: String?,
|
||||
val methodName: String,
|
||||
val user: User
|
||||
) {
|
||||
companion object {
|
||||
const val REPLY_TO = "reply-to"
|
||||
const val OBSERVATIONS_TO = "observations-to"
|
||||
const val METHOD_NAME = "method-name"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is available to RPC implementations to query the validated [User] that is calling it. Each user has a set of
|
||||
* permissions they're entitled to which can be used to control access.
|
||||
*/
|
||||
@JvmField
|
||||
val CURRENT_RPC_USER: ThreadLocal<User> = ThreadLocal()
|
||||
|
||||
/**
|
||||
* Thrown to indicate a fatal error in the RPC system itself, as opposed to an error generated by the invoked
|
||||
* method.
|
||||
@ -64,19 +35,11 @@ val CURRENT_RPC_USER: ThreadLocal<User> = ThreadLocal()
|
||||
@CordaSerializable
|
||||
open class RPCException(msg: String, cause: Throwable?) : RuntimeException(msg, cause) {
|
||||
constructor(msg: String) : this(msg, null)
|
||||
|
||||
class DeadlineExceeded(rpcName: String) : RPCException("Deadline exceeded on call to $rpcName")
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class PermissionException(msg: String) : RuntimeException(msg)
|
||||
|
||||
object RPCKryoClientKey
|
||||
object RPCKryoDispatcherKey
|
||||
object RPCKryoQNameKey
|
||||
object RPCKryoMethodNameKey
|
||||
object RPCKryoLocationKey
|
||||
|
||||
// The Kryo used for the RPC wire protocol. Every type in the wire protocol is listed here explicitly.
|
||||
// This is annoying to write out, but will make it easier to formalise the wire protocol when the time comes,
|
||||
// because we can see everything we're using in one place.
|
||||
@ -85,8 +48,7 @@ class RPCKryo(observableSerializer: Serializer<Observable<Any>>) : CordaKryo(mak
|
||||
DefaultKryoCustomizer.customize(this)
|
||||
|
||||
// RPC specific classes
|
||||
register(MultipartStream.ItemInputStream::class.java, InputStreamSerializer)
|
||||
register(MarshalledObservation::class.java, ImmutableClassSerializer(MarshalledObservation::class))
|
||||
register(InputStream::class.java, InputStreamSerializer)
|
||||
register(Observable::class.java, observableSerializer)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
register(ListenableFuture::class,
|
||||
@ -110,14 +72,14 @@ class RPCKryo(observableSerializer: Serializer<Observable<Any>>) : CordaKryo(mak
|
||||
}
|
||||
|
||||
override fun getRegistration(type: Class<*>): Registration {
|
||||
val annotated = context[RPCKryoQNameKey] != null
|
||||
if (Observable::class.java.isAssignableFrom(type)) {
|
||||
return if (annotated) super.getRegistration(Observable::class.java)
|
||||
else throw IllegalStateException("This RPC was not annotated with @RPCReturnsObservables")
|
||||
if (Observable::class.java != type && Observable::class.java.isAssignableFrom(type)) {
|
||||
return super.getRegistration(Observable::class.java)
|
||||
}
|
||||
if (ListenableFuture::class.java.isAssignableFrom(type)) {
|
||||
return if (annotated) super.getRegistration(ListenableFuture::class.java)
|
||||
else throw IllegalStateException("This RPC was not annotated with @RPCReturnsObservables")
|
||||
if (InputStream::class.java != type && InputStream::class.java.isAssignableFrom(type)) {
|
||||
return super.getRegistration(InputStream::class.java)
|
||||
}
|
||||
if (ListenableFuture::class.java != type && ListenableFuture::class.java.isAssignableFrom(type)) {
|
||||
return super.getRegistration(ListenableFuture::class.java)
|
||||
}
|
||||
if (FlowException::class.java.isAssignableFrom(type))
|
||||
return super.getRegistration(FlowException::class.java)
|
||||
|
Reference in New Issue
Block a user