mirror of
https://github.com/corda/corda.git
synced 2025-06-15 21:58:17 +00:00
FlowException serialised over RPC (subtypes are flattened), and improvement to startFlow RPC for correct exception handling
This commit is contained in:
@ -11,7 +11,6 @@ import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.node.driver.DriverBasedTest
|
||||
@ -138,13 +137,13 @@ class DistributedServiceTests : DriverBasedTest() {
|
||||
val issueHandle = aliceProxy.startFlow(
|
||||
::CashFlow,
|
||||
CashCommand.IssueCash(amount, OpaqueBytes.of(0), alice.nodeInfo.legalIdentity, raftNotaryIdentity))
|
||||
issueHandle.returnValue.toFuture().getOrThrow()
|
||||
issueHandle.returnValue.getOrThrow()
|
||||
}
|
||||
|
||||
private fun paySelf(amount: Amount<Currency>) {
|
||||
val payHandle = aliceProxy.startFlow(
|
||||
::CashFlow,
|
||||
CashCommand.PayCash(amount.issuedBy(alice.nodeInfo.legalIdentity.ref(0)), alice.nodeInfo.legalIdentity))
|
||||
payHandle.returnValue.toFuture().getOrThrow()
|
||||
payHandle.returnValue.getOrThrow()
|
||||
}
|
||||
}
|
@ -380,7 +380,7 @@ open class DriverDSL(
|
||||
registerProcess(processFuture)
|
||||
return processFuture.flatMap { process ->
|
||||
establishRpc(messagingAddress, configuration).flatMap { rpc ->
|
||||
rpc.waitUntilRegisteredWithNetworkMap().toFuture().map {
|
||||
rpc.waitUntilRegisteredWithNetworkMap().map {
|
||||
NodeHandle(rpc.nodeIdentity(), rpc, configuration, process)
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
import net.corda.core.node.services.StateMachineTransactionMapping
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.toObservable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.node.services.messaging.requirePermission
|
||||
import net.corda.node.services.startFlowPermission
|
||||
@ -97,7 +96,7 @@ class CordaRPCOpsImpl(
|
||||
return FlowHandle(
|
||||
id = stateMachine.id,
|
||||
progress = stateMachine.logic.track()?.second ?: Observable.empty(),
|
||||
returnValue = stateMachine.resultFuture.toObservable()
|
||||
returnValue = stateMachine.resultFuture
|
||||
)
|
||||
}
|
||||
|
||||
@ -111,7 +110,7 @@ class CordaRPCOpsImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun waitUntilRegisteredWithNetworkMap() = services.networkMapCache.mapServiceRegistered.toObservable()
|
||||
override fun waitUntilRegisteredWithNetworkMap() = services.networkMapCache.mapServiceRegistered
|
||||
override fun partyFromKey(key: CompositeKey) = services.identityService.partyFromKey(key)
|
||||
override fun partyFromName(name: String) = services.identityService.partyFromName(name)
|
||||
|
||||
|
@ -8,9 +8,10 @@ import com.esotericsoftware.kryo.Registration
|
||||
import com.esotericsoftware.kryo.Serializer
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.serializers.JavaSerializer
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import de.javakaffee.kryoserializers.ArraysAsListSerializer
|
||||
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer
|
||||
import de.javakaffee.kryoserializers.guava.*
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.ErrorOr
|
||||
@ -19,6 +20,8 @@ import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.IllegalFlowLogicException
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.messaging.FlowHandle
|
||||
import net.corda.core.messaging.StateMachineInfo
|
||||
@ -26,12 +29,15 @@ import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.node.*
|
||||
import net.corda.core.node.services.*
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.toObservable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.node.internal.AbstractNode
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.NetworkMapAddress
|
||||
import net.corda.node.services.statemachine.FlowSessionException
|
||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
@ -138,6 +144,7 @@ private class RPCKryo(observableSerializer: Serializer<Observable<Any>>? = null)
|
||||
register(Array<Any>(0,{}).javaClass)
|
||||
register(Class::class.java, ClassSerializer)
|
||||
|
||||
UnmodifiableCollectionsSerializer.registerSerializers(this)
|
||||
ImmutableListSerializer.registerSerializers(this)
|
||||
ImmutableSetSerializer.registerSerializers(this)
|
||||
ImmutableSortedSetSerializer.registerSerializers(this)
|
||||
@ -207,16 +214,18 @@ private class RPCKryo(observableSerializer: Serializer<Observable<Any>>? = null)
|
||||
register(SimpleString::class.java)
|
||||
register(ServiceEntry::class.java)
|
||||
// Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway.
|
||||
register(Array<StackTraceElement>::class, read = { kryo, input -> emptyArray() }, write = { kryo, output, obj -> })
|
||||
register(FlowException::class.java)
|
||||
register(FlowSessionException::class.java)
|
||||
register(IllegalFlowLogicException::class.java)
|
||||
register(RuntimeException::class.java)
|
||||
register(IllegalArgumentException::class.java)
|
||||
register(ArrayIndexOutOfBoundsException::class.java)
|
||||
register(IndexOutOfBoundsException::class.java)
|
||||
// Kryo couldn't serialize Collections.unmodifiableCollection in Throwable correctly, causing null pointer exception when try to access the deserialize object.
|
||||
register(NoSuchElementException::class.java, JavaSerializer())
|
||||
register(NoSuchElementException::class.java)
|
||||
register(RPCException::class.java)
|
||||
register(Array<StackTraceElement>::class.java, read = { kryo, input -> emptyArray() }, write = { kryo, output, o -> })
|
||||
register(Collections.unmodifiableList(emptyList<String>()).javaClass)
|
||||
register(PermissionException::class.java)
|
||||
register(Throwable::class.java)
|
||||
register(FlowHandle::class.java)
|
||||
register(KryoException::class.java)
|
||||
register(StringBuffer::class.java)
|
||||
@ -229,20 +238,45 @@ private class RPCKryo(observableSerializer: Serializer<Observable<Any>>? = null)
|
||||
pluginRegistries.forEach { it.registerRPCKryoTypes(this) }
|
||||
}
|
||||
|
||||
// Helper method, attempt to reduce boiler plate code
|
||||
private fun <T> register(type: Class<T>, read: (Kryo, Input) -> T, write: (Kryo, Output, T) -> Unit) {
|
||||
register(type, object : Serializer<T>() {
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<T>): T = read(kryo, input)
|
||||
override fun write(kryo: Kryo, output: Output, o: T) = write(kryo, output, o)
|
||||
})
|
||||
// TODO: workaround to prevent Observable registration conflict when using plugin registered kyro classes
|
||||
private val observableRegistration: Registration? = observableSerializer?.let { register(Observable::class.java, it, 10000) }
|
||||
|
||||
private val listenableFutureRegistration: Registration? = observableSerializer?.let {
|
||||
// Register ListenableFuture by making use of Observable serialisation.
|
||||
// TODO Serialisation could be made more efficient as a future can only emit one value (or exception)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
register(ListenableFuture::class,
|
||||
read = { kryo, input -> it.read(kryo, input, Observable::class.java as Class<Observable<Any>>).toFuture() },
|
||||
write = { kryo, output, obj -> it.write(kryo, output, obj.toObservable()) }
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: workaround to prevent Observable registration conflict when using plugin registered kyro classes
|
||||
val observableRegistration: Registration? = if (observableSerializer != null) register(Observable::class.java, observableSerializer, 10000) else null
|
||||
// Avoid having to worry about the subtypes of FlowException by converting all of them to just FlowException.
|
||||
// This is a temporary hack until a proper serialisation mechanism is in place.
|
||||
private val flowExceptionRegistration: Registration = register(
|
||||
FlowException::class,
|
||||
read = { kryo, input ->
|
||||
val message = input.readString()
|
||||
val cause = kryo.readObjectOrNull(input, Throwable::class.java)
|
||||
FlowException(message, cause)
|
||||
},
|
||||
write = { kryo, output, obj ->
|
||||
// The subclass may have overridden toString so we use that
|
||||
val message = if (obj.javaClass != FlowException::class.java) obj.toString() else obj.message
|
||||
output.writeString(message)
|
||||
kryo.writeObjectOrNull(output, obj.cause, Throwable::class.java)
|
||||
}
|
||||
)
|
||||
|
||||
override fun getRegistration(type: Class<*>): Registration {
|
||||
if (Observable::class.java.isAssignableFrom(type))
|
||||
return observableRegistration ?: throw IllegalStateException("This RPC was not annotated with @RPCReturnsObservables")
|
||||
return observableRegistration ?:
|
||||
throw IllegalStateException("This RPC was not annotated with @RPCReturnsObservables")
|
||||
if (ListenableFuture::class.java.isAssignableFrom(type))
|
||||
return listenableFutureRegistration ?:
|
||||
throw IllegalStateException("This RPC was not annotated with @RPCReturnsObservables")
|
||||
if (FlowException::class.java.isAssignableFrom(type))
|
||||
return flowExceptionRegistration
|
||||
return super.getRegistration(type)
|
||||
}
|
||||
}
|
||||
|
@ -368,9 +368,6 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
}
|
||||
|
||||
private fun endAllFiberSessions(fiber: FlowStateMachineImpl<*>, errorResponse: Pair<FlowException, Boolean>?) {
|
||||
// TODO Blanking the stack trace prevents the receiving flow from filling in its own stack trace
|
||||
// @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
// (errorResponse?.first as java.lang.Throwable?)?.stackTrace = emptyArray()
|
||||
openSessions.values.removeIf { session ->
|
||||
if (session.fiber == fiber) {
|
||||
session.endSession(errorResponse)
|
||||
|
@ -1,8 +1,13 @@
|
||||
package net.corda.node.messaging
|
||||
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.messaging.RPCReturnsObservables
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.success
|
||||
import net.corda.core.utilities.LogHelper
|
||||
import net.corda.node.services.RPCUserService
|
||||
import net.corda.node.services.User
|
||||
@ -120,30 +125,31 @@ class ClientRPCInfrastructureTests {
|
||||
@RPCReturnsObservables
|
||||
fun makeComplicatedObservable(): Observable<Pair<String, Observable<String>>>
|
||||
|
||||
@RPCReturnsObservables
|
||||
fun makeListenableFuture(): ListenableFuture<Int>
|
||||
|
||||
@RPCReturnsObservables
|
||||
fun makeComplicatedListenableFuture(): ListenableFuture<Pair<String, ListenableFuture<String>>>
|
||||
|
||||
@RPCSinceVersion(2)
|
||||
fun addedLater()
|
||||
|
||||
fun captureUser(): String
|
||||
}
|
||||
|
||||
lateinit var complicatedObservable: Observable<Pair<String, Observable<String>>>
|
||||
private lateinit var complicatedObservable: Observable<Pair<String, Observable<String>>>
|
||||
private lateinit var complicatedListenableFuturee: ListenableFuture<Pair<String, ListenableFuture<String>>>
|
||||
|
||||
inner class TestOpsImpl : TestOps {
|
||||
override val protocolVersion = 1
|
||||
|
||||
override fun barf(): Unit = throw IllegalArgumentException("Barf!")
|
||||
|
||||
override fun void() {
|
||||
}
|
||||
|
||||
override fun void() {}
|
||||
override fun someCalculation(str: String, num: Int) = "$str $num"
|
||||
|
||||
override fun makeObservable(): Observable<Int> = Observable.just(1, 2, 3, 4)
|
||||
|
||||
override fun makeListenableFuture(): ListenableFuture<Int> = Futures.immediateFuture(1)
|
||||
override fun makeComplicatedObservable() = complicatedObservable
|
||||
|
||||
override fun makeComplicatedListenableFuture(): ListenableFuture<Pair<String, ListenableFuture<String>>> = complicatedListenableFuturee
|
||||
override fun addedLater(): Unit = throw UnsupportedOperationException("not implemented")
|
||||
|
||||
override fun captureUser(): String = CURRENT_RPC_USER.get().username
|
||||
}
|
||||
|
||||
@ -212,6 +218,41 @@ class ClientRPCInfrastructureTests {
|
||||
assertEquals(1, clientSession.addressQuery(rpcQueuesQuery).queueNames.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `simple ListenableFuture`() {
|
||||
val value = proxy.makeListenableFuture().getOrThrow()
|
||||
assertThat(value).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `complex ListenableFuture`() {
|
||||
val serverQuote = SettableFuture.create<Pair<String, ListenableFuture<String>>>()
|
||||
complicatedListenableFuturee = serverQuote
|
||||
|
||||
val twainQuote = "Mark Twain" to Futures.immediateFuture("I have never let my schooling interfere with my education.")
|
||||
|
||||
val clientQuotes = LinkedBlockingQueue<String>()
|
||||
val clientFuture = proxy.makeComplicatedListenableFuture()
|
||||
|
||||
clientFuture.success {
|
||||
val name = it.first
|
||||
it.second.success {
|
||||
clientQuotes += "Quote by $name: $it"
|
||||
}
|
||||
}
|
||||
|
||||
val rpcQueuesQuery = SimpleString("clients.${authenticatedUser.username}.rpc.*")
|
||||
assertEquals(2, clientSession.addressQuery(rpcQueuesQuery).queueNames.size)
|
||||
|
||||
assertThat(clientQuotes).isEmpty()
|
||||
|
||||
serverQuote.set(twainQuote)
|
||||
assertThat(clientQuotes.take()).isEqualTo("Quote by Mark Twain: I have never let my schooling interfere with my education.")
|
||||
|
||||
// TODO This final assert sometimes fails because the relevant queue hasn't been removed yet
|
||||
// assertEquals(1, clientSession.addressQuery(rpcQueuesQuery).queueNames.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun versioning() {
|
||||
assertFailsWith<UnsupportedOperationException> { proxy.addedLater() }
|
||||
@ -221,5 +262,4 @@ class ClientRPCInfrastructureTests {
|
||||
fun `authenticated user is available to RPC`() {
|
||||
assertThat(proxy.captureUser()).isEqualTo(authenticatedUser.username)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -390,9 +390,8 @@ class StateMachineManagerTests {
|
||||
node2 sent sessionConfirm to node1,
|
||||
node2 sent sessionEnd(errorFlow.exceptionThrown) to node1
|
||||
)
|
||||
// TODO see StateMachineManager.endAllFiberSessions
|
||||
// // Make sure the original stack trace isn't sent down the wire
|
||||
// assertThat((sessionTransfers.last().message as SessionEnd).errorResponse!!.stackTrace).isEmpty()
|
||||
// Make sure the original stack trace isn't sent down the wire
|
||||
assertThat((sessionTransfers.last().message as SessionEnd).errorResponse!!.stackTrace).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
Reference in New Issue
Block a user