mirror of
https://github.com/corda/corda.git
synced 2025-02-21 09:51:57 +00:00
ProgressTracker emits exception thrown by the flow, allowing the ANSI renderer to correctly stop and print the error (#189)
This commit is contained in:
parent
ed093cdb9d
commit
f13817efb3
@ -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()
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user