Merge pull request #162 from corda/aslemmer-fix-netty-buffer-leak

Aslemmer fix netty buffer leak
This commit is contained in:
Andras Slemmer 2017-12-08 10:12:35 +00:00 committed by GitHub
commit c794f8418c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 258 additions and 121 deletions

View File

@ -2,19 +2,14 @@ package net.corda.flowhook
import co.paralleluniverse.fibers.Fiber import co.paralleluniverse.fibers.Fiber
import net.corda.core.internal.uncheckedCast import net.corda.core.internal.uncheckedCast
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.nodeapi.internal.persistence.DatabaseTransaction
import java.sql.Connection import java.sql.Connection
import java.time.Instant import java.time.Instant
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
/**
* This is a debugging helper class that dumps the map of Fiber->DB connection, or more precisely, the
* Fiber->(DB tx -> DB connection) map, as there may be multiple transactions per fiber.
*/
data class MonitorEvent(val type: MonitorEventType, val keys: List<Any>, val extra: Any? = null) data class MonitorEvent(val type: MonitorEventType, val keys: List<Any>, val extra: Any? = null)
@ -34,47 +29,62 @@ enum class MonitorEventType {
FiberResumed, FiberResumed,
FiberEnded, FiberEnded,
ExecuteTransition SmExecuteTransition,
SmScheduleEvent,
NettyThreadLocalMapCreated,
SetThreadLocals,
SetInheritableThreadLocals,
GetThreadLocals,
GetInheritableThreadLocals
} }
/**
* This is a monitor processing events coming from [FlowHookContainer]. It just appends them to a log that allows
* analysis of the events.
*
* Suggested way of debugging using this class and IntelliJ:
* 1. Hook the function calls you're interested in using [FlowHookContainer].
* 2. Add an associated event type in [MonitorEventType].
* 3. Call [newEvent] in the hook. Provide some keys to allow analysis. Example keys are the current fiber ID or a
* specific DB transaction. You can also provide additional info about the event using [MonitorEvent.extra].
* 4. Run your test and break on [newEvent] or [inspect].
* 5. Inspect the [correlator] in the debugger. E.g. you can add a watch for [MonitorEventCorrelator.getByType].
* You can search for specific objects by using filter expressions in the debugger.
*/
object FiberMonitor { object FiberMonitor {
private val log = contextLogger() private val log = loggerFor<FiberMonitor>()
private val jobQueue = LinkedBlockingQueue<Job>()
private val started = AtomicBoolean(false) private val started = AtomicBoolean(false)
private var trackerThread: Thread? = null private var executor: ScheduledExecutorService? = null
val correlator = MonitorEventCorrelator() val correlator = MonitorEventCorrelator()
sealed class Job {
data class NewEvent(val event: FullMonitorEvent) : Job()
object Finish : Job()
}
fun newEvent(event: MonitorEvent) { fun newEvent(event: MonitorEvent) {
if (trackerThread != null) { if (executor != null) {
jobQueue.add(Job.NewEvent(FullMonitorEvent(Instant.now(), Exception().stackTrace.toList(), event))) val fullEvent = FullMonitorEvent(Instant.now(), Exception().stackTrace.toList(), event)
executor!!.execute {
processEvent(fullEvent)
}
} }
} }
fun start() { fun start() {
if (started.compareAndSet(false, true)) { if (started.compareAndSet(false, true)) {
require(trackerThread == null) require(executor == null)
trackerThread = thread(name = "Fiber monitor", isDaemon = true) { executor = Executors.newSingleThreadScheduledExecutor()
while (true) { executor!!.scheduleAtFixedRate(this::inspect, 100, 100, TimeUnit.MILLISECONDS)
val job = jobQueue.poll(1, TimeUnit.SECONDS)
when (job) {
is Job.NewEvent -> processEvent(job)
Job.Finish -> return@thread
}
}
}
} }
} }
private fun processEvent(job: Job.NewEvent) { // Break on this function or [newEvent].
correlator.addEvent(job.event) private fun inspect() {
checkLeakedTransactions(job.event.event) }
checkLeakedConnections(job.event.event)
private fun processEvent(event: FullMonitorEvent) {
correlator.addEvent(event)
checkLeakedTransactions(event.event)
checkLeakedConnections(event.event)
} }
inline fun <reified R, A : Any> R.getField(name: String): A { inline fun <reified R, A : Any> R.getField(name: String): A {
@ -124,7 +134,7 @@ object FiberMonitor {
private fun checkLeakedConnections(event: MonitorEvent) { private fun checkLeakedConnections(event: MonitorEvent) {
if (event.type == MonitorEventType.FiberParking) { if (event.type == MonitorEventType.FiberParking) {
val events = correlator.events[event.keys[0]]!! val events = correlator.merged()[event.keys[0]]!!
val acquiredConnections = events.mapNotNullTo(HashSet()) { val acquiredConnections = events.mapNotNullTo(HashSet()) {
if (it.event.type == MonitorEventType.ConnectionAcquired) { if (it.event.type == MonitorEventType.ConnectionAcquired) {
it.event.keys.mapNotNull { it as? Connection }.first() it.event.keys.mapNotNull { it as? Connection }.first()
@ -147,35 +157,47 @@ object FiberMonitor {
} }
} }
/**
* This class holds the event log.
*
* Each event has a list of key associated with it. "Relatedness" is then the transitive closure of two events sharing a key.
*
* [merged] returns a map from key to related events. Note that an eventlist may be associated by several keys.
* [getUnique] makes these lists unique by keying on the set of keys associated with the events.
* [getByType] simply groups by the type of the keys. This is probably the most useful "top-level" breakdown of events.
*/
class MonitorEventCorrelator { class MonitorEventCorrelator {
private val _events = HashMap<Any, ArrayList<FullMonitorEvent>>() private val events = ArrayList<FullMonitorEvent>()
val events: Map<Any, ArrayList<FullMonitorEvent>> get() = _events
fun getUnique() = events.values.toSet().associateBy { it.flatMap { it.event.keys }.toSet() } fun getUnique() = merged().values.toSet().associateBy { it.flatMap { it.event.keys }.toSet() }
fun getByType() = events.entries.groupBy { it.key.javaClass } fun getByType() = merged().entries.groupBy { it.key.javaClass }
fun addEvent(fullMonitorEvent: FullMonitorEvent) { fun addEvent(fullMonitorEvent: FullMonitorEvent) {
val list = link(fullMonitorEvent.event.keys) events.add(fullMonitorEvent)
list.add(fullMonitorEvent)
for (key in fullMonitorEvent.event.keys) {
_events[key] = list
}
} }
fun link(keys: List<Any>): ArrayList<FullMonitorEvent> { fun merged(): Map<Any, List<FullMonitorEvent>> {
val merged = HashMap<Any, ArrayList<FullMonitorEvent>>()
for (event in events) {
val eventLists = HashSet<ArrayList<FullMonitorEvent>>() val eventLists = HashSet<ArrayList<FullMonitorEvent>>()
for (key in keys) { for (key in event.event.keys) {
val list = _events[key] val list = merged[key]
if (list != null) { if (list != null) {
eventLists.add(list) eventLists.add(list)
} }
} }
return when { val newList = when (eventLists.size) {
eventLists.isEmpty() -> ArrayList() 0 -> ArrayList()
eventLists.size == 1 -> eventLists.first() 1 -> eventLists.first()
else -> mergeAll(eventLists) else -> mergeAll(eventLists)
} }
newList.add(event)
for (key in event.event.keys) {
merged[key] = newList
}
}
return merged
} }
fun mergeAll(lists: Collection<List<FullMonitorEvent>>): ArrayList<FullMonitorEvent> { fun mergeAll(lists: Collection<List<FullMonitorEvent>>): ArrayList<FullMonitorEvent> {

View File

@ -1,14 +1,8 @@
package net.corda.flowhook package net.corda.flowhook
import co.paralleluniverse.fibers.Fiber import co.paralleluniverse.fibers.Fiber
import net.corda.node.services.statemachine.ActionExecutor
import net.corda.node.services.statemachine.Event import net.corda.node.services.statemachine.Event
import net.corda.node.services.statemachine.FlowFiber
import net.corda.node.services.statemachine.StateMachineState
import net.corda.node.services.statemachine.transitions.TransitionResult
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
import rx.subjects.Subject
import java.sql.Connection import java.sql.Connection
@Suppress("UNUSED") @Suppress("UNUSED")
@ -26,6 +20,12 @@ object FlowHookContainer {
FiberMonitor.newEvent(MonitorEvent(MonitorEventType.FiberStarted, keys = listOf(Fiber.currentFiber()))) FiberMonitor.newEvent(MonitorEvent(MonitorEventType.FiberStarted, keys = listOf(Fiber.currentFiber())))
} }
@JvmStatic
@Hook("net.corda.node.services.statemachine.FlowStateMachineImpl", passThis = true)
fun scheduleEvent(fiber: Any, event: Event) {
FiberMonitor.newEvent(MonitorEvent(MonitorEventType.SmScheduleEvent, keys = listOf(fiber), extra = listOf(event, currentFiberOrThread())))
}
@JvmStatic @JvmStatic
@Hook("co.paralleluniverse.fibers.Fiber") @Hook("co.paralleluniverse.fibers.Fiber")
fun onCompleted() { fun onCompleted() {
@ -45,13 +45,13 @@ object FlowHookContainer {
} }
@JvmStatic @JvmStatic
@Hook("net.corda.node.utilities.DatabaseTransaction", passThis = true, position = HookPosition.After) @Hook("net.corda.nodeapi.internal.persistence.DatabaseTransaction", passThis = true, position = HookPosition.After)
fun DatabaseTransaction( fun DatabaseTransaction(
transaction: Any, transaction: Any,
isolation: Int, isolation: Int,
threadLocal: ThreadLocal<*>, threadLocal: Any,
transactionBoundaries: Subject<*, *>, transactionBoundaries: Any,
cordaPersistence: CordaPersistence cordaPersistence: Any
) { ) {
val keys = ArrayList<Any>().apply { val keys = ArrayList<Any>().apply {
add(transaction) add(transaction)
@ -60,9 +60,65 @@ object FlowHookContainer {
FiberMonitor.newEvent(MonitorEvent(MonitorEventType.TransactionCreated, keys = keys)) FiberMonitor.newEvent(MonitorEvent(MonitorEventType.TransactionCreated, keys = keys))
} }
@JvmStatic
@Hook("io.netty.util.internal.InternalThreadLocalMap", passThis = true, position = HookPosition.After)
fun InternalThreadLocalMap(
internalThreadLocalMap: Any
) {
val keys = listOf(
internalThreadLocalMap,
currentFiberOrThread()
)
FiberMonitor.newEvent(MonitorEvent(MonitorEventType.NettyThreadLocalMapCreated, keys = keys))
}
@JvmStatic
@Hook("co.paralleluniverse.concurrent.util.ThreadAccess")
fun setThreadLocals(thread: Thread, threadLocals: Any?) {
FiberMonitor.newEvent(MonitorEvent(
MonitorEventType.SetThreadLocals,
keys = listOf(currentFiberOrThread()),
extra = threadLocals?.let { FiberMonitor.getThreadLocalMapEntryValues(it) }
))
}
@JvmStatic
@Hook("co.paralleluniverse.concurrent.util.ThreadAccess")
fun setInheritableThreadLocals(thread: Thread, threadLocals: Any?) {
FiberMonitor.newEvent(MonitorEvent(
MonitorEventType.SetInheritableThreadLocals,
keys = listOf(currentFiberOrThread()),
extra = threadLocals?.let { FiberMonitor.getThreadLocalMapEntryValues(it) }
))
}
@JvmStatic
@Hook("co.paralleluniverse.concurrent.util.ThreadAccess")
fun getThreadLocals(thread: Thread): (threadLocals: Any?) -> Unit {
return { threadLocals ->
FiberMonitor.newEvent(MonitorEvent(
MonitorEventType.GetThreadLocals,
keys = listOf(currentFiberOrThread()),
extra = threadLocals?.let { FiberMonitor.getThreadLocalMapEntryValues(it) }
))
}
}
@JvmStatic
@Hook("co.paralleluniverse.concurrent.util.ThreadAccess")
fun getInheritableThreadLocals(thread: Thread): (threadLocals: Any?) -> Unit {
return { threadLocals ->
FiberMonitor.newEvent(MonitorEvent(
MonitorEventType.GetInheritableThreadLocals,
keys = listOf(currentFiberOrThread()),
extra = threadLocals?.let { FiberMonitor.getThreadLocalMapEntryValues(it) }
))
}
}
@JvmStatic @JvmStatic
@Hook("com.zaxxer.hikari.HikariDataSource") @Hook("com.zaxxer.hikari.HikariDataSource")
fun getConnection(): (Connection) -> Unit { fun getConnection(): (Any) -> Unit {
val transactionOrThread = currentTransactionOrThread() val transactionOrThread = currentTransactionOrThread()
FiberMonitor.newEvent(MonitorEvent(MonitorEventType.ConnectionRequested, keys = listOf(transactionOrThread))) FiberMonitor.newEvent(MonitorEvent(MonitorEventType.ConnectionRequested, keys = listOf(transactionOrThread)))
return { connection -> return { connection ->
@ -81,19 +137,23 @@ object FlowHookContainer {
@JvmStatic @JvmStatic
@Hook("net.corda.node.services.statemachine.TransitionExecutorImpl") @Hook("net.corda.node.services.statemachine.TransitionExecutorImpl")
fun executeTransition( fun executeTransition(
fiber: FlowFiber, fiber: Any,
previousState: StateMachineState, previousState: Any,
event: Event, event: Any,
transition: TransitionResult, transition: Any,
actionExecutor: ActionExecutor actionExecutor: Any
) { ) {
FiberMonitor.newEvent(MonitorEvent(MonitorEventType.ExecuteTransition, keys = listOf(fiber), extra = object { FiberMonitor.newEvent(MonitorEvent(MonitorEventType.SmExecuteTransition, keys = listOf(fiber), extra = object {
val previousState = previousState val previousState = previousState
val event = event val event = event
val transition = transition val transition = transition
})) }))
} }
private fun currentFiberOrThread(): Any {
return Fiber.currentFiber() ?: Thread.currentThread()
}
private fun currentTransactionOrThread(): Any { private fun currentTransactionOrThread(): Any {
return try { return try {
DatabaseTransactionManager.currentOrNull() DatabaseTransactionManager.currentOrNull()

View File

@ -56,28 +56,36 @@ class Hooker(hookContainer: Any) : ClassFileTransformer {
private fun instrumentClass(clazz: CtClass): CtClass? { private fun instrumentClass(clazz: CtClass): CtClass? {
val hookMethods = hooks[clazz.name] ?: return null val hookMethods = hooks[clazz.name] ?: return null
val usedHookMethods = HashSet<Method>() val usedHookMethods = HashSet<Method>()
var isAnyInstrumented = false
for (method in clazz.declaredBehaviors) { for (method in clazz.declaredBehaviors) {
val hookMethod = instrumentBehaviour(method, hookMethods) usedHookMethods.addAll(instrumentBehaviour(method, hookMethods))
if (hookMethod != null) {
isAnyInstrumented = true
usedHookMethods.add(hookMethod)
}
} }
val unusedHookMethods = hookMethods.values.mapTo(HashSet()) { it.first } - usedHookMethods val unusedHookMethods = hookMethods.values.mapTo(HashSet()) { it.first } - usedHookMethods
if (usedHookMethods.isNotEmpty()) {
println("Hooked methods $usedHookMethods")
}
if (unusedHookMethods.isNotEmpty()) { if (unusedHookMethods.isNotEmpty()) {
println("Unused hook methods $unusedHookMethods") println("Unused hook methods $unusedHookMethods")
} }
return if (isAnyInstrumented) { return if (usedHookMethods.isNotEmpty()) {
clazz clazz
} else { } else {
null null
} }
} }
private fun instrumentBehaviour(method: CtBehavior, methodHooks: MethodHooks): Method? { private val objectName = Any::class.java.name
val signature = Signature(method.name, method.parameterTypes.map { it.name }) private fun instrumentBehaviour(method: CtBehavior, methodHooks: MethodHooks): List<Method> {
val (hookMethod, annotation) = methodHooks[signature] ?: return null val pairs = methodHooks.mapNotNull { (signature, pair) ->
if (signature.functionName != method.name) return@mapNotNull null
if (signature.parameterTypes.size != method.parameterTypes.size) return@mapNotNull null
for (i in 0 until signature.parameterTypes.size) {
if (signature.parameterTypes[i] != objectName && signature.parameterTypes[i] != method.parameterTypes[i].name) {
return@mapNotNull null
}
}
pair
}
for ((hookMethod, annotation) in pairs) {
val invocationString = if (annotation.passThis) { val invocationString = if (annotation.passThis) {
"${hookMethod.declaringClass.canonicalName}.${hookMethod.name}(this, \$\$)" "${hookMethod.declaringClass.canonicalName}.${hookMethod.name}(this, \$\$)"
} else { } else {
@ -99,19 +107,32 @@ class Hooker(hookContainer: Any) : ClassFileTransformer {
when { when {
Function0::class.java.isAssignableFrom(hookMethod.returnType) -> { Function0::class.java.isAssignableFrom(hookMethod.returnType) -> {
method.addLocalVariable("after", classPool.get("kotlin.jvm.functions.Function0")) method.addLocalVariable("after", classPool.get("kotlin.jvm.functions.Function0"))
method.insertHook("after = $invocationString;") method.insertHook("after = null; ${wrapTryCatch("after = $invocationString;")}")
method.insertAfter("after.invoke();") method.insertAfter("if (after != null) ${wrapTryCatch("after.invoke();")}")
} }
Function1::class.java.isAssignableFrom(hookMethod.returnType) -> { Function1::class.java.isAssignableFrom(hookMethod.returnType) -> {
method.addLocalVariable("after", classPool.get("kotlin.jvm.functions.Function1")) method.addLocalVariable("after", classPool.get("kotlin.jvm.functions.Function1"))
method.insertHook("after = $invocationString;") method.insertHook("after = null; ${wrapTryCatch("after = $invocationString;")}")
method.insertAfter("after.invoke((\$w)\$_);") method.insertAfter("if (after != null) ${wrapTryCatch("after.invoke((\$w)\$_);")}")
} }
else -> { else -> {
method.insertHook("$invocationString;") method.insertHook(wrapTryCatch("$invocationString;"))
} }
} }
return hookMethod }
return pairs.map { it.first }
}
companion object {
fun wrapTryCatch(statement: String): String {
return "try { $statement } catch (Throwable throwable) { ${Hooker::class.java.canonicalName}.${Hooker.Companion::exceptionInHook.name}(throwable); }"
}
@JvmStatic
fun exceptionInHook(throwable: Throwable) {
throwable.printStackTrace()
}
} }
} }
@ -122,7 +143,11 @@ enum class HookPosition {
} }
@Target(AnnotationTarget.FUNCTION) @Target(AnnotationTarget.FUNCTION)
annotation class Hook(val clazz: String, val position: HookPosition = HookPosition.Before, val passThis: Boolean = false) annotation class Hook(
val clazz: String,
val position: HookPosition = HookPosition.Before,
val passThis: Boolean = false
)
private data class Signature(val functionName: String, val parameterTypes: List<String>) private data class Signature(val functionName: String, val parameterTypes: List<String>)

View File

@ -17,6 +17,7 @@ import net.corda.nodeapi.internal.config.User
import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.internal.performance.div import net.corda.testing.internal.performance.div
import net.corda.testing.internal.performance.startPublishingFixedRateInjector import net.corda.testing.internal.performance.startPublishingFixedRateInjector
@ -128,21 +129,40 @@ class NodePerformanceTests : IntegrationTest() {
driver( driver(
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY.name, rpcUsers = listOf(user))), notarySpecs = listOf(NotarySpec(DUMMY_NOTARY.name, rpcUsers = listOf(user))),
startNodesInProcess = true, startNodesInProcess = true,
extraCordappPackagesToScan = listOf("net.corda.finance") extraCordappPackagesToScan = listOf("net.corda.finance"),
portAllocation = PortAllocation.Incremental(20000)
) { ) {
val notary = defaultNotaryNode.getOrThrow() as NodeHandle.InProcess val notary = defaultNotaryNode.getOrThrow() as NodeHandle.InProcess
val metricRegistry = startReporter(shutdownManager, notary.node.services.monitoringService.metrics) val metricRegistry = startReporter(shutdownManager, notary.node.services.monitoringService.metrics)
notary.rpcClientToNode().use("A", "A") { connection -> notary.rpcClientToNode().use("A", "A") { connection ->
println("ISSUING") println("ISSUING")
val doneFutures = (1..100).toList().parallelStream().map { val doneFutures = (1..100).toList().map {
connection.proxy.startFlow(::CashIssueFlow, 1.DOLLARS, OpaqueBytes.of(0), defaultNotaryIdentity).returnValue connection.proxy.startFlow(::CashIssueFlow, 1.DOLLARS, OpaqueBytes.of(0), defaultNotaryIdentity).returnValue
}.toList() }.toList()
doneFutures.transpose().get() doneFutures.transpose().get()
println("STARTING PAYMENT") println("STARTING PAYMENT")
startPublishingFixedRateInjector(metricRegistry, 8, 5.minutes, 100L / TimeUnit.SECONDS) { startPublishingFixedRateInjector(metricRegistry, 8, 5.minutes, 5L / TimeUnit.SECONDS) {
connection.proxy.startFlow(::CashPaymentFlow, 1.DOLLARS, defaultNotaryIdentity).returnValue.get() connection.proxy.startFlow(::CashPaymentFlow, 1.DOLLARS, defaultNotaryIdentity).returnValue.get()
} }
} }
} }
} }
@Test
fun `single pay`() {
val user = User("A", "A", setOf(startFlow<CashIssueFlow>(), startFlow<CashPaymentFlow>()))
driver(
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY.name, rpcUsers = listOf(user))),
startNodesInProcess = true,
extraCordappPackagesToScan = listOf("net.corda.finance"),
portAllocation = PortAllocation.Incremental(20000)
) {
val notary = defaultNotaryNode.getOrThrow() as NodeHandle.InProcess
val metricRegistry = startReporter(shutdownManager, notary.node.services.monitoringService.metrics)
notary.rpcClientToNode().use("A", "A") { connection ->
connection.proxy.startFlow(::CashIssueFlow, 1.DOLLARS, OpaqueBytes.of(0), defaultNotaryIdentity).returnValue.getOrThrow()
connection.proxy.startFlow(::CashPaymentFlow, 1.DOLLARS, defaultNotaryIdentity).returnValue.getOrThrow()
}
}
}
} }

View File

@ -294,12 +294,14 @@ class P2PMessagingClient(config: NodeConfiguration,
// processing a message but if so, it'll be parked waiting for us to count down the latch, so // processing a message but if so, it'll be parked waiting for us to count down the latch, so
// the session itself is still around and we can still ack messages as a result. // the session itself is still around and we can still ack messages as a result.
override fun acknowledge() { override fun acknowledge() {
messagingExecutor.fetchFrom {
state.locked { state.locked {
artemisMessage.individualAcknowledge() artemisMessage.individualAcknowledge()
artemis.started!!.session.commit() artemis.started!!.session.commit()
} }
} }
} }
}
deliverTo(msg, HandlerRegistration(msg.topic, deliverTo), acknowledgeHandle) deliverTo(msg, HandlerRegistration(msg.topic, deliverTo), acknowledgeHandle)
} else { } else {
log.warn("Received message ${msg.uniqueMessageId} for ${msg.topic} that doesn't have any registered handlers yet") log.warn("Received message ${msg.uniqueMessageId} for ${msg.topic} that doesn't have any registered handlers yet")

View File

@ -6,6 +6,7 @@ import net.corda.core.utilities.contextLogger
import net.corda.node.services.statemachine.* import net.corda.node.services.statemachine.*
import net.corda.node.services.statemachine.transitions.FlowContinuation import net.corda.node.services.statemachine.transitions.FlowContinuation
import net.corda.node.services.statemachine.transitions.TransitionResult import net.corda.node.services.statemachine.transitions.TransitionResult
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
/** /**
@ -28,13 +29,16 @@ class DumpHistoryOnErrorInterceptor(val delegate: TransitionExecutor) : Transiti
actionExecutor: ActionExecutor actionExecutor: ActionExecutor
): Pair<FlowContinuation, StateMachineState> { ): Pair<FlowContinuation, StateMachineState> {
val (continuation, nextState) = delegate.executeTransition(fiber, previousState, event, transition, actionExecutor) val (continuation, nextState) = delegate.executeTransition(fiber, previousState, event, transition, actionExecutor)
val transitionRecord = TransitionDiagnosticRecord(fiber.id, previousState, nextState, event, transition, continuation) val transitionRecord = TransitionDiagnosticRecord(Instant.now(), fiber.id, previousState, nextState, event, transition, continuation)
val record = records.compute(fiber.id) { _, record -> val record = records.compute(fiber.id) { _, record ->
(record ?: ArrayList()).apply { add(transitionRecord) } (record ?: ArrayList()).apply { add(transitionRecord) }
} }
if (nextState.checkpoint.errorState is ErrorState.Errored) { if (nextState.checkpoint.errorState is ErrorState.Errored) {
log.warn("Flow ${fiber.id} dirtied, dumping all transitions:\n${record!!.joinToString("\n")}") log.warn("Flow ${fiber.id} dirtied, dumping all transitions:\n${record!!.joinToString("\n")}")
for (error in nextState.checkpoint.errorState.errors) {
log.warn("Flow ${fiber.id} error", error.exception)
}
} }
if (transition.newState.isRemoved) { if (transition.newState.isRemoved) {

View File

@ -5,6 +5,7 @@ import net.corda.core.utilities.contextLogger
import net.corda.node.services.statemachine.* import net.corda.node.services.statemachine.*
import net.corda.node.services.statemachine.transitions.FlowContinuation import net.corda.node.services.statemachine.transitions.FlowContinuation
import net.corda.node.services.statemachine.transitions.TransitionResult import net.corda.node.services.statemachine.transitions.TransitionResult
import java.time.Instant
/** /**
* This interceptor simply prints all state machine transitions. Useful for debugging. * This interceptor simply prints all state machine transitions. Useful for debugging.
@ -23,7 +24,7 @@ class PrintingInterceptor(val delegate: TransitionExecutor) : TransitionExecutor
actionExecutor: ActionExecutor actionExecutor: ActionExecutor
): Pair<FlowContinuation, StateMachineState> { ): Pair<FlowContinuation, StateMachineState> {
val (continuation, nextState) = delegate.executeTransition(fiber, previousState, event, transition, actionExecutor) val (continuation, nextState) = delegate.executeTransition(fiber, previousState, event, transition, actionExecutor)
val transitionRecord = TransitionDiagnosticRecord(fiber.id, previousState, nextState, event, transition, continuation) val transitionRecord = TransitionDiagnosticRecord(Instant.now(), fiber.id, previousState, nextState, event, transition, continuation)
log.info("Transition for flow ${fiber.id} $transitionRecord") log.info("Transition for flow ${fiber.id} $transitionRecord")
return Pair(continuation, nextState) return Pair(continuation, nextState)
} }

View File

@ -6,12 +6,14 @@ import net.corda.node.services.statemachine.Event
import net.corda.node.services.statemachine.StateMachineState import net.corda.node.services.statemachine.StateMachineState
import net.corda.node.services.statemachine.transitions.TransitionResult import net.corda.node.services.statemachine.transitions.TransitionResult
import net.corda.node.utilities.ObjectDiffer import net.corda.node.utilities.ObjectDiffer
import java.time.Instant
/** /**
* This is a diagnostic record that stores information about a state machine transition and provides pretty printing * This is a diagnostic record that stores information about a state machine transition and provides pretty printing
* by diffing the two states. * by diffing the two states.
*/ */
data class TransitionDiagnosticRecord( data class TransitionDiagnosticRecord(
val timestamp: Instant,
val flowId: StateMachineRunId, val flowId: StateMachineRunId,
val previousState: StateMachineState, val previousState: StateMachineState,
val nextState: StateMachineState, val nextState: StateMachineState,
@ -26,6 +28,7 @@ data class TransitionDiagnosticRecord(
listOf( listOf(
"", "",
" --- Transition of flow $flowId ---", " --- Transition of flow $flowId ---",
" Timestamp: $timestamp",
" Event: $event", " Event: $event",
" Actions: ", " Actions: ",
" ${transition.actions.joinToString("\n ")}", " ${transition.actions.joinToString("\n ")}",