ProgressTracker emits exception thrown by the flow, allowing the ANSI renderer to correctly stop and print the error (#189)

This commit is contained in:
Shams Asari 2017-02-15 10:14:24 +00:00
parent ed093cdb9d
commit f13817efb3
10 changed files with 186 additions and 106 deletions

View File

@ -403,3 +403,9 @@ private class ObservableToFuture<T>(observable: Observable<T>) : AbstractFuture<
/** Return the sum of an Iterable of [BigDecimal]s. */ /** Return the sum of an Iterable of [BigDecimal]s. */
fun Iterable<BigDecimal>.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + b } fun Iterable<BigDecimal>.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + b }
fun codePointsString(vararg codePoints: Int): String {
val builder = StringBuilder()
codePoints.forEach { builder.append(Character.toChars(it)) }
return builder.toString()
}

View File

@ -1,5 +1,7 @@
package net.corda.core.utilities package net.corda.core.utilities
import net.corda.core.codePointsString
/** /**
* A simple wrapper class that contains icons and support for printing them only when we're connected to a terminal. * A simple wrapper class that contains icons and support for printing them only when we're connected to a terminal.
*/ */
@ -7,15 +9,17 @@ object Emoji {
// Unfortunately only Apple has a terminal that can do colour emoji AND an emoji font installed by default. // Unfortunately only Apple has a terminal that can do colour emoji AND an emoji font installed by default.
val hasEmojiTerminal by lazy { listOf("Apple_Terminal", "iTerm.app").contains(System.getenv("TERM_PROGRAM")) } val hasEmojiTerminal by lazy { listOf("Apple_Terminal", "iTerm.app").contains(System.getenv("TERM_PROGRAM")) }
const val CODE_SANTA_CLAUS = "\ud83c\udf85" @JvmStatic val CODE_SANTA_CLAUS: String = codePointsString(0x1F385)
const val CODE_DIAMOND = "\ud83d\udd37" @JvmStatic val CODE_DIAMOND: String = codePointsString(0x1F537)
const val CODE_BAG_OF_CASH = "\ud83d\udcb0" @JvmStatic val CODE_BAG_OF_CASH: String = codePointsString(0x1F4B0)
const val CODE_NEWSPAPER = "\ud83d\udcf0" @JvmStatic val CODE_NEWSPAPER: String = codePointsString(0x1F4F0)
const val CODE_RIGHT_ARROW = "\u27a1\ufe0f" @JvmStatic val CODE_RIGHT_ARROW: String = codePointsString(0x27A1, 0xFE0F)
const val CODE_LEFT_ARROW = "\u2b05\ufe0f" @JvmStatic val CODE_LEFT_ARROW: String = codePointsString(0x2B05, 0xFE0F)
const val CODE_GREEN_TICK = "\u2705" @JvmStatic val CODE_GREEN_TICK: String = codePointsString(0x2705)
const val CODE_PAPERCLIP = "\ud83d\udcce" @JvmStatic val CODE_PAPERCLIP: String = codePointsString(0x1F4CE)
const val CODE_COOL_GUY = "\ud83d\ude0e" @JvmStatic val CODE_COOL_GUY: String = codePointsString(0x1F60E)
@JvmStatic val CODE_NO_ENTRY: String = codePointsString(0x1F6AB)
@JvmStatic val SKULL_AND_CROSSBONES: String = codePointsString(0x2620)
/** /**
* When non-null, toString() methods are allowed to use emoji in the output as we're going to render them to a * When non-null, toString() methods are allowed to use emoji in the output as we're going to render them to a
@ -55,4 +59,5 @@ object Emoji {
emojiMode.set(null) emojiMode.set(null)
} }
} }
} }

View File

@ -55,7 +55,7 @@ class ProgressTracker(vararg steps: Step) {
/** This class makes it easier to relabel a step on the fly, to provide transient information. */ /** This class makes it easier to relabel a step on the fly, to provide transient information. */
open inner class RelabelableStep(currentLabel: String) : Step(currentLabel) { open inner class RelabelableStep(currentLabel: String) : Step(currentLabel) {
override val changes = BehaviorSubject.create<Change>() override val changes: BehaviorSubject<Change> = BehaviorSubject.create()
var currentLabel: String = currentLabel var currentLabel: String = currentLabel
set(value) { set(value) {
@ -105,27 +105,26 @@ class ProgressTracker(vararg steps: Step) {
var currentStep: Step var currentStep: Step
get() = steps[stepIndex] get() = steps[stepIndex]
set(value) { set(value) {
if (currentStep != value) { check(!hasEnded) { "Cannot rewind a progress tracker once it has ended" }
check(currentStep != DONE) { "Cannot rewind a progress tracker once it reaches the done state" } if (currentStep == value) return
val index = steps.indexOf(value) val index = steps.indexOf(value)
require(index != -1) require(index != -1)
if (index < stepIndex) { if (index < stepIndex) {
// We are going backwards: unlink and unsubscribe from any child nodes that we're rolling back // We are going backwards: unlink and unsubscribe from any child nodes that we're rolling back
// through, in preparation for moving through them again. // through, in preparation for moving through them again.
for (i in stepIndex downTo index) { for (i in stepIndex downTo index) {
removeChildProgressTracker(steps[i]) removeChildProgressTracker(steps[i])
}
} }
curChangeSubscription?.unsubscribe()
stepIndex = index
_changes.onNext(Change.Position(this, steps[index]))
curChangeSubscription = currentStep.changes.subscribe { _changes.onNext(it) }
if (currentStep == DONE) _changes.onCompleted()
} }
curChangeSubscription?.unsubscribe()
stepIndex = index
_changes.onNext(Change.Position(this, steps[index]))
curChangeSubscription = currentStep.changes.subscribe( { _changes.onNext(it) }, { _changes.onError(it) })
if (currentStep == DONE) _changes.onCompleted()
} }
/** Returns the current step, descending into children to find the deepest step we are up to. */ /** Returns the current step, descending into children to find the deepest step we are up to. */
@ -149,6 +148,15 @@ class ProgressTracker(vararg steps: Step) {
_changes.onNext(Change.Structural(this, step)) _changes.onNext(Change.Structural(this, step))
} }
/**
* Ends the progress tracker with the given error, bypassing any remaining steps. [changes] will emit the exception
* as an error.
*/
fun endWithError(error: Throwable) {
check(!hasEnded) { "Progress tracker has already ended" }
_changes.onError(error)
}
/** The parent of this tracker: set automatically by the parent when a tracker is added as a child */ /** The parent of this tracker: set automatically by the parent when a tracker is added as a child */
var parent: ProgressTracker? = null var parent: ProgressTracker? = null
private set private set
@ -195,6 +203,9 @@ class ProgressTracker(vararg steps: Step) {
* if a step changed its label or rendering). * if a step changed its label or rendering).
*/ */
val changes: Observable<Change> get() = _changes val changes: Observable<Change> get() = _changes
/** Returns true if the progress tracker has ended, either by reaching the [DONE] step or prematurely with an error */
val hasEnded: Boolean get() = _changes.hasCompleted() || _changes.hasThrowable()
} }

View File

@ -15,6 +15,7 @@ import net.corda.core.flows.FlowStateMachine
import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.StateMachineRunId
import net.corda.core.random63BitValue import net.corda.core.random63BitValue
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
@ -56,8 +57,8 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
@Transient private var _logger: Logger? = null @Transient private var _logger: Logger? = null
/** /**
* Return the logger for this state machine. The logger name incorporates [id] and so including this in the log * Return the logger for this state machine. The logger name incorporates [id] and so including it in the log message
* message is not necessary. * is not necessary.
*/ */
override val logger: Logger get() { override val logger: Logger get() {
return _logger ?: run { return _logger ?: run {
@ -94,14 +95,12 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
} catch (e: FlowException) { } catch (e: FlowException) {
// Check if the FlowException was propagated by looking at where the stack trace originates (see suspendAndExpectReceive). // Check if the FlowException was propagated by looking at where the stack trace originates (see suspendAndExpectReceive).
val propagated = e.stackTrace[0].className == javaClass.name val propagated = e.stackTrace[0].className == javaClass.name
actionOnEnd(e, propagated) processException(e, propagated)
_resultFuture?.setException(e)
logger.debug(if (propagated) "Flow ended due to receiving exception" else "Flow finished with exception", e) logger.debug(if (propagated) "Flow ended due to receiving exception" else "Flow finished with exception", e)
return return
} catch (t: Throwable) { } catch (t: Throwable) {
logger.warn("Terminated by unexpected exception", t) logger.warn("Terminated by unexpected exception", t)
actionOnEnd(t, false) processException(t, false)
_resultFuture?.setException(t)
return return
} }
@ -112,6 +111,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
// This is to prevent actionOnEnd being called twice if it throws an exception // This is to prevent actionOnEnd being called twice if it throws an exception
actionOnEnd(null, false) actionOnEnd(null, false)
_resultFuture?.set(result) _resultFuture?.set(result)
logic.progressTracker?.currentStep = ProgressTracker.DONE
logger.debug { "Flow finished with result $result" } logger.debug { "Flow finished with result $result" }
} }
@ -121,6 +121,12 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
logger.trace { "Starting database transaction ${TransactionManager.currentOrNull()} on ${Strand.currentStrand()}" } logger.trace { "Starting database transaction ${TransactionManager.currentOrNull()} on ${Strand.currentStrand()}" }
} }
private fun processException(exception: Throwable, propagated: Boolean) {
actionOnEnd(exception, propagated)
_resultFuture?.setException(exception)
logic.progressTracker?.endWithError(exception)
}
internal fun commitTransaction() { internal fun commitTransaction() {
val transaction = TransactionManager.current() val transaction = TransactionManager.current()
try { try {

View File

@ -24,7 +24,6 @@ import net.corda.core.messaging.send
import net.corda.core.random63BitValue import net.corda.core.random63BitValue
import net.corda.core.serialization.* import net.corda.core.serialization.*
import net.corda.core.then import net.corda.core.then
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
@ -391,7 +390,6 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
} }
fiber.actionOnEnd = { exception, propagated -> fiber.actionOnEnd = { exception, propagated ->
try { try {
fiber.logic.progressTracker?.currentStep = ProgressTracker.DONE
mutex.locked { mutex.locked {
stateMachines.remove(fiber)?.let { checkpointStorage.removeCheckpoint(it) } stateMachines.remove(fiber)?.let { checkpointStorage.removeCheckpoint(it) }
notifyChangeObservers(fiber, AddOrRemove.REMOVE) notifyChangeObservers(fiber, AddOrRemove.REMOVE)

View File

@ -2,7 +2,6 @@ package net.corda.node.utilities
import net.corda.core.ThreadBox import net.corda.core.ThreadBox
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.utilities.ProgressTracker
import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.statemachine.StateMachineManager
import java.util.* import java.util.*
@ -35,13 +34,12 @@ class ANSIProgressObserver(val smm: StateMachineManager) {
if (currentlyRendering?.progressTracker != null) { if (currentlyRendering?.progressTracker != null) {
ANSIProgressRenderer.progressTracker = currentlyRendering!!.progressTracker ANSIProgressRenderer.progressTracker = currentlyRendering!!.progressTracker
} }
} while (currentlyRendering?.progressTracker?.currentStep == ProgressTracker.DONE) } while (currentlyRendering?.progressTracker?.hasEnded ?: false)
} }
} }
private fun removeFlowLogic(flowLogic: FlowLogic<*>) { private fun removeFlowLogic(flowLogic: FlowLogic<*>) {
state.locked { state.locked {
flowLogic.progressTracker?.currentStep = ProgressTracker.DONE
if (currentlyRendering == flowLogic) { if (currentlyRendering == flowLogic) {
wireUpProgressRendering() wireUpProgressRendering()
} }
@ -51,7 +49,7 @@ class ANSIProgressObserver(val smm: StateMachineManager) {
private fun addFlowLogic(flowLogic: FlowLogic<*>) { private fun addFlowLogic(flowLogic: FlowLogic<*>) {
state.locked { state.locked {
pending.add(flowLogic) pending.add(flowLogic)
if ((currentlyRendering?.progressTracker?.currentStep ?: ProgressTracker.DONE) == ProgressTracker.DONE) { if (currentlyRendering?.progressTracker?.hasEnded ?: true) {
wireUpProgressRendering() wireUpProgressRendering()
} }
} }

View File

@ -1,6 +1,9 @@
package net.corda.node.utilities package net.corda.node.utilities
import net.corda.core.utilities.Emoji import net.corda.core.utilities.Emoji.CODE_GREEN_TICK
import net.corda.core.utilities.Emoji.CODE_NO_ENTRY
import net.corda.core.utilities.Emoji.CODE_RIGHT_ARROW
import net.corda.core.utilities.Emoji.SKULL_AND_CROSSBONES
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.node.utilities.ANSIProgressRenderer.progressTracker import net.corda.node.utilities.ANSIProgressRenderer.progressTracker
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
@ -43,7 +46,7 @@ object ANSIProgressRenderer {
prevMessagePrinted = null prevMessagePrinted = null
prevLinesDrawn = 0 prevLinesDrawn = 0
draw(true) draw(true)
subscription = value?.changes?.subscribe { draw(true) } subscription = value?.changes?.subscribe({ draw(true) }, { draw(true, it) })
} }
private fun setup() { private fun setup() {
@ -102,7 +105,7 @@ object ANSIProgressRenderer {
// prevLinesDraw is just for ANSI mode. // prevLinesDraw is just for ANSI mode.
private var prevLinesDrawn = 0 private var prevLinesDrawn = 0
@Synchronized private fun draw(moveUp: Boolean) { @Synchronized private fun draw(moveUp: Boolean, error: Throwable? = null) {
val pt = progressTracker!! val pt = progressTracker!!
if (!usingANSI) { if (!usingANSI) {
@ -122,7 +125,15 @@ object ANSIProgressRenderer {
// Put a blank line between any logging and us. // Put a blank line between any logging and us.
ansi.eraseLine() ansi.eraseLine()
ansi.newline() ansi.newline()
val newLinesDrawn = 1 + pt.renderLevel(ansi, 0, pt.allSteps) var newLinesDrawn = 1 + pt.renderLevel(ansi, 0, error != null)
if (error != null) {
ansi.a("$SKULL_AND_CROSSBONES $error")
ansi.eraseLine(Ansi.Erase.FORWARD)
ansi.newline()
newLinesDrawn++
}
if (newLinesDrawn < prevLinesDrawn) { if (newLinesDrawn < prevLinesDrawn) {
// If some steps were removed from the progress tracker, we don't want to leave junk hanging around below. // If some steps were removed from the progress tracker, we don't want to leave junk hanging around below.
val linesToClear = prevLinesDrawn - newLinesDrawn val linesToClear = prevLinesDrawn - newLinesDrawn
@ -140,7 +151,7 @@ object ANSIProgressRenderer {
} }
// Returns number of lines rendered. // Returns number of lines rendered.
private fun ProgressTracker.renderLevel(ansi: Ansi, indent: Int, allSteps: List<Pair<Int, ProgressTracker.Step>>): Int { private fun ProgressTracker.renderLevel(ansi: Ansi, indent: Int, error: Boolean): Int {
with(ansi) { with(ansi) {
var lines = 0 var lines = 0
for ((index, step) in steps.withIndex()) { for ((index, step) in steps.withIndex()) {
@ -149,10 +160,11 @@ object ANSIProgressRenderer {
if (indent > 0 && step == ProgressTracker.DONE) continue if (indent > 0 && step == ProgressTracker.DONE) continue
val marker = when { val marker = when {
index < stepIndex -> Emoji.CODE_GREEN_TICK + " " index < stepIndex -> "$CODE_GREEN_TICK "
index == stepIndex && step == ProgressTracker.DONE -> Emoji.CODE_GREEN_TICK + " " index == stepIndex && step == ProgressTracker.DONE -> "$CODE_GREEN_TICK "
index == stepIndex -> Emoji.CODE_RIGHT_ARROW + " " index == stepIndex -> "$CODE_RIGHT_ARROW "
else -> " " error -> "$CODE_NO_ENTRY "
else -> " "
} }
a(" ".repeat(indent)) a(" ".repeat(indent))
a(marker) a(marker)
@ -168,7 +180,7 @@ object ANSIProgressRenderer {
val child = getChildProgressTracker(step) val child = getChildProgressTracker(step)
if (child != null) if (child != null)
lines += child.renderLevel(ansi, indent + 1, allSteps) lines += child.renderLevel(ansi, indent + 1, error)
} }
return lines return lines
} }

View File

@ -3,6 +3,7 @@ package net.corda.node.services.statemachine
import co.paralleluniverse.fibers.Fiber import co.paralleluniverse.fibers.Fiber
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.*
import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.DOLLARS
import net.corda.core.contracts.DummyState import net.corda.core.contracts.DummyState
import net.corda.core.contracts.issuedBy import net.corda.core.contracts.issuedBy
@ -10,17 +11,16 @@ import net.corda.core.crypto.Party
import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.generateKeyPair
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.getOrThrow
import net.corda.core.map
import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.MessageRecipients
import net.corda.core.node.services.PartyInfo import net.corda.core.node.services.PartyInfo
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.random63BitValue
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.LogHelper import net.corda.core.utilities.LogHelper
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.ProgressTracker.Change
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import net.corda.flows.CashIssueFlow import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow import net.corda.flows.CashPaymentFlow
@ -44,6 +44,7 @@ import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import rx.Notification
import rx.Observable import rx.Observable
import java.util.* import java.util.*
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -379,18 +380,36 @@ class StateMachineManagerTests {
net.runNetwork() net.runNetwork()
assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy { assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy {
resultFuture.getOrThrow() resultFuture.getOrThrow()
}.withMessageContaining(String::class.java.name) }.withMessageContaining(String::class.java.name) // Make sure the exception message mentions the type the flow was expecting to receive
} }
@Test @Test
fun `non-FlowException thrown on other side`() { fun `non-FlowException thrown on other side`() {
node2.services.registerFlowInitiator(ReceiveFlow::class) { ExceptionFlow { Exception("evil bug!") } } val erroringFlowFuture = node2.initiateSingleShotFlow(ReceiveFlow::class) {
val resultFuture = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture ExceptionFlow { Exception("evil bug!") }
net.runNetwork()
val exceptionResult = assertFailsWith(FlowSessionException::class) {
resultFuture.getOrThrow()
} }
assertThat(exceptionResult.message).doesNotContain("evil bug!") val erroringFlowSteps = erroringFlowFuture.flatMap { it.progressSteps }
val receiveFlow = ReceiveFlow(node2.info.legalIdentity)
val receiveFlowSteps = receiveFlow.progressSteps
val receiveFlowResult = node1.services.startFlow(receiveFlow).resultFuture
net.runNetwork()
assertThat(erroringFlowSteps.get()).containsExactly(
Notification.createOnNext(ExceptionFlow.START_STEP),
Notification.createOnError(erroringFlowFuture.get().exceptionThrown)
)
val receiveFlowException = assertFailsWith(FlowSessionException::class) {
receiveFlowResult.getOrThrow()
}
assertThat(receiveFlowException.message).doesNotContain("evil bug!")
assertThat(receiveFlowSteps.get()).containsExactly(
Notification.createOnNext(ReceiveFlow.START_STEP),
Notification.createOnError(receiveFlowException)
)
assertSessionTransfers( assertSessionTransfers(
node1 sent sessionInit(ReceiveFlow::class) to node2, node1 sent sessionInit(ReceiveFlow::class) to node2,
node2 sent sessionConfirm to node1, node2 sent sessionConfirm to node1,
@ -400,11 +419,15 @@ class StateMachineManagerTests {
@Test @Test
fun `FlowException thrown on other side`() { fun `FlowException thrown on other side`() {
val erroringFlowFuture = node2.initiateSingleShotFlow(ReceiveFlow::class) { val erroringFlow = node2.initiateSingleShotFlow(ReceiveFlow::class) {
ExceptionFlow { MyFlowException("Nothing useful") } ExceptionFlow { MyFlowException("Nothing useful") }
} }
val erroringFlowSteps = erroringFlow.flatMap { it.progressSteps }
val receivingFiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)) as FlowStateMachineImpl val receivingFiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)) as FlowStateMachineImpl
net.runNetwork() net.runNetwork()
assertThatExceptionOfType(MyFlowException::class.java) assertThatExceptionOfType(MyFlowException::class.java)
.isThrownBy { receivingFiber.resultFuture.getOrThrow() } .isThrownBy { receivingFiber.resultFuture.getOrThrow() }
.withMessage("Nothing useful") .withMessage("Nothing useful")
@ -412,13 +435,18 @@ class StateMachineManagerTests {
databaseTransaction(node2.database) { databaseTransaction(node2.database) {
assertThat(node2.checkpointStorage.checkpoints()).isEmpty() assertThat(node2.checkpointStorage.checkpoints()).isEmpty()
} }
val errorFlow = erroringFlowFuture.getOrThrow()
assertThat(receivingFiber.isTerminated).isTrue() assertThat(receivingFiber.isTerminated).isTrue()
assertThat((errorFlow.stateMachine as FlowStateMachineImpl).isTerminated).isTrue() assertThat((erroringFlow.get().stateMachine as FlowStateMachineImpl).isTerminated).isTrue()
assertThat(erroringFlowSteps.get()).containsExactly(
Notification.createOnNext(ExceptionFlow.START_STEP),
Notification.createOnError(erroringFlow.get().exceptionThrown)
)
assertSessionTransfers( assertSessionTransfers(
node1 sent sessionInit(ReceiveFlow::class) to node2, node1 sent sessionInit(ReceiveFlow::class) to node2,
node2 sent sessionConfirm to node1, node2 sent sessionConfirm to node1,
node2 sent erroredEnd(errorFlow.exceptionThrown) to node1 node2 sent erroredEnd(erroringFlow.get().exceptionThrown) to node1
) )
// Make sure the original stack trace isn't sent down the wire // Make sure the original stack trace isn't sent down the wire
assertThat((sessionTransfers.last().message as ErrorSessionEnd).errorResponse!!.stackTrace).isEmpty() assertThat((sessionTransfers.last().message as ErrorSessionEnd).errorResponse!!.stackTrace).isEmpty()
@ -606,6 +634,15 @@ class StateMachineManagerTests {
private infix fun MockNode.sent(message: SessionMessage): Pair<Int, SessionMessage> = Pair(id, message) private infix fun MockNode.sent(message: SessionMessage): Pair<Int, SessionMessage> = Pair(id, message)
private infix fun Pair<Int, SessionMessage>.to(node: MockNode): SessionTransfer = SessionTransfer(first, second, node.net.myAddress) private infix fun Pair<Int, SessionMessage>.to(node: MockNode): SessionTransfer = SessionTransfer(first, second, node.net.myAddress)
private val FlowLogic<*>.progressSteps: ListenableFuture<List<Notification<ProgressTracker.Step>>> get() {
return progressTracker!!.changes
.ofType(Change.Position::class.java)
.map { it.newStep }
.materialize()
.toList()
.toFuture()
}
private class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic<Unit>() { private class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic<Unit>() {
@Transient var flowStarted = false @Transient var flowStarted = false
@ -630,17 +667,22 @@ class StateMachineManagerTests {
private class ReceiveFlow(vararg val otherParties: Party) : FlowLogic<Unit>() { private class ReceiveFlow(vararg val otherParties: Party) : FlowLogic<Unit>() {
private var nonTerminating: Boolean = false object START_STEP : ProgressTracker.Step("Starting")
object RECEIVED_STEP : ProgressTracker.Step("Received")
init { init {
require(otherParties.isNotEmpty()) require(otherParties.isNotEmpty())
} }
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP, RECEIVED_STEP)
private var nonTerminating: Boolean = false
@Transient var receivedPayloads: List<String> = emptyList() @Transient var receivedPayloads: List<String> = emptyList()
@Suspendable @Suspendable
override fun call() { override fun call() {
progressTracker.currentStep = START_STEP
receivedPayloads = otherParties.map { receive<String>(it).unwrap { it } } receivedPayloads = otherParties.map { receive<String>(it).unwrap { it } }
progressTracker.currentStep = RECEIVED_STEP
if (nonTerminating) { if (nonTerminating) {
Fiber.park() Fiber.park()
} }
@ -664,8 +706,13 @@ class StateMachineManagerTests {
} }
private class ExceptionFlow<E : Exception>(val exception: () -> E) : FlowLogic<Nothing>() { private class ExceptionFlow<E : Exception>(val exception: () -> E) : FlowLogic<Nothing>() {
object START_STEP : ProgressTracker.Step("Starting")
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP)
lateinit var exceptionThrown: E lateinit var exceptionThrown: E
override fun call(): Nothing { override fun call(): Nothing {
progressTracker.currentStep = START_STEP
exceptionThrown = exception() exceptionThrown = exception()
throw exceptionThrown throw exceptionThrown
} }

View File

@ -4,31 +4,34 @@ import com.google.common.util.concurrent.Futures
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.flows.IssuerFlow import net.corda.flows.IssuerFlow
import net.corda.node.driver.driver
import net.corda.node.services.User import net.corda.node.services.User
import net.corda.node.services.messaging.CordaRPCClient
import net.corda.node.services.startFlowPermission import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.testing.node.NodeBasedTest
import org.junit.Test import org.junit.Test
class TraderDemoTest { class TraderDemoTest : NodeBasedTest() {
@Test fun `runs trader demo`() { @Test
driver(isDebug = true) { fun `runs trader demo`() {
val permissions = setOf( val permissions = setOf(
startFlowPermission<IssuerFlow.IssuanceRequester>(), startFlowPermission<IssuerFlow.IssuanceRequester>(),
startFlowPermission<net.corda.traderdemo.flow.SellerFlow>()) startFlowPermission<net.corda.traderdemo.flow.SellerFlow>())
val demoUser = listOf(User("demo", "demo", permissions)) val demoUser = listOf(User("demo", "demo", permissions))
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>())) val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
val (nodeA, nodeB) = Futures.allAsList( val (nodeA, nodeB) = Futures.allAsList(
startNode("Bank A", rpcUsers = demoUser), startNode("Bank A", rpcUsers = demoUser),
startNode("Bank B", rpcUsers = demoUser), startNode("Bank B", rpcUsers = demoUser),
startNode("BankOfCorda", rpcUsers = listOf(user)), startNode("BankOfCorda", rpcUsers = listOf(user)),
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type))) startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
).getOrThrow() ).getOrThrow()
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB)
.map { it.rpcClientToNode().start(demoUser[0].username, demoUser[0].password).proxy() }
assert(TraderDemoClientApi(nodeARpc).runBuyer()) val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map {
assert(TraderDemoClientApi(nodeBRpc).runSeller(counterparty = nodeA.nodeInfo.legalIdentity.name)) val client = CordaRPCClient(it.configuration.artemisAddress, it.configuration)
client.start(demoUser[0].username, demoUser[0].password).proxy()
} }
TraderDemoClientApi(nodeARpc).runBuyer()
TraderDemoClientApi(nodeBRpc).runSeller(counterparty = nodeA.info.legalIdentity.name)
} }
} }

View File

@ -24,7 +24,7 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) {
val logger = loggerFor<TraderDemoClientApi>() val logger = loggerFor<TraderDemoClientApi>()
} }
fun runBuyer(amount: Amount<Currency> = 30000.0.DOLLARS): Boolean { fun runBuyer(amount: Amount<Currency> = 30000.0.DOLLARS) {
val bankOfCordaParty = rpc.partyFromName(BOC.name) val bankOfCordaParty = rpc.partyFromName(BOC.name)
?: throw Exception("Unable to locate ${BOC.name} in Network Map Service") ?: throw Exception("Unable to locate ${BOC.name} in Network Map Service")
val me = rpc.nodeIdentity() val me = rpc.nodeIdentity()
@ -35,31 +35,25 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) {
} }
Futures.allAsList(resultFutures).getOrThrow() Futures.allAsList(resultFutures).getOrThrow()
return true
} }
fun runSeller(amount: Amount<Currency> = 1000.0.DOLLARS, counterparty: String): Boolean { fun runSeller(amount: Amount<Currency> = 1000.0.DOLLARS, counterparty: String) {
val otherParty = rpc.partyFromName(counterparty) val otherParty = rpc.partyFromName(counterparty) ?: throw IllegalStateException("Don't know $counterparty")
if (otherParty != null) { // The seller will sell some commercial paper to the buyer, who will pay with (self issued) cash.
// The seller will sell some commercial paper to the buyer, who will pay with (self issued) cash. //
// // The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an
// The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an // attachment. Make sure we have the transaction prospectus attachment loaded into our store.
// attachment. Make sure we have the transaction prospectus attachment loaded into our store. //
// // This can also be done via an HTTP upload, but here we short-circuit and do it from code.
// This can also be done via an HTTP upload, but here we short-circuit and do it from code. if (!rpc.attachmentExists(SellerFlow.PROSPECTUS_HASH)) {
if (!rpc.attachmentExists(SellerFlow.PROSPECTUS_HASH)) { javaClass.classLoader.getResourceAsStream("bank-of-london-cp.jar").use {
javaClass.classLoader.getResourceAsStream("bank-of-london-cp.jar").use { val id = rpc.uploadAttachment(it)
val id = rpc.uploadAttachment(it) assertEquals(SellerFlow.PROSPECTUS_HASH, id)
assertEquals(SellerFlow.PROSPECTUS_HASH, id)
}
} }
// The line below blocks and waits for the future to resolve.
val stx = rpc.startFlow(::SellerFlow, otherParty, amount).returnValue.getOrThrow()
logger.info("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(stx.tx)}")
return true
} else {
return false
} }
// The line below blocks and waits for the future to resolve.
val stx = rpc.startFlow(::SellerFlow, otherParty, amount).returnValue.getOrThrow()
logger.info("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(stx.tx)}")
} }
} }