mirror of
https://github.com/corda/corda.git
synced 2025-02-01 08:48:09 +00:00
Integrate a simple progress tracking system into the protocol framework.
The progress tracker API lets you model a tree of steps, along the same structure as protocols and subprotocols. Each step has an (optionally changing) label, and thus progress trackers can be arranged in a tree. Updates to the progress at each level flow up the tree via an RxJava observable (I guess we will use this more in future). A simple console renderer is provided that uses ANSI escapes and Emoji to show animated progress through a protocol. The trader demo is enhanced to use this framework, when run outside of Gradle.
This commit is contained in:
parent
ab2b447a5e
commit
c3f86f6557
21
build.gradle
21
build.gradle
@ -55,7 +55,9 @@ dependencies {
|
|||||||
compile "com.google.code.findbugs:jsr305:3.0.1"
|
compile "com.google.code.findbugs:jsr305:3.0.1"
|
||||||
compile "org.slf4j:slf4j-jdk14:1.7.13"
|
compile "org.slf4j:slf4j-jdk14:1.7.13"
|
||||||
|
|
||||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
compile("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version") {
|
||||||
|
force = true
|
||||||
|
}
|
||||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||||
|
|
||||||
@ -63,9 +65,11 @@ dependencies {
|
|||||||
force = true // Conflict between Quasar and Artemis
|
force = true // Conflict between Quasar and Artemis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JOpt: for command line flags.
|
||||||
compile "net.sf.jopt-simple:jopt-simple:4.9"
|
compile "net.sf.jopt-simple:jopt-simple:4.9"
|
||||||
|
|
||||||
compile("com.esotericsoftware:kryo:3.0.3")
|
// Kryo: object graph serialization.
|
||||||
|
compile "com.esotericsoftware:kryo:3.0.3"
|
||||||
compile "de.javakaffee:kryo-serializers:0.37"
|
compile "de.javakaffee:kryo-serializers:0.37"
|
||||||
|
|
||||||
// Quasar: for the bytecode rewriting for state machines.
|
// Quasar: for the bytecode rewriting for state machines.
|
||||||
@ -78,7 +82,10 @@ dependencies {
|
|||||||
}
|
}
|
||||||
compile "org.apache.activemq:artemis-core-client:${artemis_version}"
|
compile "org.apache.activemq:artemis-core-client:${artemis_version}"
|
||||||
|
|
||||||
// For visualisation
|
// JAnsi: for drawing things to the terminal in nicely coloured ways.
|
||||||
|
compile "org.fusesource.jansi:jansi:1.11"
|
||||||
|
|
||||||
|
// GraphStream: For visualisation
|
||||||
compile "org.graphstream:gs-core:1.3"
|
compile "org.graphstream:gs-core:1.3"
|
||||||
compile "org.graphstream:gs-ui:1.3"
|
compile "org.graphstream:gs-ui:1.3"
|
||||||
compile("com.intellij:forms_rt:7.0.3") {
|
compile("com.intellij:forms_rt:7.0.3") {
|
||||||
@ -91,6 +98,10 @@ dependencies {
|
|||||||
// slower but you get a much better error message if you forget to annotate a method with @Suspendable that needs it.
|
// slower but you get a much better error message if you forget to annotate a method with @Suspendable that needs it.
|
||||||
//
|
//
|
||||||
// In Java 9 (hopefully) the requirement to annotate methods as @Suspendable will go away.
|
// In Java 9 (hopefully) the requirement to annotate methods as @Suspendable will go away.
|
||||||
|
|
||||||
|
applicationDefaultJvmArgs = ["-javaagent:${configurations.quasar.singleFile}"]
|
||||||
|
mainClassName = 'core.node.TraderDemoKt'
|
||||||
|
|
||||||
tasks.withType(Test) {
|
tasks.withType(Test) {
|
||||||
jvmArgs "-javaagent:${configurations.quasar.singleFile}"
|
jvmArgs "-javaagent:${configurations.quasar.singleFile}"
|
||||||
jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation"
|
jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation"
|
||||||
@ -100,7 +111,6 @@ tasks.withType(JavaExec) {
|
|||||||
jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation"
|
jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation"
|
||||||
}
|
}
|
||||||
|
|
||||||
mainClassName = 'core.node.TraderDemoKt'
|
|
||||||
|
|
||||||
task runDemoBuyer(type: JavaExec, dependsOn: ':classes') {
|
task runDemoBuyer(type: JavaExec, dependsOn: ':classes') {
|
||||||
classpath = sourceSets.main.runtimeClasspath
|
classpath = sourceSets.main.runtimeClasspath
|
||||||
@ -111,6 +121,5 @@ task runDemoBuyer(type: JavaExec, dependsOn: ':classes') {
|
|||||||
task runDemoSeller(type: JavaExec, dependsOn: ':classes') {
|
task runDemoSeller(type: JavaExec, dependsOn: ':classes') {
|
||||||
classpath = sourceSets.main.runtimeClasspath
|
classpath = sourceSets.main.runtimeClasspath
|
||||||
main = 'core.node.TraderDemoKt'
|
main = 'core.node.TraderDemoKt'
|
||||||
args = ['--dir=seller', '--fake-trade-with=localhost', '--network-address=localhost:31338',
|
args = ['--dir=seller', '--fake-trade-with=localhost', '--network-address=localhost:31338', '--timestamper-identity-file=buyer/identity-public', '--timestamper-address=localhost']
|
||||||
'--timestamper-identity-file=buyer/identity-public', '--timestamper-address=localhost']
|
|
||||||
}
|
}
|
||||||
|
@ -26,13 +26,20 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
compile 'junit:junit:4.12'
|
||||||
|
|
||||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||||
|
|
||||||
|
// SLF4J: Logging framework.
|
||||||
compile "org.slf4j:slf4j-jdk14:1.7.13"
|
compile "org.slf4j:slf4j-jdk14:1.7.13"
|
||||||
|
|
||||||
compile("com.google.guava:guava:19.0")
|
// Guava: Google utilities library.
|
||||||
|
compile "com.google.guava:guava:19.0"
|
||||||
|
|
||||||
|
// RxJava: observable streams of events.
|
||||||
|
compile "io.reactivex:rxkotlin:0.40.1"
|
||||||
|
|
||||||
// Quasar: for the bytecode rewriting for state machines.
|
// Quasar: for the bytecode rewriting for state machines.
|
||||||
compile("co.paralleluniverse:quasar-core:${quasar_version}:jdk8")
|
compile("co.paralleluniverse:quasar-core:${quasar_version}:jdk8")
|
||||||
|
170
core/src/main/kotlin/core/utilities/ProgressTracker.kt
Normal file
170
core/src/main/kotlin/core/utilities/ProgressTracker.kt
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||||
|
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||||
|
* set forth therein.
|
||||||
|
*
|
||||||
|
* All other rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package core.utilities
|
||||||
|
|
||||||
|
import core.TransientProperty
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Subscription
|
||||||
|
import rx.lang.kotlin.BehaviourSubject
|
||||||
|
import rx.subjects.BehaviorSubject
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
// TODO: Expose the concept of errors.
|
||||||
|
// TODO: It'd be helpful if this class was at least partly thread safe.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A progress tracker helps surface information about the progress of an operation to a user interface or API of some
|
||||||
|
* kind. It lets you define a set of _steps_ that represent an operation. A step is represented by an object (typically
|
||||||
|
* a singleton).
|
||||||
|
*
|
||||||
|
* Steps may logically be children of other steps, which models the case where a large top level operation involves
|
||||||
|
* sub-operations which may also have a notion of progress. If a step has children, then the tracker will report the
|
||||||
|
* steps children as the "next step" after the parent. In other words, a parent step is considered to involve actual
|
||||||
|
* reportable work and is a thing. If the parent step simply groups other steps, then you'll have to step over it
|
||||||
|
* manually.
|
||||||
|
*
|
||||||
|
* Each step has a label. It is assumed by default that the label does not change. If you want a label to change, then
|
||||||
|
* you can emit a [ProgressTracker.Change.Rendering] object on the [ProgressTracker.Step.changes] observable stream
|
||||||
|
* after it changes. That object will propagate through to the top level trackers [changes] stream, which renderers can
|
||||||
|
* subscribe to in order to learn about progress.
|
||||||
|
*
|
||||||
|
* An operation can move both forwards and backwards through steps, thus, a [ProgressTracker] can represent operations
|
||||||
|
* that include loops.
|
||||||
|
*
|
||||||
|
* A progress tracker is *not* thread safe. You may move events from the thread making progress to another thread by
|
||||||
|
* using the [Observable] subscribeOn call.
|
||||||
|
*/
|
||||||
|
class ProgressTracker(vararg steps: Step) {
|
||||||
|
sealed class Change {
|
||||||
|
class Position(val newStep: Step) : Change()
|
||||||
|
class Rendering(val ofStep: Step) : Change()
|
||||||
|
class Structural(val parent: Step) : Change()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The superclass of all step objects. */
|
||||||
|
open class Step(open val label: String) {
|
||||||
|
open val changes: Observable<Change> = Observable.empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This class makes it easier to relabel a step on the fly, to provide transient information. */
|
||||||
|
open class RelabelableStep(currentLabel: String) : Step(currentLabel) {
|
||||||
|
override val changes = BehaviourSubject<Change>()
|
||||||
|
|
||||||
|
var currentLabel: String = currentLabel
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
changes.onNext(ProgressTracker.Change.Rendering(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
override val label: String get() = currentLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
object UNSTARTED : Step("Unstarted")
|
||||||
|
object DONE : Step("Done")
|
||||||
|
|
||||||
|
/** The steps in this tracker, same as the steps passed to the constructor but with UNSTARTED and DONE inserted. */
|
||||||
|
val steps = arrayOf(UNSTARTED, *steps, DONE)
|
||||||
|
|
||||||
|
/** The zero-based index of the current step in the [steps] array (i.e. with UNSTARTED and DONE) */
|
||||||
|
var stepIndex: Int = 0
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reading returns the value of steps[stepIndex], writing moves the position of the current tracker. Once moved to
|
||||||
|
* the [DONE] state, this tracker is finished and the current step cannot be moved again.
|
||||||
|
*/
|
||||||
|
var currentStep: Step
|
||||||
|
get() = steps[stepIndex]
|
||||||
|
set(value) {
|
||||||
|
if (currentStep != value) {
|
||||||
|
check(currentStep != DONE) { "Cannot rewind a progress tracker once it reaches the done state" }
|
||||||
|
|
||||||
|
val index = steps.indexOf(value)
|
||||||
|
require(index != -1)
|
||||||
|
|
||||||
|
if (index < stepIndex) {
|
||||||
|
// We are going backwards: unlink and unsubscribe from any child nodes that we're rolling back
|
||||||
|
// through, in preparation for moving through them again.
|
||||||
|
for (i in stepIndex downTo index) {
|
||||||
|
childrenFor.remove(steps[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
curChangeSubscription?.unsubscribe()
|
||||||
|
stepIndex = index
|
||||||
|
_changes.onNext(Change.Position(steps[index]))
|
||||||
|
curChangeSubscription = currentStep.changes.subscribe { _changes.onNext(it) }
|
||||||
|
|
||||||
|
if (currentStep == DONE) _changes.onCompleted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the current step, descending into children to find the deepest step we are up to. */
|
||||||
|
val currentStepRecursive: Step
|
||||||
|
get() = childrenFor[currentStep]?.currentStepRecursive ?: currentStep
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writable map that lets you insert child [ProgressTracker]s for particular steps. It's OK to edit this even
|
||||||
|
* after a progress tracker has been started.
|
||||||
|
*/
|
||||||
|
var childrenFor = object : HashMap<Step, ProgressTracker>() {
|
||||||
|
override fun put(key: Step, value: ProgressTracker): ProgressTracker? {
|
||||||
|
val r = super.put(key, value)
|
||||||
|
childSubscriptions[value] = value.changes.subscribe({ _changes.onNext(it) }, { _changes.onError(it) })
|
||||||
|
_changes.onNext(Change.Structural(key))
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(key: Step): ProgressTracker? {
|
||||||
|
if (containsKey(key))
|
||||||
|
childSubscriptions[this[key]]?.let { it.unsubscribe(); childSubscriptions.remove(this[key]) }
|
||||||
|
_changes.onNext(Change.Structural(key))
|
||||||
|
return super.remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val childSubscriptions = HashMap<ProgressTracker, Subscription>()
|
||||||
|
|
||||||
|
private fun _allSteps(level: Int = 0): List<Pair<Int, Step>> {
|
||||||
|
val result = ArrayList<Pair<Int, Step>>()
|
||||||
|
for (step in steps) {
|
||||||
|
if (step == UNSTARTED) continue
|
||||||
|
if (level > 0 && step == DONE) continue
|
||||||
|
result += Pair(level, step)
|
||||||
|
childrenFor[step]?.let { result += it._allSteps(level + 1) }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of all steps in this ProgressTracker and the children, with the indent level provided starting at zero.
|
||||||
|
* Note that UNSTARTED is never counted, and DONE is only counted at the calling level.
|
||||||
|
*/
|
||||||
|
val allSteps: List<Pair<Int, Step>> get() = _allSteps()
|
||||||
|
|
||||||
|
private var curChangeSubscription: Subscription? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates the progress tracker. If the current step has a child, the child is iterated instead (recursively).
|
||||||
|
* Returns the latest step at the bottom of the step tree.
|
||||||
|
*/
|
||||||
|
fun nextStep(): Step {
|
||||||
|
currentStep = steps[steps.indexOf(currentStep) + 1]
|
||||||
|
return currentStep
|
||||||
|
}
|
||||||
|
|
||||||
|
// These two fields won't be serialized.
|
||||||
|
private val _changes by TransientProperty { BehaviorSubject.create<Change>(Change.Position(UNSTARTED)) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An observable stream of changes: includes child steps, resets and any changes emitted by individual steps (e.g.
|
||||||
|
* if a step changed its label or rendering).
|
||||||
|
*/
|
||||||
|
val changes: Observable<Change> get() = _changes
|
||||||
|
}
|
91
core/src/test/kotlin/core/utilities/ProgressTrackerTest.kt
Normal file
91
core/src/test/kotlin/core/utilities/ProgressTrackerTest.kt
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||||
|
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||||
|
* set forth therein.
|
||||||
|
*
|
||||||
|
* All other rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package core.utilities
|
||||||
|
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFails
|
||||||
|
|
||||||
|
class ProgressTrackerTest {
|
||||||
|
object SimpleSteps {
|
||||||
|
object ONE : ProgressTracker.Step("one")
|
||||||
|
object TWO : ProgressTracker.Step("two")
|
||||||
|
object THREE : ProgressTracker.Step("three")
|
||||||
|
object FOUR : ProgressTracker.Step("four")
|
||||||
|
|
||||||
|
fun tracker() = ProgressTracker(ONE, TWO, THREE, FOUR)
|
||||||
|
}
|
||||||
|
object ChildSteps {
|
||||||
|
object AYY : ProgressTracker.Step("ayy")
|
||||||
|
object BEE : ProgressTracker.Step("bee")
|
||||||
|
object SEA : ProgressTracker.Step("sea")
|
||||||
|
|
||||||
|
fun tracker() = ProgressTracker(AYY, BEE, SEA)
|
||||||
|
}
|
||||||
|
|
||||||
|
lateinit var pt: ProgressTracker
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun before() {
|
||||||
|
pt = SimpleSteps.tracker()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check basic steps`() {
|
||||||
|
assertEquals(ProgressTracker.UNSTARTED, pt.currentStep)
|
||||||
|
assertEquals(0, pt.stepIndex)
|
||||||
|
var stepNotification: ProgressTracker.Step? = null
|
||||||
|
pt.changes.subscribe { stepNotification = (it as? ProgressTracker.Change.Position)?.newStep }
|
||||||
|
|
||||||
|
assertEquals(SimpleSteps.ONE, pt.nextStep())
|
||||||
|
assertEquals(1, pt.stepIndex)
|
||||||
|
assertEquals(SimpleSteps.ONE, stepNotification)
|
||||||
|
|
||||||
|
assertEquals(SimpleSteps.TWO, pt.nextStep())
|
||||||
|
assertEquals(SimpleSteps.THREE, pt.nextStep())
|
||||||
|
assertEquals(SimpleSteps.FOUR, pt.nextStep())
|
||||||
|
assertEquals(ProgressTracker.DONE, pt.nextStep())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cannot go beyond end`() {
|
||||||
|
pt.currentStep = SimpleSteps.FOUR
|
||||||
|
assertFails { pt.nextStep() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `nested children are stepped correctly`() {
|
||||||
|
val pt2 = ChildSteps.tracker()
|
||||||
|
|
||||||
|
val stepNotification = LinkedList<ProgressTracker.Change>()
|
||||||
|
pt.changes.subscribe {
|
||||||
|
stepNotification += it
|
||||||
|
}
|
||||||
|
|
||||||
|
pt.currentStep = SimpleSteps.ONE
|
||||||
|
pt.childrenFor[SimpleSteps.TWO] = pt2
|
||||||
|
pt.nextStep()
|
||||||
|
|
||||||
|
assertEquals(ChildSteps.AYY, pt.nextStep())
|
||||||
|
assertEquals(ChildSteps.AYY, (stepNotification.pollFirst() as ProgressTracker.Change.Position).newStep)
|
||||||
|
assertEquals(SimpleSteps.TWO, (stepNotification.pollFirst() as ProgressTracker.Change.Structural).parent)
|
||||||
|
assertEquals(ChildSteps.BEE, pt2.nextStep())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `can be rewound`() {
|
||||||
|
val pt2 = ChildSteps.tracker()
|
||||||
|
pt.childrenFor[SimpleSteps.TWO] = pt2
|
||||||
|
repeat(4) { pt.nextStep() }
|
||||||
|
pt.currentStep = SimpleSteps.ONE
|
||||||
|
assertEquals(SimpleSteps.TWO, pt.nextStep())
|
||||||
|
}
|
||||||
|
}
|
@ -511,3 +511,52 @@ the fact that it takes minimal resources and can survive node restarts.
|
|||||||
this problem doesn't occur. It's also restored for you when a protocol state machine is restored after a node
|
this problem doesn't occur. It's also restored for you when a protocol state machine is restored after a node
|
||||||
restart.
|
restart.
|
||||||
|
|
||||||
|
Progress tracking
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Not shown in the code snippets above is the usage of the ``ProgressTracker`` API. Progress tracking exports information
|
||||||
|
from a protocol about where it's got up to in such a way that observers can render it in a useful manner to humans who
|
||||||
|
may need to be informed. It may be rendered via an API, in a GUI, onto a terminal window, etc.
|
||||||
|
|
||||||
|
A ``ProgressTracker`` is constructed with a series of ``Step`` objects, where each step is an object representing a
|
||||||
|
stage in a piece of work. It is therefore typical to use singletons that subclass ``Step``, which may be defined easily
|
||||||
|
in one line when using Kotlin. Typical steps might be "Waiting for response from peer", "Waiting for signature to be
|
||||||
|
approved", "Downloading and verifying data" etc.
|
||||||
|
|
||||||
|
Each step exposes a label. By default labels are fixed, but by subclassing ``RelabelableStep``
|
||||||
|
you can make a step that can update its label on the fly. That's useful for steps that want to expose non-structured
|
||||||
|
progress information like the current file being downloaded. By defining your own step types, you can export progress
|
||||||
|
in a way that's both human readable and machine readable.
|
||||||
|
|
||||||
|
Progress trackers are hierarchical. Each step can be the parent for another tracker. By altering the
|
||||||
|
``ProgressTracker.childrenFor[step] = tracker`` map, a tree of steps can be created. It's allowed to alter the hierarchy
|
||||||
|
at runtime, on the fly, and the progress renderers will adapt to that properly. This can be helpful when you don't
|
||||||
|
fully know ahead of time what steps will be required. If you _do_ know what is required, configuring as much of the
|
||||||
|
hierarchy ahead of time is a good idea, as that will help the users see what is coming up.
|
||||||
|
|
||||||
|
Every tracker has not only the steps given to it at construction time, but also the singleton
|
||||||
|
``ProgressTracker.UNSTARTED`` step and the ``ProgressTracker.DONE`` step. Once a tracker has become ``DONE`` its
|
||||||
|
position may not be modified again (because e.g. the UI may have been removed/cleaned up), but until that point, the
|
||||||
|
position can be set to any arbitrary set both forwards and backwards. Steps may be skipped, repeated, etc. Note that
|
||||||
|
rolling the current step backwards will delete any progress trackers that are children of the steps being reversed, on
|
||||||
|
the assumption that those subtasks will have to be repeated.
|
||||||
|
|
||||||
|
Trackers provide an `Rx observable <http://reactivex.io/>`_ which streams changes to the hierarchy. The top level
|
||||||
|
observable exposes all the events generated by its children as well. The changes are represented by objects indicating
|
||||||
|
whether the change is one of position (i.e. progress), structure (i.e. new subtasks being added/removed) or some other
|
||||||
|
aspect of rendering (i.e. a step has changed in some way and is requesting a re-render).
|
||||||
|
|
||||||
|
The protocol framework is somewhat integrated with this API. Each ``ProtocolLogic`` may optionally provide a tracker by
|
||||||
|
overriding the ``protocolTracker`` property (``getProtocolTracker`` method in Java). If the
|
||||||
|
``ProtocolLogic.subProtocol`` method is used, then the tracker of the sub-protocol will be made a child of the current
|
||||||
|
step in the parent protocol automatically, if the parent is using tracking in the first place. The framework will also
|
||||||
|
automatically set the current step to ``DONE`` for you, when the protocol is finished.
|
||||||
|
|
||||||
|
Because a protocol may sometimes wish to configure the children in its progress hierarchy _before_ the sub-protocol
|
||||||
|
is constructed, for sub-protocols that always follow the same outline regardless of their parameters it's conventional
|
||||||
|
to define a companion object/static method (for Kotlin/Java respectively) that constructs a tracker, and then allow
|
||||||
|
the sub-protocol to have the tracker it will use be passed in as a parameter. This allows all trackers to be built
|
||||||
|
and linked ahead of time.
|
||||||
|
|
||||||
|
In future, the progress tracking framework will become a vital part of how exceptions, errors, and other faults are
|
||||||
|
surfaced to human operators for investigation and resolution.
|
@ -16,10 +16,11 @@ import core.*
|
|||||||
import core.crypto.DigitalSignature
|
import core.crypto.DigitalSignature
|
||||||
import core.crypto.signWithECDSA
|
import core.crypto.signWithECDSA
|
||||||
import core.messaging.LegallyIdentifiableNode
|
import core.messaging.LegallyIdentifiableNode
|
||||||
import core.protocols.ProtocolLogic
|
|
||||||
import core.messaging.SingleMessageRecipient
|
import core.messaging.SingleMessageRecipient
|
||||||
import core.messaging.StateMachineManager
|
import core.messaging.StateMachineManager
|
||||||
import core.node.TimestampingProtocol
|
import core.node.TimestampingProtocol
|
||||||
|
import core.protocols.ProtocolLogic
|
||||||
|
import core.utilities.ProgressTracker
|
||||||
import core.utilities.trace
|
import core.utilities.trace
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -86,22 +87,40 @@ object TwoPartyTradeProtocol {
|
|||||||
val assetToSell: StateAndRef<OwnableState>,
|
val assetToSell: StateAndRef<OwnableState>,
|
||||||
val price: Amount,
|
val price: Amount,
|
||||||
val myKeyPair: KeyPair,
|
val myKeyPair: KeyPair,
|
||||||
val buyerSessionID: Long) : ProtocolLogic<SignedTransaction>() {
|
val buyerSessionID: Long,
|
||||||
|
override val progressTracker: ProgressTracker = Seller.tracker()) : ProtocolLogic<SignedTransaction>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
object AWAITING_PROPOSAL : ProgressTracker.Step("Awaiting transaction proposal")
|
||||||
|
object VERIFYING : ProgressTracker.Step("Verifying transaction proposal")
|
||||||
|
object SIGNING : ProgressTracker.Step("Signing transaction")
|
||||||
|
object TIMESTAMPING : ProgressTracker.Step("Timestamping transaction")
|
||||||
|
object SENDING_SIGS : ProgressTracker.Step("Sending transaction signatures to buyer")
|
||||||
|
|
||||||
|
fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, TIMESTAMPING, SENDING_SIGS)
|
||||||
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
val partialTX: SignedTransaction = receiveAndCheckProposedTransaction()
|
val partialTX: SignedTransaction = receiveAndCheckProposedTransaction()
|
||||||
|
|
||||||
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
|
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
|
||||||
val ourSignature = signWithOurKey(partialTX)
|
val ourSignature = signWithOurKey(partialTX)
|
||||||
val tsaSig = subProtocol(TimestampingProtocol(timestampingAuthority, partialTX.txBits))
|
val tsaSig = timestamp(partialTX)
|
||||||
|
|
||||||
val signedTransaction = sendSignatures(partialTX, ourSignature, tsaSig)
|
return sendSignatures(partialTX, ourSignature, tsaSig)
|
||||||
|
}
|
||||||
|
|
||||||
return signedTransaction
|
@Suspendable
|
||||||
|
private fun timestamp(partialTX: SignedTransaction): DigitalSignature.LegallyIdentifiable {
|
||||||
|
progressTracker.currentStep = TIMESTAMPING
|
||||||
|
return subProtocol(TimestampingProtocol(timestampingAuthority, partialTX.txBits))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun receiveAndCheckProposedTransaction(): SignedTransaction {
|
private fun receiveAndCheckProposedTransaction(): SignedTransaction {
|
||||||
|
progressTracker.currentStep = AWAITING_PROPOSAL
|
||||||
|
|
||||||
val sessionID = random63BitValue()
|
val sessionID = random63BitValue()
|
||||||
|
|
||||||
// Make the first message we'll send to kick off the protocol.
|
// Make the first message we'll send to kick off the protocol.
|
||||||
@ -109,7 +128,11 @@ object TwoPartyTradeProtocol {
|
|||||||
|
|
||||||
val maybeSTX = sendAndReceive<SignedTransaction>(TRADE_TOPIC, otherSide, buyerSessionID, sessionID, hello)
|
val maybeSTX = sendAndReceive<SignedTransaction>(TRADE_TOPIC, otherSide, buyerSessionID, sessionID, hello)
|
||||||
|
|
||||||
|
progressTracker.currentStep = VERIFYING
|
||||||
|
|
||||||
maybeSTX.validate {
|
maybeSTX.validate {
|
||||||
|
progressTracker.nextStep()
|
||||||
|
|
||||||
// Check that the tx proposed by the buyer is valid.
|
// Check that the tx proposed by the buyer is valid.
|
||||||
val missingSigs = it.verify(throwIfSignaturesAreMissing = false)
|
val missingSigs = it.verify(throwIfSignaturesAreMissing = false)
|
||||||
if (missingSigs != setOf(myKeyPair.public, timestampingAuthority.identity.owningKey))
|
if (missingSigs != setOf(myKeyPair.public, timestampingAuthority.identity.owningKey))
|
||||||
@ -148,11 +171,16 @@ object TwoPartyTradeProtocol {
|
|||||||
subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide))
|
subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun signWithOurKey(partialTX: SignedTransaction) = myKeyPair.signWithECDSA(partialTX.txBits)
|
@Suspendable
|
||||||
|
open fun signWithOurKey(partialTX: SignedTransaction): DigitalSignature.WithKey {
|
||||||
|
progressTracker.currentStep = SIGNING
|
||||||
|
return myKeyPair.signWithECDSA(partialTX.txBits)
|
||||||
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun sendSignatures(partialTX: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
private fun sendSignatures(partialTX: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
||||||
tsaSig: DigitalSignature.LegallyIdentifiable): SignedTransaction {
|
tsaSig: DigitalSignature.LegallyIdentifiable): SignedTransaction {
|
||||||
|
progressTracker.currentStep = SENDING_SIGS
|
||||||
val fullySigned = partialTX + tsaSig + ourSignature
|
val fullySigned = partialTX + tsaSig + ourSignature
|
||||||
|
|
||||||
logger.trace { "Built finished transaction, sending back to secondary!" }
|
logger.trace { "Built finished transaction, sending back to secondary!" }
|
||||||
@ -168,14 +196,24 @@ object TwoPartyTradeProtocol {
|
|||||||
val typeToBuy: Class<out OwnableState>,
|
val typeToBuy: Class<out OwnableState>,
|
||||||
val sessionID: Long) : ProtocolLogic<SignedTransaction>() {
|
val sessionID: Long) : ProtocolLogic<SignedTransaction>() {
|
||||||
|
|
||||||
|
object RECEIVING : ProgressTracker.Step("Waiting for seller trading info")
|
||||||
|
object VERIFYING : ProgressTracker.Step("Verifying seller assets")
|
||||||
|
object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal")
|
||||||
|
object SWAPPING_SIGNATURES : ProgressTracker.Step("Swapping signatures with the seller")
|
||||||
|
|
||||||
|
override val progressTracker = ProgressTracker(RECEIVING, VERIFYING, SIGNING, SWAPPING_SIGNATURES)
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
val tradeRequest = receiveAndValidateTradeRequest()
|
val tradeRequest = receiveAndValidateTradeRequest()
|
||||||
|
|
||||||
|
progressTracker.currentStep = SIGNING
|
||||||
val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest)
|
val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest)
|
||||||
val stx = signWithOurKeys(cashSigningPubKeys, ptx)
|
val stx = signWithOurKeys(cashSigningPubKeys, ptx)
|
||||||
|
|
||||||
val signatures = swapSignaturesWithSeller(stx, tradeRequest.sessionID)
|
val signatures = swapSignaturesWithSeller(stx, tradeRequest.sessionID)
|
||||||
|
|
||||||
logger.trace { "Got signatures from seller, verifying ... "}
|
logger.trace { "Got signatures from seller, verifying ... " }
|
||||||
val fullySigned = stx + signatures.timestampAuthoritySig + signatures.sellerSig
|
val fullySigned = stx + signatures.timestampAuthoritySig + signatures.sellerSig
|
||||||
fullySigned.verify()
|
fullySigned.verify()
|
||||||
|
|
||||||
@ -185,9 +223,11 @@ object TwoPartyTradeProtocol {
|
|||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun receiveAndValidateTradeRequest(): SellerTradeInfo {
|
private fun receiveAndValidateTradeRequest(): SellerTradeInfo {
|
||||||
|
progressTracker.currentStep = RECEIVING
|
||||||
// Wait for a trade request to come in on our pre-provided session ID.
|
// Wait for a trade request to come in on our pre-provided session ID.
|
||||||
val maybeTradeRequest = receive<SellerTradeInfo>(TRADE_TOPIC, sessionID)
|
val maybeTradeRequest = receive<SellerTradeInfo>(TRADE_TOPIC, sessionID)
|
||||||
|
|
||||||
|
progressTracker.currentStep = VERIFYING
|
||||||
maybeTradeRequest.validate {
|
maybeTradeRequest.validate {
|
||||||
// What is the seller trying to sell us?
|
// What is the seller trying to sell us?
|
||||||
val asset = it.assetForSale.state
|
val asset = it.assetForSale.state
|
||||||
@ -211,6 +251,7 @@ object TwoPartyTradeProtocol {
|
|||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun swapSignaturesWithSeller(stx: SignedTransaction, theirSessionID: Long): SignaturesFromSeller {
|
private fun swapSignaturesWithSeller(stx: SignedTransaction, theirSessionID: Long): SignaturesFromSeller {
|
||||||
|
progressTracker.currentStep = SWAPPING_SIGNATURES
|
||||||
logger.trace { "Sending partially signed transaction to seller" }
|
logger.trace { "Sending partially signed transaction to seller" }
|
||||||
|
|
||||||
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
|
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
|
||||||
|
@ -24,6 +24,7 @@ import core.serialization.THREAD_LOCAL_KRYO
|
|||||||
import core.serialization.createKryo
|
import core.serialization.createKryo
|
||||||
import core.serialization.deserialize
|
import core.serialization.deserialize
|
||||||
import core.serialization.serialize
|
import core.serialization.serialize
|
||||||
|
import core.utilities.ProgressTracker
|
||||||
import core.utilities.trace
|
import core.utilities.trace
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@ -51,6 +52,8 @@ import javax.annotation.concurrent.ThreadSafe
|
|||||||
* TODO: Timeouts
|
* TODO: Timeouts
|
||||||
* TODO: Surfacing of exceptions via an API and/or management UI
|
* TODO: Surfacing of exceptions via an API and/or management UI
|
||||||
* TODO: Ability to control checkpointing explicitly, for cases where you know replaying a message can't hurt
|
* TODO: Ability to control checkpointing explicitly, for cases where you know replaying a message can't hurt
|
||||||
|
* TODO: Make Kryo (de)serialize markers for heavy objects that are currently in the service hub. This avoids mistakes
|
||||||
|
* where services are temporarily put on the stack.
|
||||||
*/
|
*/
|
||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor) {
|
class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor) {
|
||||||
@ -65,7 +68,7 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor)
|
|||||||
// class that inserts itself into a ThreadLocal. That then gets caught in fiber serialisation, which we don't
|
// class that inserts itself into a ThreadLocal. That then gets caught in fiber serialisation, which we don't
|
||||||
// want because it can't get recreated properly. It turns out there's no good workaround for this! All the obvious
|
// want because it can't get recreated properly. It turns out there's no good workaround for this! All the obvious
|
||||||
// approaches fail. Pending resolution of https://github.com/puniverse/quasar/issues/153 we just disable
|
// approaches fail. Pending resolution of https://github.com/puniverse/quasar/issues/153 we just disable
|
||||||
// checkpointing when unit tests are run inside Gradle. The right fix is probably to make Quasar's
|
// checkpointing when unit tests are run inside Gradle. The right fix is probably to stop Quasar's
|
||||||
// bit-too-clever-for-its-own-good ThreadLocal serialisation trick. It already wasted far more time than it can
|
// bit-too-clever-for-its-own-good ThreadLocal serialisation trick. It already wasted far more time than it can
|
||||||
// ever recover.
|
// ever recover.
|
||||||
val checkpointing: Boolean get() = !System.err.javaClass.name.contains("LinePerThreadBufferingOutputStream")
|
val checkpointing: Boolean get() = !System.err.javaClass.name.contains("LinePerThreadBufferingOutputStream")
|
||||||
@ -205,6 +208,7 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor)
|
|||||||
|
|
||||||
// We're back! Check if the fiber is finished and if so, clean up.
|
// We're back! Check if the fiber is finished and if so, clean up.
|
||||||
if (psm.isTerminated) {
|
if (psm.isTerminated) {
|
||||||
|
psm.logic.progressTracker?.currentStep = ProgressTracker.DONE
|
||||||
_stateMachines.remove(psm.logic)
|
_stateMachines.remove(psm.logic)
|
||||||
checkpointsMap.remove(prevCheckpointKey)
|
checkpointsMap.remove(prevCheckpointKey)
|
||||||
}
|
}
|
||||||
|
@ -13,13 +13,16 @@ import com.google.common.net.HostAndPort
|
|||||||
import contracts.CommercialPaper
|
import contracts.CommercialPaper
|
||||||
import contracts.protocols.TwoPartyTradeProtocol
|
import contracts.protocols.TwoPartyTradeProtocol
|
||||||
import core.*
|
import core.*
|
||||||
|
import core.crypto.DigitalSignature
|
||||||
import core.crypto.generateKeyPair
|
import core.crypto.generateKeyPair
|
||||||
import core.messaging.LegallyIdentifiableNode
|
import core.messaging.LegallyIdentifiableNode
|
||||||
import core.protocols.ProtocolLogic
|
|
||||||
import core.messaging.SingleMessageRecipient
|
import core.messaging.SingleMessageRecipient
|
||||||
|
import core.protocols.ProtocolLogic
|
||||||
import core.serialization.deserialize
|
import core.serialization.deserialize
|
||||||
|
import core.utilities.ANSIProgressRenderer
|
||||||
import core.utilities.BriefLogFormatter
|
import core.utilities.BriefLogFormatter
|
||||||
import core.utilities.Emoji
|
import core.utilities.Emoji
|
||||||
|
import core.utilities.ProgressTracker
|
||||||
import joptsimple.OptionParser
|
import joptsimple.OptionParser
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
@ -33,7 +36,6 @@ import kotlin.system.exitProcess
|
|||||||
//
|
//
|
||||||
// Please see docs/build/html/running-the-trading-demo.html
|
// Please see docs/build/html/running-the-trading-demo.html
|
||||||
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
val parser = OptionParser()
|
val parser = OptionParser()
|
||||||
val networkAddressArg = parser.accepts("network-address").withRequiredArg().required()
|
val networkAddressArg = parser.accepts("network-address").withRequiredArg().required()
|
||||||
@ -60,7 +62,8 @@ fun main(args: Array<String>) {
|
|||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
BriefLogFormatter.initVerbose("platform.trade")
|
// Suppress the Artemis MQ noise, and activate the demo logging.
|
||||||
|
BriefLogFormatter.initVerbose("+demo.buyer", "+demo.seller", "-org.apache.activemq")
|
||||||
|
|
||||||
val dir = Paths.get(options.valueOf(dirArg))
|
val dir = Paths.get(options.valueOf(dirArg))
|
||||||
val configFile = dir.resolve("config")
|
val configFile = dir.resolve("config")
|
||||||
@ -90,7 +93,9 @@ fun main(args: Array<String>) {
|
|||||||
val node = logElapsedTime("Node startup") { Node(dir, myNetAddr, config, timestamperId) }
|
val node = logElapsedTime("Node startup") { Node(dir, myNetAddr, config, timestamperId) }
|
||||||
|
|
||||||
if (listening) {
|
if (listening) {
|
||||||
node.smm.add("demo.buyer", TraderDemoProtocolBuyer()).get() // This thread will halt forever here.
|
val buyer = TraderDemoProtocolBuyer()
|
||||||
|
ANSIProgressRenderer.progressTracker = buyer.progressTracker
|
||||||
|
node.smm.add("demo.buyer", buyer).get() // This thread will halt forever here.
|
||||||
} else {
|
} else {
|
||||||
if (!options.has(fakeTradeWithArg)) {
|
if (!options.has(fakeTradeWithArg)) {
|
||||||
println("Need the --fake-trade-with command line argument")
|
println("Need the --fake-trade-with command line argument")
|
||||||
@ -98,7 +103,9 @@ fun main(args: Array<String>) {
|
|||||||
}
|
}
|
||||||
val peerAddr = HostAndPort.fromString(options.valuesOf(fakeTradeWithArg).single()).withDefaultPort(Node.DEFAULT_PORT)
|
val peerAddr = HostAndPort.fromString(options.valuesOf(fakeTradeWithArg).single()).withDefaultPort(Node.DEFAULT_PORT)
|
||||||
val otherSide = ArtemisMessagingService.makeRecipient(peerAddr)
|
val otherSide = ArtemisMessagingService.makeRecipient(peerAddr)
|
||||||
node.smm.add("demo.seller", TraderDemoProtocolSeller(myNetAddr, otherSide)).get()
|
val seller = TraderDemoProtocolSeller(myNetAddr, otherSide)
|
||||||
|
ANSIProgressRenderer.progressTracker = seller.progressTracker
|
||||||
|
node.smm.add("demo.seller", seller).get()
|
||||||
node.stop()
|
node.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,6 +113,12 @@ fun main(args: Array<String>) {
|
|||||||
// We create a couple of ad-hoc test protocols that wrap the two party trade protocol, to give us the demo logic.
|
// We create a couple of ad-hoc test protocols that wrap the two party trade protocol, to give us the demo logic.
|
||||||
|
|
||||||
class TraderDemoProtocolBuyer() : ProtocolLogic<Unit>() {
|
class TraderDemoProtocolBuyer() : ProtocolLogic<Unit>() {
|
||||||
|
companion object {
|
||||||
|
object WAITING_FOR_SELLER_TO_CONNECT : ProgressTracker.Step("Waiting for seller to connect to us")
|
||||||
|
object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset")
|
||||||
|
}
|
||||||
|
override val progressTracker = ProgressTracker(WAITING_FOR_SELLER_TO_CONNECT, STARTING_BUY)
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
// Give us some cash. Note that as nodes do not currently track forward pointers, we can spend the same cash over
|
// Give us some cash. Note that as nodes do not currently track forward pointers, we can spend the same cash over
|
||||||
@ -119,15 +132,11 @@ class TraderDemoProtocolBuyer() : ProtocolLogic<Unit>() {
|
|||||||
//
|
//
|
||||||
// As the seller initiates the DVP/two-party trade protocol, here, we will be the buyer.
|
// As the seller initiates the DVP/two-party trade protocol, here, we will be the buyer.
|
||||||
try {
|
try {
|
||||||
println()
|
progressTracker.currentStep = WAITING_FOR_SELLER_TO_CONNECT
|
||||||
println("Waiting for a seller to connect to us!")
|
|
||||||
|
|
||||||
val hostname = receive<HostAndPort>("test.junktrade", 0).validate { it.withDefaultPort(Node.DEFAULT_PORT) }
|
val hostname = receive<HostAndPort>("test.junktrade", 0).validate { it.withDefaultPort(Node.DEFAULT_PORT) }
|
||||||
val newPartnerAddr = ArtemisMessagingService.makeRecipient(hostname)
|
val newPartnerAddr = ArtemisMessagingService.makeRecipient(hostname)
|
||||||
val sessionID = random63BitValue()
|
val sessionID = random63BitValue()
|
||||||
println()
|
progressTracker.currentStep = STARTING_BUY
|
||||||
println("Got a new junk trade request from $newPartnerAddr, sending back a fresh session ID and starting buy protocol")
|
|
||||||
println()
|
|
||||||
send("test.junktrade", newPartnerAddr, 0, sessionID)
|
send("test.junktrade", newPartnerAddr, 0, sessionID)
|
||||||
|
|
||||||
val tsa = serviceHub.networkMapService.timestampingNodes[0]
|
val tsa = serviceHub.networkMapService.timestampingNodes[0]
|
||||||
@ -135,52 +144,58 @@ class TraderDemoProtocolBuyer() : ProtocolLogic<Unit>() {
|
|||||||
CommercialPaper.State::class.java, sessionID)
|
CommercialPaper.State::class.java, sessionID)
|
||||||
val tradeTX: SignedTransaction = subProtocol(buyer)
|
val tradeTX: SignedTransaction = subProtocol(buyer)
|
||||||
|
|
||||||
println()
|
logger.info("Purchase complete - we are a happy customer! Final transaction is: " +
|
||||||
println("Purchase complete - we are a happy customer! Final transaction is:")
|
"\n\n${Emoji.renderIfSupported(tradeTX.tx)}")
|
||||||
println()
|
|
||||||
println(Emoji.renderIfSupported(tradeTX.tx))
|
|
||||||
println()
|
|
||||||
println("Waiting for another seller to connect. Or press Ctrl-C to shut me down.")
|
|
||||||
} catch(e: Exception) {
|
} catch(e: Exception) {
|
||||||
println()
|
logger.error("Something went wrong whilst trading!", e)
|
||||||
println("Something went wrong whilst trading!")
|
|
||||||
println()
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TraderDemoProtocolSeller(val myAddress: HostAndPort,
|
class TraderDemoProtocolSeller(val myAddress: HostAndPort,
|
||||||
val otherSide: SingleMessageRecipient) : ProtocolLogic<Unit>() {
|
val otherSide: SingleMessageRecipient,
|
||||||
|
override val progressTracker: ProgressTracker = TraderDemoProtocolSeller.tracker()) : ProtocolLogic<Unit>() {
|
||||||
|
companion object {
|
||||||
|
object ANNOUNCING : ProgressTracker.Step("Announcing to the buyer node")
|
||||||
|
object SELF_ISSUING : ProgressTracker.Step("Got session ID back, issuing and timestamping some commercial paper")
|
||||||
|
object TRADING : ProgressTracker.Step("Starting the trade protocol")
|
||||||
|
|
||||||
|
// We vend a progress tracker that already knows there's going to be a TwoPartyTradingProtocol involved at some
|
||||||
|
// point: by setting up the tracker in advance, the user can see what's coming in more detail, instead of being
|
||||||
|
// surprised when it appears as a new set of tasks below the current one.
|
||||||
|
fun tracker() = ProgressTracker(ANNOUNCING, SELF_ISSUING, TRADING).apply {
|
||||||
|
childrenFor[TRADING] = TwoPartyTradeProtocol.Seller.tracker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
println()
|
progressTracker.currentStep = ANNOUNCING
|
||||||
println("Announcing ourselves to the buyer node!")
|
|
||||||
println()
|
|
||||||
|
|
||||||
val sessionID = sendAndReceive<Long>("test.junktrade", otherSide, 0, 0, myAddress).validate { it }
|
val sessionID = sendAndReceive<Long>("test.junktrade", otherSide, 0, 0, myAddress).validate { it }
|
||||||
|
|
||||||
println()
|
progressTracker.currentStep = SELF_ISSUING
|
||||||
println("Got session ID back, issuing and timestamping some commercial paper")
|
|
||||||
|
|
||||||
val tsa = serviceHub.networkMapService.timestampingNodes[0]
|
val tsa = serviceHub.networkMapService.timestampingNodes[0]
|
||||||
val cpOwnerKey = serviceHub.keyManagementService.freshKey()
|
val cpOwnerKey = serviceHub.keyManagementService.freshKey()
|
||||||
val commercialPaper = makeFakeCommercialPaper(cpOwnerKey.public, tsa)
|
val commercialPaper = makeFakeCommercialPaper(cpOwnerKey.public, tsa)
|
||||||
|
|
||||||
println()
|
progressTracker.currentStep = TRADING
|
||||||
println("Timestamped my commercial paper issuance, starting the trade protocol.")
|
|
||||||
|
|
||||||
val seller = TwoPartyTradeProtocol.Seller(otherSide, tsa, commercialPaper, 1000.DOLLARS, cpOwnerKey, sessionID)
|
val seller = object : TwoPartyTradeProtocol.Seller(otherSide, tsa, commercialPaper, 1000.DOLLARS,
|
||||||
|
cpOwnerKey, sessionID, progressTracker.childrenFor[TRADING]!!) {
|
||||||
|
override fun signWithOurKey(partialTX: SignedTransaction): DigitalSignature.WithKey {
|
||||||
|
val s = super.signWithOurKey(partialTX)
|
||||||
|
// Fake delay to make it look like we're doing something more intensive than we really are, to show
|
||||||
|
// the progress tracking framework.
|
||||||
|
Thread.sleep(2000)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
val tradeTX: SignedTransaction = subProtocol(seller)
|
val tradeTX: SignedTransaction = subProtocol(seller)
|
||||||
|
|
||||||
println()
|
logger.info("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(tradeTX.tx)}")
|
||||||
println("Sale completed - we have a happy customer!")
|
|
||||||
println()
|
|
||||||
println("Final transaction is")
|
|
||||||
println()
|
|
||||||
println(Emoji.renderIfSupported(tradeTX.tx))
|
|
||||||
println()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
|
@ -11,6 +11,7 @@ package core.protocols
|
|||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import core.ServiceHub
|
import core.ServiceHub
|
||||||
import core.messaging.MessageRecipients
|
import core.messaging.MessageRecipients
|
||||||
|
import core.utilities.ProgressTracker
|
||||||
import core.utilities.UntrustworthyData
|
import core.utilities.UntrustworthyData
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ abstract class ProtocolLogic<T> {
|
|||||||
|
|
||||||
/** This is where you should log things to. */
|
/** This is where you should log things to. */
|
||||||
val logger: Logger get() = psm.logger
|
val logger: Logger get() = psm.logger
|
||||||
|
|
||||||
/** Provides access to big, heavy classes that may be reconstructed from time to time, e.g. across restarts */
|
/** Provides access to big, heavy classes that may be reconstructed from time to time, e.g. across restarts */
|
||||||
val serviceHub: ServiceHub get() = psm.serviceHub
|
val serviceHub: ServiceHub get() = psm.serviceHub
|
||||||
|
|
||||||
@ -58,9 +60,32 @@ abstract class ProtocolLogic<T> {
|
|||||||
*/
|
*/
|
||||||
@Suspendable fun <R> subProtocol(subLogic: ProtocolLogic<R>): R {
|
@Suspendable fun <R> subProtocol(subLogic: ProtocolLogic<R>): R {
|
||||||
subLogic.psm = psm
|
subLogic.psm = psm
|
||||||
return subLogic.call()
|
maybeWireUpProgressTracking(subLogic)
|
||||||
|
val r = subLogic.call()
|
||||||
|
// It's easy to forget this when writing protocols so we just step it to the DONE state when it completes.
|
||||||
|
subLogic.progressTracker?.currentStep = ProgressTracker.DONE
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun maybeWireUpProgressTracking(subLogic: ProtocolLogic<*>) {
|
||||||
|
val ours = progressTracker
|
||||||
|
val theirs = subLogic.progressTracker
|
||||||
|
if (ours != null && theirs != null)
|
||||||
|
ours.childrenFor[ours.currentStep] = theirs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override this to provide a [ProgressTracker]. If one is provided and stepped, the framework will do something
|
||||||
|
* helpful with the progress reports. If this protocol is invoked as a sub-protocol of another, then the
|
||||||
|
* tracker will be made a child of the current step in the parent. If it's null, this protocol doesn't track
|
||||||
|
* progress.
|
||||||
|
*
|
||||||
|
* Note that this has to return a tracker before the protocol is invoked. You can't change your mind half way
|
||||||
|
* through.
|
||||||
|
*/
|
||||||
|
open val progressTracker: ProgressTracker? = null
|
||||||
|
|
||||||
|
/** This is where you fill out your business logic. */
|
||||||
@Suspendable
|
@Suspendable
|
||||||
abstract fun call(): T
|
abstract fun call(): T
|
||||||
}
|
}
|
157
src/main/kotlin/core/utilities/ANSIProgressRenderer.kt
Normal file
157
src/main/kotlin/core/utilities/ANSIProgressRenderer.kt
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||||
|
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||||
|
* set forth therein.
|
||||||
|
*
|
||||||
|
* All other rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package core.utilities
|
||||||
|
|
||||||
|
import org.fusesource.jansi.Ansi
|
||||||
|
import org.fusesource.jansi.AnsiConsole
|
||||||
|
import org.fusesource.jansi.AnsiOutputStream
|
||||||
|
import rx.Subscription
|
||||||
|
import java.util.logging.ConsoleHandler
|
||||||
|
import java.util.logging.Formatter
|
||||||
|
import java.util.logging.LogRecord
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Knows how to render a [ProgressTracker] to the terminal using coloured, emoji-fied output. Useful when writing small
|
||||||
|
* command line tools, demos, tests etc. Just set the [progressTracker] field and it will go ahead and start drawing
|
||||||
|
* if the terminal supports it. Otherwise it just prints out the name of the step whenever it changes.
|
||||||
|
*
|
||||||
|
* TODO: Thread safety
|
||||||
|
*/
|
||||||
|
object ANSIProgressRenderer {
|
||||||
|
private var installedYet = false
|
||||||
|
private var subscription: Subscription? = null
|
||||||
|
|
||||||
|
private class LineBumpingConsoleHandler : ConsoleHandler() {
|
||||||
|
override fun getFormatter(): Formatter = BriefLogFormatter()
|
||||||
|
|
||||||
|
override fun publish(r: LogRecord?) {
|
||||||
|
if (progressTracker != null) {
|
||||||
|
val ansi = Ansi.ansi()
|
||||||
|
repeat(prevLinesDrawn) { ansi.eraseLine().cursorUp(1).eraseLine() }
|
||||||
|
System.out.print(ansi)
|
||||||
|
System.out.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
super.publish(r)
|
||||||
|
|
||||||
|
if (progressTracker != null)
|
||||||
|
draw(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var usingANSI = false
|
||||||
|
private var loggerRef: Logger? = null
|
||||||
|
|
||||||
|
var progressTracker: ProgressTracker? = null
|
||||||
|
set(value) {
|
||||||
|
subscription?.unsubscribe()
|
||||||
|
|
||||||
|
field = value
|
||||||
|
if (!installedYet) {
|
||||||
|
AnsiConsole.systemInstall()
|
||||||
|
|
||||||
|
// This line looks weird as hell because the magic code to decide if we really have a TTY or not isn't
|
||||||
|
// actually exposed anywhere as a function (weak sauce). So we have to rely on our knowledge of jansi
|
||||||
|
// implementation details.
|
||||||
|
usingANSI = AnsiConsole.wrapOutputStream(System.out) !is AnsiOutputStream
|
||||||
|
|
||||||
|
if (usingANSI) {
|
||||||
|
loggerRef = Logger.getLogger("").apply {
|
||||||
|
val current = handlers[0]
|
||||||
|
removeHandler(current)
|
||||||
|
val new = LineBumpingConsoleHandler()
|
||||||
|
new.level = current.level
|
||||||
|
addHandler(new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
installedYet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription = value?.changes?.subscribe { draw(true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevMessagePrinted is just for non-ANSI mode.
|
||||||
|
private var prevMessagePrinted: String? = null
|
||||||
|
// prevLinesDraw is just for ANSI mode.
|
||||||
|
private var prevLinesDrawn = 0
|
||||||
|
|
||||||
|
private fun draw(moveUp: Boolean) {
|
||||||
|
val pt = progressTracker!!
|
||||||
|
|
||||||
|
if (!usingANSI) {
|
||||||
|
val currentMessage = pt.currentStepRecursive.label
|
||||||
|
if (currentMessage != prevMessagePrinted) {
|
||||||
|
println(currentMessage)
|
||||||
|
prevMessagePrinted = currentMessage
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the case where the number of steps in a progress tracker is changed during execution.
|
||||||
|
val ansi = Ansi.ansi()
|
||||||
|
if (prevLinesDrawn > 0 && moveUp)
|
||||||
|
ansi.cursorUp(prevLinesDrawn)
|
||||||
|
|
||||||
|
// Put a blank line between any logging and us.
|
||||||
|
ansi.eraseLine()
|
||||||
|
ansi.newline()
|
||||||
|
val newLinesDrawn = 1 + pt.renderLevel(ansi, 0, pt.allSteps)
|
||||||
|
if (newLinesDrawn < prevLinesDrawn) {
|
||||||
|
// If some steps were removed from the progress tracker, we don't want to leave junk hanging around below.
|
||||||
|
val linesToClear = prevLinesDrawn - newLinesDrawn
|
||||||
|
repeat(linesToClear) {
|
||||||
|
ansi.eraseLine()
|
||||||
|
ansi.newline()
|
||||||
|
}
|
||||||
|
ansi.cursorUp(linesToClear)
|
||||||
|
}
|
||||||
|
prevLinesDrawn = newLinesDrawn
|
||||||
|
|
||||||
|
// Need to force a flush here in order to ensure stderr/stdout sync up properly.
|
||||||
|
System.out.print(ansi)
|
||||||
|
System.out.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns number of lines rendered.
|
||||||
|
private fun ProgressTracker.renderLevel(ansi: Ansi, indent: Int, allSteps: List<Pair<Int, ProgressTracker.Step>>): Int {
|
||||||
|
with(ansi) {
|
||||||
|
var lines = 0
|
||||||
|
for ((index, step) in steps.withIndex()) {
|
||||||
|
// Don't bother rendering these special steps in some cases.
|
||||||
|
if (step == ProgressTracker.UNSTARTED) continue
|
||||||
|
if (indent > 0 && step == ProgressTracker.DONE) continue
|
||||||
|
|
||||||
|
val marker = when {
|
||||||
|
index < stepIndex -> Emoji.CODE_GREEN_TICK + " "
|
||||||
|
index == stepIndex && step == ProgressTracker.DONE -> Emoji.CODE_GREEN_TICK + " "
|
||||||
|
index == stepIndex -> Emoji.CODE_RIGHT_ARROW + " "
|
||||||
|
else -> " "
|
||||||
|
}
|
||||||
|
a(" ".repeat(indent))
|
||||||
|
a(marker)
|
||||||
|
|
||||||
|
val active = index == stepIndex && step != ProgressTracker.DONE
|
||||||
|
if (active) bold()
|
||||||
|
a(step.label)
|
||||||
|
if (active) boldOff()
|
||||||
|
|
||||||
|
eraseLine(Ansi.Erase.FORWARD)
|
||||||
|
newline()
|
||||||
|
lines++
|
||||||
|
|
||||||
|
val child = childrenFor[step]
|
||||||
|
if (child != null)
|
||||||
|
lines += child.renderLevel(ansi, indent + 1, allSteps)
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user